HTTPS双向认证

双向认证是指客户端与服务器端在建立HTTPS连接时,需相互验证对方身份。相较于单向认证,其握手过程增加若干步骤。在单向认证中,客户端从服务器获取服务器公钥证书并完成验证后,即建立安全通信通道。在双向认证中,客户端除验证服务器公钥证书外,还需向服务器提交自身公钥证书,供服务器验证。仅当双方身份均通过验证后,方可建立安全通信通道并开始数据传输。

原理

单向认证流程

单向认证流程中,服务器端保存着公钥证书和私钥两个文件,整个握手过程如下:

image

  1. 客户端发起HTTPS连接请求,向服务端发送支持的SSL/TLS协议版本信息。

  2. 服务端响应并返回其公钥证书(server.crt)。

  3. 客户端验证服务器证书(server.crt)的有效性,并提取其中的服务端公钥。

  4. 客户端生成一个随机数R作为预主密钥,使用服务端公钥对该预主密钥进行加密,形成密文并发送至服务端。

  5. 服务端使用其私钥(server.key)解密该密文,恢复出预主密钥R。

  6. 双方基于该预主密钥协商生成相同的会话密钥(Session Key),后续通信过程中的数据传输均采用该对称密钥加密保护。

双向认证流程

image

  1. 客户端发起HTTPS连接请求,向服务端发送支持的SSL/TLS协议版本信息。

  2. 服务端响应并返回其公钥证书(server.crt)。

  3. 客户端验证服务器证书(server.crt),提取服务端公钥。

  4. 客户端将其公钥证书(client.crt)发送至服务端。

  5. 服务端使用受信任的根证书(root.crt)验证客户端证书,并提取客户端公钥。

  6. 客户端将其支持的加密套件列表发送给服务端。

  7. 服务端根据双方支持的算法协商出最优加密方案,并使用客户端公钥对该加密参数进行加密后返回客户端。

  8. 客户端使用自身私钥解密获取加密参数,生成预主密钥(Premaster Secret)随机数R,并用服务端公钥加密后发送至服务端。

  9. 服务端使用自身私钥解密获得该随机数R。

  10. 双方基于该预主密钥生成会话密钥(Session Key),后续通信数据均通过该对称密钥加密传输。

1. 证书准备

根据上述内容,可总结出整个双向认证流程共需六个证书文件:

  • 服务器端公钥证书:server.crt

  • 服务器端私钥文件:server.key

  • 根证书:root.crt

  • 客户端公钥证书:client.crt

  • 客户端私钥文件:client.key

  • 客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12

所有证书均可向证书颁发机构申请签发,通常需支付相应的签发费用,此时应选择主流的证书颁发机构进行采购。若仅用于企业内部场景且不面向公众使用,也可自行签发自签名证书,具体签发方法请参见自签名证书

2.在API网关配置HTTPS双向认证

准备好六个证书文件后,即可配置API网关以启用HTTPS双向认证功能。本节介绍如何将域名关联的服务器证书和根证书绑定至API网关,以实现HTTPS双向认证。

  1. 登录API网关控制台,在左侧导航栏选择API管理 > 分组管理,在分组列表页面的右上角单击创建分组

  2. 在创建分组页面选择网关实例,填写分组名称BasePath,单击确定

  3. 分组列表页面,单击创建的分组,进入分组详情页面。

  4. 在分组详情页面的独立域名区域,单击绑定域名

  5. 在绑定域名弹框中,填写域名,进行绑定。

  6. 绑定域名后,单击域名列对应的选择证书链接。

  7. 在选择证书弹框中,单击手动添加证书链接。

  8. 创建证书弹框中,分别将证书准备中提到的三个证书:

    • 服务器端公钥证书(server.crt)的内容填写到证书内容文本框中。

    • 服务器端私钥文件(server.key)的内容填写到私钥文本框中。

    • 根证书(root.crt)的内容填写到根证书的文本框中。

完成上述步骤后,即可在API网关上成功配置HTTPS双向认证。

3. 自签名证书

在生成系列证书前,需首先创建一个CA根证书,并由该根证书签发服务器公钥证书和客户端公钥证书。为验证根证书签发及客户端证书认证的逻辑,使用该根证书生成两套不同的客户端证书,并同时通过这两个客户端证书发起请求,验证服务器端是否能够正确识别并处理两者。

image

3.1生成自签名根证书

  1. 创建根证书私钥:

    openssl genrsa -out root.key 2048
  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
    重要

    根证书的Common Name应设置为root,客户端和服务器端证书的Common Name必须填写对应的域名,且根证书的Common Name与客户端、服务器端证书的Common Name不得相同;根证书、服务器端证书及客户端证书的其余字段需保持一致。生成过程中涉及密码输入时,可直接回车跳过。

通过上述三个命令操作,可生成一张签名有效期为10年的根证书 root.crt,后续可用于签发服务器证书和客户端证书。

3.2 生成自签名服务器端证书

  1. 生成服务器端证书私钥:

    openssl genrsa -out server.key 2048
  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:有效期十年的服务器端公钥证书,使用根证书和服务器端私钥文件一起生成。

3.3 生成自签名客户端证书

  1. 生成客户端证书密钥:

    openssl genrsa -out client.key 2048
    openssl genrsa -out client2.key 2048
  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.key:有效期十年的客户端证书。

使用根证书和客户端私钥一起生成 client.p12/client2.p12,这个证书文件包含客户端的公钥和私钥,主要用来给浏览器访问使用。

4. 验证

使用curl加上证书路径,可以直接测试NginxHTTPS双向认证是否配置成功。下面我们测试三个用例:

  • 使用client.crt / client.key这一套客户端证书来调用服务器端。

  • 使用client2.crt / client2.key这一套客户端证书来调用服务器端。

  • 不使用证书来调用服务器端。

下面是三个用例的测试结果:

4.1 带证书的成功调用

使用client.crt /client.key这一套客户端证书来调用服务器端。

#--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.XX.XX/
*   Trying 47.93.XX.XX...
* TCP_NODELAY set
* Connected to 47.93.XX.XX (47.93.XX.XX) 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.XX.XX left intact

使用client2.crt / client2.key这一套客户端证书来调用服务器端。

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>

4.2 不带证书的调用

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>

三个用例均符合预期。从第一个测试日志可见,通信过程较长,客户端对服务器端证书进行验证的同时,将自身证书上传至服务器端完成验证。由根证书签发的两个客户端证书均可成功发起双向HTTPS认证请求。未携带客户端证书的请求将被服务器端拒绝服务。

5. 使用Java调用

由于使用自签名证书,使用ApacheHttpClient调用时,需要将服务器证书加入可信任证书库中,才能成功调用,也可以在代码中简单忽略证书。

cd $JAVA_HOME
sudo ./bin/keytool -import -alias ttt -keystore cacerts -file /Users/fred/temp/cert5/server.crt

将服务器端公钥证书设置为可信证书后,使用以下代码可以直接发起带客户端证书的HTTPS请求:

import org.apache.http.HttpEntity;
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.FileInputStream;
import java.io.InputStream;
import java.security.KeyStore;
public class HttpClientWithClientCert {
    private final static String PFX_PATH = "/Users/fred/temp/cert5/client.p12";    //客户端证书路径
    private final static String PFX_PWD = "123456";    //客户端证书密码
    public static String sslRequestGet(String url) throws Exception {
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        InputStream instream = new FileInputStream(new File(PFX_PATH));
        try {
            keyStore.load(instream, PFX_PWD.toCharArray());
        } finally {
            instream.close();
        }
        SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, PFX_PWD.toCharArray()).build();
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext
                , new String[] { "TLSv1" }    // supportedProtocols ,这里可以按需要设置
                , null    // supportedCipherSuites
                , SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
        try {
            HttpGet httpget = new HttpGet(url);
            //httpget.addHeader("host", "integration-fred2.fredhuang.com");// 设置一些heander等
            CloseableHttpResponse response = httpclient.execute(httpget);
            try {
                HttpEntity entity = response.getEntity();
                String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");//返回结果
                EntityUtils.consume(entity);
                return jsonStr;
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
    }
    public static void main(String[] args) throws Exception {
        System.out.println(System.getProperty("java.home"));
        System.out.println(sslRequestGet("https://integration-fred2.fredhuang.com"));
    }
}