您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)Java安全套接字編程以及keytool使用最佳實(shí)踐分析,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。
概述
利用 Java 的 JSSE(Java Secure Socket Extension)技術(shù),我們可以方便的編寫安全套接字程序,關(guān)于 JSSE 的介紹,可以參閱 Oracle 網(wǎng)站提供的 JSSE 指導(dǎo)。程序編寫過程中,我們需要將數(shù)字證書應(yīng)用到代碼中。通常在正式的產(chǎn)品開發(fā)中,我們可以支付一定的費(fèi)用,向正規(guī)認(rèn)證機(jī)構(gòu),例如:Verisign、Geotrust、Thawte 等申請。
如果只是為了實(shí)驗(yàn),我們還可以使用 Java 自帶的 keytool 工具來制作證書。keytool 是密鑰和證書管理工具,生成的密鑰或證書,存放在 jks(Java Key Store) 格式的文件里。從用途上來說,jks 格式的文件常用于:
1) 存儲非對稱密鑰對以及數(shù)字證書的證書庫;
2) 存儲信任證書列表的信任庫。
注意:不同版本的 Java 自帶的 keytool 命令行參數(shù)可能會略有不同。相比于 Java6,在 Java7 中 keytool 工具有如下改動:
-export 選項(xiàng)改名為 -exportcert
-genkey 選項(xiàng)改名為 -genkeypair
-import 選項(xiàng)改名為 – importcert
-keyclone 選項(xiàng)被廢棄
-identitydb 選項(xiàng)被廢棄
-selfcert 選項(xiàng)被廢棄
下面將以 Java7 中的 keytool 為例,對常見的用法進(jìn)行說明。
使用 keytool 制作證書庫以及信任庫
生成非對稱密鑰以及自簽發(fā)證書
命令:keytool -genkeypair -alias TEST_ROOT -keystore test_root.jks
解釋:生成一對密鑰以及一個(gè)自簽發(fā)證書,其中私鑰和證書以別名 TEST_ROOT 存儲在 test_root.jks 文件中。
注意:使用上述命令時(shí),命令行會交互的需要手動填寫密碼、CN、OU 等信息。也可以直接在命令行指定這些參數(shù),詳情見 參考資料中列出的 keytool 使用幫助。
生成證書請求文件
命令:keytool -certreq -file test_server.csr -alias TEST_SERVER -keystore test_server.jks
解釋:將別名為 TEST_SERVER 的公鑰和一些個(gè)人信息從 test_server.jks 文件中導(dǎo)出,作為證書請求文件。
簽發(fā)證書
命令:keytool -gencert -infile test_server.csr -outfile test_server.cer -alias TEST_ROOT -keystore TEST_ROOT.jks
解釋:使用別名為 TEST_ROOT 的私鑰為 test_server.csr 簽發(fā)證書,并保存到 test_server.cer 文件中。
從 jks 文件中導(dǎo)出證書
命令:keytool -exportcert -alias TEST_ROOT -file test_root.cer -keystore test_root.jks
解釋:從 test_root.jks 文件中導(dǎo)出別名為 TEST_ROOT 的證書并存放在 test_root.cer 文件中。
導(dǎo)入信任證書到 jks 文件
命令:keytool -importcert -alias TEST_ROOT -file test_root.cer -keystore TEST_SERVER.jks
解釋:將證書 test_root.cer 以別名 TEST_ROOT 導(dǎo)入 TEST_SERVER.jks 中。
注意:這里的目標(biāo) jks 文件里不含有指定的別名,此時(shí)的導(dǎo)入條目才會以 trustedCertEntry 信任證書的形式保存。
導(dǎo)入簽發(fā)證書到 jks 文件 ( 更新證書 )
命令:keytool -importcert -alias TEST_SERVER -file test_server.cer -keystore TEST_SERVER.jks
解釋:將證書 test_server.cer 更新到已存在別名 TEST_SERVER 的 TEST_SERVER.jks 文件中
注意:這里的命令和上述導(dǎo)入信任證書的命令在形式上完全一樣,但作用不同。
1. 這里的目標(biāo) jks 文件里要含有指定的別名,這樣 keytool 工具才會理解命令為更新證書,并以 PrivateKeyEntry 的形式保存。
2. 在更新被簽發(fā)證書之前,一定要先將相應(yīng)的 CA 證書,導(dǎo)入進(jìn) jks 文件,否則會報(bào)錯(cuò)“keytool 錯(cuò)誤 : java.lang.Exception: 無法從回復(fù)中建立鏈”。
打印證書內(nèi)容
命令:keytool – printcert – v – file test_server.cer
解釋:將證書 test_server.cer 的內(nèi)容打印出來
注意:也可以使用 -sslserver ip:port 的參數(shù),直接從網(wǎng)絡(luò)上打印出某個(gè) ssl server 提供的證書的內(nèi)容,詳情見 參考資料中列出的 keytool 使用幫助。
顯示 jks 文件里的內(nèi)容
命令:keytool – list – v – keystore test_server.jks
解釋:顯示 test_server.jks 里存儲的所有條目
注意:這里會要求提供 jks 文件的密碼,如果不輸入,也可以顯示出所有條目信息,但會提示“存儲在密鑰庫中的信息的完整性尚未得到驗(yàn)證!”
從 jks 文件刪除條目
命令:keytool -delete -alias TEST_ROOT -keystore test_server.jks
解釋:從 test_server.jks 中刪除別名為 TEST_ROOT 的條目
安全套接字程序編寫的方法
使用 Java 編寫安全套接字程序,可以遵循一定的方法,如圖 1 所示,展示了相關(guān)的各個(gè)類之間的關(guān)系。其中 Keystore、KeyManagerFactory、TrustManagerFactory、SSLContext 可以稱之為“引擎類”(engine class),對它們指定特定的參數(shù) ( 例如:協(xié)議、算法等 ),就可以產(chǎn)生符合我們要求的,用于編程的對象實(shí)例。
圖 1. 相關(guān)類之間的關(guān)系
(注:圖片引自 《 Java™ Secure Socket Extension (JSSE) Reference Guide 》)
編程的步驟可以簡單的小結(jié)為以下幾步:
1. 使用 Keystore 類將證書庫或信任庫文件加載進(jìn)來;
2. 使用 KeyManagerFactory 和加載了證書庫的 Keystore 實(shí)例,產(chǎn)生 KeyManager 實(shí)例數(shù)組;
3. 使用 TrustManagerFactory 和加載了信任庫的 Keystore 實(shí)例,產(chǎn)生 TrustManager 實(shí)例數(shù)組;
4. 使用 SSLContext 初始化 KeyManager 實(shí)例數(shù)組和 TrustManager 實(shí)例數(shù)組,從而設(shè)定好通信的環(huán)境。
5. 利用 SSLContext 產(chǎn)生的 SSLSocket 或 SSLServerSocket 進(jìn)行通信。
在編寫具體程序之前,我們需要利用前文對keytool 工具的知識介紹,準(zhǔn)備如下 jks 文件:
1. test_root.jks:該文件中存有自簽發(fā)的證書,用作 CA 來簽發(fā)證書;
2. test_server_cert.jks:該文件中存有 CA 簽名的證書,用于 SSL/TSL 通信的服務(wù)端;
3. test_server_trust.jks:該文件中存有信任客戶端的證書,用于 SSL/TSL 通信的服務(wù)端;
4. test_client_cert.jks:該文件中存有 CA 簽名的證書,用于 SSL/TSL 通信的客戶端;
5. test_client_trust.jks:該文件中存有信任服務(wù)端的證書,用于 SSL/TSL 通信的客戶端。
假定每個(gè) jks 文件的密碼都設(shè)定為“Testpassw0rd”,都存放在“D:”盤下。
通過系統(tǒng)屬性指定證書庫和信任庫
這種編寫方式比較簡單直觀,可以通過給 JVM 傳遞參數(shù),或者在代碼中使用 System.setProperty() 方法,來指定通信需要的 jks 文件。
服務(wù)端程序
要運(yùn)行如清單 1 所示的程序,可以在命令行添加如下虛擬機(jī)參數(shù),指定服務(wù)端程序要使用的證書庫和密碼:
-Djavax.net.ssl.keyStore="D:/test_server_cert.jks"
-Djavax.net.ssl.keyStorePassword="Testpassw0rd"
注意到程序中 setNeedClientAuth(false),表示不需要驗(yàn)證客戶端身份。如果這里設(shè)置為 true,則我們這里還需要指定信任庫和密碼:
-Djavax.net.ssl.trustStore="D:/test_server_trust.jks"
-Djavax.net.ssl.trustStorePassword="Testpassw0rd"
清單 1. 簡單的 SSL 通信服務(wù)端程序
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSocket; public class Simple_SSLServerSocket{ // 定義了監(jiān)聽端口號 private final static int LISTEN_PORT=54321; public static void main(String args[]) throws IOException{ SSLServerSocket serverSocket=null; SSLSocket clientSocket=null; // 使用默認(rèn)方式獲取套接字工廠實(shí)例 SSLServerSocketFactory ssf=(SSLServerSocketFactory)SSLServerSocketFactory.getDefault(); try{ serverSocket=(SSLServerSocket)ssf.createServerSocket(LISTEN_PORT); // 設(shè)置不需要驗(yàn)證客戶端身份 serverSocket.setNeedClientAuth(false); System.out.println("SSLServer is listening on "+LISTEN_PORT+" port"); // 循環(huán)監(jiān)聽端口,如果有客戶端連入就新開一個(gè)線程與之通信 while(true){ // 接受新的客戶端連接 clientSocket=(SSLSocket)serverSocket.accept(); ClientConnection clientConnection=new ClientConnection(clientSocket); // 啟動一個(gè)新的線程 Thread clientThread=new Thread(clientConnection); System.out.println("Client "+clientThread.getId()+" is connected"); clientThread.run(); } }catch(IOException ioExp){ ioExp.printStackTrace(); }catch(Exception e){ e.printStackTrace(); }finally{ serverSocket.close(); } } } class ClientConnection implements Runnable{ private Socket clientSocket=null; public ClientConnection(SSLSocket sslsocket){ clientSocket=sslsocket; } public void run(){ BufferedReader reader=null; // 將接收到的來自客戶端的文字打印出來 try{ reader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); while(true){ String line=reader.readLine(); if(line==null){ System.out.println("Communication end."); break; } System.out.println("Receive message: "+line); } reader.close(); clientSocket.close(); }catch(IOException ioExp){ ioExp.printStackTrace(); }catch(Exception e){ e.printStackTrace(); } } }
客戶端程序
對應(yīng)于清單 1 所示的服務(wù)端程序,清單 2 是客戶端程序,需要在命令行添加如下虛擬機(jī)參數(shù),指定信任庫和密碼:
-Djavax.net.ssl.trustStore="D:/test_client_trust.jks"
-Djavax.net.ssl.trustStorePassword="Testpassw0rd"
如果服務(wù)端程序 setNeedClientAuth(true) 要求驗(yàn)證客戶端身份,則我們還需要指定證書庫和密碼:
-Djavax.net.ssl.keyStore="D:/test_client_cert.jks"
-Djavax.net.ssl.keyStorePassword="Testpassw0rd"
清單 2. 簡單的 SSL 通信客戶端程序
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Writer; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; public class Simple_SSLSocket{ // 定義要連接的服務(wù)器名和端口號 private static final int DEFAULT_PORT=54321; private static final String DEFAULT_HOST="localhost"; public static void main(String args[]){ SSLSocket socket=null; // 使用默認(rèn)的方式獲取工廠實(shí)例 SSLSocketFactory sf=(SSLSocketFactory)SSLSocketFactory.getDefault(); try{ // 連接服務(wù)端的端口,完成握手過程 socket=(SSLSocket)sf.createSocket(DEFAULT_HOST, DEFAULT_PORT); socket.startHandshake(); System.out.println("Connected to "+DEFAULT_HOST+":"+DEFAULT_PORT+" !"); // 從控制臺輸入要發(fā)送給服務(wù)端的文字 BufferedReader reader=new BufferedReader(new InputStreamReader(System.in)); Writer writer=new OutputStreamWriter(socket.getOutputStream()); // 可以反復(fù)向服務(wù)端發(fā)送消息 boolean done=false; while (!done) { System.out.print("Send Message: "); String line=reader.readLine(); if (line!=null) { writer.write(line+"\n"); writer.flush(); }else{ done=true; } } socket.close(); }catch (Exception e) { System.out.println("Connection failed: "+e); try{ socket.close(); }catch(IOException ioe){} socket=null; } } }
通過 SSLContext 指定證書庫和信任庫
前文描述的,通過系統(tǒng)參數(shù)指定證書庫和信任庫的方法,雖然簡單易用,但是缺點(diǎn)也是顯而易見的,整個(gè)程序的環(huán)境都得使用同樣的 jks 文件。如果程序里有不同的 SSL/TSL 通信,則需要使用不同的 jks 文件,該怎么做呢?
可以使用 SSLContext 來指定 jks 文件,只需要把清單 3 的代碼片段替換到清單 1 的“SSLServerSocketFactory ssf”生成處;把清單 4 的代碼片段替換到清單 2 的“SSLSocketFactory sf”生成處,再稍作代碼調(diào)整即可。
(注:實(shí)際上,在使用 SSLSocketFactory.getDefault() 或者 SSLServerSocketFactory.getDefault() 創(chuàng)建套接字的時(shí)候,程序內(nèi)部已經(jīng)使用了默認(rèn)的 context,其參數(shù)就是通過系統(tǒng)屬性指定的 )
清單 3. SSLContext 指定證書庫
// 相關(guān)的 jks 文件及其密碼定義 private final static String CERT_STORE="D:/test_server_cert.jks"; private final static String CERT_STORE_PASSWORD="Testpassw0rd"; // 載入 jks 文件 FileInputStream f_certStore=new FileInputStream(CERT_STORE); KeyStore ks=KeyStore.getInstance("jks"); ks.load(f_certStore, CERT_STORE_PASSWORD.toCharArray()); f_certStore.close(); // 創(chuàng)建并初始化證書庫工廠 String alg=KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmFact=KeyManagerFactory.getInstance(alg); kmFact.init(ks, CERT_STORE_PASSWORD.toCharArray()); KeyManager[] kms=kmFact.getKeyManagers(); // 創(chuàng)建并初始化 SSLContext 實(shí)例 SSLContext context=SSLContext.getInstance("SSL"); context.init(kms, null, null); SSLServerSocketFactory ssf=(SSLServerSocketFactory)context.getServerSocketFactory();
清單 4. SSLContext 指定信任庫
// 相關(guān)的 jks 文件及其密碼定義 private final static String TRUST_STORE="D:/test_client_trust.jks"; private final static String TRUST_STORE_PASSWORD="Testpassw0rd"; // 載入 jks 文件 FileInputStream f_trustStore=new FileInputStream(TRUST_STORE); KeyStore ks=KeyStore.getInstance("jks"); ks.load(f_trustStore, TRUST_STORE_PASSWORD.toCharArray()); f_trustStore.close(); // 創(chuàng)建并初始化信任庫工廠 String alg=TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmFact=TrustManagerFactory.getInstance(alg); tmFact.init(ks); TrustManager[] tms=tmFact.getTrustManagers(); // 創(chuàng)建并初始化 SSLContext 實(shí)例 SSLContext context=SSLContext.getInstance("SSL"); context.init(null, tms, null); SSLSocketFactory sf=context.getSocketFactory();
當(dāng)然,如果要在服務(wù)端或者客戶端同時(shí)使用證書庫和信任庫,可將清單 3 和清單 4 的代碼用在同一處 context.init() 的地方。
這里需要說明的是,如果 context.init() 的第一個(gè) KeyManager[] 參數(shù)為 null,則表示不提供證書;如果第二個(gè) TrustManager[] 參數(shù)為 null,則會尋找系統(tǒng)默認(rèn)提供的信任庫 ( 例如:JRE 安裝目錄下的 lib/security/cacerts)。
使用 X509 證書信任管理器
X509TrustManager 接口擴(kuò)展了 TrustManager 接口,使用 TrustManager 接口,我們已經(jīng)可以在程序中自定義信任庫了,但如果對方的證書不在信任庫中,則通信會直接宣告失敗。
如果希望能自定義信任庫的一些行為 ( 例如:檢驗(yàn)對方證書,針對異常做一些處理 ),我們可以使用 X509TrustManager 接口,實(shí)現(xiàn)自己的方法。
如清單 5 所示,假定我們要在客戶端程序使用 X509TrustManager,那么就可以在 checkServerTrusted() 函數(shù)里做一些事情,檢測到服務(wù)端證書異常的話,就可以做一些自己的處理。CheckClientTrusted() 則是用于服務(wù)端檢測客戶端的證書情況。
將清單 5 的代碼替換到清單 4 的 TrustManager[] tms 的生成處,并對代碼稍作調(diào)整即可。
清單 5. X509TrustManager 的使用
// 使用自定義的 MyTrustManager 產(chǎn)生信任庫 TrustManager[] tms=new TrustManager[]{new MyTrustManager()}; …… …… class MyTrustManager implements X509TrustManager{ // 相關(guān)的 jks 文件及其密碼定義 private final static String TRUST_STORE="D:/test_client_trust.jks"; private final static String TRUST_STORE_PASSWORD="Testpassw0rd"; X509TrustManager xtm; public MyTrustManager() throws Exception { // 載入 jks 文件 KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(TRUST_STORE),TRUST_STORE_PASSWORD.toCharArray()); TrustManagerFactory tmf =TrustManagerFactory.getInstance("SunX509", "SunJSSE"); tmf.init(ks); TrustManager[] tms = tmf.getTrustManagers(); // 篩選出 X509 格式的信任證書 for (int i = 0; i < tms.length; i++) { if (tms[i] instanceof X509TrustManager) { xtm = (X509TrustManager) tms[i]; return; } } } // 服務(wù)端檢驗(yàn)客戶端證書的接口 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException{ } // 客戶端檢驗(yàn)服務(wù)端證書的接口 public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException{ try{ xtm.checkServerTrusted(chain, authType); }catch(CertificateException excep){ System.out.println(excep.getMessage()); throw excep; } } // 獲取可接受的發(fā)行者 public X509Certificate[] getAcceptedIssuers() { //return xtm.getAcceptedIssuers(); return null; } }
注意:
1. 當(dāng)服務(wù)端代碼 setNeedClientAuth(False) 時(shí),客戶端的 MyTrustManager 實(shí)現(xiàn)了 X509TrustManager 后,如果 checkServerTrusted() 方法的實(shí)現(xiàn)為空,則無論服務(wù)端使用什么證書,客戶端都會默認(rèn)接受;如果要對服務(wù)端證書進(jìn)行檢查,還需要像清單 5 中的代碼片段那樣,捕捉異常并處理。
2.getAcceptedIssuers() 方法通常不需要具體實(shí)現(xiàn),但是當(dāng)服務(wù)端要求檢驗(yàn)客戶端身份,也即 setNeedClientAuth(True) 時(shí),服務(wù)端需也需要具體實(shí)現(xiàn) X509TrustManager,且 getAcceptedIssuers() 方法要如清單 5 中注釋部分代碼那樣實(shí)現(xiàn)。
調(diào)試 SSL/TSL 程序
打開調(diào)試開關(guān)觀察通信日志
圖 2 描述了 SSL/TSL 通信的握手過程。在實(shí)際編寫程序的時(shí)候,可能會在這些環(huán)節(jié)遇到問題,導(dǎo)致無法通信,排查起來往往令人無從下手。這個(gè)時(shí)候我們可以將 SSL/TSL 通信的握手日志開關(guān)打開,進(jìn)行觀察。
圖 2.SSL 通信協(xié)議握手過程
(注:圖片引自 《Java™ Secure Socket Extension (JSSE) Reference Guide》)
打開日志開關(guān)的方法同樣是通過設(shè)定系統(tǒng)屬性,可以從命令行添加虛擬機(jī)參數(shù):
-Djavax.net.debug=ssl,handshake
當(dāng)然也可以使用 System.setProperty() 方法在代碼中打開該開關(guān)。
打開日志開關(guān)后,可以搜索“ClientHello”、“ServerHello”等關(guān)鍵字;或者搜索“*** ”( 三個(gè)星號和一個(gè)空格 ) ——這是握手階段每一個(gè)步驟日志打印的開始標(biāo)志。通過閱讀日志來定位問題。更詳細(xì)的開關(guān)信息,請參閱 JSSE 指導(dǎo)中的“Debugging Utilities”章節(jié):
選擇通信的 Cipher Suites
有的時(shí)候?yàn)榱俗鰧?shí)驗(yàn),我們會選用特定的 Cipher Suites,我們可以使用 SSLServerSocket 或 SSLSocket 的 getEnabledCipherSuites() 觀察到默認(rèn)支持的所有加密套件的信息,然后使用 setEnabledCipherSuites() 進(jìn)行設(shè)置。
清單 6 展示了從服務(wù)端過濾掉所有的匿名加密套件的代碼。
清單 6. 過濾所有的匿名加密套件
String enabled[]=serverSocket.getEnabledCipherSuites(); Set<String> filter = new LinkedHashSet<String>(); for(int i = 0; i < enabled.length; i++) { if(enabled[i].indexOf("anon")<0){ filter.add(enabled[i]); } } serverSocket.setEnabledCipherSuites(filter.toArray(new String[filter.size()]));
關(guān)于Java安全套接字編程以及keytool使用最佳實(shí)踐分析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。