negotiate.adoc 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. // tag::main[]
  2. = 安全通信——密钥协商
  3. 为确保传输数据安全,推荐使用安全协议,如 `https`,无法使用时才使用该方案(通过协商密钥算法协商会话密钥,然后通过密钥加密传输数据)。
  4. == 术语定义:
  5. * 公钥、私钥
  6. ** 公钥会进入网络传输的,该部分是公开的。
  7. ** 私钥由部分不允许公开。
  8. * `DH` 密钥交换(协商)算法
  9. ** 全称为Diffie-Hellman ,该算法仅传输双方各自的公钥即可使得双方计算出相同的key,可以使用这个key作为密钥加密信息;即使攻击者只获取到双方公钥也无法计算出Key值。
  10. ** 关键点:需要保证在互换双方公钥时不被中间人进行替换,否则可能遭受中间人攻击。
  11. * 共享密钥/会话密钥
  12. ** DH算法中,双方进行密钥(公钥)交换后,通过对方公钥和己方私钥生成的Key值(双方结果相同)。
  13. ** 只要私钥不被破解,该值就是安全的。
  14. * 数据密钥
  15. ** 真正用于加密数据的key,每次会话随机生成,使用该密钥加密敏感数据,然后用共享密钥和共享iv( `shareIv` )进行加密,随后该值将被销毁。
  16. ** 发送数据时,会将 `使用该值加密过的敏感数据密文` 、 `共享iv` 、 `使共享密钥加密过的数据密钥(该值)` 传递给对方,对方会先解密出 `数据密钥明文` ,然后解密出本次传输的敏感信息。
  17. ** 该值仅在内存中使用时临时生成和恢复,用后即销毁,因为该值是临时的,且不在网络中传输,所以可以保证敏感数据的安全。
  18. * 安全会话标识
  19. ** 类似SessionId,用于记录对方是谁,通过该值来判断是否已经进行秘钥交换、获取共享秘钥,双方都会保存该标识。
  20. * AES支持算法
  21. ** 加密用的规则,请求方所支持的AES加密种类,如果支持多个,用分号分割。这些值取决于代码实现的种类。
  22. * AES算法选型
  23. ** 响应方从请求方的AES-supports挑选一个随机的加密方法,作为本次会话中使用的AES加密算法。
  24. * B2S(bytes[])
  25. ** 代表将bytes[] 转化为字符串,一般采用base64编码。
  26. == 交互流程
  27. * 整个加密流程的实现,分成客户端部分和服务端部分,对于大部分的服务,均需实现(既会提供给服务调用,也会调用其他服务)。
  28. * 传输加密前,双方需要通过协商,来确定加密敏感信息所用的秘钥。
  29. * 每一次的协商,都会生成一个唯一的 `X-Sid` ,用于双方后续的通信。当服务端发现 `X-Sid` 过期时,需要返回约定好的错误码,让客户端重新发起协商和请求。
  30. === 客户端-发起协商请求
  31. . 生成 `ECDH` 的公私钥对(公钥 `ClientPublicKey` 和私钥 `ClientPrivateKey`)
  32. . 生成一个唯一的会话ID(`X-Sid`)
  33. . 生成客户端支持的加/解密规则(`aesSupports`)
  34. . 生成请求接口用的认证 `Token`
  35. ** Token的签名为:`<X-Sid>B2S(<ClientPublicKey>)`
  36. ** 如 X-Sid='ABC',B2S(<ClientPublicKey>)='XYZ',则最终为 `ABCXYZ`
  37. . 将Token放入header中,将S(PublicKeyA)也即转换为HEX的公钥、X-Sid、AES-supports字段放入 `Body` 中,并调用服务端的协商接口。
  38. . `可选步骤`:填充 `negotiateVersion` 为 `1`,代表使用第一个版本的协商算法(ECDH),该字段只是为了后续版本可能更新,只有一套方案时可以直接采用默认值,节省网络开销。
  39. [cols="1,1,3,3"]
  40. .发起协商请求所需参数
  41. |===
  42. | 位置 | 字段名 | 字段值表达式 | 说明
  43. | Header | X-Sid | `UUID` | 安全会话唯一标识,使用UUID即可
  44. | Header | Token | `<X-Sid>B2S(<ClientPublicKey>)` | 用于判断传输未被篡改
  45. | Body | aesSupports | `["AES-ECB-192", "AES-CBC-256"]` | 支持的算法类型,自然越多越好
  46. | Body | publicKey | `B2S(<ClientPublicKey>)` | 客户端公钥的64进制编码
  47. | Body | negotiateVersion | `"1"` | 秘钥交换算法版本,可空,默认为 `1`
  48. |===
  49. === 服务器-服务端处理协商请求
  50. . 收到客户端的协商请求,进行参数校验
  51. ** 参数完整性校验
  52. ** 参数格式校验
  53. ** `Token` 有效性校验
  54. . 根据 `negotiateVersion` 版本,做一些事情,这里只有ECDH,直接进行ECDH的椭圆曲线选择过程,简单起见,默认使用 `ANSI X9.62 Prime 256v1`
  55. . 生成自己的公私钥对(`ServerPublicKey`、`ServerPrivateKey`)
  56. . 对 `publicKey` 64进制解码得到客户端公钥值 `ClientPublicKey`
  57. . 根据 `ServerPublicKey`、`ServerPrivateKey`、`ClientPublicKey` 生成共享密钥 `shareKey`
  58. . 根据 `ServerPublicKey`、`ClientPublicKey` 生成共享加密矢量 `shareIv`
  59. . 将 `aesSupports` 与自己支持的aes加密算法列表做交集处理,在交集中随机选择一个作为本次协商的加密规则 `aesAlgorithm`
  60. . 生成一个协商有效时间 `expireTime`,如:30分钟
  61. . 缓存协商结果
  62. ** 包括:`X-Sid`、`aesAlgorithm`、`shareKey`、`shareIv`、`expireTime`
  63. . 生成一个双向认证用的Token。
  64. ** Token的签名为:`X-Sid` + `B2S(ServerPublicKey)` + `AES-param` + `expireTime`
  65. . 返回协商结果。
  66. [cols="1,1,3,3"]
  67. .响应协商请求结果
  68. |===
  69. | 位置 | 字段名 | 字段值表达式 or 举例 | 说明
  70. | Header | X-Sid | `UUID` | 安全会话唯一标识
  71. | Header | Token | `X-Sid` + `B2S(ServerPublicKey)` + `AES-param` + `expireTime` | 用于判断传输未被篡改
  72. | Body | aesAlgorithm | `"AES-CBC-256"` | 本次会话使用的加密算法
  73. | Body | publicKey | `B2S(<ServerPublicKey>)` | 服务端公钥的64进制编码
  74. | Body | expireTime | `1800` | 会话过期时间,秒
  75. |===
  76. === 客户端-处理协商返回结果
  77. . 收到服务端的协商请求,进行 `Token` 有效性校验
  78. . 根据 `ServerPublicKey`、`ClientPublicKey`、`ClientPrivateKey` 生成共享密钥 `shareKey`
  79. . 根据 `ServerPublicKey`、`ClientPublicKey` 生成共享加密矢量 `shareIv`
  80. . 缓存协商结果
  81. ** 包括:`X-Sid`、`aesAlgorithm`、`shareKey`、`shareIv`、`expireTime`
  82. [NOTE]
  83. ====
  84. 至此,协商阶段结束。为了保证客户端发起时,服务端信息未失效,故设计中客户端缓存需提前过期。如 `expireTime /= 2` 或减少固定时间等。
  85. ====
  86. === 客户端-发起数据传输请求
  87. . 查找是否已与目标服务有进行协商且未过期
  88. ** 未过期则使用
  89. ** 否则重新进行协商
  90. include::[tags=encrypt]
  91. [cols="1,1,3,3"]
  92. .响应协商请求结果
  93. |===
  94. | 位置 | 字段名 | 字段值表达式 or 举例 | 说明
  95. | Header | X-Sid | `UUID` | 安全会话唯一标识
  96. | Header | X-Dk | `Random` | 数据秘钥密文64进制编码
  97. | Header | Token | `X-Sid` + `dataCipher` | 用于判断传输未被篡改
  98. | Body | aesAlgorithm | "AES-CBC-256" | 本次会话使用的加密算法
  99. | Body | publicKey | B2S(`ServerPublicKey`) | 服务端公钥的64进制编码
  100. | Body | expireTime | `1800` | 会话过期时间,秒
  101. |===
  102. [NOTE]
  103. ====
  104. 为了避免客户请求前未过期,收到服务端响应时缓存意外过期情况。可以在请求前通过快照机制处理,不推荐判断是否 `即将过期` (过期时间小于接口最大超时时间)。
  105. ====
  106. === 服务端-数据传输处理
  107. ==== 处理前解密
  108. include::[tags=decrypt]
  109. 业务处理
  110. ==== 业务处理后响应前加密
  111. 注:与客户端请求前加密流程相同
  112. include::[tags=encrypt]
  113. === 客户端-处理服务端响应
  114. include::[tags=decrypt]
  115. 业务处理
  116. // end::main[]
  117. // tag::encrypt[]
  118. . 随机生成 `数据秘钥`(`dataKey`,也成临时加密秘钥)
  119. . 以 `dataKey` 为密钥, `shareIv` 为加密矢量,采用 `aesAlgorithm` 对应算法加密 `敏感数据`(`data`),并对其进行64进制编码得到敏感数据密文(`dataCipher`)
  120. . 以 `shareKey` 为密钥,`shareIv`为加密矢量,采用 `aesAlgorithm` 对应算法加密 `数据秘钥`(`dataKey`),并对其进行64进制编码得到数据秘钥密文(`dataKeyCipher`)
  121. . 生成认证Token,签名为:`X-Sid` + `dataCipher`
  122. // end::encrypt[]
  123. // tag::decrypt[]
  124. . 校验请求合法性
  125. ** 校验 Header 中字段的完整性
  126. ** 校验Token的合法性
  127. . 判断 `X-Sid` 对应的协商是否过期,如果过期,直接返回错误码,提示需要重新协商。
  128. . 从 `X-Sid` 对应的缓存中取出协商缓存
  129. . 对数据秘钥密文 `dataKeyCipher` base64解码,再根据 共享秘钥 `shareKey`、加密矢量 `shareIv` 以及加密算法 `aesAlgorithm` 对其解密得到 `数据秘钥`(`dataKey`)。
  130. . 对敏感数据密文 `dataCipher` base64解码,再根据 数据秘钥 `dataKey`、加密矢量 `shareIv` 以及加密算法 `aesAlgorithm` 对其解密得到 `敏感数据明文`(`data`)
  131. // end::decrypt[]