1. 前期準備工作
雙向證書認證的雙方稱爲client和server,首先爲client和server生成證書。由於僅僅是自己學習使用,因此可以在本地自建一個CA,然後用CA的證書分別簽發client和server的證書。CA的創建和簽發使用OpenSSL。
在windows環境上安裝OpenSSL,然後依據OpenSSL目錄下的openssl.cnf中[ CA_default ]的配置創建相應的文件夾和文件
- demoCA/ —- CA的根目錄
- |– newcerts/—- CA籤發出去的證書
- |– private/ —- CA自己的私鑰,默認名稱是cakey.pem
- |– serial —- 存放證書序列號的文件
- |– index.txt —- 簽發過的證書的記錄,文本文件
serial這個文件中可以初始寫入一行記錄,包含兩個字符01,表示下一個簽發的證書採用的序列號是01
接下來生成CA自己的公私鑰(public/private key),生成證書籤名請求(CSR, Certificate Signing Request)文件並對該請求進行自簽名
在openssl的根目錄下運行
- openssl genrsa -out ./demoCA/private/cakey.pem 2048
很多人將genrsa解釋爲只生成private key,這是不對的。可以用下面的命令從文件中解出公鑰
- 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的內容格式是
- —–BEGIN CERTIFICATE REQUEST—–
- MIICnzCCAYcCAQAwWjELMAkGA1UEBhMCQ04xCzAJBgNVBAgTAlpKMQswCQYDVQQH
- … …
- ZYu4AZp0VzqnQzCTeYTbC+AsA0RrPVjr95Il46AHvhq2JQpFw8DhrS8Ja1VburI4
- ngFK
- —–END CERTIFICATE REQUEST—–
最後將該請求文件給CA機構做簽名,但我們現在是想在本地建CA,因此自己對該文件進行自簽名即可。
- openssl ca -selfsign -in careq.pem -out cacert.pem
其實,上面生成CSR然後做自簽名的兩個步驟可合併到一步完成
- 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握手一方所信任的證書,一般沒有這些證書所對應的私鑰
- 生成client的keystore 和key pair
- keytool -genkey -alias client -keyalg RSA -keystore client.keystore -keysize 2048
- 生成csr
- keytool -certreq -alias client -keystore client.keystore -file client.csr
- 用本地CA對該csr簽名
client證書中我們想添加證書的一項擴展,比如client id,用來區分client的身份,因此需要額外的一份擴展文件client.cnf,內容如下- [v3_req]
- 1.2.3.412=ASN1:UTF8String:0000001444
可以將該csr和client.cnf文件拷貝到openssl根目錄下,運行- openssl ca -in client.csr -out client.pem -config ./openssl.cnf -extensions v3_req -extfile client.cnf
- 將簽過名的client.pem導入到keystore文件中
在導入之前,需要先將CA的證書導入keystore文件- keytool -keystore client.keystore -importcert -alias CA -file cacert.pem
- keytool -keystore client.keystore -importcert -alias client -file client.pem
keystore文件內容的查看可以使用
keytool -list -v -keystore client.keystore
或者使用可視化工具KeyStore Explorer查看
由於server的證書也是本地CA簽發的,因此client只要信任CA的證書那麼自然會信任CA簽發出的證書,所以我們只需將CA的證書導入trust store即可
- keytool -import -alias cacert -file cacert.pem -keystore clienttruststore.keystore
由於clienttruststore.keystore文件尚不存在,此命令首先創建該文件並將CA的證書導入該trust store
server的證書和key store和trust store可類似創建
- keytool -genkey -alias server -keyalg RSA -keystore server.keystore -keysize 2048
- keytool -certreq -alias server -keystore server.keystore -file server.csr
- openssl ca -in server.csr -out server.pem -config ./openssl.cnf
- keytool -keystore server.keystore -importcert -alias CA -file cacert.pem
- keytool -keystore server.keystore -importcert -alias server -file server.pem
- 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裏面的證書
- public class SslContextFactory
- {
- private static final String PROTOCOL = "TLS"; // TODO: which protocols will be adopted?
- private static final SSLContext SERVER_CONTEXT;
- private static final SSLContext CLIENT_CONTEXT;
- static
- {
- SSLContext serverContext = null;
- SSLContext clientContext = null;
- String keyStorePassword = "aerohive";
- try
- {
- KeyStore ks = KeyStore.getInstance("JKS");
- ks.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\server.keystore"), keyStorePassword.toCharArray());
- // Set up key manager factory to use our key store
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(ks, keyStorePassword.toCharArray());
- // truststore
- KeyStore ts = KeyStore.getInstance("JKS");
- ts.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\servertruststore.keystore"), keyStorePassword.toCharArray());
- // set up trust manager factory to use our trust store
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(ts);
- // Initialize the SSLContext to work with our key managers.
- serverContext = SSLContext.getInstance(PROTOCOL);
- serverContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
- } catch (Exception e)
- {
- throw new Error("Failed to initialize the server-side SSLContext", e);
- }
- try
- {
- // keystore
- KeyStore ks = KeyStore.getInstance("JKS");
- ks.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\client.keystore"), keyStorePassword.toCharArray());
- // Set up key manager factory to use our key store
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(ks, keyStorePassword.toCharArray());
- // truststore
- KeyStore ts = KeyStore.getInstance("JKS");
- ts.load(SslContextFactory.class.getClassLoader().getResourceAsStream("cert\\clienttruststore.keystore"), keyStorePassword.toCharArray());
- // set up trust manager factory to use our trust store
- TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmf.init(ts);
- clientContext = SSLContext.getInstance(PROTOCOL);
- clientContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
- } catch (Exception e)
- {
- throw new Error("Failed to initialize the client-side SSLContext", e);
- }
- SERVER_CONTEXT = serverContext;
- CLIENT_CONTEXT = clientContext;
- }
- public static SSLContext getServerContext()
- {
- return SERVER_CONTEXT;
- }
- public static SSLContext getClientContext()
- {
- return CLIENT_CONTEXT;
- }
- ... ...
- }
io.netty.example.securechat.SecureChatClientInitializer類的構造器接收一個io.netty.handler.ssl.SslContext類型的對象,這個SslContext的對象最終被用來創建SslHandler,而上面factory產生的是javax.net.ssl.SSLContext的對象,因此可以做改動如下
- public class ClientInitializer extends ChannelInitializer
- {
- private final javax.net.ssl.SSLContext sslCtx;
- public ClientInitializer(javax.net.ssl.SSLContext sslCtx)
- {
- this.sslCtx = sslCtx;
- }
- @Override
- public void initChannel(SocketChannel ch) throws Exception
- {
- ChannelPipeline pipeline = ch.pipeline();
- SSLEngine sslEngine = sslCtx.createSSLEngine(Client.HOST, Client.PORT);
- sslEngine.setUseClientMode(true);
- pipeline.addLast(new SslHandler(sslEngine));
- // On top of the SSL handler, add the text line codec.
- pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));
- pipeline.addLast(new StringDecoder());
- pipeline.addLast(new StringEncoder());
- // and then business logic.
- pipeline.addLast(new ClientHandler());
- }
- }
最後添加jvm參數
- -Djavax.net.debug=ssl,handshake
-
-Ddeployment.security.TLSv1.1=true
-Ddeployment.security.TLSv1.2=true
來查看ssl握手過程控制檯的log
具體實現請參考附件源碼。
http://download.csdn.net/detail/virgilli/8373319附錄:
openssl的配置
對證書籤名時,遇到openssl異常failed to update database TXT_DB error
可以有三種方法解決這個問題
方法一:
修改demoCA下 index.txt.attr
將
改爲
方法二:
刪除demoCA下的index.txt,並再touch下
方法三:
將 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