API 加密协议

为什么使用加密协议?

为维护双方数据和信息安全,保护资源方(开发者)提供的资源数据不被第三方窃取,以及双方的通讯不被中间人劫持,我们在设计与资源方 Webhook 通信接口时,使用增强加密的协议保证接口调用的安全性。

加密协议选择

为降低资源方理解和实现的复杂度,我们采取 IETF 标准提案 RFC5861 JWE (JSON Web Encryption) 对应用层 JSON 数据进行数据加密、完整性保护和来源认证,加密后的内容通过 HTTP 协议传输。协议数据结构如下图所示:

API 加密协议

作为开放的互联网标准提案,JWE 有很多种开源实现。资源方可以根据自身需求,选择合适的开源函数库集成到后台服务中。

协议细节和示例

发送给 webhook 的请求

小程序开放阿拉丁发送给 webhook 的请求 JSON Object,主要由以下公共字段组成:

  1. 1234567
  1. { "type": "sp_ala", // 智能小程序阿拉丁请求,以便与其它请求区分 "srcid": "123", // 卡片的 ID,每个卡片不同 "surface": "mobile", // 卡片展示在哪个产品上, mobile: 支持小程序的移动搜索,web_h5: 支持 H5 的移动搜索 "intent": { // 卡片的 intent,每个卡片不同 }}

Webhook 返回的结果

Webhook 返回的结果 JSON Object,主要由以下公共字段组成:

  1. 1234567
  1. { "status": 0, // 结果状态码,0 代表正确,1 代表无结果,2 代表请求参数错误,3 代表内部服务错误 "msg": "", // 出错消息 "data": { // 资源的结果内容,每个资源分类不同 }, "lifetime": 1559705004 // 可选字段,秒级时间戳,指示该数据的有效期}

对请求和结果 JSON 消息加密

为了降低加密复杂度和计算开销,JWE 选择的加密算法组合是:

  1. 1234
  1. { "alg":"A128KW", "enc":"A128CBC-HS256"}

这一组合。其中:

  • A128KW 代表密钥生成算法通过预共享密钥 PSK (Pre Shared Key) 生成每次会话使用的内容加密密钥 CEK (Content Encryption Key)

  • A128CBC-HS256 代表加密算法底层使用 AES_128_CBC_HMAC_SHA_256 算法产生密文以及认证 TAG。除了保护了数据内容之外,同时对数据的来源和完整性进行了签名。

同时,为了尽量节省带宽,我们采取 Compact 格式序列化 JWE 对象

加密参数

JWE 需要传入预共享密钥 PSK 参数进行初始化。在我们的加密算法组合下,PSK 是 16 字节(128位)长度的密钥,由资源方在开放平台中设置。

由于 PSK 允许为任意二进制字节串,包括不可打印字符。为便于输入和展示,资源方需要在平台中输入 base64url(PSK),即经过 base64url 编码的 PSKPSK仅存储在双方服务器中,用于加密内容密钥 CEK,不会在消息中传递。

在 JWE 的 protected header 里,需要额外传输一个 kid 字段,向对方标识使用的是哪个 PSKkid 主要为了便于在开发者更新 PSK 时,双方加密通信不受影响。

开发者每一次更新 PSKkid 都会 +1。开发者在开放平台页面上,可以看到当前 PSK 对应的 kid 值。当开发者更新 PSK 时,已经收到更新的开放平台服务器会用新的 kid,PSK 组合请求 webhook,未收到更新的开放平台服务器会使用旧的 kid,PSK 组合请求 webhook。资源方可以通过 kid 判断应该使用哪个 PSK 解密请求消息和加密返回消息。

注意:接收方服务器需要使用同一个 kid,PSK 组合加密返回消息,以免发送方服务器无法解密返回消息内容。

API 加密协议 - 图2

会话参数

为了便于双方进行调试或者日志追查,请求方需要在 JWE 的 protected header 里额外传输一个 rid 字段,代表请求会话 ID。rid 的格式为 毫秒时间戳-随机数,响应方需将 rid 原样返回给发送方以保证响应的是最新的请求。

rid放在 JWE 协议的 protected header 中,没有被加密,可以通过 base64url decode 直接解码出来。但 rid 是签名内容的一部分,也就是说 rid 如果被篡改会导致整个消息签名不通过。

示例

我们下面以一个例子说明一个遵从 JWE 标准的序列化消息生成过程。开发者可以使用同样的参数对序列化后的 JWE 消息进行解密,以验证加解密实现是否正确。

  1. 123456789101112131415161718192021222324252627282930313233343536373839
  1. # INPUT:# PSKBase64url: "MDEyMzQ1Njc4OWFiY2RlZg" 即 base64url("0123456789abcdef")# kid: "0"# rid: "1559123682789-315431431"# text: {# "type":"sp_ala",# "srcid":"123",# "surface":"mobile",# "intent":{# "query":"hello"# }# }# Python Example Codefrom jwcrypto import jwk, jwefrom jwcrypto.common import json_encode# 使用 JWK 标准格式导入对称密钥key = jwk.JWK.from_json('{"kty":"oct","k":"MDEyMzQ1Njc4OWFiY2RlZg"}')# 要传输的内容明文,即应用层数据text = json_encode({"type":"sp_ala","srcid":"123","surface":"mobile","intent":{"query":"hello"}})# 根据输入参数生成 JWE 对象jwetoken = jwe.JWE(plaintext=text.encode('utf-8'), protected=json_encode({"alg": "A128KW", "enc":"A128CBC-HS256","kid":"0","rid":"1559123682789-315431431"}), recipient=key)# JWE JSON 序列化:print jwetoken.serialize()# 输出结果:由于 CEK 和 IV 在每轮会话中都会重新生成,所以输出加密内容每次运行时都不同# JSON 格式含省略内容,仅供理解,实际上使用的协议是下文 Compact 序列化格式#{# "protected":"eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q……",# "encrypted_key":"KUiCWj2y24pCjQ6urYkYLZJJfF172Rvjo_wW8lSvKv5HogoKBGfx3g",# "iv":"uN51R6JUGwMoVDH1cKFBRA",# "ciphertext":"wbuvYqJEG2iVCEG2mzozAM3e6ymstfxH……",# "tag":"2RokPmpNfg20YcW2czuYnQ"#}# JWE Compact 序列化(最终发送的消息格式)print jwetoken.serialize(compact=True)# eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Iiwia2lkIjoiMCIsInJpZCI6IjE1NTkxMjM2ODI3ODktMzE1NDMxNDMxIn0.KUiCWj2y24pCjQ6urYkYLZJJfF172Rvjo_wW8lSvKv5HogoKBGfx3g.uN51R6JUGwMoVDH1cKFBRA.wbuvYqJEG2iVCEG2mzozAM3e6ymstfxH-f5yanNyBmMhQt1F_Jwd4fgJlf0A9hu9GkgIrz5cwayGlzvObFbwbNAOGIfzNPfF8gjOcqD1ahc.2RokPmpNfg20YcW2czuYnQ

通过 HTTP POST 发送

传输层我们采取规范的 HTTP 传输协议,将加密层的内容放入 HTTP body 中,使用 POST 请求发送给接收方。待发送的字符串会以原串的方式放到 body 中,设置 HTTP Header “Content-Type: application/jwt“ (遵从 RFC7519 Section 10.3 约定)表明数据格式是 JWE 加密数据,以示与常见的 x-www-form-urlencoded 数据区分。

应用层的错误会有应用层的错误码处理,但由于我们仅支持加密传输,未解密前无法看到消息内容。如果发送方采取了不恰当的加密方式(例如出现了bug),接收方无法解密应用层内容,需要返回对应的错误码。我们复用 HTTP 错误码 400 加纯文本出错内容来实现这一功能。

  1. 12
  1. HTTP status 400HTTP body: Cannot decode JWE content.