重新认识SSH(一)

重新认识SSH(一)

欢迎访问我的博客:emous.github.io (知乎专栏中部分流程图排版出现问题)访问更多博文。

自从我尝试使用Public Key Authentication登陆ssh,我本就应该做好要把SSH所有的rfc都看完的打算的,在本篇文章中我将尽可能详细地阐明我对密码学如何在ssh中使用的认识。:C

按照SSH协议架构,标准将SSH分成三层:传输层协议、用户认证协议、连接协议。最底层的是传输层协议,它负责认证服务器加密数据确保数据完整性,虽然它运行在TCP之上,但其实它可以运行在任意可靠的数据流之上;第二次是用户认证协议,它负责认证使用者是否是ssh服务器的用户Public Key Authentication登陆ssh就将在这一层实现;最上层是连接协议,它将把多路加密的通道转换成逻辑上的Channel。本文将主要描述前两层协议。




环境介绍

我的Host机器是使用OpenSSH_7.4p1的 CentOS 7,而Client机是使用OpenSS_7.9p1的macOS,因此我会尽可能的使用新的SSH标准进行阐述。

版本交换

下图是我使用wireshark抓取到的SSH登陆过程中往返的数据包,


可以发现,最开始的由客户端发起的一个往返数据包,它用于交换客户端和服务器SSH版本信息。其中第一个数字,是SSH版本信息,第二个则是软件版本信息。关于服务器和客户端版本不匹配的问题,如果是服务器则可以设置兼容模式(enables compatibility)来自动应对低版本的客户端,而客户端遇到低版本的服务器必须断开连接后手动切换版本(这是因为协议没有约束客户端必须等到服务器的回应再发出命令,这就意味着可能客户端会先发出其他命令从而把旧的协议占用)。

二进制包协议

大概率是因为要解决TCP粘包以及长度整除密钥方便处理的问题,在版本交换后SSH都必须以二进制包协议的格式进行传输。

​ payload [byte[packet_length - padding_length - 1]] ^ | packet_length | mac (Message Authentication Code - MAC) [uint32] | [byte[mac_length]] ^ | ^ | | | | | | +-------+--------------------------------------------------------------------+ | | | | ssh | | | | +-----+---+--------------------------------+----------+----------+ | | | | | | | | | | | | | | | | | | | | | | | | | | tcp | | | | | | | | | | | | | | + | + | + | + | + | | | | | | | | | | | | | | | | | | | | | | | | | | +----------------------------------------------------------------+ | +-------+--------------------------------------------------------------------+ | | | | | | v v [byte] [byte[padding_length]] padding_length random padding

</pre> 其中random padding用于补全长度,payload为有效负载,mac为消息认证码。所以单个packet是有最大长度的,未压缩的包大小必须小于35000字节。

若压缩算法协商成功,payload区域将被压缩,packet_lengthmac区域将被重新计算。加密将在压缩之后执行。

在密钥交换成功之后,一个加密密钥将会被计算出来。之后,packet lengthpadding lengthpayloadrandom padding将必须被加密。同时,两个方向的加密算法可以被允许是不相同的。

当密钥交换完毕后,会有一个消息认证码算法被协商出,这是一个给定输出长度mac的摘要算法MAC,之后运算 mac = MAC(shared secret, packet sequence number, entire unencrpyted packet without 'mac')。packet sequence number是一个隐性的数字,它从第一个packet开始以0计算,每2^32次后又重新开始,不受密钥再协商协议的影响。由于shared secret只有双方知晓,且双方都能认证对方(或者,在shell登陆前server并不需要对client进行认证)因此可以确保这个消息并不来自于第三方;由于packet sequence number是同步于双方的,因此确保这个消息是有序的;最后再把完整的未加密packet放在一起运算,可以保证消息的完整性。

密钥交换

密钥交换(Key Exchange)是SSH中紧接着的第二步,它最直接的目的有两个:用一个安全的方式将接下来的packet加密密钥传到双方手中让客户端相信“同时获得密钥的另一个人”就是千真万确的服务器



第一步,客户端在payload中表明Message Code = 20(Key Exchange Init),并毫无保留的把自己支持的各种加密算法告诉服务器,同时猜测服务器支持哪些算法。需要提供的算法类型(算法用途)有密钥交换算法用于认证服务器的公钥属于什么公钥算法客户端->服务端的数据加密使用什么算法服务端->客户端的数据加密使用什么算法客户端->服务端的数据使用什么MAC算法服务端->客户端的数据使用什么MAC算法客户端->服务端的数据使用什么压缩算法服务端->客户端的数据使用什么压缩算法。同时还要提供和估测,客户端到服务端的数据使用何种语言,服务端到客户端的数据使用何种语言;first_kex_packet_follows表示是否要先在服务器提供他的列表并得出协商结果前直接尝试一次客户端自己猜测的交换算法,如果设为true,客户端会在收到消息之前就发出初始化交换算法的请求,如果猜错了服务端会直接无视;一个随机数cookie(它将被用来生成session_id,同时确保双方中的任意一方没有完全的能力控制其初始化的结果);一个暂未定义的数据项。

第二部,服务端把自己的算法列表公布给客户端。同时双方根据某种约定好的协商机制(大体上就是,选双方都有的,选第一个),再之后就由客户端开始双方协商好的密钥交换算法。

进行完密钥交换算法后,双方都一定会得到两个输出结果:

  1. 一个仅(它甚至都不曾在链路中出现)共享在双方间的秘密(shared secret)K。
  2. 一个交换摘要(exchange hash)H,并且约定在第一次密钥交换中得到的H即为session_id(它能唯一确定当前的连接)它被用来标记一个真正拥有服务端私钥的证据。

最后双方协商,通过这两个数据和一些约定的公共数据(硬编码在协议里),使用密钥交换摘要算法生成各上述的用作各种用途的确切密钥。

DH

DH(Diffie-Hellman Key Exchange)算法是历史上的第一个密钥交换算法,只有双方都参与才能生成shared secret。同时,交换完成时它将从服务器返回3个数据。

  1. host key
  2. f
  3. signature of HASH(client_id,server_id,payload of client's SSH_MSG_KEXINIT,payload of server's SSH_MSG_KEXINIT,host_key, e, f, K)

通过,df算法可以将客户端提供的e和服务端提供的f再配合之前获取的GROUP生成秘密的shared_secret K。因此如果在HASH中有host_keyK同时作为参数,若hash匹配则能证明host key确实是协商者提供的,再之后则只需要验证host_key的确有来自于host的权威性就行了。

下图是DH交换算法在SSH协议中的流程(KEXINIT)。


ECDH

ECDH(Elliptic Curve Diffie Hellman)椭圆曲线DH,它是基于ECC的交换算法。对于交换算法本身而言,它和DH类似,它需要提供的协商的参数包括(p,a,b,G,n,h)

它的一般交换流程如下:

​ Client Server ------ ------ Generate ephemeral key pair. SSH_MSG_KEX_ECDH_INIT -------------->

                                  Verify received key is valid.
                                   Generate ephemeral key pair.
                                         Compute shared secret.
                               Generate and sign exchange hash.
                         <------------- SSH_MSG_KEX_ECDH_REPLY

</pre> 可以发现,有一些步骤对于它而言是必须的:~协商公共参数~,生成各自的keypair并提交公钥,各自验证对方提供的公钥。

curve25519

也是机缘巧合,因为我的机器就使用的是这个算法(其实也是因为更新的rfc强制要求默认使用这个算法)。

A new set of Elliptic Curve Diffie-Hellman ssh-curves exist. The curve25519-sha256 MUST be adopted where possible.

我发现wireshark抓包后,尽然直截了当的仅仅不可思议的两步就完成了密钥交换:

  1. 客户端提供e
  2. 服务器提供f


一直很纳闷那之前说的那个g去哪了?还有那些要协商的各种参数又去哪了?怎么你来我往一下直接就生成shared secret了?


但其实并不是,他们都"pre install"了,作者找到了一个很Special但并不可疑的特定的曲线curve25519,在这个曲线中g[32] = {9}。因此,使用他,DH的流程可以更加简化。

​Client Server ------ ------ Generate ephemeral key pair. SSH_MSG_KEX_ECDH_INIT --------> Verify that client public key length is 32 bytes. Generate ephemeral key pair. Compute shared secret. Generate and sign exchange hash. <-------- SSH_MSG_KEX_ECDH_REPLY Verify that server public key length is 32 bytes. * Verify host keys belong to server. Compute shared secret. Generate exchange hash. Verify server's signature.

其作者在论文中写到,这个算法有着极致的速度免疫timing attack提供短的secret keys只需要32字节的public key只要32字节的输入都是合法的,不需要验证程序代码简短的优点。

服务端认证

诚如SSH RFC所说,由于SSH早于TSL问世,哪怕它已经解决了可绝大多数telnet会遇到的安全问题,它依旧面对一些情况依旧会遇到安全隐患。比如,初次建立SSH连接时可能会遇到的中间人攻击

The protocol provides the option that the server name - host key association is not checked when connecting to the host for the first time. This allows communication without prior communication of host keys or certification. The connection still provides protection against passive listening; however, it becomes vulnerable to active man-in-the-middle attacks. Implementations SHOULD NOT normally allow such connections by default, as they pose a potential security problem. However, as there is no widely deployed key infrastructure available on the Internet at the time of this writing, this option makes the protocol much more usable during the transition time until such an infrastructure emerges, while still providing a much higher level of security than that offered by older solutions (e.g., telnet RFC0854 and rlogin RFC1282).

然而,绝大多数情况SSH还是可以提供安全可靠的服务端认证的。


摘要具体格式如下(DH):

The hash H is computed as the HASH hash of the concatenation of the following:
      string    V_C, the client's identification string (CR and LF
                excluded)
      string    V_S, the server's identification string (CR and LF
                excluded)
      string    I_C, the payload of the client's SSH_MSG_KEXINIT
      string    I_S, the payload of the server's SSH_MSG_KEXINIT
      string    K_S, the host key
      mpint     e, exchange value sent by the client
      mpint     f, exchange value sent by the server
      mpint     K, the shared secret

所以说,MITM只可能发生在第一次认证服务器的时候,这个时候客户端或许不能确认host key的真实身份。而之后,host key一定是被保障的,中间人也一定无法用被保障的private key签名。(其实也是存在一种可能的,就是某个受信任的host充当中间人的角色),引用一段我觉得很精彩的论述

All of this relies on the magic of asymmetric cryptography: You can verify a signature with the public key, but producing new signatures requires knowledge of the private key, and the private key cannot be (practically) deduced from the public key. The DH key exchange consists in publicly exchanged messages, sender and receiver finally agree on a "shared secret" obtained by processing these messages, and yet someone observing only these messages cannot recompute the same secret. Mathematics are involved. @Thomas Pornin

客户端认证

认证完服务器后,紧接着客户端就会发送一个New key(SSH_MSG_NEWKEYS21)表明接下来要使用对称加密进行通信了。之后,wireshark就无法抓取到可读包裹了,只能通过ssh -vvv选项学习理解。


扩展协商

在讨论客户端认证流程之前必须提及2018年4月更新的关于扩展协商的相关标准,它提供了一种机制去帮助客户端和服务器在密钥交换完成之后,秘密地交换各自的扩展。这个标准制定的主要原因,是因为SSH在原本的设计中没有一个良好的机制去确认服务端支持何种公钥认证机制,尤其是当RSA with SHA-256、SHA-512被广泛使用以后,如果没有一个良好的扩展协商机制,就无法避免客户端再认证时候的尝试与错误,以及认证错误的惩罚。

标准规定,支持扩展协商的客户端和服务端可以在密钥交换初始化过程中,将自己支持扩展交换这个机制以一种加密算法的形式写如各自的算法列表(如果他们希望知道对方的扩展)。

  • 服务端写入ext-info-s
  • 客户端写入ext-info-c

这样做有两个好处:

  1. 可以知道对方是否支持扩展协商
  2. 而这个扩展协商因为双方各自表示的不同,自然地不可能被当作加密算法被使用。

同时,双方一旦标注自己支持扩展协商,就必须做好被对方使用SSH_MSG_EXT_INFOpacket告知其扩展信息的准备。同时,这个告知对方自身扩展信息SSH_MSG_EXT_INFOpacket,对使用顺序有明确的规定。 如果由客户端发送自身扩展信息则必须满足一个条件:

  1. 这个packet是紧接着客户端的SSH_MSG_NEWKEYS发送的。

如果由服务端发送自身扩展信息则必须满足以下任意一个条件:

  1. 这个packet是紧接着服务端的SSH_MSG_NEWKEYS的。 因为客户端需要这个扩展信息来进行认证处理,所以它应该尽可能快的发送过去(紧接着SSH_MSG_NEWKEYS).
    但是客户端不能指望必须有这个消息,因为服务端发送这个消息并不是被要求的。同时,由于网络延迟,客户端可能已经请求了SSH_MSG_SERVICE_REQUEST,并开启了之后的认证协议的过程,但是如果及时送达,那客户端就可以在这个基础上进行认证。
  2. 在准备发送服务端的SSH_MSG_USERAUTH_SUCCESS之前立马发送。
    这是服务器第二次发送的机会,不论它是否在条件1发送过。标注了ext-info-c的客户端都必须接受服务端的SSH_MSG_USERAUTH_SUCCESS不论是来自于哪一个条件的发送,但是客户端不准期待(要求)服务器总是会发送它(可能服务器并不支持这个扩展交换的标准)。
    这使得服务器可以发送一些不愿意公布给未认证的客户端的扩展信息。如果,服务器发送了第二种情况的SSH_MSG_EXT_INFO,它将取代所有的第一次发送的扩展,服务器和客户端需要重新计算使得扩展生效。服务器的第二次发送,同样匹配客户端的第一次发送。
    标准决定第二次发送在这样的时机下是由于以下的原因:如果这个消息过早得发送,服务器无法保留住必要的信息在客户端被认证之前。如果它稍晚德发送,确实需要这个第二次信息地客户端在他被认证之后将没有一个可靠的方法去知道是否要期待(等待)这个消息。扩展的生效时间需要被明确指出,而扩展必须和其陈列的顺序无关。

server-sig-algs

这个扩展只允许存在在服务端中,它包含服务端支持的所有公钥认证算法列表,早期的一些SSH服务器或许没有提供所有的算法。

一个希望使用公钥认证的客户端或许可以等待来自服务端的SSH_MSG_EXT_INFO,这样他就能提交合适的公钥,而不是试错。同时,支持了公钥认证的SSH服务器应该支持这一扩展。

如果客户端没有发送这个扩展,客户端不准做出服务端支持任何算法的假设,而是应该试错,同时这些实现必须知道他们提供错误的公钥算法的公钥进行认证会受到惩罚。

认证惩罚由服务器发出,用来阻止暴力密码猜测用户名枚举和一些其他的被程序实现者或服务器管理员认为有嫌疑的行为类型。惩罚或许会包括IP限流或制止,这些惩罚或许会触发服务器的邮件提醒审计记录

delay-compression

它的extension-value包含两个算法列表:c->s的列表和s->c的列表。这个扩展的意义在于,它允许客户端和服务端直接的协商压缩算法,而不需要发起一个key re-exchange

这个扩展只有在双方都发送的时候才生效,名称列表或许会包含在算法协商时可以被采纳的算法,除了那些自定义的算法(因此只允许标准定义的算法)。比如说:'zlib,none'时一个符合规范的名称列表,但是'zlib@openssh.com'却不是。

如果双方都发送了这个扩展,且找不到共同的算法,则使用如同协商失败一样的方式断开连接。如果生效,则在每一端的触发消息之后都将使用新的压缩算法:

  1. 来自服务端方向的packet,在发送SSH_MSG_USERAUTH_SUCCESS之后。
  2. 来自客户端方向的packet,在SSH_MSG_NEWCOMPRESS之后。

如果扩展生效,客户端必须在收到SSH_MSG_USERAUTH_SUCCESS后的合理数目的发出消息后发送SSH_MSG_NEWCOMPRESS消息,它并不要求是之后的第一个消息。

这样做的原因是为了避免触发race condition(竞争条件)——服务器无法可靠的知道来自客户端的消息是它收到SSH_MSG_USERAUTH_SUCCESS之前或是之后的。比如在登陆期间,客户端可能会发送keep-alive消息。和其他的扩展一样,除非另有说明,服务器会直到第二次发送SSH_MSG_EXT_INFO并且在发送SSH_MSG_USERAUTH_SUCCESS之前才启用扩展。这也使得服务端可以避免在客户端被认证前花费算力使用压缩。

当各方重新协商压缩算法且又正在使用这一扩展时,如果重新协商的压缩算法在一个或连两个方向上没有改变,一旦重新协商的算法生效,内部的压缩状态也必须在每个方向上被重置。

由于这个扩展的时间区间可能会和Key Re-Exchange发生冲突,标准做出规定对于已经声明扩展,或将要声明扩展的一方禁止初始化Key Re-Exchange直到下面的情况有一项满足:

  1. 扩展已经协商完成,并且这希望开始rekey的一方已经发送了它的上文所述的trigger message。
  2. 这一方(如果是服务器)已经发送或者(如果是客户端)接收了SSH_MSG_USERAUTH_SUCCESS且扩展没有协商成功。

no-flow_control

这个扩展有两个值可选:'p'代表推荐的,'s'表示支持的。它只有在双方都提供扩展且至少有一方为'p'时才生效。

如果扩展生效,在SSH_MSG_CHANNEL_OPENSSH_MSG_CHANNEL_OPEN_CONFIRMATIONpackets的initial window size区域将被认为是无意义的,频道将被视作所有的窗口大小都是无穷大的。同时所有接收到的SSH_MSG_CHANNEL_WINDOW_ADJUST消息也都将被忽略。

这个扩展是为(但不局限于)文件传输应用而设计的,它们只准备使用一个频道,对于他们而言SSH提供的流控并不是一个功能而是一个阻碍。如果这个扩展生效,实现必须阻止打开超过一个同时的频道,但服务器应该支持客户端打开不止一个非同时的频道。

标准推荐优先实现这一扩展,在没有这一扩展之前一些应用都会选择不实现流控,而是通过发送一个初始大小为2^32-1字节的频道窗口,标准不建议再这么做因为:

  1. 当传输超过2^32字节的时候是合理的,但是如果另一端实现SSH流控,那么这一端的服务将被挂起。
  2. 这样的实现无法处理大尺寸的频道窗口,而且它会出现不优雅的行为,包括断开连接。

elevation

elevation(提升)或者elevated指的是一种操作系统的机制,这种机制使得管理员登陆的会话可以关联到两个安全上下文:一个受限和一个拥有管理员权限。提升会话就是启用完全的管理员权限(这是Windows的一个机制: WINADMINWINTOKEN)。

该扩展提供三个选项,'y'、'n'、'd'分别表示提升、不提升、由服务器决定。设计这个扩展的好处是,这可以减少提供SSH登录的Windows服务器的受攻击表面。对于不支持此扩展的客户端,服务端必须提升会话以确保一登录就有完全的管理员权限,否则可以随时提升会话只要客户端发出该请求。

公钥认证

公钥类型

在标准中使用了三个层面去定义公钥的类型:

  1. 公钥格式
    这个公钥是如何编码的,证书是如何被展现的。在协议中的key blob或许除了包括公钥外,还会额外包括证书。
  2. 签名、加密算法
    或许有一些公钥类型既不支持签名也不支持加密。同时,公钥的使用也会被政策条款约束。所以,不同的公钥类型应该被定义应对不同的政策。
  3. 签名或加密数据本身的编码
    这包括但不限于padding、字节序规定、数据格式等。

例如:

ssh-rsa RECOMMENDED sign Raw RSA Key(初始定义的RSA公钥类型),它表示ssh-rsa是被推荐使用的公钥类型,它是可签名的,同时它不使用证书而是用RSA公钥ssh-rsa是这个公钥类型的标识。

      string    "ssh-rsa"
      mpint     e
      mpint     n

这表示,这个公钥类型的公钥格式符合如下编码:字符串ssh-rsa紧接着一个mpint的参数empint的参数n。同时en将被用于构建签名的key blob

同时标准规定使用这个公钥格式进行签名和校验需要使用SHA-1 hash并配合RSASSA-PKCS1-v1_5 scheme。签名的结果将以如下形式编码:

      string    "ssh-rsa"
      string    rsa_signature_blob (该值被编码成一个包含无长度或padding、无符号符合网络字节序的整型的字符串)

publickkey公钥认证方法

publickey方法是唯一被要求服务端必须实现的认证方法(其他的还包括password、hostbased),在这个认证模型中有私钥权限的一方(若使用publickey进行客户端认证,那这一方就是客户端)必须为认证提供数据。这个方法的大体流程如下:

  1. 使用用户的私钥创建一个签名,并将其发送给服务器。
  2. 服务器必须确认:
    1. 公钥是否是受信任的。
    2. 签名是否是有效的。


  1. 如果都满足则必须通过认证,否则必须阻止。

私钥一般以一种加密的形式存储在客户端,用户必须提供passphrase解密私钥才能生成签名。即使它们不是这样存储的,签名本身也应该需要涉及一些昂贵的计算。为了避免不必要的处理过程和用户交互,在生成签名前需要提供下面的消息给服务端,询问使用这个公钥进行公钥认证是否是可被接受的:

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name in ISO-10646 UTF-8 encoding [RFC3629]
      string    service name in US-ASCII
      string    "publickey"
      boolean   FALSE
      string    public key algorithm name
      string    public key blob 

如果是被接受的(在authorized_keys文件中),服务器则发送如下消息:

      byte      SSH_MSG_USERAUTH_PK_OK
      string    public key algorithm name from the request
      string    public key blob from the request

否则发送 SSH_MSG_USERAUTH_FAILUR。 之后客户端发送签名数据正式请求认证:

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "publickey"
      boolean   TRUE
      string    public key algorithm name
      string    public key to be used for authentication
      string    signature

其中signature私钥对如下字段进行的签名

      string    session identifier
      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "publickey"
      boolean   TRUE
      string    public key algorithm name
      string    public key to be used for authentication

若成功则服务端发送SSH_MSG_USERAUTH_SUCCESS;若失败则发送SSH_MSG_USERAUTH_FAILURE——客户端继续尝试。

rsa-sha2-256

2018年3月新发布的rfc定义了rsa-sha2-256rsa-sha2-512算法配合上文所述的server-sig-algs扩展让SSH使用这些算法进行服务端、客户端认证。

我的设备使用的就是推荐使用的rsa-sha2-256,具体的认证请求和上文所述的ssh-rsa基本一致:

     byte      SSH_MSG_USERAUTH_REQUEST
     string    user name
     string    service name
     string    "publickey"
     boolean   TRUE
     string    "rsa-sha2-256"
     string    public key blob:
         string    "ssh-rsa"
         mpint     e
         mpint     n
     string    signature:
         string    "rsa-sha2-256"
         string    rsa_signature_blob

同时在标准中还有如下关于7.2版本openssh错误的警告。

OpenSSH 7.2 (but not 7.2p2) incorrectly encodes the algorithm in the signature as "ssh-rsa" when the algorithm in SSH_MSG_USERAUTH_REQUEST is "rsa-sha2-256" or "rsa-sha2-512". In this case, the signature does actually use either SHA-256 or SHA-512. A server MAY, but is not required to, accept this variant or another variant that corresponds to a good-faith implementation and is considered safe to accept.

traffic analysis

因为加密的原因,这里将根据-vvv与上述标准分析ssh如何执行认证流程。我(主要)根据The Secure Shell (SSH) Protocol Assigned Numbers翻阅替换,总结如下。

SSH_MSG_NEWKEYS             21
SSH_MSG_SERVICE_REQUEST     5
SSH_MSG_EXT_INFO            7
SSH_MSG_SERVICE_ACCEPT      6
SSH_MSG_USERAUTH_REQUEST    50
SSH_MSG_USERAUTH_FAILURE    51
SSH_MSG_USERAUTH_SUCCESS    52

​详情

具体流程如下:

  1. 交换密钥完成,发送SSH2_MSG_NEWKEYS表明以后的数据使用对称加密通信(服务端使用相同packet响应)。
  2. 客户端根据-i选项找到明确指定的将要尝试认证的公钥文件。
  3. SSH_MSG_SERVICE_REQUEST请求服务端开启认证协议。
  4. 同时服务端根据客户端在密钥交换算法中的'ext-info-c'请求,使用SSH_MSG_EXT_INFO公示自身的公钥算法扩展,告知客户端自己支持rsa-sha2-256,rsa-sha2-512
  5. 服务端接受请求,SSH_MSG_SERVICE_ACCEPT。
  6. 客户端发送SSH_MSG_USERAUTH_REQUEST,并选择method为publickey(又或许是空的)。
  7. 服务端发送SSH_MSG_USERAUTH_FAILURE,并标明其接受的认证方法只有publickey。
  8. 客户端直接启用publickey模式,并寻找privatekey。
  9. 对于第一个发现的(被指定的)privatekey,请求输入passphrase解密私钥,客户端直接签名并发送请求。(而没有尝试询问服务器,这个publickey是否是被授权的(另:openssh client没有,而Bitvise SSH client有))
  10. 服务端返回SSH_MSG_USERAUTH_SUCCESS

密码认证

如果使用密码认证,客户端需要发送如下的packet:

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "password"
      boolean   FALSE
      string    plaintext password in ISO-10646 UTF-8 encoding [RFC3629]

这里规定协议传输的密码串必须是以ISO-10646 UTF-8格式编码的,这意味着客户端和服务端在接受输入和最后对比系统(编码)密码的时候都或需要做相应的转换。同时如果协商出的加密算法是none则,不允许使用密码认证,密码修改也不应该被允许。

对于密码国际化的问题,SSH希望用户输入密码的时候,不考虑当前系统和软件是什么,认证工作依旧能顺利进行。所以需要先对能支持非ASCII密码的系统中的密码和用户名进行规范化当他们要添加数据库或对比的时候,SSH的实现应该使用[RFC4013]对双方存储和对比的密码进行规范化。

最后如果密码过期,服务端应该发送如下packet:

      byte      SSH_MSG_USERAUTH_PASSWD_CHANGEREQ
      string    prompt in ISO-10646 UTF-8 encoding [RFC3629]
      string    language tag [RFC3066]

客户端则需要回复如下:

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "password"
      boolean   TRUE
      string    plaintext old password in ISO-10646 UTF-8 encoding
                 [RFC3629]
      string    plaintext new password in ISO-10646 UTF-8 encoding
                 [RFC3629]

最后的服务器会有如下回复:

  • SSH_MSG_USERAUTH_SUCCESS 密码已经被改变,已经认证成功
  • SSH_MSG_USERAUTH_FAILURE with partial success 密码已经被改变,但是需要再认证一次
  • SSH_MSG_USERAUTH_FAILURE without partial success 密码没有被改变,要么是修改密码功能不被支持,要么是旧的密码是错误的。注意,如果服务器已经发送了SSH_MSG_USERAUTH_PASSWD_CHANGEREQ,就说明他是支持修改密码的。
  • SSH_MSG_USERAUTH_CHANGEREQ 修改的密码不被接受,可能是修改的密码太简单或者其他。

Host-Based认证

一些希望站点在自己主机上登陆远程主机用户的认证方式。这样不符合高的安全等级,但他确实很方便。这是一个可选的认证方式,在实现的时候一定要注意防止一个普通的用户有能力窃取到服务器的private host key。

如果这这种认证方式被启用,客户端只需要提供包括远程host名称远程host公钥远程host私钥签名等数据在内的packet:

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "hostbased"
      string    public key algorithm for host key
      string    public host key and certificates for client host
      string    client host name expressed as the FQDN in US-ASCII
      string    user name on the client host in ISO-10646 UTF-8 encoding
                 [RFC3629]
      string    signature

被签名的内容为:

      string    session identifier
      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name
      string    service name
      string    "hostbased"
      string    public key algorithm for host key
      string    public host key and certificates for client host
      string    client host name expressed as the FQDN in US-ASCII
      string    user name on the client host in ISO-10646 UTF-8 encoding
                 [RFC3629]

服务器甚至可以忽略具体的user name,它必须要做的只有检查host key是否是真的,已经签名是否是有效的。

比较奇怪的是,协议中没有明确的规定如何返回这类认证被拒绝具体packet的解释,比如当远程host签名无效,又或者服务器不支持远程host的公钥算法等。

keyboard-interactive认证

前面的各种认证方法都十分依赖于双端的实现,如果新版本的服务器由启用了一个新的认证方法,那就意味着客户端也必须更新添加方法。又因为“认证”这件事的特殊性(本质上它的计算工作是完全在服务端完成的),人们发现可以设计一种通用的认证方法,像HTTP协议传送的HTML一样(服务端传给客户端的东西客户端并不需要知道他具体是什么,只需要把它显示出来,剩下的交给用户去识别和输入),这种方法的设计目的就是尽可能的让客户端对于认证过程完全不知情,并且一切都基于用户使用键盘进行的数据交互。使用这种方法,服务端可以轻松的扩展具体的认证算法,而客户端可以不需要做任何的更新。

使用这种认证方法,可以轻松的实现挑战应答式认证(客户端返回Hash(随机数(挑战)+密码))和OTP(每次一密码,如短信登陆)。但是,它在客户端依然是有缺陷的,如果要使用它在认证机制中则不允许有特殊编码,比如硬件驱动或者是password mangling(把简单密码变成复杂密码的工具)。

主要的消息有3个,SSH_MSG_USERAUTH_REQUEST、SSH_MSG_USERAUTH_INFO_REQUEST、SSM_MSG_USERAUTH_INFO_RESPONSE。

SSH_MSG_USERAUTH_REQUEST由客户端发送给服务端,用于启用这一认证方法,语言标签是不推荐使用的。同时如果客户端足够确信可以把用户期望使用的方法填在submethods里,逗号分隔。之后服务端可以根据自己的实现选择是否参照submethods,提示用户选择具体的认证方法。

      byte      SSH_MSG_USERAUTH_REQUEST
      string    user name (ISO-10646 UTF-8, as defined in [RFC-3629])
      string    service name (US-ASCII)
      string    "keyboard-interactive" (US-ASCII)
      string    language tag (as defined in [RFC-3066])
      string    submethods (ISO-10646 UTF-8)

服务端收到上述方法后必须回复SSH_MSG_USERAUTH_SUCCESS、SSH_MSG_USERAUTH_FAILURE、SSH_MSG_USERAUTH_INFO_REQUEST消息其中的一个。并且协议规定如果用户填写了错误的service name或者user name服务器并不能直接发送SSH_MSG_USERAUTH_FAILURE。相反的,它应该再一次发送SSH_MSG_USERAUTH_INFO_REQUEST,然后忽略客户端对于这个request的回应(不论他是否是完全认证正确的),再在一个可配置的时间(默认是2秒)后发送SSH_MSG_USERAUTH_FAILURE,这么做的目的是阻止用户通过对比结果猜测出合法的用户名。

之后服务端则根据自己的实现选择一个认证方式询问客户端。

      byte      SSH_MSG_USERAUTH_INFO_REQUEST
      string    name (ISO-10646 UTF-8)
      string    instruction (ISO-10646 UTF-8)
      string    language tag (as defined in [RFC-3066])
      int       num-prompts
      string    prompt[1] (ISO-10646 UTF-8)
      boolean   echo[1]
      ...
      string    prompt[num-prompts] (ISO-10646 UTF-8)
      boolean   echo[num-prompts]

name为请求信息的名称,instruction为具体的详细指示用户填写的内容,每个prompts为每次STDIN打开前输出的提示。协议考虑到不同的终端设备在显示上的能力或许会有局限,这个输出局限或许会对认证的结果造成影响。所以,应该至少保证instruction完全被显示,name和prompt至少30个字符显示,且如果被截取必须明确地让用户知道发生了截取。哪怕,num-prompt为0时,instruction和name一样需要被正常显示(比如,通知最后认证成功的时候,就不再需要用户输入了)。

如下是用户做出的应答的消息格式,num-responses必须和num-prompts长度一致。密码输入、传输的部分和上文一样,一样需要被规范化。

      byte      SSH_MSG_USERAUTH_INFO_RESPONSE
      int       num-responses
      string    response[1] (ISO-10646 UTF-8)
      ...
      string    response[num-responses] (ISO-10646 UTF-8)

最后,协议规定如果认证发生错误(密码错误之类),不应该发送SSH_MSG_USERAUTH_INFO_REQUEST让用户重新输入,除非是这个认证是(类似于或运算)的补充形式。因为,还有其他的认证方法需要被尝试。下文是RFC描述的一个可以被运用的样例:

​详情

S: byte SSH_MSG_USERAUTH_INFO_REQUEST S: string "Password changed" S: string "Password successfully changed for user23." S: string "en-US" S: int 0 [Client displays message to user]

C: byte SSH_MSG_USERAUTH_INFO_RESPONSE C: int 0

S: byte SSH_MSG_USERAUTH_SUCCESS</code></pre> </details>

小结

花了三四天的时间整理这篇文章,有了一些体会与理解:

  1. 在密码学的相关程序实现中,密钥对是1-1对应的。
  2. 非对称加解密的速度不及对称加解密,所以在SSH中的private/public key主要是用来认证的。
    1. private key加密的数据叫做签名。(签名 + public key)is a proof of possession of a private key。
    2. public key可以从private key文件中导出(所以客户端一个private文件就可以认证)。
  • ssh-keygen -y -f ~/.ssh/id_rsa openssh标准
    • The key type
    • A chunk of PEM-encoded data
    • A comment


      • ssh-keygen -e -f ~/.ssh/id_rsa ssh标准rfc4716


    1. 最原始的安全保障是由Key Exchange Algrorithm提供的。


  1. 椭圆曲线密钥交换算法的绝大多数参数都是预设的,不像DH需要临时协商。
  2. 如果需要制定一套协议或是标准,例如rfc,数据类型的编码是必须要被考虑在内的;同时应该尽量的将协议设计成不依赖于次序的。
  3. ssh的verbose选项提供最多3个v,来输出不同层级的debug信息。
  4. 因为私钥不需要传递所以rfc4176只表明其是公钥格式,但其实它也是私钥文件的格式,在ssh-keygen中有说明,默认keygen出来的私钥就是符合rfc4176格式的。
  5. ssh -i选项可以手动临时添加私钥,并且它将最先被尝试。之后将一次尝试 ssh-add -L中的私钥,最后尝试.ssh/id_rsa等。

Reference

  1. OpenSSH Specifications
    这是OpenSSH所展现的最直接的资料页面,但是有很多细节部分的规格与实现没有罗列。而且RFC文档错综复杂,有很多地方都引用不全必须靠“幸运”才能翻看到。
  2. Cryptography of SSH
    在这个页面可以很直观的看到到DH密钥交换协议的流程。
  3. The Secure Shell (SSH) Protocol Architecture
    SSH协议架构的rfc页面,它将SSH分为三部分,传输、认证和连接。
  4. The Secure Shell (SSH) Transport Layer Protocol
    SSH传输层协议的rfc页面,在这里主要讨论如何为协议提供加密、服务端认证、数据完整性保护、压缩协议内容等功能。
  5. Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer
    这篇rfc将ECDH算法集成在SSH传输层协议中,用作签名和密钥交换。
  6. curve25519-sha256@libssh.org.txt
    这篇文章描述了curve25519密钥交换算法,它提供了一个可以替代的方法给DH算法或ECDH算法。
  7. Key Exchange (KEX) Method Updates and Recommendations for Secure Shell (SSH)
    2016年更新的SSH密钥交换算法更新建议。
  8. Secure Shell (SSH) Key Exchange Method using Curve25519 and Curve448 draft-ietf-curdle-ssh-curves-00
    这篇文章描述了在SSH中如何实现Curve25519。
  9. Curve25519: New Diffie-Hellman Speed Records
    25519曲线原论文。
  10. Elliptic Curves for Security
    具体介绍Curve25519在密码学中的应用,相关函数使用说明。
  11. Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints
    介绍SSH fingerprint以及推荐的使用方式。
  12. Extension Negotiation in the Secure Shell (SSH) Protocol
    介绍SSH中如何查询扩展信息,并使用共有的(认证)相关扩展。
  13. Use of RSA Keys with SHA-256 and SHA-512 in the Secure Shell (SSH) Protocol
    标准规定扩展使用RSA配合SHA摘要算法完成认证操作。
  14. The Secure Shell (SSH) Protocol Assigned Numbers
    规定协议中各种ID(宏)所使用的序号。
  15. The Secure Shell (SSH) Public Key File Format
    SSH定义的公钥文件格式标准。
  16. Generic Message Exchange Authentication for the Secure Shell Protocol (SSH)
    这个标准定义了一种基于键盘交互的通用认证方法。
编辑于 2019-05-17