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 注册阶段

注册阶段的目标是让验证者存储密码的验证数据,而不存储密码本身。具体步骤如下:

  1. 证明者选择密码 pw。
  2. 证明者计算 \(w_0 = H_0(pw) \mod n\),\(w_1 = H_1(pw) \mod n\),其中 \(H_0, H_1\) 是两个不同的哈希函数(或从同一哈希函数派生)。
  3. 证明者计算 \(L = w_1 \cdot G\)(即 \(w_1\) 与生成元 G 的标量乘)。
  4. 证明者将 \((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)
  1. 证明者生成随机数 \(x \in_R [1, n-1]\)。
  2. 证明者计算 \(X = x \cdot G + w_0 \cdot M\),其中 M 是固定的模糊点。
  3. 证明者将 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)
  1. 验证者生成随机数 \(y \in_R [1, n-1]\)。
  2. 验证者计算 \(Y = y \cdot G + w_0 \cdot N\),其中 N 是另一个固定的模糊点。
  3. 验证者计算 \(Z = y \cdot (X - w_0 \cdot M) = y \cdot x \cdot G\)。
  4. 验证者计算 \(V = y \cdot L\)。
  5. 验证者计算转录 TT(包含所有上下文信息和交互数据)。
  6. 验证者使用 KDF 从 TT 派生确认密钥 \(K_{confirmP}, K_{confirmV}\) 和会话密钥 \(K_{shared}\)。
  7. 验证者计算 \(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)
  1. 证明者计算 \(Z = x \cdot (Y - w_0 \cdot N) = x \cdot y \cdot G\)。
  2. 证明者计算 \(V = w_1 \cdot (Y - w_0 \cdot N) = w_1 \cdot y \cdot G = y \cdot L\)。
  3. 证明者计算相同的转录 TT。
  4. 证明者使用 KDF 派生 \(K_{confirmP}, K_{confirmV}\) 和 \(K_{shared}\)。
  5. 证明者验证 MacV 是否等于 \(MAC(K_{confirmV}, X)\)。
  6. 证明者计算 \(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)
  1. 验证者验证 MacP 是否等于 \(MAC(K_{confirmP}, Y)\)。
  2. 如果验证通过,双方确认 \(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(&params[0], CRYPT_PARAM_KDF_MAC_ID, BSL_PARAM_TYPE_UINT32, &macId, sizeof(macId));
    // ... 错误处理
    ret = BSL_PARAM_InitValue(&params[1], CRYPT_PARAM_KDF_MODE, BSL_PARAM_TYPE_UINT32, &mode, sizeof(mode));
    // ... 错误处理
    ret = BSL_PARAM_InitValue(&params[2], CRYPT_PARAM_KDF_KEY, BSL_PARAM_TYPE_OCTETS, kMain, kMainLen);
    // ... 错误处理
    ret = BSL_PARAM_InitValue(&params[3], CRYPT_PARAM_KDF_SALT, BSL_PARAM_TYPE_OCTETS, salt, saltLen);
    // ... 错误处理
    ret = BSL_PARAM_InitValue(&params[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(&params[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 + 协议具有以下重要的安全特性:

  1. 抵抗离线字典攻击:攻击者即使截获了所有交互消息,也无法离线进行字典攻击来猜测密码,因为每个消息都与随机数绑定。
  2. 抵抗中间人攻击:由于双方都需要证明知道密码,中间人无法在不知道密码的情况下完成协议。
  3. 前向安全性:即使密码泄露,之前协商的会话密钥仍然是安全的,因为会话密钥依赖于临时随机数。
  4. 服务器妥协抵抗:服务器存储的是验证数据 \((w_0, L)\) 而不是密码本身,即使服务器数据泄露,攻击者也需要进行在线字典攻击才能冒充用户。

5.2 实现中的安全考虑

在 openHiTLS 的实现中,我们可以看到以下安全最佳实践:

  1. 敏感数据清理:使用 BSL_SAL_ClearFree 等函数在释放内存前清理敏感数据,防止数据通过内存泄露。
  2. 常量时间比较:使用 ConstTimeMemcmp 进行 MAC 验证,防止时序攻击。
  3. 安全的随机数生成:使用密码学安全的随机数生成器(CRYPT_EAL_RandbytesEx)生成临时随机数。
  4. 错误处理:在发生错误时,及时清理已分配的资源和敏感数据。

5.3 使用建议

在使用 SPAKE2 + 协议时,还应注意以下几点:

  1. 选择合适的密码套件:根据安全需求选择合适的椭圆曲线(如 P-256 或 P-384)和哈希函数(如 SHA-256 或 SHA-512)。
  2. 保护验证数据:服务器应妥善存储验证数据 \((w_0, L)\),防止未授权访问。
  3. 限制尝试次数:为了防止在线字典攻击,服务器应限制用户的认证尝试次数。
  4. 使用安全的密码:虽然 SPAKE2 + 能抵抗离线字典攻击,但使用强密码仍然是重要的。

6. 总结

SPAKE2 + 是一种优秀的密码认证密钥交换协议,它结合了椭圆曲线密码学的高效性和 PAKE 协议的安全性,能够在不安全的信道上安全地完成身份认证和密钥协商。

本文深入剖析了 SPAKE2 + 协议的数学基础、协议流程,并结合 openHiTLS 的实现代码,详细讲解了各个模块的功能和实现细节。通过本文的学习,读者应该能够理解 SPAKE2 + 协议的工作原理,并能够在实际项目中正确使用这一协议。

SPAKE2 + 协议在现代网络安全中有着广泛的应用前景,特别是在需要无证书认证的场景中,如物联网设备认证、移动应用登录等。随着密码学技术的不断发展,我们相信会有更多高效、安全的 PAKE 协议被设计出来,为网络通信提供更强的安全保障。


参考文献

  • RFC 9383: SPAKE2, a Simple Password-Authenticated Key Exchange Protocol
  • openHiTLS 开源项目代码
  • 椭圆曲线密码学相关教材和标准
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐