2022年11月10日 星期四

Spring Boot 啟用 mTLS

最近在幫專案導入 Spring Boot,導入時比較麻煩的地方,是要保持專案的權限控制,也就是要讓它依然能夠正常以 Mutual TLS 的方式進行驗證。雖然在這個狀況下,往好處想是我們已經有現成的測試程序,能夠用來驗證是否 Mutual TLS 有正常運作,不過當驗證異常的時候,要找出問題還是蠻費工夫的…。這篇會稍微紀錄一點相關的資訊。

什麼是 Mutual TLS?

Mutual TLS(或者簡稱 mTLS)是驗證範圍更寬一點的 HTTPS,一般討論 HTTPS 時,比較常見的討論對象是單向的 TLS,也就是讓客戶端驗證伺服器是否真的是它宣稱的伺服器。而 Mutual TLS 則是雙向的 TLS,不但客戶端要驗證伺服器、伺服器也要驗證客戶端。換句話說,伺服器和客戶端都需要有私鑰和憑證。

TLS 相關名詞解釋

TLS 在運作時,會需要有公鑰(public key)和私鑰(private key),這裡就不解釋各自的用途了。實際 TLS 在運作時,通常會以私鑰和憑證(certificate)的形式在表示,其中憑證的內容是包含了公鑰、發行者(Issuer)和有效時間等資訊的檔案。也就是說,憑證會拿來提供給對方做驗證。

在使用 curl 時,會注意到 curl 允許的 --key--cert 的格式是 PEM,這是因為 curl 就只支援 PEM 格式。不過在 Java 的世界中,比較麻煩的是 Java 一般不支援 PEM,所以網路上的文章常常會看到有使用 keytool 的流程,這個流程是為了把 PEM 格式的私鑰和憑證,轉換成 PKCS #12(.p12)或者 Java KeyStore 格式(.jks)的檔案。可以參考公鑰密碼學標準

除了 KeyStore 以外,另外還會有個 TrustStore 的玩意兒。兩個的功能剛好相反,KeyStore 是用來讓別人做驗證、TrustStore 的功能則是用來驗證別人。所以說,KeyStore 的產生會使用自己的私鑰和憑證,這是因為產生出來的 KeyStore 內容也會包含自己的憑證資訊。

設定 Spring Boot 的 SSL

具體設定方法,可以參考官方文件中 How-to 章節的 SSL 部份。不過我個人其實覺得官方文件寫得還挺籠統的…。

實際我自己在設定時,在 Spring Boot + Jetty 底下有遇到幾種奇怪的狀況:

  1. 使用 keystore(JKS) 和 truststore,然後 Spring Boot 顯示有正常啟動 HTTPS,但結果用 curl 或 openssl 檢查時,都檢查不到有 HTTPS。
  2. 使用 PEM,然後 HTTPS 正常啟動,憑證也能正常被客戶端讀取,但 mTLS 一直失敗。

最後這裡直接紀錄目前會成功的版本,是採用 PKCS12 的格式讓 Spring Boot 讀取。

server.port=8443
server.ssl.enabled=true
server.ssl.key-store=/path/to/key/store/my_keystore.p12  # PKCS12 檔案的路徑
server.ssl.key-alias=accio  # 別名,在產生 PKCS12 檔的時候指定的名字
server.ssl.key-store-password=mypassword  # PKCS12 的密碼
server.ssl.trust-store=/path/to/trust/store/my_truststore.jks  # trust store 檔案的路徑
server.ssl.trust-store-type=JCEKS  # trust store 檔案的格式
server.ssl.trust-store-password=mypassword  # trust store 的密碼
server.ssl.client-auth=want  # 啟用 mTLS,但不強制

如何檢查 HTTPS?

使用 curl 或者 openssl 都可以做檢查 HTTPS。

openssl

openssl 時,就是讓 openssl 模擬成 client,指令如下:

openssl s_client -connect localhost:8443

以我的狀況,如果 HTTPS 啟動但有問題 (?) 的話,會出現以下這樣的訊息。

[root@a976d6ac5637 ~]# openssl s_client -connect localhost:8443
CONNECTED(00000003)
140387372447632:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure:s23_clnt.c:769:
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 7 bytes and written 289 bytes
---
New, (NONE), Cipher is (NONE)
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : 0000
    Session-ID:
    Session-ID-ctx:
    Master-Key:
    Key-Arg   : None
    Krb5 Principal: None
    PSK identity: None
    PSK identity hint: None
    Start Time: 1667406122
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
---

訊息中可以明確看出,openssl 並沒有找到合適的 cipher 能夠進行加密連線。

而如果是正常的 HTTPS 的話,會像是這樣:

[root@a976d6ac5637 ~]# openssl s_client -connect google.com:443
CONNECTED(00000005)
depth=3 C = BE, O = GlobalSign nv-sa, OU = Root CA, CN = GlobalSign Root CA
verify return:1
depth=2 C = US, O = Google Trust Services LLC, CN = GTS Root R1
verify return:1
depth=1 C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
verify return:1
depth=0 CN = *.google.com
verify return:1
---
Certificate chain
 0 s:/CN=*.google.com
   i:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
 1 s:/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
   i:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
 2 s:/C=US/O=Google Trust Services LLC/CN=GTS Root R1
   i:/C=BE/O=GlobalSign nv-sa/OU=Root CA/CN=GlobalSign Root CA
---
Server certificate
-----BEGIN CERTIFICATE-----
....(ignored)....
-----END CERTIFICATE-----
subject=/CN=*.google.com
issuer=/C=US/O=Google Trust Services LLC/CN=GTS CA 1C3
---
No client certificate CA names sent
Server Temp Key: ECDH, X25519, 253 bits
---
SSL handshake has read 7270 bytes and written 281 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-CHACHA20-POLY1305
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-CHACHA20-POLY1305
    Session-ID: D0F1CB0DAE17297143F933D8C93BB905D088849CAAFED06698803C8F14367771
    Session-ID-ctx:
    Master-Key: ADC789FEFEF3A3747E6741D23B090CAA3E1BBEB8B6DED6EB44910E45257F9C977378CCAB42C387828EDC2CC8D4B91059
    TLS session ticket lifetime hint: 100800 (seconds)
    TLS session ticket:
    ....(ignored)....

    Start Time: 1668083091
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---

部分細節我就直接刪掉了,不過可以看出正常狀況它應該要能解析出 server 的憑證內容,並且顯示使用什麼協定來跟伺服器溝通。

附錄:Jetty 的 log

由於我用的是 Jetty,因此如果想追 Jetty 這邊的 log,可以打開 org.eclipse.jetty 的 debug log,從訊息中可以看出,Jetty 在啟用 HTTPS 時,會建立 SslContextFactory 的物件來進行(廢話)。更具體來說,應該會是 SslContextFactory.Server 物件。雖然說,出現這段看起來有啟動 HTTPS 的 log,實際上並不代表 HTTPS 就有正常被啟動了。我的實驗過程有遇到一個情況是 log 顯示 HTTPS 似乎啟動了,但結果真的用 curlopenssl 打都找不到憑證…。

2022-11-02 15:48:15,603 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - starting SslConnectionFactory@40ee0a22{SSL->HTTP/1.1}
2022-11-02 15:48:15,603 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - starting Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,636 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - managers=[org.eclipse.jetty.util.ssl.AliasedX509ExtendedKeyManager@5cc152f9] for Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Selected Protocols [TLSv1.3, TLSv1.2] of [TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, SSLv3, SSLv2Hello]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Selected Ciphers   [TLS_AES_256_GCM_SHA384, ....]
2022-11-02 15:48:15,640 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Customize sun.security.ssl.SSLEngineImpl@1117cc7c
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - STARTED @119497ms Server@7bde1f3a[provider=null,keyStore=file:////path/to/key/store/my_keystore.p12,trustStore=file:////path/to/trust/store/my_truststore.jks]
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.ssl.SslContextFactory - Customize sun.security.ssl.SSLEngineImpl@4797023d
2022-11-02 15:48:15,641 [main] DEBUG org.eclipse.jetty.util.component.AbstractLifeCycle - STARTED @119497ms SslConnectionFactory@40ee0a22{SSL->HTTP/1.1}