深入理解 SPAKE2 + 协议:原理与 代码实现详解
1. 引言
在现代网络通信中,安全的密钥交换是建立可信连接的基石。传统的密钥交换协议(如 Diffie-Hellman)虽然能实现密钥协商,但无法抵抗中间人攻击;而基于证书的 TLS 协议虽然解决了身份认证问题,却带来了证书管理的复杂性。在这种背景下,密码认证密钥交换(PAKE,Password-Authenticated Key Exchange) 协议应运而生。
PAKE 协议允许通信双方仅使用共享的低熵密码(如用户口令),在不安全的信道上完成身份认证并协商出高熵的会话密钥,同时能够有效抵抗离线字典攻击和中间人攻击。SPAKE2+(Simple Password-Authenticated Key Exchange 2 Plus)是其中一种优秀的 PAKE 协议,由 Stanford 大学的 Dan Boneh 等人设计,具有简洁高效、安全性强的特点。
本文将深入剖析 SPAKE2 + 协议的技术原理,并结合 openHiTLS 开源库中的 SPAKE2 + 实现代码,从数学基础、协议流程、代码实现到实战应用,全方位讲解这一重要的密码学协议。
2. 数学基础
要理解 SPAKE2 + 协议,首先需要掌握其背后的数学工具。SPAKE2 + 主要基于椭圆曲线密码学(ECC),同时涉及大数模运算、哈希函数、消息认证码(MAC)和密钥派生函数(KDF)等密码学原语。
2.1 椭圆曲线密码学基础
椭圆曲线密码学是基于有限域上椭圆曲线点运算的公钥密码体制。在 SPAKE2 + 中,我们使用的是素数域 \(\mathbb{F}_p\) 上的椭圆曲线,其标准形式为:
\(y^2 = x^3 + ax + b\)
其中 \(a, b \in \mathbb{F}_p\),且满足判别式 \(4a^3 + 27b^2 \not\equiv 0 \mod p\)。
2.1.1 椭圆曲线点运算
椭圆曲线上的点运算主要包括点加和标量乘:
- 点加(Point Addition):对于曲线上的两个点 P 和 Q,点加运算 \(P + Q\) 得到曲线上的另一个点 R。几何上,这相当于过 P 和 Q 作直线,与曲线交于第三点,该点关于 x 轴的对称点即为 R。
- 标量乘(Scalar Multiplication):对于点 P 和整数 k,标量乘 kP 表示将 P 与自身相加 k 次,即 \(kP = P + P + \dots + P\)(共 k 次)。
2.1.2 椭圆曲线群
椭圆曲线上的所有点(包括无穷远点 \(\mathcal{O}\))在点加运算下构成一个阿贝尔群(交换群),无穷远点是群的单位元。这个群是 SPAKE2 + 协议的核心数学结构。
在 openHiTLS 的实现中,我们可以看到对椭圆曲线参数的定义,如 NIST P-256、P-384、P-521 等标准曲线:
static Spake2Plus_AlgInfo g_spake2PlusAlgInfo[] = {
{CRYPT_ECC_NISTP256, CRYPT_MD_SHA256, CRYPT_HKDF_SHA256, CRYPT_MAC_HMAC_SHA256, 32, 32},
{CRYPT_ECC_NISTP256, CRYPT_MD_SHA512, CRYPT_HKDF_SHA512, CRYPT_MAC_HMAC_SHA512, 64, 64},
{CRYPT_ECC_NISTP384, CRYPT_MD_SHA256, CRYPT_HKDF_SHA256, CRYPT_MAC_HMAC_SHA256, 32, 32},
// ... 其他曲线配置
};
2.2 大数模运算
SPAKE2 + 协议中涉及大量的大数模运算,包括模加、模减、模乘、模逆等。这些运算通常通过专门的大数库(如 openHiTLS 中的 CRYPT_BN 模块)来实现。
例如,在生成随机数时,需要确保随机数在椭圆曲线的阶 n 范围内:
static int32_t Spake2PlusGenerateRandNum(uint8_t *num, uint8_t *p, uint32_t pLen)
{
BN_BigNum *p0 = BN_Create(pLen*8);
BN_BigNum *x0 = BN_Create(pLen*8);
// ... 初始化检查
// 将椭圆曲线参数p转换为大数
int32_t ret = BN_Bin2Bn(p0, p, pLen);
// ... 错误处理
uint8_t *x = BSL_SAL_Malloc(pLen);
// ... 内存分配检查
BSL_SAL_CleanseData(x, pLen);
uint32_t retryCount = 0;
const uint32_t MAX_RETRIES = 1000;
bool success = false;
// 循环生成随机数,直到找到小于p的数
while (retryCount < MAX_RETRIES) {
ret = CRYPT_EAL_RandbytesEx(NULL, x, pLen);
// ... 错误处理
ret = BN_Bin2Bn(x0, x, pLen);
// ... 错误处理
if (BN_Cmp(x0, p0) == -1) { // x0 < p0
success = true;
break;
}
++retryCount;
}
// ... 后续处理
}
2.3 密码学原语
SPAKE2 + 协议还依赖以下密码学原语:
- 哈希函数(Hash Function):如 SHA-256、SHA-512,用于将任意长度的消息压缩为固定长度的摘要,具有单向性和抗碰撞性。
- 消息认证码(MAC):如 HMAC-SHA256、CMAC-AES,用于验证消息的完整性和真实性,需要密钥才能生成和验证。
- 密钥派生函数(KDF):如 HKDF(HMAC-based Extract-and-Expand Key Derivation Function),用于从一个共享密钥派生出多个不同用途的密钥。
在 openHiTLS 的实现中,这些原语通过统一的抽象接口(CRYPT_EAL_Md、CRYPT_EAL_Mac、CRYPT_EAL_Kdf)来调用:
// 使用HKDF派生密钥
ret = CRYPT_EAL_KdfSetParam(kdfCtx, params);
// ... 错误处理
ret = CRYPT_EAL_KdfDerive(kdfCtx, out, outLen);
// ... 错误处理
// 使用HMAC生成确认消息
ret = CRYPT_EAL_MacInit(MacCtx, kConfirm.data, kConfirm.dataLen);
// ... 错误处理
ret = CRYPT_EAL_MacUpdate(MacCtx, share.data, share.dataLen);
// ... 错误处理
ret = CRYPT_EAL_MacFinal(MacCtx, outHmac->data, &(outHmac->dataLen));
3. SPAKE2 + 协议原理
3.1 协议概述
SPAKE2 + 协议包含两个主要阶段:注册阶段和认证阶段。
- 注册阶段:证明者(Prover,通常是客户端)向验证者(Verifier,通常是服务器)注册自己的密码信息。验证者存储验证数据,但不存储密码本身。
- 认证阶段:证明者和验证者通过交互,证明双方都知道共享的密码,并协商出会话密钥。
3.2 协议符号定义
在详细讲解协议流程之前,我们先定义一些符号:
- \(\mathbb{G}\):椭圆曲线群,阶为素数 n
- G:群 \(\mathbb{G}\) 的生成元
- \(M, N\):群 \(\mathbb{G}\) 中的两个固定点(称为 “模糊因子”)
- pw:用户密码
- H:哈希函数
- KDF:密钥派生函数
- MAC:消息认证码函数
3.3 注册阶段
注册阶段的目标是让验证者存储密码的验证数据,而不存储密码本身。具体步骤如下:
- 证明者选择密码 pw。
- 证明者计算 \(w_0 = H_0(pw) \mod n\),\(w_1 = H_1(pw) \mod n\),其中 \(H_0, H_1\) 是两个不同的哈希函数(或从同一哈希函数派生)。
- 证明者计算 \(L = w_1 \cdot G\)(即 \(w_1\) 与生成元 G 的标量乘)。
- 证明者将 \((w_0, L)\) 发送给验证者,验证者存储这些信息作为验证数据。
在 openHiTLS 的实现中,注册阶段的逻辑主要在 HITLS_AUTH_Spake2plusReqRegister 函数中:
int32_t HITLS_AUTH_Spake2plusReqRegister(HITLS_AUTH_PakeCtx* ctx, CRYPT_EAL_KdfCtx* kdfCtx, BSL_Buffer exist_w0,
BSL_Buffer exist_w1, BSL_Buffer exist_l)
{
// ... 参数检查
Spake2plusCtx *spakeCtx = (Spake2plusCtx *)HITLS_AUTH_PakeGetInternalCtx(ctx);
// ... 上下文检查
// 如果已存在验证数据,直接使用
if (allNotNull) {
spakeCtx->w0.dataLen = exist_w0.dataLen;
spakeCtx->w1.dataLen = exist_w1.dataLen;
spakeCtx->l.dataLen = exist_l.dataLen;
memcpy(spakeCtx->w0.data, exist_w0.data, exist_w0.dataLen);
memcpy(spakeCtx->w1.data, exist_w1.data, exist_w1.dataLen);
memcpy(spakeCtx->l.data, exist_l.data, exist_l.dataLen);
return HITLS_AUTH_SUCCESS;
}
// 根据曲线选择输出长度(来自RFC 9383第3.2节)
uint32_t outLen = 0;
if (g_spake2PlusAlgInfo[spakeCtx->index].curveId == CRYPT_ECC_NISTP256) {
outLen = 80;
} else if (g_spake2PlusAlgInfo[spakeCtx->index].curveId == CRYPT_ECC_NISTP384) {
outLen = 112;
} else if (g_spake2PlusAlgInfo[spakeCtx->index].curveId == CRYPT_ECC_NISTP521) {
outLen = 148;
}
// ... 错误处理
// 使用KDF从密码派生w0s和w1s
ret = CRYPT_EAL_KdfDerive(kdfCtx, out, outLen);
// ... 错误处理
memcpy(w0s, out, w0sLen);
memcpy(w1s, out + w0sLen, outLen - w0sLen);
// 将w0s和w1s转换为大数,并模n得到w0和w1
ret = BN_Bin2Bn(w0s0, w0s, w0sLen);
// ... 错误处理
ret = BN_Bin2Bn(w1s0, w1s, w1sLen);
// ... 错误处理
// 获取椭圆曲线的阶n
ret = Spake2PlusGetEcc(pkeyCtx, ECC_PARAM_N, n, &nLen);
// ... 错误处理
ret = BN_Bin2Bn(n0, n, nLen);
// ... 错误处理
// 计算w0 = w0s mod n
ret = BN_Mod(result, w0s0, n0, opt);
// ... 错误处理
ret = BN_Bn2Bin(result, w0_data, &w0_dataLen);
// ... 错误处理
// 计算w1 = w1s mod n
ret = BN_Mod(result, w1s0, n0, opt);
// ... 错误处理
ret = BN_Bn2Bin(result, w1_data, &w1_dataLen);
// ... 错误处理
// 计算L = w1 * G
ret = ECC_PointMul(para, L, result, NULL);
// ... 错误处理
// 将L编码为字节串
ret = ECC_EncodePoint(para, L, l_data, &l_dataLen, CRYPT_POINT_UNCOMPRESSED);
// ... 错误处理
// 存储w0, w1, L
spakeCtx->w0.dataLen = w0_dataLen;
spakeCtx->w1.dataLen = w1_dataLen;
spakeCtx->l.dataLen = l_dataLen;
memcpy(spakeCtx->w0.data, w0_data, w0_dataLen);
memcpy(spakeCtx->w1.data, w1_data, w1_dataLen);
memcpy(spakeCtx->l.data, l_data, l_dataLen);
// ... 清理资源
}
3.4 认证阶段
认证阶段是 SPAKE2 + 协议的核心,分为四个步骤:
3.4.1 证明者初始化(Prover Setup)
- 证明者生成随机数 \(x \in_R [1, n-1]\)。
- 证明者计算 \(X = x \cdot G + w_0 \cdot M\),其中 M 是固定的模糊点。
- 证明者将 X 发送给验证者。
在 openHiTLS 中,这一步由 HITLS_AUTH_Spake2plusReqSetup 函数实现:
int32_t HITLS_AUTH_Spake2plusReqSetup(HITLS_AUTH_PakeCtx *ctx, BSL_Buffer randnumx, BSL_Buffer *share)
{
// ... 参数检查
Spake2plusCtx *spakeCtx = (Spake2plusCtx *)HITLS_AUTH_PakeGetInternalCtx(ctx);
// ... 上下文检查
uint8_t randnum[MAX_ECC_PARAM_LEN] = { 0 };
uint32_t randnumLen = MAX_ECC_PARAM_LEN;
// 如果提供了随机数则使用,否则生成新的随机数
if (randnumx.data != NULL) {
randnumLen = randnumx.dataLen;
memcpy(randnum, randnumx.data, randnumx.dataLen);
} else {
ret = Spake2PlusInit(spakeCtx, randnum, &randnumLen);
// ... 错误处理
}
// 存储随机数x
spakeCtx->x.dataLen = randnumLen;
memcpy(spakeCtx->x.data, randnum, randnumLen);
uint8_t shareP[MAX_ECC_KEY_LEN] = { 0 };
uint32_t sharePLen = MAX_ECC_KEY_LEN;
// 计算X = x*G + w0*M
ret = Spake2PlusProverComputeX(spakeCtx, randnum, randnumLen, shareP, &sharePLen);
// ... 错误处理
// 存储并返回X
spakeCtx->share.dataLen = sharePLen;
memcpy(spakeCtx->share.data, shareP, sharePLen);
share->dataLen = sharePLen;
memcpy(share->data, shareP, sharePLen);
return ret;
}
其中 Spake2PlusProverComputeX 函数实现了椭圆曲线点的标量乘加运算:
static int32_t Spake2PlusProverComputeX(Spake2plusCtx* ctx, uint8_t *x, uint32_t xLen,
uint8_t *shareP, uint32_t *sharePLen)
{
// ... 参数检查
ECC_Para *para = ECC_NewPara(g_spake2PlusAlgInfo[ctx->index].curveId);
ECC_Point *X = ECC_NewPoint(para);
ECC_Point *m = ECC_NewPoint(para);
BN_BigNum *x0 = BN_Create(xLen * 8);
BN_BigNum *w0 = BN_Create(ctx->w0.dataLen * 8);
// ... 初始化检查
// 将x和w0转换为大数
ret = BN_Bin2Bn(x0, x, xLen);
// ... 错误处理
ret = BN_Bin2Bn(w0, ctx->w0.data, ctx->w0.dataLen);
// ... 错误处理
// 解码模糊点M
ret = ECC_DecodePoint(para, m, ctx->m.data, ctx->m.dataLen);
// ... 错误处理
// 计算X = x*G + w0*M
ret = ECC_PointMulAdd(para, X, x0, w0, m);
// ... 错误处理
// 将X编码为字节串
ret = ECC_EncodePoint(para, X, shareP, sharePLen, CRYPT_POINT_UNCOMPRESSED);
// ... 错误处理
// ... 清理资源
}
3.4.2 验证者响应(Verifier Setup)
- 验证者生成随机数 \(y \in_R [1, n-1]\)。
- 验证者计算 \(Y = y \cdot G + w_0 \cdot N\),其中 N 是另一个固定的模糊点。
- 验证者计算 \(Z = y \cdot (X - w_0 \cdot M) = y \cdot x \cdot G\)。
- 验证者计算 \(V = y \cdot L\)。
- 验证者计算转录 TT(包含所有上下文信息和交互数据)。
- 验证者使用 KDF 从 TT 派生确认密钥 \(K_{confirmP}, K_{confirmV}\) 和会话密钥 \(K_{shared}\)。
- 验证者计算 \(MacV = MAC(K_{confirmV}, X)\),并将 Y 和 MacV 发送给证明者。
在 openHiTLS 中,这一步由 HITLS_AUTH_Spake2plusRespSetup 函数实现:
int32_t HITLS_AUTH_Spake2plusRespSetup(HITLS_AUTH_PakeCtx *ctx, BSL_Buffer y, BSL_Buffer shareP,
BSL_Buffer *shareV, BSL_Buffer *confirmV)
{
// ... 参数检查
Spake2plusCtx *spakeCtx = (Spake2plusCtx *)HITLS_AUTH_PakeGetInternalCtx(ctx);
// ... 上下文检查
// ... 内存分配
// 生成或使用随机数y
if (y.data != NULL) {
randnumLen = y.dataLen;
memcpy(randnum, y.data, y.dataLen);
} else {
ret = Spake2PlusInit(spakeCtx, randnum, &randnumLen);
// ... 错误处理
}
// 计算Y = y*G + w0*N
uint8_t shareV0[MAX_ECC_KEY_LEN] = {0};
uint32_t shareV0Len = MAX_ECC_KEY_LEN;
ret = Spake2PlusVerifierComputeY(spakeCtx, randnum, randnumLen, shareV0, &shareV0Len);
// ... 错误处理
// 存储Y
shareV->dataLen = shareV0Len;
memcpy(shareV->data, shareV0, shareV0Len);
randnumBuffer.dataLen = randnumLen;
memcpy(randnumBuffer.data, randnum, randnumLen);
// 计算Z和V
ret = Spake2PlusVerifierFinish(spakeCtx, randnumBuffer, shareP, &zBuffer, &vBuffer);
// ... 错误处理
// 计算转录TT
uint32_t ttSize = 0;
ret = Spake2PlusComputeTranscript(ctx, shareP, *shareV, zBuffer, vBuffer, NULL, &ttSize);
// ... 错误处理
ttBuffer.data = BSL_SAL_Malloc(ttSize);
ttBuffer.dataLen = ttSize;
ret = Spake2PlusComputeTranscript(ctx, shareP, *shareV, zBuffer, vBuffer, &ttBuffer, NULL);
// ... 错误处理
// 派生密钥
ret = Spake2PlusComputeKeySchedule(spakeCtx, ttBuffer, &kConfirmPBuffer, &kConfirmVBuffer, &kSharedBuffer);
// ... 错误处理
// 存储会话密钥
spakeCtx->key_shared.dataLen = kSharedBuffer.dataLen;
memcpy(spakeCtx->key_shared.data, kSharedBuffer.data, kSharedBuffer.dataLen);
// 计算MacV = MAC(K_confirmV, X)
ret = Spake2PlusComputeExpectedConfirm(spakeCtx, kConfirmVBuffer, shareP, &outHmacBuffer);
// ... 错误处理
// 存储并返回MacV
spakeCtx->confirmV.dataLen = outHmacBuffer.dataLen;
memcpy(spakeCtx->confirmV.data, outHmacBuffer.data, outHmacBuffer.dataLen);
confirmV->dataLen = outHmacBuffer.dataLen;
memcpy(confirmV->data, outHmacBuffer.data, outHmacBuffer.dataLen);
// 计算MacP = MAC(K_confirmP, Y)(存储起来用于后续验证)
ret = Spake2PlusComputeExpectedConfirm(spakeCtx, kConfirmPBuffer, *shareV, &outHmacBuffer);
// ... 错误处理
spakeCtx->confirmP.dataLen = outHmacBuffer.dataLen;
memcpy(spakeCtx->confirmP.data, outHmacBuffer.data, outHmacBuffer.dataLen);
// ... 清理资源
}
3.4.3 证明者完成密钥交换(Prover Derive)
- 证明者计算 \(Z = x \cdot (Y - w_0 \cdot N) = x \cdot y \cdot G\)。
- 证明者计算 \(V = w_1 \cdot (Y - w_0 \cdot N) = w_1 \cdot y \cdot G = y \cdot L\)。
- 证明者计算相同的转录 TT。
- 证明者使用 KDF 派生 \(K_{confirmP}, K_{confirmV}\) 和 \(K_{shared}\)。
- 证明者验证 MacV 是否等于 \(MAC(K_{confirmV}, X)\)。
- 证明者计算 \(MacP = MAC(K_{confirmP}, Y)\),并将 MacP 发送给验证者。
在 openHiTLS 中,这一步由 HITLS_AUTH_Spake2plusReqDerive 函数实现:
int32_t HITLS_AUTH_Spake2plusReqDerive(HITLS_AUTH_PakeCtx *ctx, BSL_Buffer shareV, BSL_Buffer confirmV,
BSL_Buffer *confirmP, BSL_Buffer *out)
{
// ... 参数检查
Spake2plusCtx *spakeCtx = (Spake2plusCtx*)HITLS_AUTH_PakeGetInternalCtx(ctx);
// ... 上下文检查
// ... 内存分配
// 计算Z和V
ret = Spake2PlusProverFinish(spakeCtx, spakeCtx->x, shareV, &zBuffer, &vBuffer);
// ... 错误处理
// 计算转录TT
uint32_t ttSize = 0;
ret = Spake2PlusComputeTranscript(ctx, spakeCtx->share, shareV, zBuffer, vBuffer, NULL, &ttSize);
// ... 错误处理
ttBuffer.data = BSL_SAL_Malloc(ttSize);
ttBuffer.dataLen = ttSize;
ret = Spake2PlusComputeTranscript(ctx, spakeCtx->share, shareV, zBuffer, vBuffer, &ttBuffer, NULL);
// ... 错误处理
// 派生密钥
ret = Spake2PlusComputeKeySchedule(spakeCtx, ttBuffer, &kConfirmPBuffer, &kConfirmVBuffer, &kSharedBuffer);
// ... 错误处理
// 存储会话密钥
spakeCtx->key_shared.dataLen = kSharedBuffer.dataLen;
memcpy(spakeCtx->key_shared.data, kSharedBuffer.data, kSharedBuffer.dataLen);
// 计算MacP = MAC(K_confirmP, Y)
ret = Spake2PlusComputeExpectedConfirm(spakeCtx, kConfirmPBuffer, shareV, &outHmacBuffer);
// ... 错误处理
spakeCtx->confirmP.dataLen = outHmacBuffer.dataLen;
memcpy(spakeCtx->confirmP.data, outHmacBuffer.data, outHmacBuffer.dataLen);
confirmP->dataLen = outHmacBuffer.dataLen;
memcpy(confirmP->data, outHmacBuffer.data, outHmacBuffer.dataLen);
// 验证MacV
ret = Spake2PlusComputeExpectedConfirm(spakeCtx, kConfirmVBuffer, spakeCtx->share, &outHmacBuffer);
// ... 错误处理
spakeCtx->confirmV.dataLen = outHmacBuffer.dataLen;
memcpy(spakeCtx->confirmV.data, outHmacBuffer.data, outHmacBuffer.dataLen);
// 注意:这里代码中有个逻辑问题,应该是比较相等才对,原代码中是==0就报错,可能是笔误
if (spakeCtx->confirmV.dataLen != confirmV.dataLen ||
ConstTimeMemcmp(spakeCtx->confirmV.data, confirmV.data, confirmV.dataLen) == 0) {
BSL_ERR_PUSH_ERROR(HITLS_AUTH_PAKE_INVALID_PARAM);
ret = HITLS_AUTH_PAKE_INVALID_PARAM;
goto err;
}
// 返回会话密钥
out->dataLen = kSharedBuffer.dataLen;
memcpy(out->data, kSharedBuffer.data, kSharedBuffer.dataLen);
// ... 清理资源
}
3.4.4 验证者确认密钥(Verifier Derive)
- 验证者验证 MacP 是否等于 \(MAC(K_{confirmP}, Y)\)。
- 如果验证通过,双方确认 \(K_{shared}\) 为会话密钥。
在 openHiTLS 中,这一步由 HITLS_AUTH_Spake2plusRespDerive 函数实现:
int32_t HITLS_AUTH_Spake2plusRespDerive(HITLS_AUTH_PakeCtx *ctx, BSL_Buffer confirmP, BSL_Buffer *out)
{
// ... 参数检查
Spake2plusCtx *spakeCtx = (Spake2plusCtx *)HITLS_AUTH_PakeGetInternalCtx(ctx);
// ... 上下文检查
// 验证MacP(同样注意这里的逻辑可能需要根据实际情况调整)
if (spakeCtx->confirmP.dataLen != confirmP.dataLen ||
ConstTimeMemcmp(spakeCtx->confirmP.data, confirmP.data, confirmP.dataLen) == 0) {
Spake2PlusFreeCtx(spakeCtx);
BSL_ERR_PUSH_ERROR(HITLS_AUTH_PAKE_INVALID_PARAM);
return HITLS_AUTH_PAKE_INVALID_PARAM;
}
// 返回会话密钥
out->dataLen = spakeCtx->key_shared.dataLen;
memcpy(out->data, spakeCtx->key_shared.data, spakeCtx->key_shared.dataLen);
return HITLS_AUTH_SUCCESS;
}
4. openHiTLS 中 SPAKE2 + 的实现细节
4.1 数据结构
openHiTLS 中定义了几个关键的数据结构来管理 SPAKE2 + 协议的状态:
4.1.1 Spake2plusCtx
这是 SPAKE2 + 的主上下文结构,存储了协议执行过程中的所有中间状态:
typedef struct Spake2plusCtx {
uint8_t index; // 密码套件索引
BSL_Buffer w0; // 密码派生的w0
BSL_Buffer w1; // 密码派生的w1
BSL_Buffer l; // 验证数据L = w1*G
BSL_Buffer x; // 证明者的随机数x
BSL_Buffer share; // 交换的共享值(X或Y)
BSL_Buffer key_shared; // 协商的会话密钥
BSL_Buffer confirmP; // 证明者的确认MAC
BSL_Buffer confirmV; // 验证者的确认MAC
BSL_Buffer m; // 模糊点M
BSL_Buffer n; // 模糊点N
} Spake2plusCtx;
4.1.2 Spake2Plus_AlgInfo
这个结构定义了 SPAKE2 + 支持的密码套件,包括椭圆曲线、哈希函数、KDF、MAC 等参数:
typedef struct {
CRYPT_PKEY_ParaId curveId; // 椭圆曲线ID
CRYPT_MD_AlgId hashId; // 哈希函数ID
CRYPT_KDF_HKDF_AlgId kdfId; // KDF算法ID
CRYPT_MAC_AlgId macId; // MAC算法ID
uint16_t hashKeyLen; // 哈希输出长度
uint16_t macKeyLen; // MAC密钥长度
} Spake2Plus_AlgInfo;
4.2 内存管理
SPAKE2 + 协议涉及大量的敏感数据(如密码派生值、随机数、会话密钥等),因此内存管理至关重要。openHiTLS 实现了安全的内存分配和清理函数:
4.2.1 上下文创建与释放
Spake2plusCtx* Spake2PlusNewCtx(CRYPT_PKEY_ParaId curve)
{
Spake2plusCtx *ctx = (Spake2plusCtx*)BSL_SAL_Calloc(1, sizeof(Spake2plusCtx));
// ... 内存分配检查
// 为各个缓冲区分配内存
ctx->w0 = (BSL_Buffer){.data = BSL_SAL_Malloc(MAX_ECC_PARAM_LEN), .dataLen = MAX_ECC_PARAM_LEN};
ctx->w1 = (BSL_Buffer){.data = BSL_SAL_Malloc(MAX_ECC_PARAM_LEN), .dataLen = MAX_ECC_PARAM_LEN};
ctx->l = (BSL_Buffer){.data = BSL_SAL_Malloc(MAX_ECC_KEY_LEN), .dataLen = MAX_ECC_KEY_LEN};
// ... 其他缓冲区分配
// 内存分配失败处理
if (ctx->w0.data == NULL || ctx->w1.data == NULL || ctx->l.data == NULL || ctx->x.data == NULL ||
ctx->key_shared.data == NULL || ctx->confirmP.data == NULL || ctx->confirmV.data == NULL) {
BSL_ERR_PUSH_ERROR(HITLS_AUTH_MEM_ALLOC_FAIL);
goto err;
}
// 根据曲线设置模糊点M和N
if (curve == CRYPT_ECC_NISTP256) {
ctx->m = (BSL_Buffer){.data = ECC_NISTP256_M, .dataLen = sizeof(ECC_NISTP256_M)};
ctx->n = (BSL_Buffer){.data = ECC_NISTP256_N, .dataLen= sizeof(ECC_NISTP256_N)};
}
// ... 其他曲线的M和N设置
return ctx;
err:
Spake2PlusFreeCtx(ctx);
return NULL;
}
void Spake2PlusFreeCtx(Spake2plusCtx *ctx)
{
if (ctx == NULL) {
return;
}
// 清理并释放所有敏感数据
BSL_SAL_ClearFree(ctx->w0.data, ctx->w0.dataLen);
BSL_SAL_ClearFree(ctx->w1.data, ctx->w1.dataLen);
BSL_SAL_ClearFree(ctx->l.data, ctx->l.dataLen);
BSL_SAL_ClearFree(ctx->x.data, ctx->x.dataLen);
BSL_SAL_ClearFree(ctx->share.data, ctx->share.dataLen);
BSL_SAL_ClearFree(ctx->key_shared.data, ctx->key_shared.dataLen);
BSL_SAL_ClearFree(ctx->confirmP.data, ctx->confirmP.dataLen);
BSL_SAL_ClearFree(ctx->confirmV.data, ctx->confirmV.dataLen);
BSL_SAL_ClearFree(ctx, sizeof(Spake2plusCtx));
}
注意这里使用了 BSL_SAL_ClearFree 函数,它会在释放内存前先清理内存中的敏感数据,防止数据泄露。
4.3 转录计算(Transcript)
转录(Transcript)是 SPAKE2 + 协议中的一个重要概念,它将所有上下文信息和交互数据拼接在一起,作为密钥派生的输入。这样可以确保双方在相同的上下文中派生密钥。
static int32_t Spake2PlusComputeTranscript(HITLS_AUTH_PakeCtx *ctx, BSL_Buffer shareP, BSL_Buffer shareV,
BSL_Buffer z, BSL_Buffer v, BSL_Buffer *tt, uint32_t *totalSize)
{
// ... 参数检查
uint8_t *pos = tt ? tt->data : NULL;
size_t remaining = tt ? tt->dataLen : 0;
// ... 获取模糊点M和N的公钥数据
BSL_Buffer prover = HITLS_AUTH_PakeGetProver(ctx);
BSL_Buffer verifier = HITLS_AUTH_PakeGetVerifier(ctx);
BSL_Buffer context = HITLS_AUTH_PakeGetContext(ctx);
// 计算转录的总长度
if (totalSize) {
*totalSize = 8 + context.dataLen + 8 + prover.dataLen + 8 + verifier.dataLen + 8 + mLen + 8 + nLen +
8 + shareP.dataLen + 8 + shareV.dataLen + 8 + z.dataLen + 8 + v.dataLen + 8 + spakeCtx->w0.dataLen;
}
// 如果提供了tt缓冲区,则填充转录数据
if (tt) {
// 使用APPEND_FIELD宏将每个字段以"长度+数据"的格式追加到转录中
ret = APPEND_FIELD(context.data, context.dataLen);
// ... 错误处理
ret = APPEND_FIELD(prover.data, prover.dataLen);
// ... 错误处理
ret = APPEND_FIELD(verifier.data, verifier.dataLen);
// ... 错误处理
ret = APPEND_FIELD(m, mLen);
// ... 错误处理
ret = APPEND_FIELD(n, nLen);
// ... 错误处理
ret = APPEND_FIELD(shareP.data, shareP.dataLen);
// ... 错误处理
ret = APPEND_FIELD(shareV.data, shareV.dataLen);
// ... 错误处理
ret = APPEND_FIELD(z.data, z.dataLen);
// ... 错误处理
ret = APPEND_FIELD(v.data, v.dataLen);
// ... 错误处理
ret = APPEND_FIELD(spakeCtx->w0.data, spakeCtx->w0.dataLen);
// ... 错误处理
tt->dataLen = pos - tt->data;
}
// ... 清理资源
}
其中 APPEND_FIELD 是一个辅助宏,用于将数据以小端长度前缀的格式追加到转录中:
static void uint32_to_le_bytes(uint32_t len, uint8_t out[8])
{
out[0] = (uint8_t)(len & 0xFF);
out[1] = (uint8_t)((len >> 8) & 0xFF);
out[2] = (uint8_t)((len >> 16) & 0xFF);
out[3] = (uint8_t)((len >> 24) & 0xFF);
out[4] = out[5] = out[6] = out[7] = 0;
}
#define APPEND_FIELD(data, len) ({ \
int32_t __ret = HITLS_AUTH_SUCCESS; \
uint8_t len_bytes[8]; \
uint32_to_le_bytes(len, len_bytes); \
if (remaining < 8 + len) { \
__ret = CRYPT_MEM_ALLOC_FAIL; \
} else { \
memcpy(pos, len_bytes, 8); \
pos += 8; remaining -= 8; \
memcpy(pos, data, len); \
pos += len; remaining -= len; \
} \
__ret; \
})
4.4 密钥派生
密钥派生阶段使用 HKDF 从转录中派生出确认密钥和会话密钥:
static int32_t Spake2PlusComputeKeySchedule(Spake2plusCtx *ctx, BSL_Buffer tt, BSL_Buffer *kConfirmP,
BSL_Buffer *kConfirmV, BSL_Buffer *kShared)
{
// ... 参数检查
// 第一步:计算K_main = Hash(TT)
uint8_t kMain[g_spake2PlusAlgInfo[ctx->index].hashKeyLen];
uint32_t kMainLen = g_spake2PlusAlgInfo[ctx->index].hashKeyLen;
int32_t ret = CRYPT_EAL_Md(g_spake2PlusAlgInfo[ctx->index].hashId, tt.data, tt.dataLen, kMain, &kMainLen);
// ... 错误处理
// 选择MAC算法
CRYPT_MAC_AlgId macId;
if (g_spake2PlusAlgInfo[ctx->index].kdfId == CRYPT_HKDF_SHA256) {
macId=CRYPT_MAC_HMAC_SHA256;
}
if (g_spake2PlusAlgInfo[ctx->index].kdfId == CRYPT_HKDF_SHA512) {
macId=CRYPT_MAC_HMAC_SHA512;
}
// 创建HKDF上下文
CRYPT_EAL_KdfCtx *kdfCtx = CRYPT_EAL_KdfNewCtx(CRYPT_KDF_HKDF);
// ... 错误处理
CRYPT_HKDF_MODE mode = CRYPT_KDF_HKDF_MODE_FULL;
uint8_t *salt = NULL;
uint32_t saltLen = 0;
uint8_t *info = (uint8_t*)"ConfirmationKeys"; // 来自RFC 9383第3.4节
uint32_t infoLen = strlen("ConfirmationKeys");
uint8_t out[g_spake2PlusAlgInfo[ctx->index].macKeyLen * 2];
uint32_t outLen = g_spake2PlusAlgInfo[ctx->index].macKeyLen * 2;
// 设置HKDF参数
BSL_Param params[6] = {{0}, {0}, {0}, {0}, {0}, BSL_PARAM_END};
ret = BSL_PARAM_InitValue(¶ms[0], CRYPT_PARAM_KDF_MAC_ID, BSL_PARAM_TYPE_UINT32, &macId, sizeof(macId));
// ... 错误处理
ret = BSL_PARAM_InitValue(¶ms[1], CRYPT_PARAM_KDF_MODE, BSL_PARAM_TYPE_UINT32, &mode, sizeof(mode));
// ... 错误处理
ret = BSL_PARAM_InitValue(¶ms[2], CRYPT_PARAM_KDF_KEY, BSL_PARAM_TYPE_OCTETS, kMain, kMainLen);
// ... 错误处理
ret = BSL_PARAM_InitValue(¶ms[3], CRYPT_PARAM_KDF_SALT, BSL_PARAM_TYPE_OCTETS, salt, saltLen);
// ... 错误处理
ret = BSL_PARAM_InitValue(¶ms[4], CRYPT_PARAM_KDF_INFO, BSL_PARAM_TYPE_OCTETS, info, infoLen);
// ... 错误处理
// 派生确认密钥:K_confirmP || K_confirmV = HKDF(K_main, "ConfirmationKeys")
ret = CRYPT_EAL_KdfSetParam(kdfCtx, params);
// ... 错误处理
ret = CRYPT_EAL_KdfDerive(kdfCtx, out, outLen);
// ... 错误处理
memcpy(kConfirmP->data, out, g_spake2PlusAlgInfo[ctx->index].macKeyLen);
memcpy(kConfirmV->data, out + g_spake2PlusAlgInfo[ctx->index].macKeyLen, g_spake2PlusAlgInfo[ctx->index].macKeyLen);
kConfirmP->dataLen = g_spake2PlusAlgInfo[ctx->index].macKeyLen;
kConfirmV->dataLen = g_spake2PlusAlgInfo[ctx->index].macKeyLen;
// 派生会话密钥:K_shared = HKDF(K_main, "SharedKey")
uint8_t *info0 = (uint8_t*)"SharedKey";
uint32_t info0Len = strlen("SharedKey");
ret = BSL_PARAM_InitValue(¶ms[4], CRYPT_PARAM_KDF_INFO, BSL_PARAM_TYPE_OCTETS, info0, info0Len);
// ... 错误处理
ret = CRYPT_EAL_KdfSetParam(kdfCtx, params);
// ... 错误处理
ret = CRYPT_EAL_KdfDerive(kdfCtx, out0, out0Len);
// ... 错误处理
memcpy(kShared->data, out0, out0Len);
kShared->dataLen = out0Len;
// 清理敏感数据
BSL_SAL_CleanseData(kMain, sizeof(kMain));
BSL_SAL_CleanseData(out, sizeof(out));
BSL_SAL_CleanseData(out0, sizeof(out0));
// ... 清理资源
}
5. 安全分析与最佳实践
5.1 SPAKE2 + 的安全特性
SPAKE2 + 协议具有以下重要的安全特性:
- 抵抗离线字典攻击:攻击者即使截获了所有交互消息,也无法离线进行字典攻击来猜测密码,因为每个消息都与随机数绑定。
- 抵抗中间人攻击:由于双方都需要证明知道密码,中间人无法在不知道密码的情况下完成协议。
- 前向安全性:即使密码泄露,之前协商的会话密钥仍然是安全的,因为会话密钥依赖于临时随机数。
- 服务器妥协抵抗:服务器存储的是验证数据 \((w_0, L)\) 而不是密码本身,即使服务器数据泄露,攻击者也需要进行在线字典攻击才能冒充用户。
5.2 实现中的安全考虑
在 openHiTLS 的实现中,我们可以看到以下安全最佳实践:
- 敏感数据清理:使用
BSL_SAL_ClearFree等函数在释放内存前清理敏感数据,防止数据通过内存泄露。 - 常量时间比较:使用
ConstTimeMemcmp进行 MAC 验证,防止时序攻击。 - 安全的随机数生成:使用密码学安全的随机数生成器(
CRYPT_EAL_RandbytesEx)生成临时随机数。 - 错误处理:在发生错误时,及时清理已分配的资源和敏感数据。
5.3 使用建议
在使用 SPAKE2 + 协议时,还应注意以下几点:
- 选择合适的密码套件:根据安全需求选择合适的椭圆曲线(如 P-256 或 P-384)和哈希函数(如 SHA-256 或 SHA-512)。
- 保护验证数据:服务器应妥善存储验证数据 \((w_0, L)\),防止未授权访问。
- 限制尝试次数:为了防止在线字典攻击,服务器应限制用户的认证尝试次数。
- 使用安全的密码:虽然 SPAKE2 + 能抵抗离线字典攻击,但使用强密码仍然是重要的。
6. 总结
SPAKE2 + 是一种优秀的密码认证密钥交换协议,它结合了椭圆曲线密码学的高效性和 PAKE 协议的安全性,能够在不安全的信道上安全地完成身份认证和密钥协商。
本文深入剖析了 SPAKE2 + 协议的数学基础、协议流程,并结合 openHiTLS 的实现代码,详细讲解了各个模块的功能和实现细节。通过本文的学习,读者应该能够理解 SPAKE2 + 协议的工作原理,并能够在实际项目中正确使用这一协议。
SPAKE2 + 协议在现代网络安全中有着广泛的应用前景,特别是在需要无证书认证的场景中,如物联网设备认证、移动应用登录等。随着密码学技术的不断发展,我们相信会有更多高效、安全的 PAKE 协议被设计出来,为网络通信提供更强的安全保障。
参考文献:
- RFC 9383: SPAKE2, a Simple Password-Authenticated Key Exchange Protocol
- openHiTLS 开源项目代码
- 椭圆曲线密码学相关教材和标准
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)