Appearance
端对端安全协议
FDP端对端安全协议为IoT设备提供传输层无关的安全通信能力。支持蓝牙、MQTT、WiFi Direct等多种传输方式,实现离线点对点身份认证和加密通信。
核心问题与解决方案
| 问题 | 解决方案 |
|---|---|
| 如何确认对方身份? | Ed25519签名 + Credential验证 |
| 如何验证对方权限? | 设备端权限表 |
| 如何防止窃听和篡改? | ChaCha20-Poly1305 AEAD |
设计理念:借鉴HTTPS的握手+会话模式。握手阶段交换身份凭证、协商密钥(~400字节一次性开销),通信阶段使用会话密钥加密(28字节/消息)。
算法依赖
| 算法 | 用途 | 必须使用的原因 |
|---|---|---|
| Ed25519 | 数字签名 | 设备身份认证,32字节公钥作为Device ID |
| X25519 | ECDH密钥交换 | 安全协商会话密钥,支持前向安全 |
| HKDF-SHA256 | 密钥派生 | 从共享密钥派生会话密钥 |
| ChaCha20-Poly1305 | AEAD加密 | 消息加密+认证,适合无硬件加速的嵌入式设备 |
| CBOR | 二进制序列化 | 最小化消息体积,适合蓝牙低带宽场景 |
| Base58 | 编码 | Device ID可读性,无易混淆字符 |
实现库推荐
| 平台 | 推荐库 |
|---|---|
| Python | cryptography, cbor2, base58 |
| C/嵌入式 | libsodium(推荐), mbedtls, wolfssl |
| JavaScript | @noble/ed25519, @noble/hashes |
密码学基础
设备身份:Ed25519
每个设备拥有一对Ed25519密钥:
- 私钥:32字节,设备安全存储,永不传输
- 公钥:32字节,Base58编码后约44字符
- Device ID = Base58(公钥)
自证明身份:公钥本身就是身份标识。能用私钥签名的,必定是该设备。
身份凭证:Credential
认证服务器签发的身份证明(永久有效):
json
{
"uid": "user-001",
"device_id": "5D7xB8...",
"issued_at": 1700000000,
"issuer_sig": "base64_ed25519_signature"
}关键要点:
- 设备必须预置认证服务器的Ed25519公钥(issuer_pubkey)
- 设备首次注册时获得Credential
- 本地永久缓存
- 握手时出示给对方验证身份
密钥交换:X25519 ECDH
双方各生成临时密钥对,交换公钥后推导共享密钥:
session_key = HKDF-SHA256(
shared_secret = ECDH(my_ephemeral_priv, peer_ephemeral_pub),
salt = session_id,
info = "fd-p2p-v1"
)前向安全:临时密钥用完即销毁。即使长期私钥泄露,历史会话仍安全。
消息加密:ChaCha20-Poly1305 AEAD
AEAD = Authenticated Encryption with Associated Data
一次操作完成:加密(机密性)+ 认证(完整性)
- 输入:plaintext + nonce(12字节) + associated_data
- 输出:ciphertext + 16字节Tag
- 解密时自动验证Tag,失败则拒绝
握手协议
建立安全会话需要三步:
SessionRequest
json
{
"type": "session_request",
"initiator": "5D7xB8...",
"ephemeral_pub": "base64_32bytes",
"credential": {
"uid": "user-001",
"device_id": "5D7xB8...",
"issued_at": 1700000000,
"issuer_sig": "base64_64bytes"
},
"timestamp": 1700000000,
"sig": "base64_64bytes"
}SessionResponse
json
{
"type": "session_response",
"session_id": "base64_16bytes",
"responder": "7K9mC2...",
"ephemeral_pub": "base64_32bytes",
"credential": {
"uid": "device-lock-001",
"device_id": "7K9mC2...",
"issued_at": 1700000000,
"issuer_sig": "base64_64bytes"
},
"timestamp": 1700000001,
"sig": "base64_64bytes"
}SessionConfirm
json
{
"type": "session_confirm",
"session_id": "base64_16bytes",
"confirm_tag": "base64_16bytes"
}confirm_tag = HMAC(session_key, "confirm" + session_id)
验证流程
- 验证对方的Ed25519签名(证明持有私钥)
- 验证对方的Credential签名(证明身份由认证服务器确认)
- 缓存对方的uid,用于后续查询本地权限表
消息格式
握手完成后,所有业务消息使用AEAD加密。
二进制消息结构
┌─────────────┬──────────┬─────────────────────┐
│ session_id │ seq │ ciphertext │
│ (4 bytes) │ (2 bytes)│ (变长 + 16字节Tag) │
└─────────────┴──────────┴─────────────────────┘- session_id[0:4]:会话标识(取前4字节)
- seq:消息序号,uint16,防重放
- ciphertext:AEAD加密后的数据(含16字节Tag)
Nonce管理
AEAD要求Nonce不可重复。使用 session_id[0:12] XOR seq 构造96位Nonce。
Payload格式(CBOR)
使用CBOR(二进制JSON)而非JSON,大幅减少体积:
json
{
"a": 1,
"t": 1700000000
}CBOR编码后约6字节。
收发流程
发送:
- 构造payload(action + timestamp)
- CBOR序列化(~6字节)
- 构造Nonce(session_id + seq)
- AEAD加密(生成密文 + Tag)
- 打包:session_id[0:4] + seq + ciphertext
- 总长度:4 + 2 + 6 + 16 = 28字节
接收:
- 解析头部(session_id_prefix + seq)
- 查找会话
- 防重放检查(seq必须递增)
- AEAD解密(自动验证Tag,失败则拒绝)
- 解析payload
- 检查时间戳(5分钟窗口)
- 查询本地权限表
- 更新序号
安全性分析
威胁模型
| 攻击类型 | 防御措施 |
|---|---|
| 身份伪造 | Ed25519签名。没有私钥无法通过握手验证 |
| 身份凭证伪造 | Credential由认证服务器签名,本地无法伪造 |
| 权限绕过 | 权限表存储在设备端,由设备控制,通信层无法绕过 |
| 消息窃听 | ChaCha20加密,没有session_key无法解密 |
| 消息篡改 | Poly1305 Tag验证,任何修改都会导致Tag不匹配 |
| 重放攻击 | seq递增 + timestamp检查 |
| 中间人攻击 | ECDH密钥交换绑定Ed25519身份,无法插入 |
| 私钥泄露 | 前向安全:临时ECDH密钥,历史会话不受影响 |
为什么不能省略Tag?
没有Tag的加密 = 没有锁的保险箱
流加密(ChaCha20)的XOR特性导致:攻击者无需密钥,只需知道消息格式,即可通过位翻转精确修改消息内容。
python
# 攻击者截获密文(不知道密钥)
ciphertext = intercept()
# 攻击者知道JSON格式,猜测明文包含 {"a":3} (查询)
# 想改成 {"a":1} (解锁)
# 直接修改密文对应位置
tampered = ciphertext
tampered[offset] ^= ord('3') ^ ord('1') # 3→1
# 发送篡改密文 → 设备解密 → 得到 {"a":1} → 门锁打开!有Tag保护:任何对密文的修改都会导致Tag验证失败,消息被拒绝。
性能分析
蓝牙场景
BLE默认MTU = 23字节,有效载荷 = 20字节
| 方案 | 消息大小 | BLE包数 | 延迟 |
|---|---|---|---|
| JSON + HMAC | ~190字节 | 10包 | 10 × RTT |
| 本协议 | 28字节 | 2包 | 2 × RTT |
优化效果:
- 延迟降低 80%
- 功耗降低 80%(BLE主要功耗在传输)
- 适合电池供电的门锁设备
MQTT场景
| 阶段 | 消息大小 | 频率 |
|---|---|---|
| 握手 | ~400字节 | 每次连接一次 |
| 业务消息 | 28字节 | 每次操作 |
对于MQTT,28字节的开销可忽略不计。
实现建议
预置信息
- 认证服务器公钥(issuer_pubkey):用于验证Credential签名
- 本地权限表:uid → permissions映射,由设备端管理
密钥存储
| 密钥类型 | 存储位置 | 生命周期 |
|---|---|---|
| 设备私钥 | 安全元件或加密分区 | 永久 |
| 会话密钥 | 内存 | 会话结束后清除 |
| Credential | 本地存储 | 永久 |
会话管理
- 会话超时:30分钟无活动自动销毁
- 最大会话数:限制同时会话数(如10个)
- 序号溢出:seq达到65535时,重新握手
错误处理
| 错误 | 处理方式 |
|---|---|
| Tag验证失败 | 立即断开,不泄露任何信息 |
| Credential签名无效 | 拒绝握手 |
| 权限不足 | 返回错误码,不执行操作 |
| 私钥泄露 | 重置设备(生成新密钥对,重新注册) |
