EC-SPEKE协商流程全景解析——四步握手与安全密钥派生
一、EC-SPEKE在OpenHarmony统一互联中的角色
OpenHarmony统一互联的安全底座分为两个层级。富设备(手机、平板)通过分布式软总线(DSoftBus)认证模块,基于HiChain框架运行PAKE协议族完成设备间安全认证。瘦设备(如CH585 BLE SoC)资源受限,无法承载HiChain的完整状态机和protobuf报文,转而通过 iot_connect_sdk 内置的EC-SPEKE协议实现轻量级口令密钥协商。
EC-SPEKE(Elliptic Curve Simple Password Exponential Key Exchange)的核心思想:将用户口令通过Elligator2算法映射为Curve25519椭圆曲线上的生成元,双方各生成随机私钥,通过Diffie-Hellman交换协商共享密钥,再经HKDF派生出会话密钥和数据加密密钥。与DSoftBus的PAKE相比,EC-SPEKE报文更小(JSON格式)、计算开销更低(单次X25519标量乘),适合在128KB RAM的MCU上运行。
协议的完整实现封装在 iot_connect_sdk 预编译库 libiotc.a 中,源码路径为 foundation/communication/iot_connect/core/infrastructure/security/speke/。移植层只需提供三个安全输入——PINCODE口令、AC_KEY密钥种子、TRNG随机数源——以及mbedTLS加密适配,协议逻辑本身由SDK内部完成。
二、四步协商报文体系
四步协商报文标识符定义于 security_speke_defs.h:spekeMsg1、spekeMsg2、spekeMsg3、spekeMsg4,分别对应 SPEKE_MSG_ID_REQ(0x0001)、SPEKE_MSG_ID_RSP(0x8001)、SPEKE_MSG_ID_CLIENT_CFM(0x0002)、SPEKE_MSG_ID_SERVER_CFM(0x8002)。SpekeProcessPacket(定义于 security_speke_session.c)作为消息分发器,通过 strcmp 匹配 msgId 字段,将报文路由到对应的处理函数:
| 报文 | msgId | 方向 | 处理函数 | JSON关键字段 |
|---|---|---|---|---|
| Msg1 | spekeMsg1 | App→设备 | SpekeServerProcessReq | version |
| Msg2 | spekeMsg2 | 设备→App | SpekeClientProcessRsp | challenge1, salt, pk1, iterations |
| Msg3 | spekeMsg3 | App→设备 | SpekeServerProcessCfm | challenge2, pk2, kcfData2 |
| Msg4 | spekeMsg4 | 设备→App | SpekeClientProcessCfm | kcfData1 |
协议版本号校验由 SpekeCommonVerifyVersion 完成,期望值为 "2.0"。所有报文通过JSON格式封装,使用cJSON库(libcjson_static.a)进行序列化与反序列化,经BLE GATT通道传输。
三、四步协商完整时序
┌──────────┐ ┌──────────┐
│ 设备端 │ SpekeServer* │ App端 │ SpekeClient*
│ (CH585) │ SPEKE_TYPE_SERVER │(OH富设备) │ SPEKE_TYPE_CLIENT
└────┬─────┘ └────┬─────┘
│ │
│ 【会话初始化 - Server端预初始化】 │ 【会话初始化 - Client端】
│ SpekeInitSession() │ SpekeInitSession()
│ · GetPincode() → pinCode (8字节) │ · GetPincode() → pinCode (8字节)
│ · SecurityRandom() → salt (16B) │ (仅获取pinCode,不初始化negoContext)
│ · NegoContextInit() ← 预初始化协商上下文 │
│ · NegoGenPbkdf2SecretV2 (SHA-512, 96B) │
│ · IotcEcpElligator2Curve25519() │
│ · GenNegoKeyPair() → localSk, pubKey │
│ │
│◄══════ spekeMsg1 (Req) ══════════════════════│
│ {version:"2.0"} │
│ │
│ SpekeServerProcessReq() │
│ · SpekeCommonVerifyVersion("2.0") │
│ · 使用预初始化的negoContext │
│ · 组装 challenge1, salt, pk1, iterations │
│ │
│══════ spekeMsg2 (Rsp) ══════════════════════►│
│ {challenge1, salt, pk1, iterations} │
│ │
│ │ SpekeClientProcessRsp()
│ │ · 解析 challenge1, salt, pk1, iterations
│ │ · NegoContextInit() — 生成本地challenge2,
│ │ pk2, 椭圆曲线基点
│ │ · NegoContextSetRemoteChallenge(challenge1)
│ │ · NegoContextGenSessionKey()
│ │ └→ IotcEcpX25519Compute(localSk, pk1)
│ │ └→ IotcHkdf(salt, info=sessionkey_info)
│ │ → identityEncKey(16B)+hmacKey(16B)
│ │ · NegoContextGenHmac() → kcfData2
│ │
│◄══════ spekeMsg3 (Cfm) ══════════════════════│
│ {challenge2, pk2, kcfData2} │
│ │
│ SpekeServerProcessCfm() │
│ · 解析 challenge2, pk2, kcfData2 │
│ · NegoContextSetRemoteChallenge(challenge2)│
│ · NegoContextGenSessionKey(pk2) │
│ · NegoContextVerifyHmac(kcfData2) │
│ · NegoContextGenHmac() → kcfData1 │
│ · NegoContextGenDataEncKey() │
│ · NotifyNegoFinish() │
│ │
│══════ spekeMsg4 (Cfm) ══════════════════════►│
│ {kcfData1} │
│ │
│ │ SpekeClientProcessCfm()
│ │ · 解析 kcfData1
│ │ · NegoContextVerifyHmac(kcfData1)
│ │ · NegoContextGenDataEncKey()
│ │ └→ IotcHkdf(identityEncKey) → dataEncKey
│ │ · NotifyNegoFinish()
│ │
│ ══════ 协商完成,密钥已派生 ══════ │
│ │
│ 事件: IOTC_BLE_SPEKE_NEGOTIATION_SUCCESS │
│ │
│ 后续: LinkLayerSetEncryptType(SPEKE) │
│ 后续: SpekeEncryptData / SpekeDecryptData │
└───────────────────────────────────────────────┘
四、会话初始化与基点生成
SpekeInitSession(定义于 security_speke_session.c)是协商的起点,其行为随角色不同而分化:Client端(App)仅通过 GetPincode 回调获取8字节口令,不初始化协商上下文;Server端(设备)在获取口令后,立即调用 SecurityRandom 生成16字节随机 salt,并调用 NegoContextInit 预初始化协商上下文。也就是说,设备的 negoContext 在 SpekeInitSession 阶段就已就绪,而非等到收到Msg1时才创建。
NegoContextInit(定义于 security_speke_nego_ctx.c)是最关键的初始化函数,负责完成口令到椭圆曲线基点的映射和密钥对生成。其内部调用链如下:
NegoContextInit
├── NegoGenPbkdf2SecretV2
│ └── IotcPkcs5Pbkdf2Hmac(pinCode, salt‖SPEKE_BASE_INFO_V2, iterations, IOTC_MD_SHA512)
│ → 派生96字节口令密钥,拆分为 u[0]、u[1] 两个48字节半
├── IotcEcpHashToPointX25519
│ ├── SHA-512 哈希
│ ├── IotcEcpElligator2Curve25519 — Elligator2 hash-to-curve
│ │ → 使用 u[0], u[1] 两个候选点
│ ├── IotcEcpPointAdd — 点加法
│ └── IotcEcpClearCofactor — 清除余因子
│ → 生成基点 G (Curve25519上的点, 32B)
├── GenNegoKeyPair
│ ├── SecurityRandom() → localSk (私钥, 32B, clamped)
│ └── IotcEcpX25519Compute(localSk, G) → pubKey (公钥, 32B)
└── 保存 salt, localChallenge, localSk, pubKey, base 到 NegoContext
其中 NegoGenPbkdf2SecretV2 使用 IOTC_MD_SHA512(SHA-512)作为PBKDF2的底层哈希,输出96字节,并划分为 u[0] 与 u[1] 两个48字节半,分别对应Elligator2映射的两个候选点。PBKDF2的salt并非裸 context->salt,而是 context->salt 与固定字符串 SPEKE_BASE_INFO_V2 拼接而成,该字符串取值为 "ohos_connect_speke_base_info_curve25519_XMD:SHA-512_ELL2_RO_",直接编码了曲线类型、哈希算法和映射方法,体现了OpenHarmony统一互联对SPEKE协议的定制化定义。
NegoContext 结构(定义于 security_speke_nego_ctx.h)承载了协商全过程的中间状态:
typedef struct {
uint8_t salt[16];
uint32_t saltLen;
uint8_t *pubKey; // 32B公钥
uint8_t *localSk; // 32B私钥
uint8_t *base; // 32B基点
uint8_t localChallenge[16];
uint8_t remoteChallenge[16];
uint8_t identityEncKey[16];
uint8_t hmacKey[16];
uint32_t iterations;
} NegoContext;
五、会话密钥派生与HMAC双向验证
5.1 共享密钥计算
设备端在收到Msg3后,SpekeServerProcessCfm(定义于 security_speke_server.c)解析出App端的公钥 pk2 和挑战值 challenge2,先调用 NegoContextSetRemoteChallenge(challenge2),再调用 NegoContextGenSessionKey(定义于 security_speke_nego_ctx.c):
NegoContextGenSessionKey
├── IotcEcpX25519Compute(localSk, pk2)
│ → 共享密钥 S = localSk · pk2 = localSk · clientSk · G
└── IotcHkdf(S, salt=context->salt, info="ohos_connect_speke_sessionkey_info", IOTC_MD_SHA256)
→ 32字节会话密钥,拆分为 identityEncKey(16B) + hmacKey(16B)
HKDF使用HMAC-SHA256作为底层PRF,输出的32字节会话密钥被拆分为两部分:前16字节 identityEncKey 用于身份加密,后16字节 hmacKey 用于后续HMAC验证。X25519标量乘法是整个协商中计算量最大的操作,CH585纯软件实现约需200ms。
5.2 HMAC双向验证
会话密钥派生后,双方使用 hmacKey 通过 GenNegoHmac(定义于 security_speke_nego_ctx.c)计算HMAC确认数据。GenNegoHmac 使用 IOTC_MD_SHA256(SHA-256)算法,key为16字节 hmacKey,data为两个挑战值的拼接。注意生成与验证时挑战值顺序相反:
NegoContextGenHmac(生成):data = challenge1 ‖ challenge2NegoContextVerifyHmac(验证):data = challenge2 ‖ challenge1(顺序反转)
HMAC输出长度 HMAC_LEN 为32字节。设备端通过 NegoContextVerifyHmac 验证App端在Msg3中发来的 kcfData2,验证通过后通过 NegoContextGenHmac 生成自己的 kcfData1,包含在Msg4中发送给App端。App端收到Msg4后同样通过 NegoContextVerifyHmac 验证 kcfData1。
| 步骤 | 生成方 | 验证方 | HMAC数据 | 验证内容 |
|---|---|---|---|---|
| Msg3 | App端 | 设备端 | kcfData2 | App确实持有相同共享密钥 |
| Msg4 | 设备端 | App端 | kcfData1 | 设备确实持有相同共享密钥 |
HMAC验证失败会返回错误码 IOTC_CORE_COMM_SEC_ERR_SPEKE_VERIFY_HMAC,表示协商过程中可能存在中间人篡改或密钥不一致。
5.3 数据加密密钥派生
设备端生成 kcfData1 并发送Msg4后,调用 NegoContextGenDataEncKey(定义于 security_speke_nego_ctx.c)从会话密钥派生独立的数据加密密钥:
NegoContextGenDataEncKey
└── IotcHkdf(identityEncKey, salt=context->salt, info="ohos_speke_session_key", IOTC_MD_SHA256)
→ dataEncKey (数据加密密钥)
HKDF的输入材料为16字节 identityEncKey(而非整个会话密钥),info为 "ohos_speke_session_key",salt仍为 context->salt。密钥分离设计确保数据加密密钥的泄露不影响会话密钥安全性。此后 SpekeEncryptData / SpekeDecryptData(定义于 security_speke_session.c)使用该密钥进行AES-GCM加解密(IotcAesGcmEncrypt / IotcAesGcmDecrypt),其中 GCM_IV_LEN=12、GCM_TAG_LEN=16、GCM_VER_LEN=1,保护配网数据的机密性和完整性。
六、链路层加密集成
协商完成后,NotifyNegoFinish 触发 IOTC_BLE_SPEKE_NEGOTIATION_SUCCESS 事件,链路层加密调度模块切换到SPEKE加密模式:
/* 链路层加密类型 */
typedef enum {
LINKLAYER_ENCRYPT_TYPE_NONE, /* 无加密 */
LINKLAYER_ENCRYPT_TYPE_SPEKE, /* SPEKE协商加密 */
LINKLAYER_ENCRYPT_TYPE_SESSKEY, /* 会话密钥加密 */
} LinkLayerEncryptType;
LinkLayerSetEncryptType(SPEKE)
→ 设置全局变量 g_encryptType = LINKLAYER_ENCRYPT_TYPE_SPEKE
LinkLayerRegisterSpekeSessionGetCb(callback)
→ 注册全局回调 g_spekeSessionGetCb
数据发送:
LinkLayerEncryptData(plaintext)
→ if (g_encryptType == SPEKE)
LinkLayerSpekeEncrypt(plaintext)
→ g_spekeSessionGetCb() 获取 SpekeSession
→ SpekeEncryptData(session, plaintext) [尾调用]
数据接收:
LinkLayerDecryptData(ciphertext)
→ if (g_encryptType == SPEKE)
LinkLayerSpekeDecrypt(ciphertext)
→ g_spekeSessionGetCb() 获取 SpekeSession
→ SpekeDecryptData(session, ciphertext)
→ memcpy_s(plaintext, ...)
七、错误码体系
SPEKE协商过程中的错误通过 iotc_errcode.h 中定义的模块化错误码上报,是排查协商失败的关键依据:
| 错误码 | 含义 | 触发场景 |
|---|---|---|
IOTC_CORE_COMM_SEC_ERR_SPEKE_GET_PINCODE |
获取PINCODE失败 | GetPincode回调返回错误 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_NEGOCTX_INIT |
协商上下文初始化失败 | NegoContextInit失败 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_RANDOM_GEN |
随机数生成失败 | TRNG回调返回错误 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_VER_NOT_SUPP |
版本不支持 | SpekeCommonVerifyVersion失败 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_PUBKEY |
公钥错误 | 公钥格式/值非法 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_VERIFY_HMAC |
HMAC验证失败 | 中间人篡改或密钥不一致 |
IOTC_CORE_COMM_SEC_ERR_SPEKE_CREATE |
SPEKE创建失败 | SpekeInitSession失败 |
IOTC_CORE_COMM_SEC_ERR_SESS_KEY_NOT_READY |
会话密钥未就绪 | 加密时密钥尚未派生 |
IOTC_CORE_BLE_LL_ERR_SPEKE_NULL |
SPEKE会话为空 | 未创建会话即发送数据 |
八、与DSoftBus认证模块的对比
| 维度 | DSoftBus认证(富设备) | EC-SPEKE(CH585瘦设备) |
|---|---|---|
| 协议族 | PAKE | SPEKE |
| 椭圆曲线 | NIST P-256 / X25519 | Curve25519 + Elligator2 |
| 协商步数 | 4状态FSM | 4步握手(Msg1~Msg4) |
| 报文格式 | TLV/protobuf | JSON (cJSON) |
| 口令映射 | 固定生成元 | Elligator2 hash-to-curve |
| 密钥派生 | HKDF | HKDF (info含"ohos_connect_speke_") |
| HMAC验证 | 双向 | 双向(kcfData1/kcfData2) |
| 安全框架 | HiChain | iot_connect_sdk security模块 |
| 代码位置 | dsoftbus/core/authentication/ | iot_connect/core/infrastructure/security/speke/ |
两者共同构成OpenHarmony统一互联"All Devices, One Connect"安全底座的不同层级,富设备用PAKE保证高强度认证,瘦设备用EC-SPEKE在资源约束下实现等效的安全目标。
九、总结
EC-SPEKE的四步协商流程(Msg1→Msg2→Msg3→Msg4)完整覆盖了从口令映射到密钥派生的安全链路。设备侧在每个步骤中执行的核心加密操作为:
| 步骤 | 设备侧操作 | 核心加密原语 |
|---|---|---|
| 初始化 | 口令映射为曲线基点、生成salt/密钥对 | PBKDF2-SHA512 + Elligator2 + X25519 |
| 收Msg1 | 校验version、组装challenge1/salt/pk1/iterations | — |
| 收Msg3 | 计算共享密钥、派生会话密钥、验证kcfData2 | X25519 + HKDF-SHA256 + HMAC-SHA256 |
| 发Msg4 | 生成HMAC确认数据kcfData1 | HMAC-SHA256 |
| 发Msg4后 | 派生数据加密密钥 | HKDF-SHA256 |
协议设计中值得关注的工程细节:PBKDF2的base信息字符串 "ohos_connect_speke_base_info_curve25519_XMD:SHA-512_ELL2_RO_" 直接编码了曲线类型、哈希算法和映射方法,使得协议参数自描述;会话密钥HKDF与数据密钥HKDF分别使用 "ohos_connect_speke_sessionkey_info" 和 "ohos_speke_session_key" 作为info实现密钥分离,会话密钥进一步拆分为 identityEncKey 与 hmacKey;kcfData1/kcfData2的双向HMAC验证在四步握手内完成,无需额外轮次。
更多推荐
所有评论(0)