SSL/TLS
在前文中,我們通過實例了解了使用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,就能編寫出具備一定安全強度的通信軟件。
SSL/TLS