背景
在开篇之前,让我们先对 SSH 协议有个宏观的大致了解,这样更有利于我们对本文的加深了解。首先要提到的就是计算机网络协议,所谓计算机网络协议,简单的说就是定义了一套标准和规则,使得不同计算机之间能够进行正常的网络通信,不至于出现在一台机器上发出的指令到另一台机器上成了不可认的乱码,SSH 就是众多协议的其中之一。经典的七层 OSI 模型(Open System Interconnection Reference Model)出现后,大大地解决了网络互联的兼容性问题,它将网络划分成服务、接口和协议三个部分,而协议就是说明本层的服务是如何实现的。SSH、Telnet 协议则主要被使用在用户层中(如图 1 深色部分所示),即应用层、表现层和会话层。
图 1. 七层 OSI 模型
介绍 SSH
什么是 SSH
SSH(Secure Shell Protocol)是在一个不安全的网络,进行安全远程登录和其他安全网络服务的协议。这个定义出自于 IETF(Internet Engineering Task Force)。在 TCP/IP 五层模型中,SSH 是被应用于应用层和传输层的安全协议。
SSH 的优点
传统的网络传输,如:Telnet、FTP 等,采用的是明文传输数据和口令,这样很容易被黑客这样的中间人嗅探到传输过程中的数据,大大降低了网络的通信安全。而 SSH 协议则采用数据加密的方式建立起一个安全的网络传输信道,增强了数据在网络传输过程中的安全性。数据加密程度的复杂,会导致占用更多的网络资源。SSH 会对加密数据进行一定的压缩操作,从而减缓对网络带宽的占用。总结起来,SSH 的优点如下:
- 数据加密,提高安全性
- 数据压缩,提高网络的传输速度。
SSH 的架构
在对 SSH 有了一个初步的认识之后,我们来看看 SSH 协议是如何做到数据的安全通信。首先来看下 SSH 协议的主要架构:
图 2. SSH 协议的构成
传输层协议: 通常运行在 TCP/IP 的上层,是许多安全网络服务的基础,提供了数据加密、压缩、服务器认证以及保证数据的完整性。比如,公共密钥算法、对称加密算法、消息验证算法等。
用户认证协议:运行在 SSH 协议的传输层之上,用来检测客户端的验证方式是否合法。
连接协议:运行在用户认证层之上,提供了交互登录会话、远程命令的执行、转发 TCP/IP 连接等功能,给数据通讯提供一个安全的,可靠的加密传输信道。
SSH 的应用
在实际的工作中,很多目标机器往往是我们无法直接操作的,这些机器可能是一个公司机房的服务器,也可能是一个远在大洋彼岸的客户环境。这时候我们必须要远程登录到目标机器,执行我们需要的操作,这样不仅降低了运营成本,也提高了执行效率。我们常见的远程登录协议有 SSH、Telnet 等。如上文所提到,Telnet 使用的是明文传输,这样对别有用心的“中间人”来说就有了可乘之机,相对 Telnet 协议,SSH 协议的安全性就高了很多。这样的特性,也使得 SSH 协议迅速被推广,很多的大型项目中都或多或少的使用到了这个协议。下面本文主要讨论 SSH 协议中用户认证协议层,并且下文中统一将远程机器称为服务器(Server),本地机器称为客户端 (Client)。
SSH 的认证协议
常见的 SSH 协议认证方式有如下几种:
- 基于口令的验证方式(password authentication method),通过输入用户名和密码的方式进行远程机器的登录验证。
- 基于公共密钥的安全验证方式(public key authentication method),通过生成一组密钥(public key/private key)来实现用户的登录验证。
- 基于键盘交互的验证方式(keyboard interactive authentication method),通过服务器向客户端发送提示信息,然后由客户端根据相应的信息通过手工输入的方式发还给服务器端。
SSH 认证协议的工作原理
SSH 的主要工作流程:
图 3. SSH 登录工作流程
通过这个张流程图,我们可以看出,在用户对远程机器访问的时候,首先,是得到了服务器端的一个连接句柄,这里可以理解为是一个 session,然后客户端可以通过这个句柄取得一些服务器的基本信息,如 SSH 的版本,服务器的版本信息以及一些加密的算法信息等。其次,客户端可以对这些信息作分析,来匹配当前的客户端的加密算法、验证方式是否符合服务器的配置,然后取得彼此可接受的方式,这里可以认为是双方的协商。最后,当双方达成一致后,一个安全的信道也就真正建立起来了,此时用户就可以对远程机器做想要的操作了。当我们对此有了一定的了解后,就可以初步判断,在平时工作中,我们通过 SSH 协议去连接一个远程机器报错的时候,问题出现在哪个流程上。下面通过具体的 Java 例子来讲解用户验证方式的原理。
常见认证方式的 Java 实现
在开始前,我们要做一些环境的准备工作。
- 一台本地机器,操作系统是 Windows 用来作为客户端
- 一台远程机器,操作系统是 Linux 用来作为服务器端
- OpenSSH 工具
- Putty 工具
首先,要确保服务器端上已经安装了 OpenSSH 工具,并且 SSH 的服务已经启动,可以通过如下命令来进行查看:
查看是否已经安装了 OpenSSH
清单 1. OpenSSH 版本
# rpm -qa | grep ssh openssh-5.1p1-41.31.36 openssh-askpass-5.1p1-41.31.36
查看 SSH 服务是否启动。
清单 2. SSH 的服务状态
#/etc/init.d/sshd status
Checking for service sshd running 在 Windows 机器,即客户端上尝试使用 Putty 工具连接远程机器。
图 4. SSH 连接成功
到目前为止,我们已经可以正常的连接到这台远程机器。下面我们就要通过 Java 代码的方式来实现我们自己的这个远程登录的操作。
验证 service name
在 SSH 协议中定义了一些消息代码,而 50 至 79 这些代码是保留给用户认证协议层使用的,而 80 以上的数字是用于协议运行的,所以如果在用户认证协议验证之前,如果我们得到的消息代码是这个范围的,SSH 会返回错误信息,并断开连接。例如如以下几种消息所对应的代码号:
SSH_MSG_USERAUTH_REQUEST 50:用户发送一个验证请求。
SSH_MSG_USERAUTH_FAILURE 51:用户验证请求失败。
SSH_MSG_USERAUTH_SUCCESS 52:用户验证请求成功。
那么对于不同的认证方式,又有其各自的消息代码。
在每次客户端发送请求的时候,服务器都会检查当前的 service name 和 username 是否合法,如果当前的 service name 或者 username 不可用,那么服务器端会立刻断掉请求连接。
下面来实现一个对 service name 验证的请求,发送数据格式如下:
byte SSH_MSG_SERVICE_REQUEST
string service name in US-ASCIII
具体代码如下:
清单 3. 类 AuthServiceRequest
package com.my.test.ssh2.auth; import com.my.test.ssh2.common.ProcessTypes; public class AuthServiceRequest { private String serviceName; public AuthServiceRequest(String serviceName){ this.serviceName = serviceName; } /** * 取得指定服务器名称的认证消息 * @return request – 返回一条十六进制消息 **/ public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_SERVICE_REQUEST); type.asString(serviceName); request = type.getBytes(); return request; }}
转换后发送的消息如下:
[5, 0, 0, 0, 12, 115, 115, 104, 45, 117, 115, 101, 114, 97, 117, 116, 104]
然后再对此进行算法加密,发送到服务器端。
当前协议使用的 service name 是”ssh-userauth”,如果客户端请求的不是这个 service name,那么服务器会报如下错误:
清单 4. service name 异常
Caused by: java.io.IOException: Peer sent DISCONNECT message (reason code 2): bad service request demo-ssh-auth
如果客户端通过了 service name 的验证后,下一步我们就可以实现具体的认证方式了。流程图如下所示:
图 5. Authentication 类图
TransportManager 类是用来处理传输协议层的业务逻辑。在这里主要处理数据的解密、加密、压缩等操作,这些功能的具体实现主要通过 TransportControl 类来完成,TrasportControl 类会根据客户端和服务器端协商的数据算法来选择具体的算法如 sha-1、MD5 等。通过 TransportControl 类处理完的数据会存储到 Packets 类里,生成一个数据包的列表,为 AuthManager 类提供必要的数据信息。Connect 类是用来处理连接协议层的业务逻辑。主要用于得到一个远程机器的连接句柄、产生一个安全信道、对 TransportManager 类做数据初始化操作等。AuthManager 类是用来处理认证协议层的业务逻辑。主要是对不同登录认证方式的请求和从服务器端得到的请求回复做处理,通过客户端选择的不同的认证方式调用不同的认证方式实现类,比如 AuthRequestByPassword 类定义了通过密码认证方式的实现。
图 6. 认证协议流程图
首先,开启一个线程用来接收从服务器发送的加密数据包,然后对这个数据包做算法解密处理并放到一个模型化的堆栈 (Packet List),而另一个线程会监听当前的 Packet List 里是否有可用的数据包,并对其做解析处理包括对数据包是否合法、是否满足某种认证算法等。如果数据包所包含的认证方式和当前客户端请求的认证方式不匹配,那么,客户端就会失去服务器的连接。反之,如果客户端请求的认证方式包含在服务器开启的认证方式,客户端会返回给服务器一个成功请求,并建立连接会话。
none 认证方式
无认证方式(none authentication),这种认证方式通常是在第一次请求发送的时候使用的,因为通过这个认证方式,我们可以得到当前服务器端支持的所有认证方式的列表,通过这个列表我们就可以验证我们想要使用的认证方式是否被服务器端所支持。当然,如果远程目标机器支持这种 none 认证方式,那么客户端就直接得到了一个会话连接,但是这种认证方式是 SSH 协议里所不推荐使用的。
实现代码如下:
清单 5. 类 AuthRequestByNone
package com.my.test.ssh2.auth; import com.my.test.ssh2.common.ProcessTypes; public class AuthRequestByNone { private String userName; private String serviceName; public AuthRequestByNone(String serviceName, String user) { this.serviceName = serviceName; this.userName = user; } /** * 取得指定服务器名称和用户名的认证消息 * @return request - 返回一条十六进制消息 * */ public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_USERAUTH_REQUEST); type.asString(userName); type.asString(serviceName); type.asString(AuthConstant.SSH_NONE_AUTHENTICATION_METHOD); request = type.getBytes(); return request; } }
从流程图 6 中可以看出,当我们发送一个 none 认证方式的时候,如果服务器不支持 none 认证,那么客户端就可以取得服务器端的认证方式列表。首先解析服务器端返回的包信息,例如:
[51, 0, 0, 0, 34, 112, 117, 98, 108, 105, 99, 107, 101, 121, 44, 103, 115, 115, 97, 112, 105, 45, 119, 105, 116, 104, 45, 109, 105, 99, 44, 112, 97, 115, 115, 119, 111, 114, 100, 0]
经过客户端的算法解析之后,我们可以得到含有如下信息的数据包:( 对于算法原理的具体说明是定义在传输层协议里,不是本文所讨论的范围。)
代码 51,说明用户验证请求失败。
从第 5 位到第 34 位记录了所当前服务器所支持的认证方法,解析后可得到 publickey, gssapi-with-mic, password 的一个由逗号隔开的认证方式字符串。
最后一位 0 表示,当前的请求失败,但并不是说整个的连接就断掉了。
解析数据包的具体算法如下:
清单 6. 解析数据算法
((arr[pos++] & 0xff) << 24) | ((arr[pos++] & 0xff) << 16) | ((arr[pos++] & 0xff) << 8) | (arr[pos++] & 0xff);
对于服务器端来说,它要对客户端的请求作一反馈,从而说明当前的请求是否成功。
数据格式如下:
byte SSH_MSG_USERAUTH_FAILURE name-list authentications that can continue boolean partial success
所以,这就正好解释了上述,从服务器端得到的数据解析结果。
实现部分代码如下:
清单 7. 初始化函数
public boolean initialize(String userName) throws IOException{ // 预处理服务名称的请求 AuthServiceRequest serviceRequest = new AuthServiceRequest(AuthConstant.SSH_SERVICE_NAME); IManager transManager = ManagerFactory.getManager(Constant.TRANSPORT_LAYER); transManager.sendMessage(serviceRequest.getRequestMessage()); // 处理无认证方式的消息请求 AuthRequestByNone authNone = new AuthRequestByNone(AuthConstant.SSH_CONN_SERVICE_NAME,userName); transManager.sendMessage(authNone.getRequestMessage()); byte[] message = getMessage(); // 验证当前的服务名称是否合法 if(!isAccepted(message)){ return false; } // 取得无认证方式的请求数据包 message = getMessage(); // 验证当前的请求是否成功 if(isRequestFailed(message)){ return false; } return true; } private boolean isRequestFailed(byte [] messages) throws IOException { if (messages[0] == AuthConstant.SSH_MSG_USERAUTH_SUCCESS){ return true; } if (messages[0] == AuthConstant.SSH_MSG_USERAUTH_FAILURE){ AuthFailure failure = new AuthFailure(messages); authentications = failure.getAuthThatCanContinue(); isPartialSuccess = failure.isPartialSuccess(); return false; } throw new IOException("Unexpected SSH message (type " + messages[0] + ")"); }
当客户端得到了这个 authentications 数组之后,客户端就可以验证当前用户使用的远程登录认证方式是否是服务器所支持的。如果是那么再发送一条匹配的认证方式,从而返回登录认证成功,否则失败并打印出合理的错误信息。下面用 password 的认证方式为例做进一步说明。
password 认证方式
对于 password 认证方式来说,它的数据请求格式如下:
byte SSH_MSG_USERAUTH_REQUEST string user name string service name string "password" boolean FALSE string plaintext password in ISO-10646 UTF-8 encoding
具体类的实现方式和 none 认证类的实现类似,只是 getRequestMessage() 方法所有不同。
实现代码:
清单 8. 生成请求数据函数
public byte [] getRequestMessage() { byte [] request; ProcessTypes type = new ProcessTypes(); type.asByte(AuthConstant.SSH_MSG_USERAUTH_REQUEST); type.asString(userName); type.asString(serviceName); type.asString(AuthConstant.SSH_PASSWORD_AUTHENTICATION_METHOD); type.asString(password); request = type.getBytes(); return request; }
在这里我们需要提供给服务器端一个用户口令,这个口令会在发送给服务器端之前被进行算法加密的处理。调用 password 认证方式的代码如下:
清单 9. password 认证函数
public boolean passwordAuthentication(String user, String pass) throws IOException{ // 初始化请求 initialize(user); // 验证指定的认证方式是否是 SSH 服务器所支持的 if(verifyAuthenticatonMethods(AuthConstant.SSH_PASSWORD_AUTHENTICATION_METHOD)){ return false; } // 调用密码认证方式 AuthRequestByPassword passwordRequest = new AuthRequestByPassword(AuthConstant.SSH_CONN_SERVICE_NAME,user,pass); // 发送一个消息请求到服务器端 IManager transManager = ManagerFactory.getManager(Constant.TRANSPORT_LAYER); transManager.sendMessage(passwordRequest.getRequestMessage()); // 从服务器端获取数据包 byte[] message = getMessage(); // 验证当前的请求是否成功 if(isRequestFailed(message)){ return false; } return true; }
客户端首先会做初始化操作,包括数据加密算法的协商、得到服务器端支持的认证方式等。其次客户端会检查当前用户使用的登录认证方式是否合法,然后再发送一个请求给服务器端,告诉服务器当前使用 password 认证进行远程登录。最后,服务器会返回一个数据包,里面包含了对这个请求的回复,如果验证成功,那么连接就可以开启一个安全的会话了。至此,password 认证方式的解析就完成了,接下来用户就可以对远程机器做操作了,这部分的具体说明是在 SSH 的连接层协议里,不是本文的讨论范围。
总结
篇幅所限,本文就以 password 的认证方式为例进行了客户端远程登录的认证方式的讨论,对于其他认证方式会在以后的文章中讨论。在客户端用 SSH 协议进行远程登录的时候,提供了很多常见的认证方式,每种认证方式发送的数据包的数据结构略有不同,同时也提供了对外扩展接口,可以自定义认证方式。通过对本文的阅读,可以初步了解到,SSH 协议在用户认证层的基本原理,希望能对读者在以后的项目开发中,对 SSH 协议的使用有所帮助。
原文:http://www.ibm.com/developerworks/cn/java/j-lo-sshauthentication/