首頁 > 科技

如何輕鬆地設定雙向TLS保護應用程式安全

2021-06-08 14:25:33

在安全實踐領域,TLS身份驗證作為一種技術手段,通常能夠保證任何使用者通過證書,瀏覽到真實、安全的Web應用。而在此基礎上發展而來的雙向TLS(Two-Way TLS),則可以僅允許部分使用者訪問或呼叫目標應用。

下面,我將以示例的形式,並配合自動化指令碼,依次向您展示:搭建伺服器,向伺服器傳送未加密的hello訊息,以HTTPS的方式在伺服器上啟用單向TLS,要求客戶端通過雙向TLS來標識自己,基於可信的CA(證書頒發機構)實現雙向TLS,以及對HTTP客戶端進行相關測試。

基本定義

  • 身份標識(Identity):一對私鑰與公鑰,通常被存放在信任儲存庫中。

  • 信任儲存庫(TrustStore):庫中包含了一個或多個可信證書(也稱為公鑰)的列表。

  • 單向認證(也稱為單向tls、單向ssl):客戶端在驗證對方證書時用到的https連線。

  • 雙向認證(也稱為雙向tls、雙向 ssl、雙向認證):客戶端與對方相互驗證證書時的https 連線。

實用連結

通過如下的統一參考頁,我向社群里正在使用Apache http、Java、Kotlin、以及Scala等開發人員,提供了包含40多個http客戶端的配置示例。

  • Keytool參考頁

  • Openssl參考頁

  • http客戶端配置參考頁

  • Spring應用程式屬性概述

在處理http請求的過程中,它們可能會導致應用在初始構建時,需要花費一定的時間來下載大量依賴項。因此,我也通過GitHub - SSLContext Kickstart來簡化客戶端的配置。由於每一個http客戶端都可能需要不同的ssl物件來啟用ssl,因此程式碼庫需要提供基本的ssl配置。

啟動伺服器

首先,我們需要做好如下準備:

  • Java 11

  • Maven 3.5.0

  • Eclipse、Intellij IDEA(或任何其他文字編輯器,如 VIM)

  • 一個終端

  • 從https://github.com/Hakky54/mutual-tls處克隆項目

由於該項目已經包含了一個maven包裝器(wrapper),因此您可以在無需額外安裝的情況下,運行該項目。同時,下面將涉及到的各種包含了maven包裝器的命令,都已被預設包含在mvn命令中。

如果您想使用Java 8來運行該項目,則可以使用git命令:git checkout tags/java-8-compatible,來運行一個相容的版本。

為了啟動服務端,您可以在服務端的項目中運行App類的main方法,或者在終端的根目錄下運行命令:cd server/ && mvn spring-boot:run,以及使用maven包裝器:cd server-with-spring-boot/ && ./../mvnw spring-boot:run。

向伺服器傳送未加密的hello

由於當前運行在預設埠8080上的伺服器端是未經加密的,因此您可以在終端中使用以下curl命令:curl -i -XGET http://localhost:8080/api/hello,來呼叫hello:

其響應內容如下(純文字):

HTTP/1.1 200 Content-Type: text/plain;charset=UTF-8 Content-Length: 5 Date: Sun, 11 Nov 2018 14:21:50 GMT  Hello

您還可以使用客戶端目錄中所提供的客戶端應用,去呼叫伺服器。由於客戶端依賴於本項目的其他元件,所以您需要先在根目錄下運行mvn install或./mvnw install。

此處的客戶端是基於Cucumber的整合測試。您可以通過從IDE處運行ClientRunnerIT類、或從根目錄中的終端運行:cd client/ && mvn exec:java、亦或使用maven的包裝器命令:cd client/ && ./../mvnw exec:java,來啟動之。您可以在客戶端項目的測試資源中,通過Hello.feature檔案,來獲悉整合測試的具體步驟。

為了同時運行伺服器和客戶端中的方法,您可以在根目錄中使用命令:mvn clean verify,或使用maven的包裝器:./mvnw clean verify。如果服務端與客戶端同處一臺伺服器,那麼客戶端會預設向localhost傳送請求;如果它們在不同的主機上運行,您需要為客戶端提供帶有VM參數:-Durl=http://[HOST]:[PORT]的定製化的url。

在伺服器上啟用 HTTPS(即單向的TLS)

下面,我們來討論如何通過啟用TLS,來保護伺服器端。您可以通過將如下所需的屬性(YAML),新增到名為application.yml的應用屬性檔案中來實現:

server: port: 8443 ssl:     enabled: true

在此,您可能會對為何將埠設定為 8443表示疑惑。其原因在於:帶有https的tomcat服務的約定埠為8443,而對於http則是8080。雖然我們可以使用埠8080進行https連線,但這並不是一種推薦的做法。

您可以通過重啟伺服器,來生效那些對於應用的更改。當然,您也可能會收到異常資訊:IllegalArgumentException: Resource location must not be null。該訊息的產生,是因為伺服器需要帶有伺服器證書的金鑰庫,以確保與外界的安全連線。

如果您提供的VM參數為:

Djavax.net.debug=SSL,keymanager,trustmanager,ssl:handshake,那麼伺服器可以為您提供更多的資訊。

顯然,為了解決此問題,您需要創建一個帶有伺服器公鑰和私鑰的金鑰庫。其中的公鑰可與使用者共享,以便加密彼此之間的通訊;而伺服器的私鑰則可用來解密。值得注意的是,我們絕對不可以共享伺服器的私鑰,以避免被其他人用來破解截獲到的通訊,進而獲悉被加密的通訊內容。

因此,若要創建帶有公鑰和私鑰的金鑰庫,請在終端中執行以下命令(純文字):

keytool -v -genkeypair -dname "CN=Hakan,OU=Amsterdam,O=Thunderberry,C=NL" -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias server -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,DNS:raspberrypi.local,IP:12 0.1

為了告知伺服器金鑰庫的位置,以及具體的密碼,請將如下YAML內容貼上到您的application.yml檔案中:

server:   port: 8443   ssl:     enabled: true     key-store: classpath:identity.jks     key-password: secret     key-store-password: secret

至此,您已成功啟用了伺服器和客戶端之間的TLS加密連線。您可以嘗試著使用curl命令:curl -i --insecure -v -XGET https://localhost:8443/api/hello,去呼叫伺服器。

當您在ClientRunnerIT類中運行客戶端時,您可能會看到一條錯誤訊息:java.net.ConnectException: Connection refused (Connection refused)。從字面上看,它是指客戶端試圖向伺服器建立連線,可以被拒絕了。其深層原因是:客戶端試圖使用的是埠8080,而伺服器只在埠8443上處於活躍狀態。因此,您需要進行如下修改,並將其應用到Constants類中,即從:

private static final String DEFAULT_SERVER_URL = "http://localhost:8080";

改為:

private static final String DEFAULT_SERVER_URL = "https://localhost:8443";

在完成修改之後,讓我們再次運行客戶端。您會看到另一條訊息:「javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target」。這意味著客戶端希望通過HTTPS進行通訊,但是在它握手的過程中,收到了無法識別的伺服器證書。可見,您還需要創建一個包含了各種受信任證書的信任庫,以方便客戶端在SSL握手過程中,將收到的證書與其信任庫裡的證書內容進行比較。如果相匹配的話,則可以繼續SSL的握手過程。當然,在創建信任庫之前,您需要事先獲得伺服器的證書。

匯出伺服器的證書

您可以使用如下命令,匯出伺服器的證書:

keytool -v -exportcert -file shared-server-resources/src/main/resources/server.cer -alias server -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret -rfc

接著,您可以為客戶端創建一個信任庫,並使用如下命令匯入伺服器的證書:

keytool -v -importcert -file shared-server-resources/src/main/resources/server.cer -alias server -keystore client/src/test/resources/truststore.jks -storepass secret -noprompt

為了讓客戶端知曉信任庫的存在,您還需要告知其信任庫的正確位置、密碼、以及身份驗證已啟用。您可以在客戶端的application.yml檔案中,提供如下屬性:

client:   ssl:     one-way-authentication-enabled: true     two-way-authentication-enabled: false     trust-store: truststore.jks     trust-store-password: secret

對客戶端進行身份驗證(雙向TLS)

接下來,伺服器端需要驗證客戶端的身份,以判斷其是否可信。其實現方式為:通過client-auth屬性放入伺服器的application.yml中,以告知伺服器去驗證客戶端。

server:   port: 8443   ssl:     enabled: true     key-store: classpath:identity.jks     key-password: secret     key-store-password: secret     client-auth: need

當然,如果您直接運行它,則會因為客戶端根本沒有證書,而產生錯誤訊息:javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate(無效的證書資訊)。因此,我們需要通過如下命令,來創建證書:

keytool -v -genkeypair -dname "CN=Suleyman,OU=Altindag,O=Altindag,C=NL" -keystore client/src/test/resources/identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -別名客戶端 -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth

同時,您還需要為伺服器創建一個信任庫。不過,在創建信任庫之前,您需要通過如下命令獲取客戶端的證書:

keytool -v -exportcert -file client/src/test/resources/client.cer -alias client -keystore client/src/test/resources/identity.jks -storepass secret -rfc

下一步便是使用客戶端的證書,來創建伺服器的信任庫:

keytool -v -importcert -file client/src/test/resources/client.cer -alias client -keystore shared-server-resources/src/main/resources/truststore.jks -storepass secret -noprompt

同樣,為了讓客戶端獲悉該金鑰庫的存在,您還需要告知其信任庫的正確位置、密碼、以及身份驗證已啟用。您可以在客戶端的application.yml檔案中,提供如下屬性:

client:   ssl:     one-way-authentication-enabled: false     two-way-authentication-enabled: true     key-store: identity.jks     key-password: secret     key-store-password: secret     trust-store: truststore.jks    trust-store-password: secret

對應地,為了讓伺服器知曉新創建的信任庫,我們需要將當前屬性替換為以下屬性:

server:   port: 8443   ssl:     enabled: true     key-store: classpath:identity.jks     key-password: secret     key-store-password: secret     trust-store: classpath:truststore.jks     trust-store-password: secret    client-auth: need

至此,您已完成了雙向TLS的安裝。如果再次運行客戶端,您將會發現客戶端能夠以安全的方式,從伺服器端接收到hello訊息了。

基於可信CA的雙向TLS

有了前面的基礎,我們便可以採用基於可信CA的雙向(mutual)認證了。我們首先來看看它的優缺點:

優點

  • 客戶端不需要自行新增伺服器的證書。

  • 伺服器不需要新增客戶端的所有證書。

  • 由於是由CA管控著證書的有效期,因此本地運維工作會大幅減少。

缺點

  • 您無法細粒度地控制哪些客戶端可以呼叫自己的應用,哪些不可以。任何客戶端,只要持有CA頒發的證書,即可訪問您的應用程式。

其具體實現步驟如下:

1. 創建CA

通常,您需要向某個已有的證書頒發機構,提供自己的證書以獲取其簽名。下面,我們將創建一個自己的CA,並用它去簽發客戶端和伺服器的證書。

keytool -v -genkeypair -dname "CN=Root-CA,OU=Certificate Authority,O=Thunderberry,C=NL" -keystore root-ca/identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias root-ca -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,keyCertSign -ext BasicConstraints=ca:true,PathLen:3

當然,您也可以使用儲存庫預設提供的那個

2. 創建證書籤名請求

為了簽發證書,您需要通過如下命令,提供一個證書籤名請求 (.csr) 檔案。其中,伺服器的證書籤名請求為:

keytool -v -genkeypair -dname "CN=Root-CA,OU=Certificate Authority,O=Thunderberry,C=NL" -keystore root-ca/identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias root-ca -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,keyCertSign -ext BasicConstraints=ca:true,PathLen:3

而客戶端的證書籤名請求為:

keytool -v -certreq -file client/src/test/resources/client.csr -keystore client/src/test/resources/identity.jks -alias client -keypass secret -storepass secret -keyalg rsa

3. 使用證書籤名請求籤發證書

簽發客戶證書:

keytool -v -gencert -infile client/src/test/resources/client.csr -outfile client/src/test/resources/client-signed.cer -keystore root-ca/identity.jks -storepass secret -alias root-ca -validity 3650 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -rfc

簽發伺服器證書:

keytool -v -gencert -infile shared-server-resources/src/main/resources/server.csr -outfile shared-server-resources/src/main/resources/server-signed.cer -keystore root-ca/identity.jks -storepass secret -alias root-ca -validity 3650 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,DNS:raspberrypi.local,IP:127.0.0.1 -rfc

4. 用已簽名的證書替換未簽名的證書

由於我們無法直接用金鑰工具(keytool)去匯入已簽名的證書,因此我們需要將由CA簽發的證書儲存到identity.jks中。先匯出CA證書:

keytool -v -exportcert-檔案root-ca / root-ca.pem -alias root-ca -keystore root-ca / identity.jks -storepass secret -rfc

然後是客戶端:

keytool -v -importcert -file root-ca/root-ca.pem -alias root-ca -keystore client/src/test/resources/identity.jks -storepass secret -noprompt keytool -v -importcert -file client/src/test/resources/client-signed.cer -alias client -keystore client/src/test/resources/identity.jks -storepass secret keytool -v -delete -alias root-ca -keystore client/src/test/resources/identity.jks -storepass secret

最後是伺服器端:

keytool -v -importcert -file root-ca/root-ca.pem -alias root-ca -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret -noprompt keytool -v -importcert -file shared-server-resources/src/main/resources/server-signed.cer -alias server -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret keytool -v -delete -alias root-ca -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret

5. 設定僅信任CA

為了將客戶端和伺服器配置為僅信任某個CA,我們需要通過將CA證書匯入客戶端和伺服器的信任庫來實現。其中在客戶端,我們可以使用如下操作命令:

keytool -v -importcert -file root-ca/root-ca.pem -alias root-ca -keystore client/src/test/resources/truststore.jks -storepass secret -noprompt

在伺服器端則為:

keytool -v -importcert -file root-ca/root-ca.pem -alias root-ca -keystore shared-server-resources/src/main/resources/truststore.jks -storepass secret -noprompt

同時,由於信任庫仍包含客戶端和伺服器的原有特定證書,因此我們需要將其刪除。其中在客戶端,我們可以使用如下操作命令:

keytool -v -delete -alias server -keystore client/src/test/resources/truststore.jks -storepass secret

在伺服器端則為:

keytool -v -delete -alias client -keystore shared-server-resources/src/main/resources/truststore.jks -storepass secret

至此,如果您再次運行客戶端,將能夠順利通過測試。客戶端將會接收到來自伺服器的hello訊息,而且其中的證書是由該CA所頒發的。

帶有TLS身份驗證的自動化指令碼

其實,您還可以使用該項目的指令碼目錄裡的各種指令碼,來自動化執行上述步驟。例如,對於單向認證而言,可以輸入:./configure-one-way-authentication;而對於雙向認證而言,則可以輸入:./configure-two-way-authentication-by-trusting-each-other my-company-name;對於通過可信CA進行的雙向身份驗證,可以輸入:./configure-two-way-authentication-by-trusting-root-ca my-company-name。

已測試的客戶端

下面是已經通過測試的客戶端列表。您可以在ClientConfig類中找到基於純Java的http客戶端配置。該服務目錄包含了單個的http客戶端請求示例。其中,基於Kotlin和Scala的http客戶端配置是作為巢狀類被包含在內的。而且,所有客戶端示例都使用的是在SSLConfig類中創建的相同的ssl基本配置。

Java

  • Apache HttpClient -> Client configuration | Example request

  • Apache HttpAsyncClient -> Client configuration | Example request

  • Apache 5 HttpClient -> Client configuration | Example request

  • Apache 5 HttpAsyncClient -> Client configuration | Example request

  • JDK HttpClient -> Client Configuration | Example request

  • Old JDK HttpClient -> Client Configuration & Example request

  • Netty Reactor -> Client Configuration | Example request

  • Jetty Reactive HttpClient -> Client Configuration | Example request

  • Spring RestTemplate -> Client Configuration | Example request

  • Spring WebFlux WebClient Netty -> Client Configuration | Example request

  • Spring WebFlux WebClient Jetty -> Client Configuration | Example request

  • OkHttp -> Client Configuration | Example request

  • Jersey Client -> Client Configuration | Example request

  • Old Jersey Client -> Client Configuration | Example request

  • Google HttpClient -> Client Configuration | Example request

  • Unirest -> Client Configuration | Example request

  • Retrofit -> Client Configuration | Example request

  • Async Http Client -> Client Configuration | Example request

  • Feign -> Client Configuration | Example request

  • Methanol -> Client Configuration | Example request

  • Vertx Webclient -> Client Configuration & Example request

  • RPC -> Client/Server Configuration & Example request

  • ElasticSearch -> RestHighLevelClient Configuration & example request

Kotlin

  • Fuel -> Client Configuration & Example request

  • Http4k with Apache 4 -> Client Configuration | Example request

  • Http4k with Async Apache 4 -> Client Configuration | Example request

  • Http4k with Apache 5 -> Client Configuration | Example request

  • Http4k with Async Apache 5 -> Client Configuration | Example request

  • Http4k with Java Net -> Client Configuration | Example request

  • Http4k with Jetty -> Client Configuration | Example request

  • Http4k with OkHttp -> Client Configuration | Example request

  • Kohttp -> Client Configuration & Example request

  • Ktor with Android engine -> Client Configuration | Example request

  • Ktor with Apache engine -> Client Configuration | Example request

  • Ktor with CIO (Coroutine-based I/O) engine -> Client Configuration | Example request

  • Ktor with Okhttp engine -> Client Configuration | Example request

Scala

  • Twitter Finagle -> Client Configuration | Example request

  • Twitter Finagle Featherbed -> Client Configuration & Example request

  • Akka Http Client -> Client Configuration | Example request

  • Dispatch Reboot -> Client Configuration & Example request

  • ScalaJ / Simplified Http Client -> Client Configuration & Example request

  • Sttp -> Client Configuration & Example request

  • Requests-Scala -> Client Configuration & Example request

  • Http4s Blaze Client -> Client Configuration | Example request

  • Http4s Java Net Client -> Client Configuration | Example request


IT145.com E-mail:sddin#qq.com