跳到主要内容

7. 密钥交换

7. 密钥交换 (Key Exchange)

密钥交换 (key exchange, kex) 从双方各自发送所支持算法的 name-list 开始. 每一方在每个类别中都有首选算法, 并假定大多数实现会在任意给定时间使用相同首选算法. 每一方可以猜测另一方正在使用的算法, 并且如果适合该首选方法, 可以按该算法发送初始密钥交换数据包.

如果满足以下条件, 则认为猜测错误:

  • kex 算法和/或主机密钥算法猜错, 即服务器和客户端具有不同首选算法.

  • 其他任何算法无法达成一致, 过程定义见第 7.1 节.

否则, 认为猜测正确, 乐观发送的数据包必须作为第一个密钥交换数据包处理.

但是, 如果猜测错误, 并且一方或双方已乐观发送数据包, 则此类数据包必须被忽略, 即使猜测错误不会影响初始数据包内容; 适当一方必须发送正确初始数据包.

如果密钥交换消息包含签名或其他服务器真实性证明, 则该密钥交换方法使用显式服务器认证. 如果服务器为了证明自身真实性还必须通过发送客户端可验证的消息和对应 MAC 来证明它知道共享秘密 K, 则该密钥交换方法使用隐式服务器认证.

本文档定义的密钥交换方法使用显式服务器认证. 但是, 本协议也可以使用带隐式服务器认证的密钥交换方法. 在完成带隐式服务器认证的密钥交换后, 客户端必须等待其服务请求消息的响应, 然后才能发送任何更多数据.

7.1. 算法协商

密钥交换从双方各自发送以下数据包开始:

      byte         SSH_MSG_KEXINIT
byte[16] cookie (random bytes)
name-list kex_algorithms
name-list server_host_key_algorithms
name-list encryption_algorithms_client_to_server
name-list encryption_algorithms_server_to_client
name-list mac_algorithms_client_to_server
name-list mac_algorithms_server_to_client
name-list compression_algorithms_client_to_server
name-list compression_algorithms_server_to_client
name-list languages_client_to_server
name-list languages_server_to_client
boolean first_kex_packet_follows
uint32 0 (reserved for future extension)

每个算法 name-list 必须是以逗号分隔的算法名称列表, 参见 [SSH-ARCH] 中的 Algorithm Naming 以及 [SSH-NUMBERS] 中的额外信息. 每个受支持且允许的算法都必须按偏好顺序列出, 从最偏好到最不偏好.

每个 name-list 中的第一个算法必须是首选算法, 也是猜测算法. 每个 name-list 必须至少包含一个算法名称.

      cookie

cookie 必须是发送方生成的随机值. 其目的是使任一方都无法完全确定密钥和会话标识符.

      kex_algorithms

密钥交换算法已在上文定义. 第一个算法必须是首选且猜测的算法. 如果双方作出相同猜测, 则必须使用该算法. 否则, 必须使用以下算法选择密钥交换方法: 逐个遍历客户端的 kex 算法, 选择第一个满足以下条件的算法:

  • 服务器也支持该算法.

  • 如果该算法要求具有加密能力的主机密钥, 则服务器的 server_host_key_algorithms 中存在一个客户端也支持的、具有加密能力的算法.

  • 如果该算法要求具有签名能力的主机密钥, 则服务器的 server_host_key_algorithms 中存在一个客户端也支持的、具有签名能力的算法.

如果找不到满足所有这些条件的算法, 连接失败, 双方都必须断开连接.

      server_host_key_algorithms

这是服务器主机密钥所支持算法的 name-list. 服务器列出它拥有主机密钥的算法; 客户端列出它愿意接受的算法. 一个主机可以有多个主机密钥, 可能使用不同算法.

某些主机密钥可能并不同时支持签名和加密, 这一点可从算法判断, 因而并非所有主机密钥都对所有密钥交换方法有效.

算法选择取决于所选密钥交换算法要求签名能力还是加密能力主机密钥. 必须能够从公钥算法名称判断这一点. 必须选择客户端 name-list 中第一个满足要求且服务器也支持的算法. 如果没有此类算法, 双方都必须断开连接.

      encryption_algorithms

这是按偏好顺序排列的可接受对称加密算法 (也称为 cipher) name-list. 每个方向选择的加密算法必须是客户端 name-list 中也在服务器 name-list 中出现的第一个算法. 如果没有此类算法, 双方都必须断开连接.

注意, 如果 "none" 可接受, 则必须显式列出. 已定义算法名称见第 6.3 节.

      mac_algorithms

这是按偏好顺序排列的可接受 MAC 算法 name-list. 选择的 MAC 算法必须是客户端 name-list 中也在服务器 name-list 中出现的第一个算法. 如果没有此类算法, 双方都必须断开连接.

注意, 如果 "none" 可接受, 则必须显式列出. MAC 算法名称见第 6.4 节.

      compression_algorithms

这是按偏好顺序排列的可接受压缩算法 name-list. 选择的压缩算法必须是客户端 name-list 中也在服务器 name-list 中出现的第一个算法. 如果没有此类算法, 双方都必须断开连接.

注意, 如果 "none" 可接受, 则必须显式列出. 压缩算法名称见第 6.2 节.

      languages

这是按偏好顺序排列的语言标签 name-list [RFC3066]. 双方都可以忽略此 name-list. 如果没有语言偏好, 此 name-list 应当按 [SSH-ARCH] 第 5 节定义为空. 除非发送方知道需要语言标签, 否则不应出现语言标签.

      first_kex_packet_follows

该字段指示后续是否有猜测的密钥交换数据包. 如果将发送猜测数据包, 该字段必须为 TRUE. 如果不会发送猜测数据包, 该字段必须为 FALSE.

收到另一方的 SSH_MSG_KEXINIT 数据包后, 每一方都会知道自己的猜测是否正确. 如果另一方猜测错误且该字段为 TRUE, 则下一个数据包必须被静默忽略, 然后双方都必须按协商出的密钥交换方法行动. 如果猜测正确, 密钥交换必须继续使用猜测数据包.

交换 SSH_MSG_KEXINIT 消息后, 运行密钥交换算法. 根据密钥交换方法的规定, 它可能涉及多次数据包交换.

一方为密钥交换或重新交换发送 SSH_MSG_KEXINIT 消息后, 在发送 SSH_MSG_NEWKEYS 消息 (第 7.3 节) 之前, 禁止发送以下之外的任何消息:

  • 传输层通用消息 (1 到 19), 但禁止发送 SSH_MSG_SERVICE_REQUEST 和 SSH_MSG_SERVICE_ACCEPT.

  • 算法协商消息 (20 到 29), 但禁止再发送 SSH_MSG_KEXINIT 消息.

  • 特定密钥交换方法消息 (30 到 49).

第 11 节的规定适用于无法识别的消息.

不过请注意, 在密钥重新交换期间, 一方发送 SSH_MSG_KEXINIT 消息后, 在收到另一方的 SSH_MSG_KEXINIT 消息前, 必须准备好处理任意数量已经在传输途中的消息.

7.2. 密钥交换输出

密钥交换产生两个值: 共享秘密 K 和交换哈希 H. 加密密钥和认证密钥从这些值派生. 第一次密钥交换的交换哈希 H 还用作 session identifier, 即该连接的唯一标识符. 认证方法使用它作为被签名数据的一部分, 用以证明私钥持有. session identifier 一经计算, 即使之后重新交换密钥也不会改变.

每种密钥交换方法都会指定用于密钥交换的哈希函数. 密钥派生也必须使用相同哈希算法. 这里称其为 HASH.

加密密钥必须按如下方式计算, 即对已知值和 K 计算 HASH:

  • 初始 IV, 客户端到服务器: HASH(K || H || "A" || session_id) (这里 K 编码为 mpint, "A" 编码为 byte, session_id 为原始数据. "A" 表示单个字符 A, ASCII 65).

  • 初始 IV, 服务器到客户端: HASH(K || H || "B" || session_id).

  • 加密密钥, 客户端到服务器: HASH(K || H || "C" || session_id).

  • 加密密钥, 服务器到客户端: HASH(K || H || "D" || session_id).

  • 完整性密钥, 客户端到服务器: HASH(K || H || "E" || session_id).

  • 完整性密钥, 服务器到客户端: HASH(K || H || "F" || session_id).

密钥数据必须从哈希输出开头取用. 从哈希值开头取所需字节数. 如果所需密钥长度长于 HASH 输出, 则通过计算 K、H 和目前完整密钥拼接值的 HASH 来扩展密钥, 并把产生的字节追加到密钥中, 追加数量为 HASH 所生成字节数. 该过程重复直到有足够密钥材料; 密钥从该值开头取得. 换言之:

      K1 = HASH(K || H || X || session_id)   (X is e.g., "A")
K2 = HASH(K || H || K1)
K3 = HASH(K || H || K1 || K2)
...
key = K1 || K2 || K3 || ...

如果 K 中熵量大于 HASH 内部状态大小, 此过程会损失熵.

7.3. 启用密钥

密钥交换以双方各自发送 SSH_MSG_NEWKEYS 消息结束. 该消息使用旧密钥和旧算法发送. 此消息之后发送的所有消息都必须使用新密钥和新算法.

收到此消息后, 接收必须使用新密钥和新算法.

该消息的目的在于确保一方在密钥交换出错时能够用另一方可理解的 SSH_MSG_DISCONNECT 消息进行响应.

      byte      SSH_MSG_NEWKEYS