Netty中使用SSL 雙向認證

1. 前期準備工作

        雙向證書認證的雙方稱爲client和server,首先爲client和server生成證書。由於僅僅是自己學習使用,因此可以在本地自建一個CA,然後用CA的證書分別簽發client和server的證書。CA的創建和簽發使用OpenSSL。 
在windows環境上安裝OpenSSL,然後依據OpenSSL目錄下的openssl.cnf中[ CA_default ]的配置創建相應的文件夾和文件 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. demoCA/ —- CA的根目錄   
  2. |– newcerts/—- CA籤發出去的證書   
  3. |– private/ —- CA自己的私鑰,默認名稱是cakey.pem   
  4. |– serial —- 存放證書序列號的文件   
  5. |– index.txt —- 簽發過的證書的記錄,文本文件  

        serial這個文件中可以初始寫入一行記錄,包含兩個字符01,表示下一個簽發的證書採用的序列號是01 
接下來生成CA自己的公私鑰(public/private key),生成證書籤名請求(CSR, Certificate Signing Request)文件並對該請求進行自簽名 
在openssl的根目錄下運行 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. openssl genrsa -out ./demoCA/private/cakey.pem 2048   
genrsa —- 同時生成public key和private key 
很多人將genrsa解釋爲只生成private key,這是不對的。可以用下面的命令從文件中解出公鑰 
[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. openssl rsa -in cakey.pem -pubout > capublickey.pub  

注意最後的數字2048表示生成的RSA公私鑰的長度

JDK7中對證書檢查要求公鑰的長度最少爲1024位,否則會拋出異常 

java.security.cert.CertPathValidatorException: Algorithm constraints check failed 
該長度限制是可以配置的,配置文件路徑是JAVA_HOME/jre/lib/security/java.security 
jdk.certpath.disabledAlgorithms=MD2, RSA keySize < 1024 


然後用上面生成的公私鑰文件創建一個證書籤名請求文件 

openssl req -new -key ./demoCA/private/cakey.pem -out careq.pem 

req —- 創建CSR或者證書 
-key —- openssl從這個文件中讀取private key 
careq.pem的內容格式是 
[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. —–BEGIN CERTIFICATE REQUEST—–   
  2. MIICnzCCAYcCAQAwWjELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlpKMQswCQYDVQQH   
  3. … …   
  4. ZYu4AZp0VzqnQzCTeYTbC+AsA0RrPVjr95Il46AHvhq2JQpFw8DhrS8Ja1VburI4   
  5. ngFK   
  6. —–END CERTIFICATE REQUEST—–  

最後將該請求文件給CA機構做簽名,但我們現在是想在本地建CA,因此自己對該文件進行自簽名即可。 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. openssl ca -selfsign -in careq.pem -out cacert.pem  

其實,上面生成CSR然後做自簽名的兩個步驟可合併到一步完成 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. openssl req -new -x509 -key ./demoCA/private/cakey.pem -out cacert.pem  

至此,我們已經建立了自己的CA,接下去來分別簽發client和server的證書。

創建client和server的證書、key store和trust store

        以創建client的證書爲例。由於jdk自帶的keytool工具可以方便的創建key store和公私鑰,因此公私鑰和csr的創建直接使用keytool 
key store和trust store分別對應於ssl握手證書認證中自己的證書和自己所信任的證書列表,二者的文件格式相同,不同之處是key store裏面包含ssl握手一方的公私鑰和證書,trust store裏面包含ssl握手一方所信任的證書,一般沒有這些證書所對應的私鑰

  1. 生成client的keystore 和key pair 
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. keytool -genkey -alias client -keyalg RSA -keystore client.keystore -keysize 2048  
  2. 生成csr 
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. keytool -certreq -alias client -keystore client.keystore -file client.csr  
  3. 用本地CA對該csr簽名 
    client證書中我們想添加證書的一項擴展,比如client id,用來區分client的身份,因此需要額外的一份擴展文件client.cnf,內容如下
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. [v3_req]   
    2. 1.2.3.412=ASN1:UTF8String:0000001444   

    可以將該csr和client.cnf文件拷貝到openssl根目錄下,運行 
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. openssl ca -in client.csr -out client.pem -config ./openssl.cnf -extensions v3_req -extfile client.cnf  
  4. 將簽過名的client.pem導入到keystore文件中 
    在導入之前,需要先將CA的證書導入keystore文件 
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. keytool -keystore client.keystore -importcert -alias CA -file cacert.pem   
    然後導入client自己的證書。注意alias是client,與生產keystore和key pair的必須匹配 
    [html] view plain copy
     在CODE上查看代碼片派生到我的代碼片
    1. keytool -keystore client.keystore -importcert -alias client -file client.pem  

keystore文件內容的查看可以使用 
keytool -list -v -keystore client.keystore 
或者使用可視化工具KeyStore Explorer查看

5. 創建client的trust store 
由於server的證書也是本地CA簽發的,因此client只要信任CA的證書那麼自然會信任CA簽發出的證書,所以我們只需將CA的證書導入trust store即可 
[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. keytool -import -alias cacert -file cacert.pem -keystore clienttruststore.keystore   

由於clienttruststore.keystore文件尚不存在,此命令首先創建該文件並將CA的證書導入該trust store

server的證書和key store和trust store可類似創建 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. keytool -genkey -alias server -keyalg RSA -keystore server.keystore -keysize 2048   
  2. keytool -certreq -alias server -keystore server.keystore -file server.csr  
  3.   
  4. openssl ca -in server.csr -out server.pem -config ./openssl.cnf  
  5.   
  6. keytool -keystore server.keystore -importcert -alias CA -file cacert.pem   
  7. keytool -keystore server.keystore -importcert -alias server -file server.pem   
  8. keytool -import -alias ca -file cacert.pem -keystore servertruststore.keystore  

2. 創建SSL通訊的client和server

由於netty 5現在只有alpha版本,因此保險起見使用4.0.24.final版本的netty。 
netty的SSLContext提供了newClientContext來爲client創建ssl context,但查看其源碼未發現能支持雙向認證,即client端的ssl context只接收一個trust store,而不能指定自己的證書以供server端校驗。仿照netty example下的securechat的ssl實現但做了修改 
首先創建一個能提供client和server的ssl context的工具類,分別加載server和client的key store和trust store裏面的證書

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class SslContextFactory  
  2. {  
  3.         private static final String     PROTOCOL    = "TLS";    // TODO: which protocols will be adopted?  
  4.     private static final SSLContext SERVER_CONTEXT;  
  5.     private static final SSLContext CLIENT_CONTEXT;  
  6.   
  7.     static  
  8.     {  
  9.         SSLContext serverContext = null;  
  10.         SSLContext clientContext = null;  
  11.   
  12.         String keyStorePassword = "aerohive";  
  13.         try  
  14.         {  
  15.             KeyStore ks = KeyStore.getInstance("JKS");  
  16.             ks.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\server.keystore"), keyStorePassword.toCharArray());  
  17.   
  18.             // Set up key manager factory to use our key store  
  19.             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());  
  20.             kmf.init(ks, keyStorePassword.toCharArray());  
  21.   
  22.             // truststore  
  23.             KeyStore ts = KeyStore.getInstance("JKS");  
  24.             ts.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\servertruststore.keystore"), keyStorePassword.toCharArray());  
  25.   
  26.             // set up trust manager factory to use our trust store  
  27.             TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());  
  28.             tmf.init(ts);  
  29.   
  30.             // Initialize the SSLContext to work with our key managers.  
  31.             serverContext = SSLContext.getInstance(PROTOCOL);  
  32.             serverContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);  
  33.   
  34.         } catch (Exception e)  
  35.         {  
  36.             throw new Error("Failed to initialize the server-side SSLContext", e);  
  37.         }  
  38.   
  39.         try  
  40.         {  
  41.             // keystore  
  42.             KeyStore ks = KeyStore.getInstance("JKS");  
  43.             ks.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\client.keystore"), keyStorePassword.toCharArray());  
  44.   
  45.             // Set up key manager factory to use our key store  
  46.             KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());  
  47.             kmf.init(ks, keyStorePassword.toCharArray());  
  48.   
  49.             // truststore  
  50.             KeyStore ts = KeyStore.getInstance("JKS");  
  51.             ts.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\clienttruststore.keystore"), keyStorePassword.toCharArray());  
  52.   
  53.             // set up trust manager factory to use our trust store  
  54.             TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());  
  55.             tmf.init(ts);  
  56.             clientContext = SSLContext.getInstance(PROTOCOL);  
  57.             clientContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);  
  58.         } catch (Exception e)  
  59.         {  
  60.             throw new Error("Failed to initialize the client-side SSLContext", e);  
  61.         }  
  62.   
  63.         SERVER_CONTEXT = serverContext;  
  64.         CLIENT_CONTEXT = clientContext;  
  65.     }  
  66.   
  67.     public static SSLContext getServerContext()  
  68.     {  
  69.         return SERVER_CONTEXT;  
  70.     }  
  71.   
  72.     public static SSLContext getClientContext()  
  73.     {  
  74.         return CLIENT_CONTEXT;  
  75.     }  
  76.     ... ...  
  77. }  

         io.netty.example.securechat.SecureChatClientInitializer類的構造器接收一個io.netty.handler.ssl.SslContext類型的對象,這個SslContext的對象最終被用來創建SslHandler,而上面factory產生的是javax.net.ssl.SSLContext的對象,因此可以做改動如下 

[java] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. public class ClientInitializer extends ChannelInitializer   
  2. {  
  3.   
  4. private final javax.net.ssl.SSLContext  sslCtx;  
  5.   
  6. public ClientInitializer(javax.net.ssl.SSLContext sslCtx)  
  7. {  
  8.     this.sslCtx = sslCtx;  
  9. }  
  10.   
  11. @Override  
  12. public void initChannel(SocketChannel ch) throws Exception  
  13. {  
  14.     ChannelPipeline pipeline = ch.pipeline();  
  15.   
  16.     SSLEngine sslEngine = sslCtx.createSSLEngine(Client.HOST, Client.PORT);  
  17.     sslEngine.setUseClientMode(true);  
  18.     pipeline.addLast(new SslHandler(sslEngine));  
  19.   
  20.     // On top of the SSL handler, add the text line codec.  
  21.     pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));  
  22.     pipeline.addLast(new StringDecoder());  
  23.     pipeline.addLast(new StringEncoder());  
  24.   
  25.     // and then business logic.  
  26.     pipeline.addLast(new ClientHandler());  
  27. }  
  28. }  


最後添加jvm參數 

[html] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. -Djavax.net.debug=ssl,handshake   
  2. -Ddeployment.security.TLSv1.1=true -Ddeployment.security.TLSv1.2=true

來查看ssl握手過程控制檯的log

具體實現請參考附件源碼。

http://download.csdn.net/detail/virgilli/8373319

1. 本人測試,發現證書需要按照文檔重新生成
2. 如果是mac 的需要注意證書的地址(在源代碼中)
3. 在按照教程生成的過程中,需要保證省和組織機構的名稱一致。這個可以在index.txt 中查看。

附錄: 
openssl的配置 
對證書籤名時,遇到openssl異常failed to update database TXT_DB error 

可以有三種方法解決這個問題

方法一:

 

修改demoCA下 index.txt.attr

 

Java代碼  收藏代碼
  1. unique_subject = yes  

 改爲

Java代碼  收藏代碼
  1. unique_subject = no  

 方法二:

 

刪除demoCA下的index.txt,並再touch下

 

Java代碼  收藏代碼
  1. rm index.txt  
  2.   
  3. touch index.txt  

 方法三:

將 common name設置成不同的


有可能是因爲簽名的csr文件的subject中的一項或幾項在該CA之前簽發過的證書中已經出現過或者是csr中提供的國家/省份等等的名稱與CA自己的不相同,這些限制都可以在openssl.cnf文件中修改 
unique_subject=no

[ policy_match ] 
countryName = match 
#stateOrProvinceName = match 
organizationName = match 
organizationalUnitName = optional 
commonName = supplied 
emailAddress = optional


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章