在前文中,我们通过实例了解了使用java加密的一些技术,它们各自具有一些优缺点,由于它们的一些局限性,在网络传输中,需要综合使用这些技术来保证高强度的传输安全性。本单元将讨论SSL/TLS,并用jdk中的JSSE(Java Secure Socket Extension)包编写一些简单实例,最后会用wireshark工具来分析使用SSL/TLS进行通信的工作流程。
SSL
SSL由Netscape提出,在经历了3个版本之后,由IETF将其标准化,成为TLS,在rfc2246中,详细描述了该协议的目标、作用和细节。SSL/TLS在整个tcp/ip协议栈中的位置如图1所示:
图1 SSL/TLS在tcp/ip协议栈中的位置
正如图1所描绘的,SSL/TLS由两层协议组成,Record Protocol将需要传输的消息分解成易于管理的小块,可选择性的压缩这些小块数据,附加上它们的MAC值,然后加密这些数据,最后传递给传输层协议去处理(如图2所示);接收数据的时候,则进行相反的操作:解密数据,用MAC值进行验证,解压,然后重新组装成完整消息传递给上层协议处理。
图2 SSL封包过程
Record Protocol也是分层协议,它会对需要传输的消息附加上消息头用于描述协议相关信息,我们也可以从图2中看到这些消息头字段,Type表示封装的record类型,在TLSv1中,有4个类型:change_cipher_spec(通告对端进入加密模式)、alert(发出报警消息)、handshake(建立安全连接)、application_data(应用层数据);Version标识了所使用的SSL版本,对于TLSv1来说,主版本号为3,小版本号为1,3.1这个版本号有一定的历史原因,因为TLSv1是在SSLv3基础上制定的,差距甚微,所以只能算3.0版本的一个修订版;Length标识了数据部分的长度,每次压缩、加密、计算数字摘要之后都要重新填入处理后的数据长度;Data表示需要传递的消息,当Type不同时,这些数据可能是应用层协议产生的消息,也可能是Handshake Protocol、Change Cipher Spec Protocol和Alert Protocol产生的消息;MAC标识了record的数字签名,它被用来检测record的完整性。
Handshake Protocol被用来在实际的传输之前,对通讯的双方进行身份验证,协商加密算法,用Change Cipher Spec Protocol通知对端进入对称加密模式,而Alter Protocol则用来向通讯的另一方发出警告消息,比如close_notify、bad_record_mac等。
接下来我们先看一个实际的例子,对SSL协议建立一个直观的印象,然后再讨论它的工作流程。
JSSE示例
我们的示例使用这样的场景:SSLServer在8266端口侦听SSLClient的连接,SSLClient向SSLServer发起连接请求,并要求验证SSLServer的身份合法性,建立连接之后,向SSLServer发一条消息”Hello World”,SSLServer接收到消息后打印屏幕上。由于SSL用公钥加密的技术来建立连接,用数字证书来验证对端身份合法性,因此在编写实例之前,我们先用keytool为Server和Client创建两对密钥,并将Server的数字证书导入到Client的受信密钥库中。
- # 为Server创建证书和密钥库
- keytool -genkey -alias server -keysize 512 -keyalg RSA -dname "CN=znest.cn,OU=Security,O=Znest,L=C,ST=H,C=CN" -keypass 123456 -storepass 123456 -keystore server.ks
- # 为Client创建证书和密钥库
- keytool -genkey -alias client -keysize 512 -keyalg RSA -dname "C=CN" -keypass 123456 -storepass 123456 -keystore client.ks
- # 导出Server的证书
- keytool -export -trustcacerts -alias server -keystore server.ks -storepass 123456 -file server_cert
- # 将Server的证书导入到Client的密钥库
- keytool -import -trustcacerts -alias server -keystore client.ks -storepass 123456 -file server_cert
接下来编写SSLServer.java:
- import java.io.BufferedReader;
- import java.io.FileInputStream;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.UnsupportedEncodingException;
- import java.security.KeyStore;
- import java.security.SecureRandom;
- import javax.net.ssl.KeyManager;
- import javax.net.ssl.KeyManagerFactory;
- import javax.net.ssl.SSLContext;
- import javax.net.ssl.SSLServerSocket;
- import javax.net.ssl.SSLServerSocketFactory;
- import javax.net.ssl.SSLSocket;
- import javax.net.ssl.TrustManager;
- import javax.net.ssl.TrustManagerFactory;
- public class SSLServer {
- private static final int port = 8266;
- private static final String keyStore = "server.ks";
- private static final String trustStore = "server.ks";
- private static final String keyStoreType = "jks";
- private static final String trustStoreType = "jks";
- private static final String keyStorePassword = "123456";
- private static final String trustStorePassword = "123456";
- private static final String secureRandomAlgorithm = "SHA1PRNG";
- private static final String protocol = "TLSv1";
- private static KeyManager[] createKeyManagersAsArray() throws Exception {
- KeyStore ks = KeyStore.getInstance(keyStoreType);
- ks.load(new FileInputStream(keyStore), keyStorePassword.toCharArray());
- KeyManagerFactory tmf = KeyManagerFactory.getInstance(KeyManagerFactory
- .getDefaultAlgorithm());
- tmf.init(ks, keyStorePassword.toCharArray());
- return tmf.getKeyManagers();
- }
- private static TrustManager[] createTrustManagersAsArray() throws Exception {
- KeyStore ks = KeyStore.getInstance(trustStoreType);
- ks.load(new FileInputStream(trustStore), trustStorePassword
- .toCharArray());
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(ks);
- return tmf.getTrustManagers();
- }
- private static SSLServerSocket getServerSocket(int thePort) {
- SSLServerSocket socket = null;
- try {
- SSLContext sslContext = SSLContext.getInstance(protocol);
- sslContext.init(createKeyManagersAsArray(),
- createTrustManagersAsArray(), SecureRandom
- .getInstance(secureRandomAlgorithm));
- SSLServerSocketFactory factory = sslContext
- .getServerSocketFactory();
- socket = (SSLServerSocket) factory.createServerSocket(thePort);
- //socket.setNeedClientAuth(true);
- } catch (Exception e) {
- System.out.println(e);
- }
- return (socket);
- }
- public static void main(String args[]) throws IOException {
- SSLServerSocket server = getServerSocket(port);
- System.out.println("在" + port + "端口等待连接...");
- while (true) {
- final SSLSocket socket = (SSLSocket) server.accept();
- new Thread(new Runnable() {
- public void run() {
- BufferedReader in;
- try {
- in = new BufferedReader(new InputStreamReader(socket
- .getInputStream(), "gb2312"));
- String msg = in.readLine();
- System.out.println(msg);
- socket.close();
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }).start();
- }
- }
- }
以及SSLClient.java:
- import java.io.PrintWriter;
- import java.net.Socket;
- import javax.net.ssl.SSLSocketFactory;
- public class SSLClient {
- private static String addr = "192.168.80.86";
- public static void main(String args[]) {
- try {
- System.setProperty("javax.net.ssl.keyStore", "client.ks");
- System.setProperty("javax.net.ssl.keyStorePassword", "123456");
- System.setProperty("javax.net.ssl.keyStoreType", "jks");
- System.setProperty("javax.net.ssl.trustStore", "client.ks");
- System.setProperty("javax.net.ssl.trustStorePassword", "123456");
- System.setProperty("javax.net.ssl.trustStoreType", "jks");
- SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory
- .getDefault();
- Socket socket = factory.createSocket(addr, 8266);
- PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
- out.println("hello world!");
- out.close();
- socket.close();
- } catch (Exception e) {
- System.out.println(e);
- }
- }
- }
SSLContext扮演着创建SSL套接字工厂和一些其他SSL部件的角色,上面这两段代码展示了两种初始化SSLContext的方法,初始化SSLContext之后,生成SSLSocketFactory,之后的编程就和普通的socket编程没有什么区别了。为了保证运行,需要将编译后生成的SSLServer.class和server.ks放在同一目录下,将编译后生成的SSLClient.class和client.ks放在同一目录下,以便SSLContext能够从密钥库读取密钥和证书。
SSL协议分析
我们在192.168.80.86机器上打开wireshark监听流经该网卡的所有数据包,同时将SSLServer.class和server.ks放在这台机器上,然后执行下面的命令运行服务端程序准备接受客户端的连接请求。
- java -cp . SSLServer
将SSLClient.class和client.ks放在192.168.80.160机器上,执行下面的命令连接服务端。
- java -cp . SSLClient
当客户端发出的”Hello World!”在服务端的屏幕上显示之后,wireshark将记录SSL的整个连接和停止过程。为了让wireshark能够解码ssl消息头,还需要在菜单“Analyze->Decode as…”设置解码类型,如图3所示。设置完毕后,wireshark将解析出tlsv1的数据包,如图4所示。
图3 设置wireshark解码类型
图4 wireshark主界面
、
1-3行 三次握手建立tcp连接。
4行 客户端向服务端发送Client Hello,这个信息中包括它所支持的加密算法和用于生成密钥的随机数。
5行 服务端向客户端发送三条消息:(1)Server Hello包含了服务端所选择的算法(2)Certificate包含了用于验证服务器身份的证书或者证书链(3)Server Hello Done表示服务端完成了最初的加密算法协商步骤。
6行 由于服务端不需要验证客户端,因此客户端验证完服务端的身份之后,抽取服务器证书中的公钥,用这把公钥将它产生的用于之后数据交换时加密的密钥用非对称加密技术加密,并发送给服务端。
8行 客户端向服务端发送了两条消息:(1)Change cipher spec通知服务端进入对称加密模式(2)Finished通知服务端已经准备好加密传输数据了。
9行 服务端向客户端发送Change cipher spec通知客户端进入对称加密模式
11行 服务端向客户端发送Finished通知客户端已经准备好加密传输数据。
12行 客户端和服务端用对称加密算法和客户端生成的密钥加密传输应用层的数据。
13-15行 通告关闭连接
16行 客户端发送RST包关闭连接
这个过程正好和JSSE参考文档中的图一致:
小结
本文讨论了SSL/TLS,并用JSSE编写了一个示例。SSL/TLS使用数字证书来验证通信双方的合法性,使用非对称加密技术来协商数据传输的密钥,使用数字摘要来验证握手过程中消息的完整性,使用对称加密技术来传输数据,因此要完全掌握这个协议,需要对加密技术有深入了解。当然,对于大多数程序员来说,只需要简单的了解一下这些东西,熟悉JSSE,就能编写出具备一定安全强度的通信软件。