原文:https://www.jianshu.com/p/2b2d1f511959
作者:黃湘龍
研究HTTPS的雙向認證實現與原理,踩了不少坑,終於整個流程都跑通了,現在總結出一篇文檔來,把一些心得,特別是容易踩坑的地方記錄下來。
1.原理
雙向認證,顧名思義,客戶端和服務器端都需要驗證對方的身份,在建立Https連接的過程中,握手的流程比單向認證多了幾步。單向認證的過程,客戶端從服務器端下載服務器端公鑰證書進行驗證,然后建立安全通信通道。雙向認證的過程,客戶端除了需要從服務器端下載服務器的公鑰證書進行驗證外,還需要把客戶端的公鑰證書上傳到服務器端給服務器端進行驗證,等雙方都認證通過了,才開始建立安全通信通道進行數據傳輸。
1.1 單向認證流程
單向認證流程中,服務器端保存着公鑰證書和私鑰兩個文件,整個握手過程如下:
- 客戶端發起建立HTTPS連接請求,將SSL協議版本的信息發送給服務器端;
- 服務器端將本機的公鑰證書(server.crt)發送給客戶端;
- 客戶端讀取公鑰證書(server.crt),取出了服務端公鑰;
- 客戶端生成一個隨機數(密鑰R),用剛才得到的服務器公鑰去加密這個隨機數形成密文,發送給服務端;
- 服務端用自己的私鑰(server.key)去解密這個密文,得到了密鑰R
- 服務端和客戶端在后續通訊過程中就使用這個密鑰R進行通信了。
1.2 雙向認證流程

- 客戶端發起建立HTTPS連接請求,將SSL協議版本的信息發送給服務端;
- 服務器端將本機的公鑰證書(server.crt)發送給客戶端;
- 客戶端讀取公鑰證書(server.crt),取出了服務端公鑰;
- 客戶端將客戶端公鑰證書(client.crt)發送給服務器端;
- 服務器端使用根證書(root.crt)解密客戶端公鑰證書,拿到客戶端公鑰;
- 客戶端發送自己支持的加密方案給服務器端;
- 服務器端根據自己和客戶端的能力,選擇一個雙方都能接受的加密方案,使用客戶端的公鑰加密后發送給客戶端;
- 客戶端使用自己的私鑰解密加密方案,生成一個隨機數R,使用服務器公鑰加密后傳給服務器端;
- 服務端用自己的私鑰去解密這個密文,得到了密鑰R
- 服務端和客戶端在后續通訊過程中就使用這個密鑰R進行通信了。
2. 證書生成
從上一章內容中,我們可以總結出來,如果要把整個雙向認證的流程跑通,最終需要六個證書文件:
- 服務器端公鑰證書:server.crt
- 服務器端私鑰文件:server.key
- 根證書:root.crt
- 客戶端公鑰證書:client.crt
- 客戶端私鑰文件:client.key
- 客戶端集成證書(包括公鑰和私鑰,用於瀏覽器訪問場景):client.p12
生成這一些列證書之前,我們需要先生成一個CA根證書,然后由這個CA根證書頒發服務器公鑰證書和客戶端公鑰證書。為了驗證根證書頒發與驗證客戶端證書這個邏輯,我們使用根證書頒發兩套不同的客戶端證書,然后同時用兩個客戶端證書來發送請求,看服務器端是否都能識別。下面是證書生成的內在邏輯示意圖:

我們可以全程使用openssl來生成一些列的自簽名證書,自簽名證書沒有經過證書機構的認證,很多瀏覽器會認為不安全,但我們用來實驗是足夠的。需要在本機安裝了openssl后才能繼續本章的實驗。
2.1生成自簽名根證書
首先了解下這些格式的文件:
- .key: 即Private Key,通常指私鑰
- .csr: 是Certificate Signing Request的縮寫,即證書簽名申請,這不是證書,這是要求CA給證書簽名的一種正式申請,該申請包含申請證書的實體的公鑰及該實體店某些信息。該數據將成為證書的一部分。CSR始終使用它攜帶的公鑰所對應的私鑰進行簽名。
- .crt:即certificate的縮寫,即證書
- .crl: 即Certificate Revocation List證書吊銷列表,證書吊銷列表是PKI系統中的一個結構化數據文件,該文件包含了證書頒發機構(CA)已經吊銷的證書的序列號及其吊銷日期。SSL證書吊銷列表,顧名思義,證書吊銷是一個將無效證書和不可信證書與有效可信證書區分開的過程。基本上,這是CA(或CRL頒發者)知道的一種或多種數字證書由於某種原因或其他原因不再值得信賴的一種方法。當他們吊銷證書(此過程有時稱為PKI證書吊銷)時,他們實際上會在證書的到期日期之前使該證書無效。
- .jks:javakeystone是JAVA的keytools證書工具支持的證書私鑰格式,javakeystone 里面存放着key和信任的CA,key和CA可以有多個。
x509證書一般會用到三類文,key,csr,crt。
(1)創建根證書私鑰: openssl genrsa -out root.key 1024 (2)創建根證書請求文件: openssl req -new -out root.csr -key root.key 后續參數請自行填寫,下面是一個例子: Country Name (2 letter code) [XX]:cn State or Province Name (full name) []:bj Locality Name (eg, city) [Default City]:bj Organization Name (eg, company) [Default Company Ltd]:alibaba Organizational Unit Name (eg, section) []:test Common Name (eg, your name or your servers hostname) []:root Email Address []:a.alibaba.com A challenge password []: An optional company name []: (3)創建根證書: openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
在創建證書請求文件(root.csr)的時候需要注意三點,下面生成服務器請求文件和客戶端請求文件均要注意這三點:
根證書的Common Name填寫root就可以,所有客戶端和服務器端的證書這個字段需要填寫域名,一定要注意的是,根證書的這個字段和客戶端證書、服務器端證書不能一樣;
其他所有字段的填寫,根證書、服務器端證書、客戶端證書需保持一致
最后的密碼可以直接回車跳過。
經過上面三個命令行,我們最終可以得到一個簽名有效期為10年的根證書root.crt,后面我們可以用這個根證書去頒發服務器證書和客戶端證書。
2.2 生成自簽名服務器端證書
(1)生成服務器端證書私鑰: openssl genrsa -out server.key 1024 (2) 生成服務器證書請求文件,過程和注意事項參考根證書,本節不詳述: openssl req -new -out server.csr -key server.key (3) 生成服務器端公鑰證書 openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650
經過上面的三個命令,我們得到:
- server.key:服務器端的秘鑰文件
- server.crt:有效期十年的服務器端公鑰證書,使用根證書和服務器端私鑰文件一起生成
2.3 生成自簽名客戶端證書
(1)生成客戶端證書秘鑰: openssl genrsa -out client.key 1024 openssl genrsa -out client2.key 1024 (2) 生成客戶端證書請求文件,過程和注意事項參考根證書,本節不詳述: openssl req -new -out client.csr -key client.key openssl req -new -out client2.csr -key client2.key (3) 生客戶端證書 openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650 openssl x509 -req -in client2.csr -out client2.crt -signkey client2.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650 (4) 生客戶端p12格式證書,需要輸入一個密碼,選一個好記的,比如123456 openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12 openssl pkcs12 -export -clcerts -in client2.crt -inkey client2.key -out client2.p12
client.key/client2.key:客戶端的私鑰文件
client.crt/client2.crt:有效期十年的客戶端證書,使用根證書和客戶端私鑰一起生成
client.p12/client2.p12:客戶端p12格式,這個證書文件包含客戶端的公鑰和私鑰,主要用來給瀏覽器訪問使用
3.Nginx配置
有了上面的一些列證書,我們可以在Nginx服務器上配置雙向認證的HTTPS服務了,具體配置方式如下:
server { listen 443 ssl; server_name www.yourdomain.com; ssl on; ssl_certificate /data/sslKey/server.crt; #server公鑰證書 ssl_certificate_key /data/sslKey/server.key; #server私鑰 ssl_client_certificate /data/sslKey/root.crt; #根證書,可以驗證所有它頒發的客戶端證書 ssl_verify_client on; #開啟客戶端證書驗證 location / { root html; index index.html index.htm; } }
具體就是將服務器端的兩個證書文件(server.crt/server.key)和根證書文件(root.crt)的路徑配置到nginx的server節點配置中,並且把ssl_verify_client這個參數設置為on。
有一點需要注意的就是,如果客戶端證書不是由根證書直接頒發的,配置中還需要加一個配置:ssl_verify_depth 1;
配置完成后,執行nginx -s reload重新加載下就生效了。
4.使用curl作為客戶端調用驗證
使用curl加上證書路徑,可以直接測試Nginx的HTTPS雙向認證是否配置成功。下面我們測試三個用例:
- 使用client.crt/client.key這一套客戶端證書來調用服務器端
- 使用client2.crt/client2.key這一套客戶端證書來調用服務器端
- 不使用證書來調用服務器端
下面是三個用例的測試結果:
1.帶證書的成功調用:
#--cert指定客戶端公鑰證書的路徑 #--key指定客戶端私鑰文件的路徑 #-k不校驗證書的合法性,因為我們用的是自簽名證書,所以需要加這個參數 #可以使用-v來觀察具體的SSL握手過程 curl --cert ./client.crt --key ./client.key https://integration-fred2.fredhuang.com -k -v * Rebuilt URL to: https://47.93.245.203/ * Trying 47.93.245.203... * TCP_NODELAY set * Connected to 47.93.245.203 (47.93.245.203) port 443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH * successfully set certificate verify locations: * CAfile: /etc/ssl/cert.pem CApath: none * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Request CERT (13): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Certificate (11): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS handshake, CERT verify (15): * TLSv1.2 (OUT), TLS change cipher, Client hello (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS change cipher, Client hello (1): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 * ALPN, server accepted to use http/1.1 * Server certificate: * subject: C=CN; ST=BJ; L=BJ; O=Alibaba; OU=Test; CN=integration-fred2.fredhuang.com; emailAddress=a@alibaba.com * start date: Nov 2 01:01:34 2019 GMT * expire date: Oct 30 01:01:34 2029 GMT * issuer: C=CN; ST=BJ; L=BJ; O=Alibaba; OU=Test; CN=root; emailAddress=a@alibaba.com * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway. > GET / HTTP/1.1 > host:integration-fred2.fredhuang.com > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 200 OK < Server: nginx/1.17.5 < Date: Sat, 02 Nov 2019 02:39:43 GMT < Content-Type: text/html < Content-Length: 612 < Last-Modified: Wed, 30 Oct 2019 11:29:45 GMT < Connection: keep-alive < ETag: "5db97429-264" < Accept-Ranges: bytes < <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html> * Connection #0 to host 47.93.245.203 left intact
curl --cert ./client2.crt --key ./client2.key https://integration-fred2.fredhuang.com -k <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
3.不帶證書的調用
curl https://integration-fred2.fredhuang.com -k <html> <head><title>400 No required SSL certificate was sent</title></head> <body> <center><h1>400 Bad Request</h1></center> <center>No required SSL certificate was sent</center> <hr><center>nginx/1.17.5</center> </body> </html>
5. 總結
HTTPS雙向認證方式通信在一些安全級別較高的場景非常有用,擁有合法證書的客戶端才能正常訪問業務。實現這個場景需要以下幾步:
- 生成根公鑰證書和私鑰文件(root.crt/root.key);
- 使用根證書和根證書私鑰(root.crt/root.key)配合服務器端私鑰頒發服務器端證書(server.crt);
- 使用根證書和根證書私鑰(root.crt/root.key)配合客戶端私鑰頒發客戶端證書(server.crt);
- 將根證書(root.crt)、服務器端證書(server.crt)、服務器端秘鑰(server.key)配置到Nginx的Server配置中;
- 客戶端使用客戶端私鑰和根證書頒發的客戶端證書(client.crt)正常訪問業務。
根證書可以任意頒發客戶端證書並給業務方使用,為了安全起見,需要注意頒發的證書的有效期。
魔鬼藏在細節,有兩個細節再次重點強調下:
- 根證書的Common Name填寫root就可以,所有客戶端和服務器端的證書這個字段需要填寫域名,一定要注意的是,根證書的這個字段和客戶端證書、服務器端證書不能一樣;
- Nginx的ssl_client_certificate需要配置根證書root.crt,而不是客戶端證書;