如何轻松地设置双向TLS保护应用程序安全

译文
安全
本文以示例的形式,并配合自动化的脚本,向您展示如何轻松地设置双向TLS,以实现对于应用程序的安全保护。

[[404242]]

【51CTO.com快译】在安全实践领域,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客户端的配置示例。

在处理http请求的过程中,它们可能会导致应用在初始构建时,需要花费一定的时间来下载大量依赖项。因此,我也通过GitHub - SSLContext Kickstart(https://github.com/Hakky54/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,来运行一个兼容的版本。有关参数的具体设置细节,请参见链接--https://github.com/Hakky54/mutual-tls-ssl/tree/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:

其响应内容如下(纯文本):

  1. HTTP/1.1 200 
  2. Content-Type: text/plain;charset=UTF-8 
  3. Content-Length: 5 
  4. Date: Sun, 11 Nov 2018 14:21:50 GMT 
  5.  
  6. 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的应用属性文件中来实现:

  1. server: 
  2. port: 8443 
  3. ssl: 
  4.     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,那么服务器可以为您提供更多的信息。

显然,为了解决此问题,您需要创建一个带有服务器公钥和私钥的密钥库。其中的公钥可与用户共享,以便加密彼此之间的通信;而服务器的私钥则可用来解密。值得注意的是,我们绝对不可以共享服务器的私钥,以避免被其他人用来破解截获到的通信,进而获悉被加密的通信内容。

因此,若要创建带有公钥和私钥的密钥库,请在终端中执行以下命令(纯文本):

  1. 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文件中:

  1. server: 
  2.   port: 8443 
  3.   ssl: 
  4.     enabled: true 
  5.     key-store: classpath:identity.jks 
  6.     key-password: secret 
  7.     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类中,即从:

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

改为:

  1. 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的握手过程。当然,在创建信任库之前,您需要事先获得服务器的证书。

导出服务器的证书

您可以使用如下命令,导出服务器的证书:

  1. 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 

接着,您可以为客户端创建一个信任库,并使用如下命令导入服务器的证书:

  1. 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文件中,提供如下属性:

  1. client: 
  2.   ssl: 
  3.     one-way-authentication-enabled: true 
  4.     two-way-authentication-enabled: false 
  5.     trust-store: truststore.jks 
  6.     trust-store-password: secret 

对客户端进行身份验证(双向TLS)

接下来,服务器端需要验证客户端的身份,以判断其是否可信。其实现方式为:通过client-auth属性放入服务器的application.yml中,以告知服务器去验证客户端。

  1. server: 
  2.   port: 8443 
  3.   ssl: 
  4.     enabled: true 
  5.     key-store: classpath:identity.jks 
  6.     key-password: secret 
  7.     key-store-password: secret 
  8.     client-auth: need 

当然,如果您直接运行它,则会因为客户端根本没有证书,而产生错误消息:javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate(无效的证书信息)。因此,我们需要通过如下命令,来创建证书:

  1. 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 

同时,您还需要为服务器创建一个信任库。不过,在创建信任库之前,您需要通过如下命令获取客户端的证书:

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

下一步便是使用客户端的证书,来创建服务器的信任库:

  1. 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文件中,提供如下属性:

  1. client: 
  2.   ssl: 
  3.     one-way-authentication-enabled: false 
  4.     two-way-authentication-enabled: true 
  5.     key-store: identity.jks 
  6.     key-password: secret 
  7.     key-store-password: secret 
  8.     trust-store: truststore.jks 
  9.    trust-store-password: secret 

对应地,为了让服务器知晓新创建的信任库,我们需要将当前属性替换为以下属性:

  1. server: 
  2.   port: 8443 
  3.   ssl: 
  4.     enabled: true 
  5.     key-store: classpath:identity.jks 
  6.     key-password: secret 
  7.     key-store-password: secret 
  8.     trust-store: classpath:truststore.jks 
  9.     trust-store-password: secret 
  10.    client-auth: need 

至此,您已完成了双向TLS的安装。如果再次运行客户端,您将会发现客户端能够以安全的方式,从服务器端接收到hello消息了。

基于可信CA的双向TLS

有了前面的基础,我们便可以采用基于可信CA的双向(mutual)认证了。我们首先来看看它的优缺点:

优点

  • 客户端不需要自行添加服务器的证书。
  • 服务器不需要添加客户端的所有证书。
  • 由于是由CA管控着证书的有效期,因此本地运维工作会大幅减少。

缺点

  • 您无法细粒度地控制哪些客户端可以调用自己的应用,哪些不可以。任何客户端,只要持有CA颁发的证书,即可访问您的应用程序。

其具体实现步骤如下:

1. 创建CA

通常,您需要向某个已有的证书颁发机构,提供自己的证书以获取其签名。下面,我们将创建一个自己的CA,并用它去签发客户端和服务器的证书。

  1. 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 

当然,您也可以使用存储库默认提供的那个,具体请参阅identity.jks

2. 创建证书签名请求

为了签发证书,您需要通过如下命令,提供一个证书签名请求 (.csr) 文件。其中,服务器的证书签名请求为:

  1. 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 

而客户端的证书签名请求为:

  1. 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. 使用证书签名请求签发证书

签发客户证书:

  1. 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 

签发服务器证书:

  1. 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证书:

  1. keytool -v -exportcert-文件root-ca / root-ca.pem -alias root-ca -keystore root-ca / identity.jks -storepass secret -rfc 

然后是客户端:

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

最后是服务器端:

  1. keytool -v -importcert -file root-ca/root-ca.pem -alias root-ca -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret -noprompt 
  2. 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 
  3. keytool -v -delete -alias root-ca -keystore shared-server-resources/src/main/resources/identity.jks -storepass secret 

5. 设置仅信任CA

为了将客户端和服务器配置为仅信任某个CA,我们需要通过将CA证书导入客户端和服务器的信任库来实现。其中在客户端,我们可以使用如下操作命令:

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

在服务器端则为:

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

同时,由于信任库仍包含客户端和服务器的原有特定证书,因此我们需要将其删除。其中在客户端,我们可以使用如下操作命令:

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

在服务器端则为:

  1. 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

原文标题:How to Easily Set Up Mutual TLS,作者:Hakan Altındağ

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

 

责任编辑:华轩 来源: 51CTO
相关推荐

2021-10-11 09:00:00

云原生Kubernetes安全

2020-08-25 14:03:20

应用程序屏蔽应用程序内保护网络攻击

2015-02-26 09:19:00

2013-11-19 15:35:01

2011-03-30 13:28:26

2009-06-29 14:19:50

2021-07-20 09:44:34

云原生应用程序安全云安全

2021-11-24 16:51:03

gRPCGoPython

2009-07-03 06:57:32

2009-12-15 10:19:05

Linux应用程序

2012-05-29 10:04:08

2011-02-13 14:36:35

2013-02-18 16:12:55

2014-02-19 15:38:42

2022-06-22 09:00:00

安全编程语言工具

2022-09-20 23:52:50

表情符号开发面部识别

2013-10-31 10:44:54

IDE工具

2011-11-03 09:41:35

Android签名安全性

2022-03-04 10:44:01

堆喷射恶意代码

2015-11-05 10:16:33

点赞
收藏

51CTO技术栈公众号