bigywater.tech
网络协议 · 96

SSH 握手:版本交换、密钥交换与认证

SSH 的握手比 TLS 1.3 啰嗦得多:明文的版本串、双方各列一个算法清单的 KEXINIT、密钥交换、切换密钥、再认证。这篇在本机起一个 paramiko 服务器,读真实公共 SSH 服务的 KEXINIT(只读不认证),看算法的演进,验证主机密钥首次信任的机制,并量出握手需要几个往返。

mlkem768x25519-sha256
github.com 与 gitlab.com 的 KEXINIT 里排第一的密钥交换算法(后量子混合)
约 7.1 个往返
100 ms 往返的链路上,从 TCP 连上到认证成功需要的往返数(709 ms)
SHA256:…
主机密钥的指纹;换了服务器后指纹变化,客户端必须拒绝

版本串与 KEXINIT

实测 · 本机执行输出
  本机 paramiko 服务器:版本串 'SSH-2.0-paramiko_5.0.0'(格式 SSH-协议版本-软件版本 [注释];协议版本 2.0)
  github.com:版本串 'SSH-2.0-1f08b963'(格式 SSH-协议版本-软件版本 [注释];协议版本 2.0)
  gitlab.com:版本串 'SSH-2.0-GitLab-SSHD'(格式 SSH-协议版本-软件版本 [注释];协议版本 2.0)

明文的版本串,之后双方各发一个包含 10 个名字列表的 KEXINIT。

算法清单

实测 · 本机执行输出
  本机 paramiko 服务器:消息类型 20(20 = SSH_MSG_KEXINIT),cookie 4853d5f694df8a71…(16 字节随机数)
    kex_algorithms                8 个:curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group16-sha512 …
    server_host_key_algorithms    1 个:ecdsa-sha2-nistp256
    encryption c→s                9 个:aes128-ctr, aes192-ctr, aes256-ctr, aes128-cbc, aes192-cbc …
    encryption s→c                9 个:aes128-ctr, aes192-ctr, aes256-ctr, aes128-cbc, aes192-cbc …
    mac c→s                       8 个:hmac-sha2-256, hmac-sha2-512, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1 …
    mac s→c                       8 个:hmac-sha2-256, hmac-sha2-512, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1 …
    compression c→s               1 个:none
    compression s→c               1 个:none
  github.com:消息类型 20(20 = SSH_MSG_KEXINIT),cookie 34cff8247bae6780…(16 字节随机数)
    kex_algorithms               10 个:mlkem768x25519-sha256, sntrup761x25519-sha512, sntrup761x25519-sha512@openssh.com, curve25519-sha256, curve25519-sha256@libssh.org …
    server_host_key_algorithms    5 个:ssh-ed25519, ecdsa-sha2-nistp256, rsa-sha2-512, rsa-sha2-256, ssh-rsa
    encryption c→s                6 个:chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com, aes256-ctr, aes192-ctr …
    encryption s→c                6 个:chacha20-poly1305@openssh.com, aes256-gcm@openssh.com, aes128-gcm@openssh.com, aes256-ctr, aes192-ctr …
    mac c→s                       4 个:hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256
    mac s→c                       4 个:hmac-sha2-512-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512, hmac-sha2-256
    compression c→s               2 个:none, zlib@openssh.com
    compression s→c               2 个:none, zlib@openssh.com
  gitlab.com:消息类型 20(20 = SSH_MSG_KEXINIT),cookie 3f108a789590ff98…(16 字节随机数)
    kex_algorithms                9 个:mlkem768x25519-sha256, curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384 …
    server_host_key_algorithms    6 个:ssh-dss, ecdsa-sha2-nistp256, ssh-ed25519, rsa-sha2-256, rsa-sha2-512 …
    encryption c→s                6 个:aes128-gcm@openssh.com, aes256-gcm@openssh.com, chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr …
    encryption s→c                6 个:aes128-gcm@openssh.com, aes256-gcm@openssh.com, chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr …
    mac c→s                       6 个:hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 …
    mac s→c                       6 个:hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1 …
    compression c→s               1 个:none
    compression s→c               1 个:none

github 与 gitlab 都已经把后量子混合算法放在第一位;本机 paramiko 只有传统的 curve25519 / ecdh;gitlab 还保留了 ssh-dss 这样的老算法。

商定

实测 · 本机执行输出
  密钥交换完成用时 6.8 ms;服务器版本串 'SSH-2.0-paramiko_5.0.0';商定:加密 aes128-ctr,MAC hmac-sha2-256,压缩 none;kex 引擎 None
    kex algos: curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group16-sha512, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha256,
    kex algos: curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group16-sha512, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha256,
    Switch to new keys ...
  服务器主机密钥:类型 ecdsa-sha2-nistp256,指纹 SHA256:knjhudrbLItfSTRj6tCZB7oQ4AMaJe33mMdn9cM3apg(客户端要拿这个指纹去和已知的比较)

aes128-ctr + hmac-sha2-256 + 无压缩;“Switch to new keys”之后通道加密。

认证

实测 · 本机执行输出
  auth_none 的回答:服务器接受的方式 ['publickey', 'password']
  错误的密码:失败 AuthenticationException
  正确的密码:成功,仍需的方式 [];已认证 True
  错误的密钥(SHA256:YykeCmuVny3m/auCS…):失败 AuthenticationException
  正确的密钥(SHA256:8adI61bJX2J4TEO0k…):成功;已认证 True

在加密通道里进行:先问方式,密码或公钥。

主机密钥与 TOFU

实测 · 本机执行输出
  第一次连接:记下指纹 SHA256:knjhudrbLItfSTRj6tCZB7o…(首次信任)
  指纹与记录的一致,继续
  换一台新的服务器(新的主机密钥)占用同一个“地址”后:⚠ 指纹变了!记录的 SHA256:knjhudrbLItfSTRj6…,现在是 SHA256:EOeuDpMcXaxXsea2j…——客户端应该拒绝(OpenSSH 会显示 REMOTE HOST IDENTIFICATION HAS CHANGED)

第一次记下指纹,之后指纹变了就是警报。

往返数

实测 · 本机执行输出
    TCP 握手(ms)  版本交换 + 密钥交换(ms)  认证(ms)   合计(ms)   (每行一次)
          100                  407             202       709
          101                  405             204       711
          101                  405             204       709
          101                  403             201       705
  中位数:TCP 101 ms(1 个往返)、版本 + 密钥交换 405 ms(≈ 4.0 个往返)、认证 203 ms(≈ 2.0 个往返);合计 709 ms ≈ 7.1 个往返

TCP 1 + 密钥交换约 4 + 认证约 2 个往返。

分步演示

SSH 握手
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。本机服务器是 paramiko 5.0.0 写的,密钥都是临时生成的;github.com:22 和 gitlab.com:22 只读版本串和 KEXINIT 就断开,没有做认证;往返数是在 netlab(每方向 50 ms)上用 paramiko 客户端量出来的,各 4 次;kex 引擎名称的 None 是 paramiko 内部的属性,没有意义
    • 没有验证:真实密钥交换消息的内容(只看了算法名字);信道复用、端口转发(下一篇);SSH 证书认证;known_hosts 的哈希格式;密钥重协商(rekey)的触发条件;ssh-agent;OpenSSH 客户端的默认算法偏好(这里用的是 paramiko 客户端)
    京ICP备2026067502号-1京公网安备11010502063487号