常見的https網站作的是服務端認證(server authentication),瀏覽器經過證書判斷你所訪問的https://baidu.com是否真的是百度,而不是其餘人僞造的網站。同時還對流量加密,防止別人竊聽你的流量。html
tls還能夠作客戶端認證(client authentication),即服務端判斷客戶端是否爲其所信任的客戶端。因而可知,客戶端認證用於那些須要受控訪問服務端。java
在數據中心中,有些服務是很是敏感的,那麼咱們要作到:git
因此很明顯,前兩個問題能夠經過服務端認證解決,最後一個問題能夠經過客戶端認證解決。順便一提,若是要使用客戶端認證就必須使用服務端認證。github
先來說講概念而後舉個tomcat的例子講講怎麼作。web
不管是作Server authentication仍是Client authentication都須要證書。證書的來源有兩種:apache
在一切可能的狀況下都應該使用權威CA簽發的證書,爲何這麼建議?由於這裏牽涉到一個信任問題,瀏覽器、編程語言SDK和某些工具都維護了一個信任CA證書清單,只要是由這些CA簽發的證書那就信任,不然就不信任。而這個鏈條是能夠多級的,這裏就不展開了。你只須要知道由信任CA簽發的全部證書都是可信的。好比JDK自帶的信任CA證書能夠經過下面命令看到:編程
keytool -list -keystore $JAVA_HOME/jre/lib/security/cacerts verisignclass2g2ca [jdk], 2016-8-25, trustedCertEntry, 證書指紋 (SHA1): B3:EA:C4:47:76:C9:C8:1C:EA:F2:9D:95:B6:CC:A0:08:1B:67:EC:9D digicertassuredidg3 [jdk], 2016-8-25, trustedCertEntry, 證書指紋 (SHA1): F5:17:A2:4F:9A:48:C6:C9:F8:A2:00:26:9F:DC:0F:48:2C:AB:30:89 verisignuniversalrootca [jdk], 2016-8-25, trustedCertEntry, ...
讓你輸密碼的時候輸入changeit
。json
若是這個證書不是由信任CA簽發的(好比本身簽發)會發生什麼?瀏覽器、編程語言SDK、你所使用的工具會報告如下錯誤:瀏覽器
curl:tomcat
curl: (60) SSL certificate problem: self signed certificate in certificate chain
Java:
Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1964) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:328) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:322) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1614) ...
瀏覽器:
這個錯誤實際上就是在告訴你這個證書不可信任,多是一個僞造站點,讓你當心點兒。若是這個證書由權威CA簽發,那麼就沒有這個問題了。可是權威CA簽發的證書要求申請人擁有域名,若是你這個服務是內部使用的沒有域名,那就只能本身簽發了。那麼如何解決上面的問題呢?你得把本身簽發的證書加入到信任CA證書清單裏。
下圖是權威CA簽發證書的示例:
能夠看到客戶端有一個truststore,這個就是存放信任CA證書的地方,服務端有一個keystore,存放的本身的證書及對應的私鑰。
下圖是自簽發證書的示例:
在上面能夠看到咱們本身成爲了一個Root CA,把它放到客戶端的truststore裏。
前面講過客戶端認證是服務端來驗證客戶端是否可信的機制,其實作法和服務端認證相似只不過方向相反。客戶端認證大多數狀況下只能是自簽發的(由於沒有域名),雖然不是不能夠從權威CA簽發可是存在一些問題。下面解釋爲何,假設權威CA是let's encrypt,而後服務端信任它簽發的全部證書。可是let's encrypt是阿貓阿狗均可以申請的,如今有一個黑客申請了這個證書,而後請求你的服務端,服務端就承認了。
上面這個問題能夠用這個方法解決:好比你用let's encrypt申請了A證書,黑客用let's encrypt申請了B證書,你的服務端的truststore只信任A證書,那麼黑客用B證書訪問你的時候就會被拒絕。可是這就帶來另外一個問題,好比你在開發的時候客戶端證書有這麼幾套:生產用、調試用、開發用,那麼每次客戶端簽發一個證書都要更新到你的服務器的truststore裏,這也太麻煩了。
因此結合安全性和便利性,咱們把本身變成Root CA,而後服務端信任它,這樣一來服務端就能夠在開發的時候把Client Root CA內置進去,大大減輕了維護truststore的工做量,看下圖:
下面舉一個Tomcat作客戶端認證的例子,由於是測試用,因此服務端認證也是用的自簽發證書。
咱們用了cfssl這個工具來生成證書。
先弄一套目錄:
# 放自簽發的服務端CA根證書 server-secrets/ca # 放自簽發的服務端的證書 server-secrets/cert # 放服務端的keystore和truststore server-secrets/jks
新建文件:server-secrets/ca/server-root-ca-csr.json
內容以下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "server-root-ca" }
運行下面命令生成Server ROOT CA證書:
cfssl gencert --initca=true ./server-root-ca-csr.json | cfssljson --bare server-root-ca
會獲得下面幾個文件:
server-secrets/ca/ ├── server-root-ca-key.pem ├── server-root-ca.csr └── server-root-ca.pem
用下面命令驗證證書:
openssl x509 -in ./server-root-ca.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 0c:8a:1a:ca:da:fa:4c:17:6c:1f:42:40:4c:f1:90:f4:fd:1d:fe:58 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca Validity Not Before: Mar 27 05:14:00 2019 GMT Not After : Mar 25 05:14:00 2024 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca
能夠看到簽發人和被簽發人是同一個。
新建文件 server-secrets/cert/server-gencert.json,內容以下:
{ "signing": { "default": { "usages": [ "signing", "key encipherment", "server auth" ], "expiry": "87600h" } } }
能夠看到咱們會生成用來作server auth的證書。
新建文件 server-secrets/cert/demo-csr.json,內容以下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "server-demo", "hosts": [ "127.0.0.1", "localhost" ] }
看上面的hosts,你能夠根據本身的須要填寫域名或IP,這裏由於是本地演示因此是127.0.0.1和localhost。
運行下面命令生成證書
cfssl gencert \ --ca ../ca/server-root-ca.pem \ --ca-key ../ca/server-root-ca-key.pem \ --config ./server-gencert.json \ ./demo-csr.json | cfssljson --bare ./demo
獲得文件:
server-secrets/cert/ ├── demo-key.pem ├── demo.csr └── demo.pem
驗證結果:
openssl x509 -in ./demo.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 1d:d0:51:97:6c:ce:ea:29:2a:f4:3b:3c:48:a3:69:b0:ef:f3:26:7b Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-root-ca Validity Not Before: Mar 27 05:17:00 2019 GMT Not After : Mar 24 05:17:00 2029 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=server-demo
能夠看到簽發者是server-root-ca,Subject是server-demo。
到 server-secrets/jks,執行下面命令生成pkcs12格式的keystore(JDK識別這個格式)
openssl pkcs12 -export \ -in ../cert/demo.pem \ -inkey ../cert/demo-key.pem \ -out server-demo.keystore \ -name server-demo \ -CAfile ../ca/server-root-ca.pem \ -caname root -chain
過程當中會讓你輸入密碼,你就輸入:server-demo-ks。
獲得文件:
server-secrets/jks/ └── server-demo.keystore
用JDK提供的keytool看看裏面的內容:
keytool -list -keystore server-demo.keystore server-demo, 2019-3-27, PrivateKeyEntry, 證書指紋 (SHA1): B2:E5:46:63:BB:00:E7:82:48:A4:2F:EC:01:41:CE:B4:4B:CE:68:7A
讓你輸入密碼的時候就輸入:server-demo-ks。
先弄一套目錄:
# 放自簽發的客戶端CA根證書 client-secrets/ca # 放自簽發的客戶端的證書 client-secrets/cert # 放客戶端的keystore和truststore client-secrets/jks
新建文件 client-secrets/ca/client-root-ca-csr.json:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "client-root-ca" }
運行下面命令生成Client ROOT CA證書:
cfssl gencert --initca=true ./client-root-ca-csr.json | cfssljson --bare client-root-ca
會獲得下面幾個文件:
client-secrets/ca/ ├── client-root-ca-key.pem ├── client-root-ca.csr └── client-root-ca.pem
用下面命令驗證證書:
openssl x509 -in ./client-root-ca.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 7e:fc:f3:53:07:1a:17:ae:24:34:d5:1d:00:02:d6:e4:24:09:92:12 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca Validity Not Before: Mar 27 05:20:00 2019 GMT Not After : Mar 25 05:20:00 2024 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca
能夠看到簽發人和被簽發人是同一個。
新建文件 client-secrets/cert/client-gencert.json,內容以下:
{ "signing": { "default": { "usages": [ "signing", "key encipherment", "client auth" ], "expiry": "87600h" } } }
能夠看到咱們會生成用來作client auth的證書。
新建文件 client-secrets/cert/demo-csr.json,內容以下:
{ "key": { "algo": "rsa", "size": 2048 }, "names": [ { "O": "Company", "OU": "Datacenter", "L": "Shanghai", "ST": "Shanghai", "C": "CN" } ], "CN": "client-demo" }
這裏沒有hosts,這是由於咱們不須要用這個證書來作服務端認證。
運行下面命令生成證書
cfssl gencert \ --ca ../ca/client-root-ca.pem \ --ca-key ../ca/client-root-ca-key.pem \ --config ./client-gencert.json \ ./demo-csr.json | cfssljson --bare ./demo
獲得文件:
client-secrets/cert/ ├── demo-key.pem ├── demo.csr └── demo.pem
驗證結果:
openssl x509 -in ./demo.pem -text -noout Certificate: Data: Version: 3 (0x2) Serial Number: 6e:50:e2:2c:02:bb:ef:fd:03:d9:2c:0a:8f:ba:90:65:fb:c4:b5:75 Signature Algorithm: sha256WithRSAEncryption Issuer: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-root-ca Validity Not Before: Mar 27 05:21:00 2019 GMT Not After : Mar 24 05:21:00 2029 GMT Subject: C=CN, ST=Shanghai, L=Shanghai, O=Company, OU=Datacenter, CN=client-demo
能夠看到簽發者是client-root-ca,Subject是client-demo。
到 client-secrets/jks,執行下面命令生成pkcs12格式的keystore(JDK識別這個格式)
openssl pkcs12 -export \ -in ../cert/demo.pem \ -inkey ../cert/demo-key.pem \ -out client-demo.keystore \ -name client-demo \ -CAfile ../ca/client-root-ca.pem \ -caname root -chain
過程當中會讓你輸入密碼,你就輸入:client-demo-ks。
獲得文件:
client-secrets/jks/ └── client-demo.keystore
用JDK提供的keytool看看裏面的內容:
keytool -list -keystore client-demo.keystore client-demo, 2019-3-27, PrivateKeyEntry, 證書指紋 (SHA1): 83:AE:0E:5E:0C:CE:86:C9:D1:84:D7:6F:87:F3:76:1F:B4:3E:46:31
讓你輸入密碼的時候就輸入:client-demo-ks。
好了,到此爲止server和client的證書都已經生成了,接下來只須要將各自的root-ca添加到彼此都truststore中。
cd client-secrets/jks keytool -importcert \ -alias server-root-ca \ -storetype pkcs12 \ -keystore client.truststore \ -storepass client-ts \ -file ../../server-secrets/ca/server-root-ca.pem -noprompt
注意上面的-storepass參數,這個是trustore的密碼:client-ts。
獲得文件:
client-secrets/jks/ └── client.truststore
用JDK提供的keytool看看裏面的內容:
keytool -list -keystore client.truststore server-root-ca, 2019-3-27, trustedCertEntry, 證書指紋 (SHA1): 75:E3:78:97:85:B2:29:38:25:3C:FD:EC:68:97:9B:78:A0:5F:BB:9D
讓你輸入密碼的時候就輸入:client-ts。
cd server-secrets/jks keytool -importcert \ -alias client-root-ca \ -storetype pkcs12 \ -keystore server.truststore \ -storepass server-ts \ -file ../../client-secrets/ca/client-root-ca.pem -noprompt
注意上面的-storepass參數,這個是trustore的密碼:server-ts。
獲得文件:
server-secrets/jks/ └── server.truststore
用JDK提供的keytool看看裏面的內容:
keytool -list -keystore server.truststore client-root-ca, 2019-3-27, trustedCertEntry, 證書指紋 (SHA1): 1E:95:2C:12:AA:7E:6D:E7:74:F1:83:C2:B8:73:6F:EE:57:FB:CA:46
讓你輸入密碼的時候就輸入:server-ts。
好了,咱們如今client和server都有了本身證書放在了本身的keystore中,並且把彼此的root-ca證書放到了本身的truststore裏。如今咱們弄一個tomcat做爲server,而後爲他配置SSL。
修改tomcat/conf/server.xml,添加以下Connector:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="150" SSLEnabled="true"> <SSLHostConfig certificateVerification="required" truststoreFile="/path/to/server-secrets/jks/server.truststore" truststorePassword="server-ts" truststoreType="PKCS12"> <Certificate certificateKeyAlias="server-demo" certificateKeystoreFile="/path/to/server-secrets/demo-jks/server-demo.keystore" certificateKeystoreType="PKCS12" certificateKeystorePassword="server-demo-ks" type="RSA" /> </SSLHostConfig> </Connector>
能夠看到咱們開啓了客戶端認證certificateVerification="required"
,也開啓了服務端認證<Certificate>
。記得修改上面的keystore和truststore的路徑。
修改tomcat/conf/web.xml,添加以下元素:
<security-constraint> <web-resource-collection> <web-resource-name>Automatic Forward to HTTPS/SSL</web-resource-name> <url-pattern>/*</url-pattern> </web-resource-collection> <user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint>
這個做用是當訪問8080端口時,都跳轉到8443端口,強制走HTTPS。
啓動tomcat:
tomcat/bin/catalina.sh run
好了,咱們如今用curl來測試訪問一下:
curl https://localhost:8443/ curl: (60) SSL certificate problem: self signed certificate in certificate chain ...
看到curl說服務端用的是一個自簽發的證書,不可信,也就是說服務端認證失敗。添加--insecure
試試:
curl --insecure https://localhost:8443/ curl: (35) error:1401E412:SSL routines:CONNECT_CR_FINISHED:sslv3 alert bad certificate
這裏就說明客戶端認證失敗。
因此若是要正確訪問得像下面這樣,指定server-root-ca證書,以及客戶端本身簽發的證書及private key:
curl --cacert server-secrets/ca/server-root-ca.pem \ --key client-secrets/cert/demo-key.pem \ --cert client-secrets/cert/demo.pem \ https://localhost:8443/ <!DOCTYPE html> <html lang="en"> ...
咱們如今用Httpclient來訪問看看。pom.xml中添加依賴:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.7</version> </dependency>
Java代碼,記得把文件路徑改掉:
import org.apache.http.HttpEntity; import org.apache.http.HttpException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.ssl.SSLContexts; import org.apache.http.util.EntityUtils; import javax.net.ssl.SSLContext; import java.io.File; import java.io.IOException; public class Client { public static void main(String[] args) throws Exception { SSLContext sslcontext = SSLContexts.custom() .loadTrustMaterial( new File("/path/to/client-secrets/demo-jks/client.truststore"), "client-ts".toCharArray() ) .loadKeyMaterial( new File("/path/to/client-secrets/demo-jks/client-demo.keystore"), "client-demo-ks".toCharArray(), "client-demo-ks".toCharArray()) .build(); SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( sslcontext, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); CloseableHttpClient httpclient = HttpClients.custom() .setSSLSocketFactory(sslsf) .build(); HttpGet httpGet = new HttpGet("https://localhost:8443"); CloseableHttpResponse response = httpclient.execute(httpGet); try { System.out.println(response.getStatusLine()); HttpEntity entity = response.getEntity(); System.out.println(EntityUtils.toString(entity)); } finally { response.close(); } } }
由於服務端認證所須要的證書直接配置在Tomcat上的,所以在作反向代理的時候不能使用SSL Termination模式,而是得使用SSL Passthrough模式。
上面講的方法不是隻適用於Tomcat和Httpclient的,TLS的服務端認證與客戶端認證應該在絕大部分的語言、SDK、類庫都有支持,請自行參閱文檔實踐。文中的keystore和truststore是Java特有的,不過沒必要迷惑,由於它們僅僅起到一個存放證書和private key的保險箱,有些語言或工具則是直接使用證書和private key,好比前面提到的curl。
一些基礎概念:
其餘運用客戶端認證的軟件的相關文檔,頗有啓發: