在人類(lèi)建立了通信系統之后,如何保證通信的安全始終是一個(gè)重要的問(wèn)題。伴隨著(zhù)現代化通信系統的建立,人們利用數學(xué)理論找到了一些行之有效的方法來(lái)保證數字通信的安全。簡(jiǎn)單來(lái)說(shuō)就是把兩方通信的過(guò)程進(jìn)行保密處理,比如對雙方通信的內容進(jìn)行加密,這樣就可以有效防止偷聽(tīng)者輕易截獲通信的內容。目前 SSL(Secure Sockets Layer) 及其后續版本 TLS(Transport Layer Security)是比較成熟的通信加密協(xié)議,它們常被用于在客戶(hù)端和服務(wù)器之間建立加密通信通道。各種開(kāi)發(fā)語(yǔ)言都給出 SSL/TLS 協(xié)議的具體實(shí)現,Java 也不例外。在 JDK 中有一個(gè) JSSE(javax.net.ssl)包,提供了對 SSL 和 TLS 的支持。通過(guò)其所提供的一系列 API,開(kāi)發(fā)者可以像使用普通 Socket 一樣使用基于 SSL 或 TLS 的安全套接字,而不用關(guān)心 SSL 和 TLS 協(xié)議的細節,例如握手的流程等等。這使得利用 Java 開(kāi)發(fā)安全的 SSL/TLS 服務(wù)器或客戶(hù)端非常容易,本文將通過(guò)具體的例子來(lái)說(shuō)明如何用 Java 語(yǔ)言來(lái)開(kāi)發(fā) SSL/TLS 應用。
SSL/TLS 協(xié)議(RFC2246 RFC4346)處于 TCP/IP 協(xié)議與各種應用層協(xié)議之間,為數據通訊提供安全支持。
從協(xié)議內部的功能層面上來(lái)看,SSL/TLS 協(xié)議可分為兩層:
1. SSL/TLS 記錄協(xié)議(SSL/TLS Record Protocol),它建立在可靠的傳輸層協(xié)議(如 TCP)之上,為上層協(xié)議提供數據封裝、壓縮、加密等基本功能。
2. SSL/TLS 握手協(xié)議(SSL/TLS Handshake Protocol),它建立在 SSL/TLS 記錄協(xié)議之上,用于在實(shí)際的數據傳輸開(kāi)始前,通訊雙方進(jìn)行身份認證、協(xié)商加密算法、交換加密密鑰等初始化協(xié)商功能。
從協(xié)議使用方式來(lái)看,又可以分成兩種類(lèi)型:
1. SSL/TLS 單向認證,就是用戶(hù)到服務(wù)器之間只存在單方面的認證,即客戶(hù)端會(huì )認證服務(wù)器端身份,而服務(wù)器端不會(huì )去對客戶(hù)端身份進(jìn)行驗證。首先,客戶(hù)端發(fā)起握手請求,服務(wù)器收到握手請求后,會(huì )選擇適合雙方的協(xié)議版本和加密方式。然后,再將協(xié)商的結果和服務(wù)器端的公鑰一起發(fā)送給客戶(hù)端??蛻?hù)端利用服務(wù)器端的公鑰,對要發(fā)送的數據進(jìn)行加密,并發(fā)送給服務(wù)器端。服務(wù)器端收到后,會(huì )用本地私鑰對收到的客戶(hù)端加密數據進(jìn)行解密。然后,通訊雙方都會(huì )使用這些數據來(lái)產(chǎn)生雙方之間通訊的加密密鑰。接下來(lái),雙方就可以開(kāi)始安全通訊過(guò)程了。
2.SSL/TLS 雙向認證,就是雙方都會(huì )互相認證,也就是兩者之間將會(huì )交換證書(shū)?;镜倪^(guò)程和單向認證完全一樣,只是在協(xié)商階段多了幾個(gè)步驟。在服務(wù)器端將協(xié)商的結果和服務(wù)器端的公鑰一起發(fā)送給客戶(hù)端后,會(huì )請求客戶(hù)端的證書(shū),客戶(hù)端則會(huì )將證書(shū)發(fā)送給服務(wù)器端。然后,在客戶(hù)端給服務(wù)器端發(fā)送加密數據后,客戶(hù)端會(huì )將私鑰生成的數字簽名發(fā)送給服務(wù)器端。而服務(wù)器端則會(huì )用客戶(hù)端證書(shū)中的公鑰來(lái)驗證數字簽名的合法性。建立握手之后過(guò)程則和單向通訊完全保持一致。
SSL/TLS 協(xié)議建立通訊的基本流程如圖 1 所示,
步驟 1. ClientHello – 客戶(hù)端發(fā)送所支持的 SSL/TLS 最高協(xié)議版本號和所支持的加密算法集合及壓縮方法集合等信息給服務(wù)器端。
步驟 2. ServerHello – 服務(wù)器端收到客戶(hù)端信息后,選定雙方都能夠支持的 SSL/TLS 協(xié)議版本和加密方法及壓縮方法,返回給客戶(hù)端。
(可選)步驟 3. SendCertificate – 服務(wù)器端發(fā)送服務(wù)端證書(shū)給客戶(hù)端。
(可選)步驟 4. RequestCertificate – 如果選擇雙向驗證,服務(wù)器端向客戶(hù)端請求客戶(hù)端證書(shū)。
步驟 5. ServerHelloDone – 服務(wù)器端通知客戶(hù)端初始協(xié)商結束。
(可選)步驟 6. ResponseCertificate – 如果選擇雙向驗證,客戶(hù)端向服務(wù)器端發(fā)送客戶(hù)端證書(shū)。
步驟 7. ClientKeyExchange – 客戶(hù)端使用服務(wù)器端的公鑰,對客戶(hù)端公鑰和密鑰種子進(jìn)行加密,再發(fā)送給服務(wù)器端。
(可選)步驟 8. CertificateVerify – 如果選擇雙向驗證,客戶(hù)端用本地私鑰生成數字簽名,并發(fā)送給服務(wù)器端,讓其通過(guò)收到的客戶(hù)端公鑰進(jìn)行身份驗證。
步驟 9. CreateSecretKey – 通訊雙方基于密鑰種子等信息生成通訊密鑰。
步驟 10. ChangeCipherSpec – 客戶(hù)端通知服務(wù)器端已將通訊方式切換到加密模式。
步驟 11. Finished – 客戶(hù)端做好加密通訊的準備。
步驟 12. ChangeCipherSpec – 服務(wù)器端通知客戶(hù)端已將通訊方式切換到加密模式。
步驟 13. Finished – 服務(wù)器做好加密通訊的準備。
步驟 14. Encrypted/DecryptedData – 雙方使用客戶(hù)端密鑰,通過(guò)對稱(chēng)加密算法對通訊內容進(jìn)行加密。
步驟 15. ClosedConnection – 通訊結束后,任何一方發(fā)出斷開(kāi) SSL 連接的消息。
除了以上的基本流程,SSL/TLS 協(xié)議本身還有一些概念需要在此解釋說(shuō)明一下。
Key:Key 是一個(gè)比特(bit)字符串,用來(lái)加密解密數據的,就像是一把開(kāi)鎖的鑰匙。
對稱(chēng)算法(symmetric cryptography):就是需要雙方使用一樣的 key 來(lái)加密解密消息算法,常用密鑰算法有 Data Encryption Standard(DES)、triple-strength DES(3DES)、Rivest Cipher 2 (RC2)和 Rivest Cipher 4(RC4)。因為對稱(chēng)算法效率相對較高,因此 SSL 會(huì )話(huà)中的敏感數據都用通過(guò)密鑰算法加密。
非對稱(chēng)算法(asymmetric cryptography):就是 key 的組成是公鑰私鑰對 (key-pair),公鑰傳遞給對方私鑰自己保留。公鑰私鑰算法是互逆的,一個(gè)用來(lái)加密,另一個(gè)可以解密。常用的算法有 Rivest Shamir Adleman(RSA)、Diffie-Hellman(DH)。非對稱(chēng)算法計算量大比較慢,因此僅適用于少量數據加密,如對密鑰加密,而不適合大量數據的通訊加密。
公鑰證書(shū)(public key certificate):公鑰證書(shū)類(lèi)似數字護照,由受信機構頒發(fā)。受信組織的公鑰證書(shū)就是 certificate authority(CA)。多證書(shū)可以連接成證書(shū)串,第一個(gè)是發(fā)送人,下一個(gè)是給其頒發(fā)證書(shū)實(shí)體,往上到根證書(shū)是世界范圍受信組織,包括 VeriSign, Entrust, 和 GTE CyberTrust。公鑰證書(shū)讓非對稱(chēng)算法的公鑰傳遞更安全,可以避免身份偽造,比如 C 創(chuàng )建了公鑰私鑰,對并冒充 A 將公鑰傳遞給 B,這樣 C 與 B 之間進(jìn)行的通訊會(huì )讓 B 誤認是 A 與 B 之間通訊。
加密哈希功能(Cryptographic Hash Functions): 加密哈希功能與 checksum 功能相似。不同之處在于,checksum 用來(lái)偵測意外的數據變化而前者用來(lái)偵測故意的數據篡改。數據被哈希后產(chǎn)生一小串比特字符串,微小的數據改變將導致哈希串的變化。發(fā)送加密數據時(shí),SSL 會(huì )使用加密哈希功能來(lái)確保數據一致性,用來(lái)阻止第三方破壞通訊數據完整性。SSL 常用的哈希算法有 Message Digest 5(MD5)和 Secure Hash Algorithm(SHA)。
消息認證碼(Message Authentication Code): 消息認證碼與加密哈希功能相似,除了它需要基于密鑰。密鑰信息與加密哈希功能產(chǎn)生的數據結合就是哈希消息認證碼(HMAC)。如果 A 要確保給 B 發(fā)的消息不被 C 篡改,他要按如下步驟做 --A 首先要計算出一個(gè) HMAC 值,將其添加到原始消息后面。用 A 與 B 之間通訊的密鑰加密消息體,然后發(fā)送給 B。B 收到消息后用密鑰解密,然后重新計算出一個(gè) HMAC,來(lái)判斷消息是否在傳輸中被篡改。SSL 用 HMAC 來(lái)保證數據傳輸的安全。
數字簽名(Digital Signature):一個(gè)消息的加密哈希被創(chuàng )建后,哈希值用發(fā)送者的私鑰加密,加密的結果就是叫做數字簽名。
在 Java SDK 中有一個(gè)叫 JSSE(javax.net.ssl)包,這個(gè)包中提供了一些類(lèi)來(lái)建立 SSL/TLS 連接。通過(guò)這些類(lèi),開(kāi)發(fā)者就可以忽略復雜的協(xié)議建立流程,較為簡(jiǎn)單地在網(wǎng)絡(luò )上建成安全的通訊通道。JSSE 包中主要包括以下一些部分:
下面將通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)展示如何通過(guò) JSSE,在客戶(hù)端和服務(wù)器端建立一個(gè) SSL/TLS 連接。設計兩個(gè)類(lèi) SSLClient 和 SSLServer,分別來(lái)表示客戶(hù)端和服務(wù)器端??蛻?hù)端將會(huì )向服務(wù)器端發(fā)起連接請求,在通過(guò)服務(wù)器端驗證建立 SSL 連接后,服務(wù)器端將會(huì )向客戶(hù)端發(fā)送一串內容,客戶(hù)端將會(huì )把收到的內容打印出來(lái)。樣例代碼如下,
SSLClient Source code:
package example.ssl.codes; import java.io.*; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; class SSLClient { private SSLSocket socket = null; public SSLClient() throws IOException { // 通過(guò)套接字工廠(chǎng),獲取一個(gè)客戶(hù)端套接字 SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); socket = (SSLSocket) socketFactory.createSocket("localhost", 7070); } public void connect() { try { // 獲取客戶(hù)端套接字輸出流 PrintWriter output = new PrintWriter( new OutputStreamWriter(socket.getOutputStream())); // 將用戶(hù)名和密碼通過(guò)輸出流發(fā)送到服務(wù)器端 String userName = "principal"; output.println(userName); String password = "credential"; output.println(password); output.flush(); // 獲取客戶(hù)端套接字輸入流 BufferedReader input = new BufferedReader( new InputStreamReader(socket.getInputStream())); // 從輸入流中讀取服務(wù)器端傳送的數據內容,并打印出來(lái) String response = input.readLine(); response += "n " + input.readLine(); System.out.println(response); // 關(guān)閉流資源和套接字資源 output.close(); input.close(); socket.close(); } catch (IOException ioException) { ioException.printStackTrace(); } finally { System.exit(0); } } public static void main(String args[]) throws IOException { new SSLClient().connect(); } }
SSLServer Source code:
package example.ssl.codes; import java.io.*; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; class SSLServer { // 服務(wù)器端授權的用戶(hù)名和密碼 private static final String USER_NAME = "principal"; private static final String PASSWORD = "credential"; // 服務(wù)器端保密內容 private static final String SECRET_CONTENT = "This is confidential content from server X, for your eye!"; private SSLServerSocket serverSocket = null; public SSLServer() throws Exception { // 通過(guò)套接字工廠(chǎng),獲取一個(gè)服務(wù)器端套接字 SSLServerSocketFactory socketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault(); serverSocket = (SSLServerSocket)socketFactory.createServerSocket(7070); } private void runServer() { while (true) { try { System.out.println("Waiting for connection..."); // 服務(wù)器端套接字進(jìn)入阻塞狀態(tài),等待來(lái)自客戶(hù)端的連接請求 SSLSocket socket = (SSLSocket) serverSocket.accept(); // 獲取服務(wù)器端套接字輸入流 BufferedReader input = new BufferedReader( new InputStreamReader(socket.getInputStream())); // 從輸入流中讀取客戶(hù)端用戶(hù)名和密碼 String userName = input.readLine(); String password = input.readLine(); // 獲取服務(wù)器端套接字輸出流 PrintWriter output = new PrintWriter( new OutputStreamWriter(socket.getOutputStream())); // 對請求進(jìn)行認證,如果通過(guò)則將保密內容發(fā)送給客戶(hù)端 if (userName.equals(USER_NAME) && password.equals(PASSWORD)) { output.println("Welcome, " + userName); output.println(SECRET_CONTENT); } else { output.println("Authentication failed, you have no access to server X..."); } // 關(guān)閉流資源和套接字資源 output.close(); input.close(); socket.close(); } catch (IOException ioException) { ioException.printStackTrace(); } } } public static void main(String args[]) throws Exception { SSLServer server = new SSLServer(); server.runServer(); } }
SSL 樣例程序:
java -cp ./build/classes example.ssl.codes.SSLServer java -cp ./build/classes example.ssl.codes.SSLClient
執行結果如下:
服務(wù)器端輸出:
Waiting for connection... javax.net.ssl.SSLHandshakeException: no cipher suites in common Waiting for connection... at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1836) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:276) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:266)
客戶(hù)端輸出:
javax.net.ssl.SSLException: Connection has been shutdown: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at sun.security.ssl.SSLSocketImpl.checkEOF(SSLSocketImpl.java:1426) at sun.security.ssl.AppInputStream.read(AppInputStream.java:92) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177) at java.io.InputStreamReader.read(InputStreamReader.java:184) at java.io.BufferedReader.fill(BufferedReader.java:154) at java.io.BufferedReader.readLine(BufferedReader.java:317) at java.io.BufferedReader.readLine(BufferedReader.java:382) at example.ssl.codes.SSLClient.connect(SSLClient.java:29) at example.ssl.codes.SSLClient.main(SSLClient.java:44) Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:1911) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1027) at sun.security.ssl.SSLSocketImpl.performInitialHandshake (SSLSocketImpl.java:1262) at sun.security.ssl.SSLSocketImpl.writeRecord(SSLSocketImpl.java:680) at sun.security.ssl.AppOutputStream.write(AppOutputStream.java:85) at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221) at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291) at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:295) at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141) at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229) at java.io.PrintWriter.flush(PrintWriter.java:320) at example.ssl.codes.SSLClient.connect(SSLClient.java:25) ... 1 more
通過(guò)程序的錯誤輸出,我們能夠發(fā)現 SSL 建立失敗了,在握手階段雙方?jīng)]有能夠協(xié)商出加密方法等信息。這是因為默認情況下,java 虛擬機沒(méi)有與 SSL 相關(guān)的配置,需要開(kāi)發(fā)者自己按照文檔進(jìn)行一些配置。在 JDK 中提供了一個(gè)安全鑰匙與證書(shū)的管理工具 Keytool。Keytool 把鑰匙,證書(shū)以及和與它們相關(guān)聯(lián)的證書(shū)鏈儲存到一個(gè) keystore 中,默任的實(shí)現 keystore 的是一個(gè)文件,它本身有一個(gè)訪(fǎng)問(wèn)密碼來(lái)保護存儲在其中的內容。就本樣例程序而言,只需要配置客戶(hù)端和服務(wù)器端雙方信任就可以了??梢园凑杖缦聨撞絹?lái)完成:
1. 進(jìn)入本地的 java 安裝位置的 bin 目錄中 cd /java/bin
2. 創(chuàng )建一個(gè)客戶(hù)端 keystore 文件,如圖 2 所示
keytool -genkey -alias sslclient -keystore sslclientkeys
3. 將客戶(hù)端 keystore 文件導出成證書(shū)格式
keytool -export -alias sslclient -keystore sslclientkeys -file sslclient.cer
4. 創(chuàng )建一個(gè)服務(wù)器端 keystore 文件
keytool -genkey -alias sslserver -keystore sslserverkeys
5. 將服務(wù)器端 keystore 文件導出成證書(shū)格式
keytool -export -alias sslserver -keystore sslserverkeys -file sslserver.cer
6. 將客戶(hù)端證書(shū)導入到服務(wù)器端受信任的 keystore 中
keytool -import -alias sslclient -keystore sslservertrust -file sslclient.cer
7. 將服務(wù)器端證書(shū)導入到客戶(hù)端受信任的 keystore 中
keytool -import -alias sslserver -keystore sslclienttrust -file sslserver.cer
以上所有步驟都完成后,還可以通過(guò)命令來(lái)查看 keystore 文件基本信息,如圖 3 所示
keytool -list -keystore sslclienttrust
將前面創(chuàng )建的所有 keystore 文件從 java 的 bin 目錄中剪切出來(lái),移動(dòng)到樣例程序的執行目錄中,通過(guò)運行程序時(shí)候的系統屬性來(lái)指定這些文件,重新執行一遍樣例程序。
java -cp ./build/classes -Djavax.net.ssl.keyStore=sslserverkeys -Djavax.net.ssl.keyStorePassword=123456 -Djavax.net.ssl.trustStore=sslservertrust -Djavax.net.ssl.trustStorePassword=123456 example.ssl.codes.SSLServer java -cp ./build/classes -Djavax.net.ssl.keyStore=sslclientkeys -Djavax.net.ssl.keyStorePassword=123456 -Djavax.net.ssl.trustStore=sslclienttrust -Djavax.net.ssl.trustStorePassword=123456 example.ssl.codes.SSLClient
執行結果如下:
客戶(hù)端輸出
Welcome, principal This is confidential content from server X, for your eye!
客戶(hù)端與服務(wù)器端成功建立起 SSL 的連接,然后服務(wù)器端成功將字符串發(fā)送給客戶(hù)端,客戶(hù)端將其打印出來(lái)。