跳到主要内容

3. 推送消息加密

3. 推送消息加密

推送消息加密分四个阶段:

  • 使用 ECDH [ECDH] 导出共享秘密 (见本文档第 3.1 节).

  • 再将共享秘密与认证秘密组合, 生成 [RFC8188] 所用的输入密钥材料 IKM (见本文档第 3.3 节).

  • 使用 [RFC8188] 中的过程导出内容加密密钥与 nonce.

  • 再按 [RFC8188] 进行加密或解密.

密钥导出过程在第 3.4 节汇总. 对加密内容编码使用的限制见第 4 节.

3.1. Diffie-Hellman 密钥协商

用户代理为应用生成的每个新订阅还会生成一对用于 ECDH [ECDH] 的 P-256 [FIPS186] 密钥.

发送推送消息时, 应用服务器在同一 P-256 曲线上生成新的 ECDH 密钥对.

应用服务器的 ECDH 公钥作为加密内容编码首部中的 "keyid" 参数包含在内 (见 [RFC8188] 第 2.1 节).

应用服务器使用 [ECDH] 所述过程, 将其 ECDH 私钥与用户代理提供的公钥结合; 收到推送消息后, 用户代理以相同方式将其私钥与 "keyid" 参数中应用服务器提供的公钥结合. 这些运算产生相同的 ECDH 共享秘密值.

3.2. 推送消息认证

为确保推送消息得到正确认证, 在用户代理生成的信息中加入对称的认证秘密. 该认证秘密混入第 3.3 节所述的密钥导出过程.

用户代理 MUST 生成并提供难以猜测的 16 字节序列, 用于推送消息的认证. 该序列 SHOULD 由密码学强度足够的随机数生成器产生 [RFC4086].

3.3. 组合共享秘密与认证秘密

ECDH 产生的共享秘密与认证秘密通过基于 HMAC 的密钥导出函数 HKDF [RFC5869] 组合. 其输出为 [RFC8188] 使用的输入密钥材料.

HKDF 使用 SHA-256 散列算法 [FIPS180-4], 输入如下:

salt: 认证秘密

IKM: 通过 ECDH 导出的共享秘密

info: ASCII 编码字符串 "WebPush: info" (该字符串不以 NUL 结尾), 后跟一个零字节, 再跟用户代理 ECDH 公钥与应用服务器 ECDH 公钥 (两个 ECDH 公钥均采用 [X9.62] 定义的非压缩点形式). 即:

key_info = "WebPush: info" || 0x00 || ua_public || as_public

L: 32 字节 (即输出长度与底层 SHA-256 HMAC 输出长度相同)

3.4. 加密过程小结

最终的内容加密密钥与 nonce 按下列顺序生成, 此处以伪代码展示, 并将 HKDF 展开为使用 SHA-256 的 HMAC 的离散步骤:

-- For a user agent:
ecdh_secret = ECDH(ua_private, as_public)
auth_secret = random(16)
salt = <from content coding header>

-- For an application server:
ecdh_secret = ECDH(as_private, ua_public)
auth_secret = <from user agent>
salt = random(16)

-- For both:

## Use HKDF to combine the ECDH and authentication secrets
# HKDF-Extract(salt=auth_secret, IKM=ecdh_secret)
PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)
# HKDF-Expand(PRK_key, key_info, L_key=32)
key_info = "WebPush: info" || 0x00 || ua_public || as_public
IKM = HMAC-SHA-256(PRK_key, key_info || 0x01)

## HKDF calculations from RFC 8188
# HKDF-Extract(salt, IKM)
PRK = HMAC-SHA-256(salt, IKM)
# HKDF-Expand(PRK, cek_info, L_cek=16)
cek_info = "Content-Encoding: aes128gcm" || 0x00
CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]
# HKDF-Expand(PRK, nonce_info, L_nonce=12)
nonce_info = "Content-Encoding: nonce" || 0x00
NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]

注意此处省略了最终 nonce 与记录序号 (record sequence number) 的异或, 因为推送消息仅包含单条记录 (见第 4 节), 且首条记录的序号为零.