目录
Secure Sockets Layer(安全套接层),在OSI七层模型里,该协议工作在会话层和表示层。本文主要从SSL原理入手,讲解SSL认证过程,并使用java实现单向认证。
安全套接字(Secure Socket Layer,SSL)协议是一个TCP连接之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。
SSL是Netscape于1994年开发的,后来成为了世界上最通用的安全连接机制,主流的浏览器(IE/Chrome/Firefox等)以及操作系统远程连接服务(Windows的mstsc/Linux的SSH等)都支持SSL协议。
目前有如下版本:SSL2.0、SSL3.0、TLS1.0、TLS1.1、TLS1.2。因之前旧版本暴露出多个严重bug,目前推荐使用TLS1.2版本。
SSL中密钥交换算法有6种:无效(没有密钥交换)、RSA、匿名Diffie-Hellman、暂时Diffie-Hellman、固定Diffie-Hellman、Fortezza。本文以RSA与匿名Diffie-Hellman模式介绍握手协议。
RSA算法概述:
(1)选择两个大素数P、Q
(2)计算N=P*Q
(3)选择一个公钥(加密密钥)E,使其不是(P-1)与(Q-1)的因子
(4)选择私钥(解密密钥)D,满足如下条件:
(D*E) mod (P-1)(Q-1)=1
(5)加密时,明文PT计算密文CT如下:
CT= PT·E mod N
(6)解密时,从密文CT计算明文PT如下:
PT= CT·D mod N 这也是SSL中会用一种密钥交换算法。
开始加密通信之前,客户端和服务器首先必须建立连接和交换参数,这个过程叫做握手(handshake)。握手阶段分成五步。
第一步,Client给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
第二步,Server确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
第三步,Client确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给Server。
第四步,Server使用自己的私钥,获取Client发来的随机数(即Premaster secret)。
第五步,Client和Server根据约定的加密方法,使用前面的三个随机数,生成”对话密钥”(session key),用来加密接下来的整个对话过程。
上面的五步,如下图所示:
RSA加密算法的握手阶段有四点需要注意:
(1)整个握手阶段都是明文传输。如果有人窃听通信,他可以知道双方选择的加密方法,以及三个随机数中的两个。
(2)生成对话密钥一共需要三个随机数。
(3)握手之后的对话使用”对话密钥”加密(对称加密),服务器的公钥和私钥只用于加密和解密”对话密钥”(非对称加密),无其他作用。
(4)服务器公钥放在服务器的数字证书之中。
也就是说,整个对话过程中(握手阶段和其后的对话),服务器的公钥和私钥只需要用到一次。整个通话的安全,只取决于第三个随机数(Premaster secret)能不能被破解。
理论上,只要服务器的公钥足够长(比如2048位),那么Premaster secret可以保证不被破解。但是为了足够安全,我们可以考虑把握手阶段的算法从默认的RSA算法,改为Diffie-Hellman算法(简称DH算法)。
Diffie-Hellman算法概述:
(1)Alice与Bob确定两个大素数n和g,这两个数不用保密
(2)Alice选择另一个大随机数x,并计算A如下:A=gx mod n
(3)Alice将A发给Bob
(4)Bob选择另一个大随机数y,并计算B如下:B=gy mod n
(5)Bob将B发给Alice
(6)计算Alice的秘密密钥K1如下:K1=Bx mod n
(7)计算Bob的秘密密钥K2如下:K2=Ay mod n
K1=K2,因此Alice和Bob可以用其进行加解密
使用Diffie-Hellman加密算法时,握手阶段分成五步。
第一步,Client给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
第二步,Server确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。通过Client random、Server random以及服务器提供的DH参数(Server DH parameter)算出私钥。
第三步,Server端使用私钥加密Server DH parameter,发送给Client。Client使用证书中公钥解密得到Server DH parameter。
第四步,Client将客户端提供的DH参数(Client DH parameter)传给Server。
第五步,Client和Server根据约定的加密方法,使用之前双方交换的DH参数生成”秘密密钥”(Premaster secret),用来加密接下来的整个对话过程(Session key)。
上面的五步,如下图所示:
Diffie-Hellman加密算法规避了RSA中秘密密钥的传递,整个过程中只传递了参数,更加安全,但与之对应的连接资源开销也变得更大。
记录协议在客户机和服务器握手成功后使用,即客户机和服务器鉴别对方和确定安全信息交换使用的算法后,进入SSL记录协议,记录协议向SSL连接提供两个服务:
(1)保密性:使用握手协议定义的秘密密钥实现
(2)完整性:握手协议定义了MAC,用于保证消息完整性
记录协议的过程:
客户机和服务器发现错误时,向对方发送一个警报消息。如果是致命错误,则算法立即关闭SSL连接,双方还会先删除相关的会话号,秘密和密钥。每个警报消息共2个字节,第1个字节表示错误类型,如果是警报,则值为1,如果是致命错误,则值为2;第2个字节制定实际错误类型。
下面以访问百度首页为例,使用wireshark抓包查看SSL连接建立产生的数据包。
上图为Client Hello的包详情,可以看到所有信息均未加密。该包在握手协议第一个阶段,定义了Content Type=Handshake和Version=TLS 1.2。
其中Client随机数在random_bytes处,支持的加密套件在Cipher Suites处,共有26种。接下来定义压缩方法为null,扩展信息中可以看到Server_name为www.baidu.com。
下面看Server Hello的包。
可以看到服务器已经确认了加密套件为TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256。注意这里是一个套件,不是列出的所有算法都要参与Premaster secret的加密。这些套件会在Session key的加密中被用到。
接下来,Client使用证书中的公钥加密消息后继续向Server发送握手包,直到握手流程结束。如下所示:
之后,https连接建立,开始传输数据信息,所有数据均被加密。如下所示:
最后我们使用jdk1.6自带组件进行SSL连接的实现。
首先要为服务端生成一个数字证书。Java环境下,数字证书是用keytool生成的,这些证书被存储在keystore中,即证书仓库。使用cmd命令行调用keytool命令为服务端生成数字证书和保存它使用的证书仓库,如下:
keytool -genkey -v -alias bluedash-ssl-demo-server -keyalg RSA -keystore d:/zk/server_ks -dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass server -keypass 110119120
这样,我们就将服务端证书bluedash-ssl-demo-server保存在了server_ksy这个store文件当中。执行结果如下:
建立服务器端时需要设置上述证书仓库路径,用于面向client出示证书。代码如下:
package com.paic.test.ssl;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
public class Server extends Thread {
private Socket socket;
public Server(Socket socket) {
this.socket = socket;
}
public void run() {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter writer = new PrintWriter(socket.getOutputStream());
String data = reader.readLine();
writer.println(data);
writer.close();
socket.close();
} catch (IOException e) {
}
}
private static String SERVER_KEY_STORE = "D:/Security/src/com/paic/test/ssl/server_ks";
private static String SERVER_KEY_STORE_PASSWORD = "110119120";
public static void main(String[] args) throws Exception {
System.setProperty("javax.net.ssl.trustStore", SERVER_KEY_STORE);
SSLContext context = SSLContext.getInstance("TLS");
KeyStore ks = KeyStore.getInstance("jceks");
ks.load(new FileInputStream(SERVER_KEY_STORE), null);
KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
kf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());
context.init(kf.getKeyManagers(), null, null);
ServerSocketFactory factory = context.getServerSocketFactory();
ServerSocket _socket = factory.createServerSocket(8443);
((SSLServerSocket) _socket).setNeedClientAuth(false);
while (true) {
new Server(_socket.accept()).start();
}
}
}
接下来说客户端。连接时最重要的一点,服务端证书里面的CN一定和服务端的域名统一,我们的证书服务的域名是localhost,那么我们的客户端在连接服务端时一定也要用localhost来连接,否则根据SSL协议标准,域名与证书的CN不匹配,说明这个证书是不安全的,通信将无法正常运行。
现在客户端也必须要使用SSL协议连接服务端了,先为服务端也建立一个证书仓库,cmd中输入以下命令:
keytool -genkey -v -alias bluedash-ssl-demo-client -keyalg RSA -keystore d:/zk/client_ks -dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass client -keypass 110119120
回显如下:
现在,我们要把服务端的证书导出来,并导入到客户端的仓库。第一步是导出服务端的证书:
keytool -export -alias bluedash-ssl-demo-server -keystore d:/zk/server_ks -file d:/zk/server_key.cer
这里keystore的密码是server
第二步,把服务器端提供的证书导入客户端的证书仓库中,如下:
keytool -import -trustcacerts -alias bluedash-ssl-demo-server -file d:/zk/server_key.cer -keystore d:/zk/client_ks
这里keystore的密码是client。结果如下:
最后编写Client代码:
package com.paic.test.ssl;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
public class Client {
private static String CLIENT_KEY_STORE = "D:/Security/src/com/paic/test/ssl/client_ks";
public static void main(String[] args) throws Exception {
// Set the key store to use for validating the server cert.
System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);
System.setProperty("javax.net.debug", "ssl,handshake");
Client client = new Client();
Socket s = client.clientWithoutCert();
PrintWriter writer = new PrintWriter(s.getOutputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(s
.getInputStream()));
writer.println("Hello World!");
writer.flush();
System.out.println(reader.readLine());
s.close();
}
private Socket clientWithoutCert() throws Exception {
SocketFactory sf = SSLSocketFactory.getDefault();
Socket s = sf.createSocket("localhost", 8443);
return s;
}
}
完整日志如下:
keyStore is :
keyStore type is : jks
keyStore provider is :
init keystore
init keymanager of type SunX509
trustStore is: D:\Security\src\com\paic\test\ssl\client_ks
trustStore type is : jks
trustStore provider is :
init truststore
adding as trusted cert:
Subject: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Algorithm: RSA; Serial number: 0x58806140
Valid from Thu Jan 19 14:48:32 CST 2017 until Wed Apr 19 14:48:32 CST 2017
adding as trusted cert:
Subject: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Algorithm: RSA; Serial number: 0x58805a3a
Valid from Thu Jan 19 14:18:34 CST 2017 until Wed Apr 19 14:18:34 CST 2017
trigger seeding of SecureRandom
done seeding SecureRandom
Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
%% No cached client session
*** ClientHello, TLSv1
RandomCookie: GMT: 1468033624 bytes = { 214, 154, 197, 69, 110, 42, 160, 233, 199, 229, 122, 38, 230, 183, 103, 52, 25, 29, 96, 144, 86, 13, 236, 159, 228, 168, 228, 235 }
Session ID: {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods: { 0 }
***
main, WRITE: TLSv1 Handshake, length = 75
main, WRITE: SSLv2 client hello message, length = 101
main, READ: TLSv1 Handshake, length = 644
*** ServerHello, TLSv1
RandomCookie: GMT: 1468033624 bytes = { 9, 150, 107, 67, 1, 147, 77, 73, 25, 134, 74, 228, 1, 172, 47, 149, 159, 32, 205, 72, 163, 21, 240, 247, 41, 111, 26, 108 }
Session ID: {88, 128, 106, 88, 120, 114, 169, 106, 158, 74, 153, 73, 226, 136, 102, 99, 73, 5, 232, 16, 26, 208, 199, 85, 118, 7, 178, 38, 46, 14, 48, 207}
Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
Compression Method: 0
Extension renegotiation_info, renegotiated_connection:
***
%% Created: [Session-1, SSL_RSA_WITH_RC4_128_MD5]
** SSL_RSA_WITH_RC4_128_MD5
*** Certificate chain
chain [0] = [
[
Version: V3
Subject: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 1024 bits
modulus: 132010717494208823330135610754239731637209803896658954827545525773931053366786164660256992731220177418937202145579952356341491963197334472861732045115022380989341508527250473019782724735723642876007066492240045958870757117972447694316024493377798207800715178238449213037958195281193945491472218196895403430833
public exponent: 65537
Validity: [From: Thu Jan 19 14:18:34 CST 2017,
To: Wed Apr 19 14:18:34 CST 2017]
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
SerialNumber: [ 58805a3a]
]
Algorithm: [SHA1withRSA]
Signature:
0000: 6D CD A8 06 62 DA F6 7D EB 63 13 AC F5 BC A0 8F m...b....c......
0010: 28 0E 3F DD EB 7C 70 6D 17 D8 FB 56 33 E1 23 79 (.?...pm...V3.#y
0020: 9A D8 E2 7C 08 CD 02 A0 7B BF E7 86 53 65 49 CF ............SeI.
0030: 51 FC AF 3A 7B 78 36 15 83 4F 72 C3 B5 76 9E FC Q..:.x6..Or..v..
0040: F0 69 82 82 41 BE 64 41 86 5F 97 04 DE D3 C0 82 .i..A.dA._......
0050: 5C 67 54 C2 69 E9 95 B6 D5 D5 B2 80 9B EC E0 F6 \gT.i...........
0060: 64 83 D0 7D 13 6D AA BC 25 56 EF BE 9D 5B CB 54 d....m..%V...[.T
0070: 81 C7 57 78 65 4D 4B 9C 55 7A 5F 35 CD 31 D5 1F ..WxeMK.Uz_5.1..
]
***
Found trusted certificate:
[
[
Version: V3
Subject: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5
Key: Sun RSA public key, 1024 bits
modulus: 132010717494208823330135610754239731637209803896658954827545525773931053366786164660256992731220177418937202145579952356341491963197334472861732045115022380989341508527250473019782724735723642876007066492240045958870757117972447694316024493377798207800715178238449213037958195281193945491472218196895403430833
public exponent: 65537
Validity: [From: Thu Jan 19 14:18:34 CST 2017,
To: Wed Apr 19 14:18:34 CST 2017]
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
SerialNumber: [ 58805a3a]
]
Algorithm: [SHA1withRSA]
Signature:
0000: 6D CD A8 06 62 DA F6 7D EB 63 13 AC F5 BC A0 8F m...b....c......
0010: 28 0E 3F DD EB 7C 70 6D 17 D8 FB 56 33 E1 23 79 (.?...pm...V3.#y
0020: 9A D8 E2 7C 08 CD 02 A0 7B BF E7 86 53 65 49 CF ............SeI.
0030: 51 FC AF 3A 7B 78 36 15 83 4F 72 C3 B5 76 9E FC Q..:.x6..Or..v..
0040: F0 69 82 82 41 BE 64 41 86 5F 97 04 DE D3 C0 82 .i..A.dA._......
0050: 5C 67 54 C2 69 E9 95 B6 D5 D5 B2 80 9B EC E0 F6 \gT.i...........
0060: 64 83 D0 7D 13 6D AA BC 25 56 EF BE 9D 5B CB 54 d....m..%V...[.T
0070: 81 C7 57 78 65 4D 4B 9C 55 7A 5F 35 CD 31 D5 1F ..WxeMK.Uz_5.1..
]
*** ServerHelloDone
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
main, WRITE: TLSv1 Handshake, length = 134
SESSION KEYGEN:
PreMaster Secret:
0000: 03 01 93 47 7C E6 00 37 1B 28 41 F6 28 C5 6F 85 ...G...7.(A.(.o.
0010: 94 B6 60 4C 8F C4 C5 EE 03 9A 70 A9 48 1B 3C 97 ..`L......p.H.<.
0020: 1B 6A 4B AB 2B ED FE F9 AD F0 C0 7E 62 DD AD 9D .jK.+.......b...
CONNECTION KEYGEN:
Client Nonce:
0000: 58 80 6A 58 D6 9A C5 45 6E 2A A0 E9 C7 E5 7A 26 X.jX...En*....z&
0010: E6 B7 67 34 19 1D 60 90 56 0D EC 9F E4 A8 E4 EB ..g4..`.V.......
Server Nonce:
0000: 58 80 6A 58 09 96 6B 43 01 93 4D 49 19 86 4A E4 X.jX..kC..MI..J.
0010: 01 AC 2F 95 9F 20 CD 48 A3 15 F0 F7 29 6F 1A 6C ../.. .H....)o.l
Master Secret:
0000: 56 86 3F 43 70 7C 22 93 89 77 55 02 1D 49 2A 93 V.?Cp."..wU..I*.
0010: B4 A3 68 F6 96 D3 B5 E9 8A C5 CE 47 E1 33 08 22 ..h........G.3."
0020: 5C 53 BD 6A B2 71 56 FB 41 2B 90 39 9E 39 B2 8F \S.j.qV.A+.9.9..
Client MAC write Secret:
0000: 89 57 FD 3D 9A 4E A1 F0 5E B4 FB C6 9A 6E DA 65 .W.=.N..^....n.e
Server MAC write Secret:
0000: 61 10 61 13 FA 1E FB 86 57 7E 7A 4C DD B9 81 82 a.a.....W.zL....
Client write key:
0000: A4 B8 09 BB 7C C3 68 03 C2 13 8D E5 A7 D4 BD EB ......h.........
Server write key:
0000: 98 67 0B 85 89 F7 07 42 B8 68 3D 37 16 7E AF EF .g.....B.h=7....
... no IV used for this cipher
main, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
verify_data: { 96, 176, 192, 183, 120, 220, 1, 193, 132, 36, 241, 246 }
***
main, WRITE: TLSv1 Handshake, length = 32
main, READ: TLSv1 Change Cipher Spec, length = 1
main, READ: TLSv1 Handshake, length = 32
*** Finished
verify_data: { 15, 9, 125, 232, 190, 217, 60, 132, 134, 34, 19, 105 }
***
%% Cached client session: [Session-1, SSL_RSA_WITH_RC4_128_MD5]
main, WRITE: TLSv1 Application Data, length = 30
main, READ: TLSv1 Application Data, length = 30
Hello World!
main, called close()
main, called closeInternal(true)
main, SEND TLSv1 ALERT: warning, description = close_notify
main, WRITE: TLSv1 Alert, length = 18
main, called closeSocket(selfInitiated)
相比普通socket连接,SSL连接中的Client增加了使用信任证书仓库的代码。
如果连接成功将会返回客户端提交的Hello World!字符串。客户端定义的日志输出为debug,可以看到SSL单向握手的全过程。
如果需要Server与Client双向认证,将Server中((SSLServerSocket) _socket).setNeedClientAuth(false); 的false改为true,再将客户端证书导入服务端证书仓库即可。