摘要
本文是对RFC 9807所描述的增强型口令认证密钥交换协议OPAQUE的学习与梳理,包括:1)协议整体执行流程、2)注册方案和在线认证方案、3)关键模块OPRF与3DH。文章保留了原标准的章节对应关系,方便对照RFC原文阅读。
关键词:口令认证、OPAQUE、OPRF、aPAKE、3DH、密钥交换
本文导读
这是啥——这是RFC 9807 OPAQUE协议的配套学习笔记,先总览、再分模块、最后看流程,适合对照原文阅读,也适合想快速搞懂原理的读者通读一遍。
哪个读——密码协议研究者、安全开发工程师,以及正在学习PAKE、OPRF的童鞋。阅读前建议先了解OPRF(不经意伪随机函数,可参考RFC 9497)和DH密钥交换。
啷个读——建议先看第2章的架构总览,建立两个阶段和三条消息的整体印象;再按兴趣深入第3、4章的两个关键模块;第6、7章是完整的注册和登录流程,完整详情可以随时配套阅读RFC原文。
结算——OPAQUE用OPRF把口令隐蔽起来,让服务器在看不到口令的情况下,仍能和客户端完成双向认证并协商出会话密钥;即使服务器数据库泄露,攻击者也无法用彩虹表等手段批量破解口令。
5. 结构体汇总
这一章把所有结构体按使用阶段集中列一遍,主要目的是分清每个结构体在什么阶段出现。
图5-1结构体汇总图
按阶段梳理:
注册阶段(OPRF相关)
- 客户端:注册请求RegistrationRequest,内含OPRF盲化点blinded_message
- 服务器:注册响应RegistrationResponse,内含OPRF求值点evaluated_message、服务器公钥
- 客户端:注册记录RegistrationRecord,内含客户端公钥、掩码密钥masking_key、信封envelope
在线阶段(OPRF + AKE相关)
- KE1:OPRF请求credential_request(含盲化点)、AKE请求auth_request(含client_nonce、客户端临时公钥)
- KE2:OPRF响应credential_response(含求值点、masking_nonce、密文masked_response)、AKE响应auth_response(含server_nonce、服务器临时公钥、server_mac)
- KE3:仅含客户端认证标签client_mac
6. 注册阶段(对应第5章)
6.1 注册阶段全流程概述
注册阶段共三条消息:
- RegistrationRequest:客户端发往服务器,只包含序列化后的OPRF盲化消息;
- RegistrationResponse:服务器发回客户端,包含OPRF求值结果和服务器AKE公钥;
- RegistrationRecord:客户端发往服务器,包含客户端公钥、掩码密钥和完整信封,作为服务器永久存储的用户凭证。
图6-1注册阶段交互图(复用图2-3)
下图为注册阶段密钥派生的完整数据流。
图6-4注册密钥派生数据流
OPAQUE用一个叫信封(Envelope)的结构来管理客户端凭证。信封是由客户端在注册时创建、再发给服务器存储的,而不是由服务器生成。服务器从始至终只负责保管信封,无法知道信封里密钥的具体内容,更无法在注册环节就替用户决定密钥;每次登录时服务器把信封原样发回,客户端才能恢复其中的密钥材料,供AKE使用。换句话说,服务器在整个过程中只是一个加密储物柜,钥匙始终在客户端手里。
应用可以把密钥材料和身份标识绑定。如果某一方没有指定身份,就默认用该方的公钥作为身份。应用凭证里必须包含这样几类值:
- client_private_key、client_public_key:客户端AKE密钥
- server_public_key:服务器AKE公钥
- client_identity:客户端身份,如账户名或邮箱
- server_identity:服务器身份,通常是域名
6.2 结构体——CleartextCredentials凭证(对应第4章)
6.2.1 CleartextCredentials定义
明文凭证结构体定义如下:
struct {
uint8 server_public_key[Npk]; // 服务器公钥
uint8 server_identity<1..2^16-1>; // 服务器身份,变长
uint8 client_identity<1..2^16-1>; // 客户端身份,变长
} CleartextCredentials;
这里的变长字段遵循TLS表示语言的编码规则:字段内容前加2字节的大端长度前缀。因此CleartextCredentials序列化后的字节顺序是固定的:
表6-1 CleartextCredentials序列化顺序
顺序 | 内容 | 长度 |
1 | server_public_key | 固定Npk字节 |
2 | server_identity的长度 | 2字节(大端) |
3 | server_identity内容 | 长度值指定 |
4 | client_identity的长度 | 2字节(大端) |
5 | client_identity内容 | 长度值指定 |
6.2.2 CreateCleartextCredentials(构造凭证)
cleartext_credentials = CreateCleartextCredentials(server_public_key, client_public_key, server_identity, client_identity)
- 功能:根据应用凭证信息构造CleartextCredentials。
- 关键步骤:如果没有提供某一方身份,就用该方公钥代替;然后组装成结构体返回。
6.3 结构体——Envelope信封(对应4.1节)
6.3.1 Envelope定义(对应4.1.1节)
struct Envelope {
uint8 envelope_nonce[Nn]; // 随机nonce
uint8 auth_tag[Nm]; // 完整性校验码
}
信封由客户端使用,目的是保证服务器公钥和双方身份的完整性,防止交互过程中身份被替换。认证标签计算:
auth_tag = MAC(auth_key, envelope_nonce || server_public_key || Encode(server_identity) || Encode(client_identity))
其中auth_key是从OPRF结果派生出的完整性密钥。
6.3.2 Store(创建信封,对应4.1.2节)
(envelope, client_public_key, masking_key, export_key) =
Store(rpwd, server_public_key, server_identity, client_identity)
- 功能:客户端在注册时创建信封。
- 输入:随机化口令rpwd、服务器公钥、双方身份。
- 输出:信封、客户端公钥、掩码密钥、导出密钥。
- 关键步骤:
- 随机生成envelope_nonce;
- 只通过KDF的Expand,从rpwd派生出四类密钥:
masking_key = Expand(rpwd, "MaskingKey", Nh) # 掩码密钥,服务器侧防枚举
auth_key = Expand(rpwd, envelope_nonce || "AuthKey", Nh) # 认证密钥,生成信封标签
export_key = Expand(rpwd, envelope_nonce || "ExportKey", Nh) # 导出密钥,应用层使用
seed = Expand(rpwd, envelope_nonce || "PrivateKey", Nseed) # 私钥种子
- 用seed派生客户端AKE公钥((_, client_public_key) = DeriveDiffieHellmanKeyPair(seed));
- 构造明文凭证,用auth_key对nonce、公钥、双方身份计算MAC;
- 组装成信封返回。
图6-2 Store流程图
6.3.3 Recover(恢复信封,对应4.1.3节)
(client_private_key, cleartext_credentials, export_key) = Recover(randomized_password, server_public_key, envelope, server_identity, client_identity)
- 功能:客户端在登录时恢复信封中的凭证。
- 输出:客户端私钥、明文凭证、导出密钥;恢复失败抛出EnvelopeRecoveryError。
- 关键步骤:
- 从rpwd派生出auth_key、export_key和私钥种子;
- 用seed派生客户端AKE公私钥;
- 构造明文凭证,重算MAC,与信封里的auth_tag做恒时比较,不一致就报错并清除所有中间值;
- 验证通过则输出私钥、明文凭证和导出密钥。
Recover里重算MAC可以简写为:
expected_tag = MAC(auth_key, envelope.nonce || cleartext_credentials)
这与Store里那串"nonce || server_public_key || Encode(身份)"在字节上一致,因为CleartextCredentials序列化后恰好就是这些内容。
图6-3 Recover流程图
6.4 CreateRegistrationRequest(注册请求,对应5.2.1节)
(request, blind) = CreateRegistrationRequest(password)
- 功能:客户端发起注册请求。
- 关键步骤:执行OPRF的Blind得到(blind, blinded_element),把盲化点序列化成blinded_message,组装成RegistrationRequest:
struct {
uint8 blinded_message[Noe];
} RegistrationRequest;
盲化失败时抛出InvalidInputError。
6.5 CreateRegistrationResponse(注册响应,对应5.2.2节)
response = CreateRegistrationResponse(request, server_public_key, credential_identifier, oprf_seed)
- 功能:服务器生成注册响应。
- 关键步骤:
- 只用Expand从oprf_seed派生OPRF密钥种子:
seed = Expand(oprf_seed, credential_identifier || "OprfKey", Nok)
(oprf_key, _) = DeriveKeyPair(seed, "OPAQUE-DeriveKeyPair")
- 反序列化客户端的盲化点,执行BlindEvaluate;
- 把求值点编码,连同服务器公钥组装成RegistrationResponse:
struct {
uint8 evaluated_message[Noe];
uint8 server_public_key[Npk];
} RegistrationResponse;
6.6 FinalizeRegistrationRequest(完成注册,对应5.2.3节)
(record, export_key) = FinalizeRegistrationRequest(password, blind, response, server_identity, client_identity)
- 功能:客户端生成注册记录,完成注册。
- 关键步骤:
- 反序列化求值点,执行OPRF的Finalize得到oprf_output;
- 用KSF拉伸:stretched_oprf_output = Stretch(oprf_output);
- 用Extract得到随机化口令:
randomized_password = Extract("", oprf_output || stretched_oprf_output)
- 调用Store生成信封、客户端公钥、掩码密钥和导出密钥;
- 组装成RegistrationRecord:
struct {
uint8 client_public_key[Npk];
uint8 masking_key[Nh];
Envelope envelope;
} RegistrationRecord;