一、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.hspekeMsg1spekeMsg2spekeMsg3spekeMsg4,分别对应 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 预初始化协商上下文。也就是说,设备的 negoContextSpekeInitSession 阶段就已就绪,而非等到收到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 ‖ challenge2
  • NegoContextVerifyHmac(验证):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=12GCM_TAG_LEN=16GCM_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实现密钥分离,会话密钥进一步拆分为 identityEncKeyhmacKey;kcfData1/kcfData2的双向HMAC验证在四步握手内完成,无需额外轮次。


Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐