绿色健康小清新

耐得住寂寞,守得住繁华

HTTPS

Https

基本介绍

HTTP 存在的问题

没有加密,无法保证通信内容不被窃听。

没有报文完整性验证,无法确保通信内容在传输中不被改变。

没有身份鉴别,无法让通信双方确认对方身份。

HTTP over SSL,在 HTTP 传输上增加了传输层安全性(TLS)或安全套接字层(SSL),通过信息加密数据完整性校验身份鉴别为 HTTP 事务提供安全保证。SSL 会对数据进行加密并把加密数据送往 TCP 套接字,在接收方,SSL 读取 TCP 套接字的数据并解密,把数据交给应用层。

HTTPS 采用混合加密机制,使用非对称加密传输对称密钥保证传输安全,使用对称加密保证通信效率。每一个密钥对(key pairs) 都有一个 私有密钥(private key)公有密钥(public key),私有密钥是独有的,一般位于服务器上,用于解密由公共密钥加密过的信息;公有密钥是公有的,与服务器进行交互的每个客户端都可以持有公有密钥,用公钥加密的信息只能由私有密钥来解密。

当我们申请域名证书,SSL会有一个pem证书和key私钥,pem证书包含数字签名和公钥。

证书文件

证书相关文件有多种格式,常见格式:.crt.key.req.csr.pem.der

  • xx.crt:证书文件 (Certificate)
  • xx.key:私钥文件
  • xx.req:请求文件
  • xx.csr:请求文件 (Certificate Signing Request)
  • xx.pem:证书文件为 pem 格式(文本文件)
  • xx.der:证书文件为 der 格式(二进制文件)

实际上,上述文件的扩展名可以随意命名。只是为了容易理解文件的功能而选择大家都认识的命名方式。但是,上述文件是有格式的,只能是 pem 格式或者 der 格式。使用什么格式的文件取决于需求。

pem 格式的文件为文本文件,内容分别为:

证书文件:

1
2
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----

私钥文件:

1
2
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----

请求文件:

1
2
-----BEGIN CERTIFICATE REQUEST-----
-----END CERTIFICATE REQUEST-----

证书种类

证书分为根证书、服务器证书、客户端证书。根证书文件(ca.crt)和根证书对应的私钥文件(ca.key)由 CA(证书授权中心,国际认可)生成和保管。

客户端会有一个信任库,里面保存了该客户端信任的CA(证书签发机构)的证书,如果收到的证书签发机构不在信任库中,则客户端会提示用户证书不可信。

若客户端是浏览器,各个浏览器都会内置一些可信任的证书签发机构列表,在浏览器的设置中可以看到。

证书内容

一个数字证书通常包含了:

  • 公钥;
  • 持有者信息;
  • 证书认证机构(CA)的信息;
  • CA 对这份文件的数字签名及使用的算法;
  • 证书有效期;
  • 还有一些其他额外信息;

服务器获取证书步骤和客户端验证步骤

服务器获取证书:

  1. 服务器生成自己的私钥;
  2. 服务器使用私钥生成请求文件(server.req),请求文件中包含服务器的相关信息,比如域名、公钥、组织机构等;
  3. 服务器将 server.req 发送给 CA。CA 验证服务器合法后,使用 ca.keyserver.req 生成证书文件(server.crt),将请求文件中的信息进行Hash计算,得到一个Hash值,使用ca的私钥将该Hash值进行加密,生成 Certificate Signature,也就是 CA 对证书做了签名,最后将 Certificate Signature 添加在文件证书上,形成数字证书;
  4. CA 将证书文件(server.crt)发送给服务器。

由于ca.keyca.crt 是一对,ca.crt 文件中包含公钥,因此 ca.crt 可以验证 server.crt是否合法——使用公钥验证证书的签名。

客户端验证证书:

  • 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  • 浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  • 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。

证书链

但事实上,证书的验证过程中还存在一个证书信任链的问题,因为我们向 CA 申请的证书一般不是根证书签发的,而是由中间证书签发的,比如百度的证书,从下图你可以看到,证书的层级有三级:

对于这种三级层级关系的证书的验证过程如下:

  • 客户端收到 baidu.com 的证书后,发现这个证书的签发者不是根证书,就无法根据本地已有的根证书中的公钥去验证 baidu.com 证书是否可信。于是,客户端根据 baidu.com 证书中的签发者,找到该证书的颁发机构是 “GlobalSign Organization Validation CA - SHA256 - G2”,然后向 CA 请求该中间证书;
  • 请求到证书后发现 “GlobalSign Organization Validation CA - SHA256 - G2” 证书是由 “GlobalSign Root CA” 签发的,由于 “GlobalSign Root CA” 没有再上级签发机构,说明它是根证书,也就是自签证书。应用软件会检查此证书有否已预载于根证书清单上,如果有,则可以利用根证书中的公钥去验证 “GlobalSign Organization Validation CA - SHA256 - G2” 证书,如果发现验证通过,就认为该中间证书是可信的;
  • “GlobalSign Organization Validation CA - SHA256 - G2” 证书被信任后,可以使用 “GlobalSign Organization Validation CA - SHA256 - G2” 证书中的公钥去验证 baidu.com 证书的可信性,如果验证通过,就可以信任 baidu.com 证书。

在这三个步骤中,最开始客户端只信任根证书 GlobalSign Root CA 证书的,然后 “GlobalSign Root CA” 证书信任 “GlobalSign Organization Validation CA - SHA256 - G2” 证书,而 “GlobalSign Organization Validation CA - SHA256 - G2” 证书又信任 baidu.com 证书,于是客户端也信任 baidu.com 证书。

这样的一层层地验证就构成了一条信任链路,整个证书信任链验证流程如下图所示:

最后一个问题,为什么需要证书链这么麻烦的流程?Root CA 为什么不直接颁发证书,而是要搞那么多中间层级呢?

这是为了确保根证书的绝对安全性,将根证书隔离地越严格越好,不然根证书如果失守了,那么整个信任链都会有问题。

自签名证书

我们的网页要想使用HTTPS访问,就必须向证书机构去申请签发,并放在Nginx服务器下。但如果只是企业内部使用,不是给公众使用,也可以自行颁发自签名证书,比如K8S集群中就可以自行签发证书完成HTTPS双向认证。

一下以openssl来进行创建证书相关文件。

生成根证书

生成这一些列证书之前,我们需要先生成一个CA根证书,然后由这个CA根证书颁发服务器公钥证书和客户端公钥证书。为了验证根证书颁发与验证客户端证书这个逻辑,我们使用根证书生成两套不同的客户端证书,然后同时用两个客户端证书来发送请求,看服务器端是否都能识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(1)创建根证书私钥:
openssl genrsa -out ca.key 1024

(2)创建根证书请求文件:
openssl req -new -out ca.csr -key ca.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 ca.csr -out ca.crt -signkey ca.key -CAcreateserial -days 3650

在创建证书请求文件的时候需要注意三点,下面生成服务器请求文件和客户端请求文件均要注意这三点:

  • 根证书的Common Name填写root就可以,所有客户端和服务器端的证书这个字段需要填写域名,一定要注意的是,根证书的这个字段和客户端证书、服务器端证书不能一样;
  • 其他所有字段的填写,根证书、服务器端证书、客户端证书需保持一致 最后的密码可以直接回车跳过。

经过上面三个命令行,我们最终可以得到一个签名有效期为10年的根证书root.crt,后面我们可以用这个根证书去颁发服务器证书和客户端证书。

生成自签名服务器端证书

1
2
3
4
5
6
7
8
(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 ca.crt -CAkey ca.key -CAcreateserial -days 3650

经过上面的三个命令,我们得到:

server.key:服务器端的秘钥文件 server.crt:有效期十年的服务器端公钥证书,使用根证书和服务器端私钥文件一起生成

生成自签名客户端证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(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 ca.crt -CAkey ca.key -CAcreateserial -days 3650
openssl x509 -req -in client2.csr -out client2.crt -signkey client2.key -CA ca.crt -CAkey ca.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:客户端p12格式,这个证书文件包含客户端的公钥和私钥,主要用来给浏览器访问使用。

Java API调用

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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"));
}

}

cfssl的使用

除了openssl之外,还有一种cfssl也可以达到自签名证书的效果。

  1. 创建一个根证书json文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    [root@devops cfssl]# cfssl print-defaults csr > ca-csr.json
    [root@devops cfssl]# vim ca-csr.json
    {
    "CN": "kubernetes",
    "hosts": [],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [
    {
    "C": "CN",
    "ST": "Hubei",
    "L": "Wuhan",
    "O": "k8s",
    "OU": "systemGroup"
    }
    ],
    "ca": {
    "expiry": "87600h"
    }
    }
    • CN: Common Name,浏览器使用该字段验证网站是否合法,一般写的是域名,浏览器使用该字段验证网站是否合法,K8S中用作用户名
    • hosts:可以使用的主机,不写的话代表所有;
    • key:生成证书的算法;
    • names:一些其它的属性:
      • C:Country, 国家;
      • ST:State,州,省;
      • L: Locality,地区,城市;
      • O:Organization Name,组织名称,公司名称;(K8S中用作组名)
      • OU:Organization Unit Name,组织单位名称,公司部门。
  2. 生成CA证书

    1
    2
    3
    [root@devops cfssl]# cfssl gencert -initca ca-csr.json  | cfssljson -bare ca
    [root@devops cfssl]# ls
    ca.csr ca-csr.json ca-key.pem ca.pem

    生成了ca.csr, ca-key.pem, ca.pem 三个文件

  3. 创建一个服务器证书json文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    [root@devops cfssl]# vim server-csr.json
    {
    "CN": "kubernetes",
    "hosts": [
    "127.0.0.1",
    "192.168.10.11",
    "192.168.10.12",
    "192.168.10.13"
    ],
    "key": {
    "algo": "rsa",
    "size": 2048
    },
    "names": [{
    "C": "CN",
    "ST": "Hubei",
    "L": "Wuhan",
    "O": "k8s",
    "OU": "systemGroup"
    }]
    }
  4. 根据ca.key(ca私钥)和ca.crt(ca证书)生成服务器证书:

    1
    2
    3
    [root@devops cfssl]# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=kubernetes server-csr.json | cfssljson -bare server
    [root@devops cfssl]# ls
    ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem server.csr server-csr.json server-key.pem server.pem

认证方式和TLS握手(※)

https有两种认证方式:单向验证和双向验证,而使用哪一种,是由服务器决定的

单向认证

单向验证是指通信双方中一方验证另一方是否合法。通常是指客户端验证服务器

  • 客户端需要:ca.crt
  • 服务器需要:server.crtserver.key

HTTPS 流程(TLS4次握手)

HTTPS 是应用层协议,需要先完成 TCP 连接建立,然后走 TLS 握手过程后,才能建立通信安全的连接。不同的密钥交换算法,TLS 的握手过程可能会有一些区别。

以RSA的握手为例。

① 客户发送它支持的密码套件列表、一个不重数、SSL/TLS协议版本列表。不重数就是在协议的生存期只使用一次的数,用于防止重放攻击(端点鉴别),每个 TCP 会话使用不同的不重数,可以使加密密钥不同,重放记录无法通过完整性检查。

② 服务器将SSL/TLS版本、一个不重数、密码套件列表中的一个密码套件、证书(证书包含公钥)发送给客户。密码套件基本的形式是「密钥交换算法(RSA) + 签名算法(RSA) + 对称加密算法(AES) + 摘要算法(SHA384)

③ 客户通过 CA(Certificate Authority,证书认证机构) 提供的ca.crt验证证书,成功后提取证书中的公钥,生成一个前主密钥 PMS(pre-master secret) 并使用公钥加密并发送给服务器。验证主要是:

  • 证书是否过期;
  • 发型服务器证书的CA是否可靠;
  • 返回的公钥是否能正确解开返回证书中的数字签名;
  • 服务器证书上的域名是否和服务器的实际域名相匹配;
  • 验证通过后,将继续进行通信,否则,终止通信

服务器和客户端使用相同的密钥导出函数,独立的根据PMS和两个不重数中计算出主密钥MS。MS被切分为一个加密密钥和一个MAC密钥。当选择的对称密钥使用CBC时,两个初始向量也从MS获得,分别用于客户端和发送端。对每个记录附加一个MAC用于完整性检查,然后加密“记录+MAC”。MAC是数据+MAC密钥+当前序号的散列。序号是保证记录数据流不被第三方扰乱,接收方通过发送方的序号,通过在MAC的计算中包括适当的序号,验证记录的数据完整性。

然后客户端把之前发送的数据+数据的摘要(报文鉴别码Mac)进行加密,发送给服务器做一个验证,验证加密通信是否可用和之前握手信息是否有被中途篡改过。

④服务器用私钥得到PMS,和客户端一样从MS中获取加密密钥和Mac密钥,也将之前发送的数据+数据的摘要(报文鉴别码Mac)进行加密,发送给客户端做一个验证,验证加密通信是否可用和之前握手信息是否有被中途篡改过。

应用:

我们平时使用 PC 上网时使用的就是单向验证的方式。即,我们验证我们要访问的网站的合法性。PC 中的浏览器(火狐、IE、chrome等)已经包含了很多 CA 的根证书(ca.crt)。当我们访问某个网站(比如:https://www.baidu.com)时,网站会将其证书(server.crt)发送给浏览器,浏览器会使用 ca.crt 验证 server.crt 是否合法。如果发现访问的是不合法网站,浏览器会给出提示。

现实中,有的公司会使用自签发证书,即公司自己生成根证书(ca.crt)。如果我们信任此网站,那么需要手动将其证书添加到系统中。

双向认证

单向验证过程中,客户端会验证自己访问的服务器,服务器对来访的客户端身份不做任何限制。如果服务器需要限制客户端的身份,则可以选择开启服务端验证,这就是双向验证。从这个过程中我们不难发现,使用单向验证还是双向验证,是服务器决定的。

一般而言,我们的服务器都是对所有客户端开放的,所以服务器默认都是使用单向验证。如果你使用的是Tomcat服务器,在配置文件server.xml中,配置Connector节点的clientAuth属性即可。若为true,则使用双向验证,若为false,则使用单向验证。如果你的服务,只允许特定的客户端访问,那就需要使用双向验证了。

双向验证是指通信双方需要互相验证对方是否合法。服务器验证客户端,客户端验证服务器。

  • 客户端需要:ca.crtclient.crtclient.key
  • 服务器需要:ca.crtserver.crtserver.key

HTTPS流程(TLS4次握手):

双向验证基本过程与单向验证相同,不同在于:

  • 第二次握手:服务器第一次回应客户端的SeverHello消息中,会要求客户端提供“客户端的证书

  • 第三次握手:客户端验证完服务器证书后的回应内容中,会增加两个信息:

    • 客户端的证书
    • 客户端证书验证消息(CertificateVerifymessage):客户端将之前所有收到的和发送的消息组合起来,并用hash算法得到一个hash值,然后用客户端密钥库私钥对这个hash进行签名,这个签名就是CertificateVerify message
  • 第四次握手:服务器收到客户端证书后:

    • 确认这个证书是否在自己的信任库中(当然也会校验是否过期等信息),如果验证不通过则会拒绝连接;
    • 客户端证书中的公钥去验证收到的证书验证消息中的签名。这一步的作用是为了确认证书确实是客户端的。

所以,**在双向验证中,客户端需要用到密钥库,保存自己的私钥和证书,并且证书需要提前发给服务器,由服务器放到它的信任库中。**这一点至关重要,因为之前没想通这一点,导致我看K8S的kubeconfig的操作一脸懵逼。

应用:

双向验证通常用于支付系统中,比如支付宝。我们在使用支付宝时必须下载数字证书,该证书就是支付宝颁发给针对我们这台机器的证书,我们只能使用这台机器访问支付宝。如果换了机器,那么需要重新申请证书。

RSA握手具体分析

TLS 第一次握手

客户端首先会发一个「Client Hello」消息,字面意思我们也能理解到,这是跟服务器「打招呼」。

消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random),这个随机数会被服务端保留,它是生成主密钥MS的材料之一。

TLS 第二次握手

当服务端收到客户端的「Client Hello」消息后,会确认 TLS 版本号是否支持,和从密码套件列表中选择一个密码套件,以及生成随机数(Server Random)。

接着,返回「Server Hello」消息,消息里面有服务器确认的 TLS 版本号,也给出了随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件。

可以看到,服务端选择的密码套件是 “Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256”。

这个密码套件是有固定格式和规范的。基本的形式是「密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法」, 一般 WITH 单词前面有两个单词,第一个单词是约定密钥交换的算法,第二个单词是约定证书的验证算法。比如刚才的密码套件的意思就是:

  • 由于 WITH 单词只有一个 RSA,则说明握手时密钥交换算法和签名算法都是使用 RSA;
  • 握手后的通信使用 AES 对称算法,密钥长度 128 位,分组模式是 GCM;
  • 摘要算法 SHA384 用于消息认证和产生随机数;

就前面这两个客户端和服务端相互「打招呼」的过程,客户端和服务端就已确认了 TLS 版本和使用的密码套件,而且你可能发现客户端和服务端都会各自生成一个随机数,并且还会把随机数传递给对方。

那这个随机数有啥用呢?其实这两个随机数是后续作为生成「会话密钥」的条件,所谓的会话密钥就是数据传输时,所使用的对称加密密钥。

然后,服务端为了证明自己的身份,会发送「Server Certificate」给客户端,这个消息里含有数字证书。

随后,服务端发了「Server Hello Done」消息,目的是告诉客户端,我已经把该给你的东西都给你了,本次打招呼完毕。

TLS 第三次握手

客户通过 CA(Certificate Authority,证书认证机构) 提供的ca.crt验证证书,验证主要是:

  • 证书是否过期;
  • 发型服务器证书的CA是否可靠;
  • 返回的公钥是否能正确解开返回证书中的数字签名;
  • 服务器证书上的域名是否和服务器的实际域名相匹配;
  • 验证通过后,将继续进行通信,否则,终止通信。

成功后提取证书中的公钥,生成一个前主密钥 PMS 并使用公钥加密并通过「Change Cipher Key Exchange」消息传给发送给服务器。

客户端和服务端双方都共享了三个随机数,分别是 Client Random、Server Random、pre-master。于是,双方根据已经得到的三个随机数,生成会话密钥/主密钥(Master Secret),MS被切分为两个加密密钥和两个MAC密钥。当选择的对称密钥使用CBC时,两个初始向量也从MS获得,分别用于客户端和发送端。对每个记录附加一个MAC用于完整性检查,然后加密“记录+MAC”。MAC是数据+MAC密钥+当前序号的散列。序号是保证记录数据流不被第三方扰乱,接收方通过发送方的序号,通过在MAC的计算中包括适当的序号,验证记录的数据完整性。

生成完会话密钥后,然后客户端发一个「Change Cipher Spec」,告诉服务端开始使用加密方式发送消息。

然后,客户端再发一个「Encrypted Handshake Message(Finishd)」消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证,验证加密通信是否可用和之前握手信息是否有被中途篡改过。

可以发现,「Change Cipher Spec」之前传输的 TLS 握手数据都是明文,之后都是对称密钥加密的密文。

TLS 第四次握手

服务器也是同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。

最后,就用「会话密钥」加解密 HTTP 请求和响应了。

RSA 算法的缺陷

使用 RSA 密钥协商算法的最大问题是不支持前向保密。因为客户端传递的PMS(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。

为了解决这一问题,于是就有了 DH 密钥协商算法,这里简单介绍它的工作流程。

客户端和服务端各自会生成随机数,并以此作为私钥,然后根据公开的 DH 计算公示算出各自的公钥,通过 TLS 握手双方交换各自的公钥,这样双方都有自己的私钥和对方的公钥,然后双方根据各自持有的材料算出一个随机数,这个随机数的值双方都是一样的,这就可以作为后续对称加密时使用的密钥。

DH 密钥交换过程中,即使第三方截获了 TLS 握手阶段传递的公钥,在不知道的私钥的情况下,也是无法计算出密钥的,而且每一次对称加密密钥都是实时生成的,实现前向保密。

但因为 DH 算法的计算效率问题,后面出现了 ECDHE 密钥协商算法,我们现在大多数网站使用的正是 ECDHE 密钥协商算法。

ECDHE握手具体分析

可以先去了解一下ECDHE算法。

我用 Wireshark 工具抓了用 ECDHE 密钥协商算法的 TSL 握手过程,可以看到是四次握手:

使用了 ECDHE,在 TLS 第四次握手前,客户端就已经发送了加密的 HTTP 数据,而对于 RSA 握手过程,必须要完成 TLS 四次握手,才能传输应用数据。

所以,ECDHE 相比 RSA 握手过程省去了一个消息往返的时间,这个有点「抢跑」的意思,它被称为是「TLS False Start」,跟「TCP Fast Open」有点像,都是在还没连接完全建立前,就发送了应用数据,这样便提高了传输的效率。

接下来,分析每一个 ECDHE 握手过程。

TLS 第一次握手

客户端首先会发一个「Client Hello」消息,消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random)

TLS 第二次握手

服务端收到客户端的「打招呼」,同样也要回礼,会返回「Server Hello」消息,消息面有服务器确认的 TLS 版本号,也给出了一个随机数(Server Random),然后从客户端的密码套件列表选择了一个合适的密码套件。

不过,这次选择的密码套件就和 RSA 不一样了,我们来分析一下这次的密码套件的意思。

「 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384」

  • 密钥协商算法使用 ECDHE;
  • 签名算法使用 RSA;
  • 握手后的通信使用 AES 对称算法,密钥长度 256 位,分组模式是 GCM;
  • 摘要算法使用 SHA384;

接着,服务端为了证明自己的身份,发送「Certificate」消息,会把证书也发给客户端。

这一步就和 RSA 握手过程有很大到区别了,因为服务端选择了 ECDHE 密钥协商算法,所以会在发送完证书后,发送「Server Key Exchange」消息。

这个过程服务器做了三件事:

  • 选择了名为 named_curve 的椭圆曲线,选好了椭圆曲线相当于椭圆曲线基点 G 也定好了,这些都会公开给客户端;
  • 生成随机数作为服务端椭圆曲线的私钥,保留到本地;
  • 根据基点 G 和私钥计算出服务端的椭圆曲线公钥,这个会公开给客户端。
  • 为了保证这个椭圆曲线的公钥不被第三方篡改,服务端会用 RSA 签名算法给服务端的椭圆曲线公钥做个签名。

随后,就是「Server Hello Done」消息,服务端跟客户端表明:“这些就是我提供的信息,打招呼完毕”。

至此,TLS 两次握手就已经完成了,目前客户端和服务端通过明文共享了这几个信息:Client Random、Server Random 、使用的椭圆曲线、椭圆曲线基点 G、服务端椭圆曲线的公钥,这几个信息很重要,是后续生成会话密钥的材料。

TLS 第三次握手

客户端收到了服务端的证书后,自然要校验证书是否合法,如果证书合法,那么服务端到身份就是没问题的。校验证书到过程,会走证书链逐级验证,确认证书的真实性,再用证书的公钥验证签名,这样就能确认服务端的身份了,确认无误后,就可以继续往下走。

客户端会生成一个随机数作为客户端椭圆曲线的私钥,然后再根据服务端前面给的信息,生成客户端的椭圆曲线公钥,然后用「Client Key Exchange」消息发给服务端。

至此,双方都有对方的椭圆曲线公钥、自己的椭圆曲线私钥、椭圆曲线基点 G。于是,双方都就计算出点(x,y),其中 x 坐标值双方都是一样的,前面说 ECDHE 算法时候,x是一个共享密钥,但实际应用中,x 还不是最终的会话密钥。

还记得 TLS 握手阶段,客户端和服务端都会生成了一个随机数传递给对方吗?

最终的会话密钥,就是用「客户端随机数 + 服务端随机数 + x(ECDHE 算法算出的共享密钥) 」三个材料生成的。

之所以这么麻烦,是因为 TLS 设计者不信任客户端或服务器「伪随机数」的可靠性,为了保证真正的完全随机,把三个不可靠的随机数混合起来,那么「随机」的程度就非常高了,足够让黑客计算出最终的会话密钥,安全性更高。

算好会话密钥后,客户端会发一个「Change Cipher Spec」消息,告诉服务端后续改用对称算法加密通信。

接着,客户端会发「Encrypted Handshake Message」消息,把之前发送的数据做一个摘要,再用对称密钥加密一下,让服务端做个验证,验证下本次生成的对称密钥是否可以正常使用。

TLS 第四次握手

最后,服务端也会有一个同样的操作,发「Change Cipher Spec」和「Encrypted Handshake Message」消息,如果双方都验证加密和解密没问题,那么握手正式完成。于是,就可以正常收发加密的 HTTP 请求和响应了。

RSA和ECGHE握手的区别

  • RSA 密钥协商算法「不支持」前向保密,ECDHE 密钥协商算法「支持」前向保密
  • 使用了 RSA 密钥协商算法,TLS 完成四次握手后,才能进行应用数据传输,而对于 ECDHE 算法,客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间
  • 使用 ECDHE, 在 TLS 第 2 次握手中,会出现服务器端发出的「Server Key Exchange」消息,而 RSA 握手过程没有该消息;

TLS 和 TCP 能同时握手

前面说了,HTTPS 建立连接的过程,先进行 TCP 三次握手,再进行 TLS 四次握手

但「HTTPS 中的 TLS 握手过程可以同时进行三次握手」,这个场景是可能存在的,但是一定要有前提条件。

  • 如果开启了 TCP Fast Open 功能,那么建立第二次以后的通信过程,可以绕过 TCP 三次握手发送数据;
  • TLSv1.2 握手过程基本都是需要四次,也就是需要经过 2-RTT 才能完成握手,然后才能发送请求,而 TLSv1.3 只需要 1-RTT 就能完成 TLS 握手

也就是说如果客户端和服务端都开启了 TCP Fast Open 功能,且 TLS 版本是 1.3,且客户端和服务端已经完成过一次通信,那么HTTPS 中的 TLS 握手过程可以同时进行三次握手

TCP Fast Open

常规的情况下,如果要使用 TCP 传输协议进行通信,则客户端和服务端通信之前,先要经过 TCP 三次握手后,建立完可靠的 TCP 连接后,客户端才能将数据发送给服务端。

其中,TCP 的第一次和第二次握手是不能够携带数据的,而 TCP 的第三次握手是可以携带数据的,因为这时候客户端的 TCP 连接状态已经是 ESTABLISHED,表明客户端这一方已经完成了 TCP 连接建立。

就算客户端携带数据的第三次握手在网络中丢失了,客户端在一定时间内没有收到服务端对该数据的应答报文,就会触发超时重传机制,然后客户端重传该携带数据的第三次握手的报文,直到重传次数达到系统的阈值,客户端就会销毁该 TCP 连接。

说完常规的 TCP 连接后,我们再来看看 TCP Fast Open。

TCP Fast Open 是为了绕过 TCP 三次握手发送数据,在 Linux 3.7 内核版本之后,提供了 TCP Fast Open 功能,这个功能可以减少 TCP 连接建立的时延。

要使用 TCP Fast Open 功能,客户端和服务端都要同时支持才会生效,而且开启了 TCP Fast Open 功能,想要绕过 TCP 三次握手发送数据,得建立第二次以后的通信过程

在客户端首次建立连接时的过程,如下图:

具体介绍:

  • 客户端发送 SYN 报文,该报文包含 Fast Open 选项,且该选项的 Cookie 为空,这表明客户端请求 Fast Open Cookie;
  • 支持 TCP Fast Open 的服务器生成 Cookie,并将其置于 SYN-ACK 报文中的 Fast Open 选项以发回客户端;
  • 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的 Cookie

所以,第一次客户端和服务端通信的时候,还是需要正常的三次握手流程。随后,客户端就有了 Cookie 这个东西,它可以用来向服务器 TCP 证明先前与客户端 IP 地址的三向握手已成功完成。

对于客户端与服务端的后续通信,客户端可以在第一次握手的时候携带应用数据,从而达到绕过三次握手发送数据的效果,整个过程如下图:

我详细介绍下这个过程:

  • 客户端发送 SYN 报文,该报文可以携带「应用数据」以及此前记录的 Cookie;
  • 支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验:如果 Cookie 有效,服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认,服务器随后将「应用数据」递送给对应的应用程序;如果 Cookie 无效,服务器将丢弃 SYN 报文中包含的「应用数据」,且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号
  • 如果服务器接受了 SYN 报文中的「应用数据」,服务器可在握手完成之前发送「响应数据」,这就减少了握手带来的 1 个 RTT 的时间消耗;
  • 客户端将发送 ACK 确认服务器发回的 SYN 以及「应用数据」,但如果客户端在初始的 SYN 报文中发送的「应用数据」没有被确认,则客户端将重新发送「应用数据」
  • 此后的 TCP 连接的数据传输过程和非 TCP Fast Open 的正常情况一致。

所以,如果客户端和服务端同时支持 TCP Fast Open 功能,那么在完成首次通信过程后,后续客户端与服务端 的通信则可以绕过三次握手发送数据,这就减少了握手带来的 1 个 RTT 的时间消耗。

TLSv1.3

在最开始的时候,我也提到 TLSv1.3 握手过程只需 1-RTT 的时间,它到整个握手过程,如下图:

TCP 连接的第三次握手是可以携带数据的,如果客户端在第三次握手发送了 TLSv1.3 第一次握手数据,是不是就表示「HTTPS 中的 TLS 握手过程可以同时进行三次握手」?。

不是的,因为服务端只有在收到客户端的 TCP 的第三次握手后,才能和客户端进行后续 TLSv1.3 握手。

TLSv1.3 还有个更厉害到地方在于会话恢复机制,在重连 TLvS1.3 只需要 0-RTT,用“pre_shared_key”和“early_data”扩展,在 TCP 连接后立即就建立安全连接发送加密消息,过程如下图:

TCP Fast Open + TLSv1.3

在前面我们知道,客户端和服务端同时支持 TCP Fast Open 功能的情况下,在第二次以后到通信过程中,客户端可以绕过三次握手直接发送数据,而且服务端也不需要等收到第三次握手后才发送数据

如果 HTTPS 的 TLS 版本是 1.3,那么 TLS 过程只需要 1-RTT

因此如果「TCP Fast Open + TLSv1.3」情况下,在第二次以后的通信过程中,TLS 和 TCP 的握手过程是可以同时进行的

如果基于 TCP Fast Open 场景下的 TLSv1.3 0-RTT 会话恢复过程,不仅 TLS 和 TCP 的握手过程是可以同时进行的,而且 HTTP 请求也可以在这期间内一同完成。

TLS和SSL的区别

最新版本的 TLS 是 IETF(Internet Engineering Task Force,Internet 工程任务组)制定的一种新的协议,它建立在 SSL 3.0 协议规范之上,是 SSL 3.0 的后续版本。

TLS 的主要目标是使 SSL 更安全,并使协议的规范更精确和完善。TLS 在 SSL v3.0 的基础上,提供了一下增强内容:

  1. 更安全的 MAC 算法
  2. 更严密的警报
  3. “灰色区域”规范的更明确的定义

主要区别如下:

  1. **版本号:**TLS 记录格式与 SSL 记录格式相同,但版本号的值不同,TLS 的版本 1.0 使用的版本号为 SSL v3.1

  2. **报文鉴别码:**SSL v3.0 和 TLS 的 MAC 算法及 MAC 计算的范围不同。

    • TLS 使用了 RFC-2140 定义的 HMAC 算法。HMAC 算法采用的是异或运算
    • SSL v3.0 中,填充字节与密钥之间采用的是连接运算
  3. **伪随机函数:**TLS 使用了称为 PRF 的伪随机函数来将密钥扩展成数据块,是更安全的方式。

  4. **报警代码:**TLS 支持几乎所有的 SSL v3.0 报警代码,而且 TLS 还补充定义了很多报警代码。

    如解密失败(decryption_failed)、记录溢出(record_overflow)、未知CA(unknown_ca)、拒绝访问(access_denied)等

  5. **密文族和客户证书:**SSL v3.0 和 TLS 存在少量差别,即 TLS 不支持 Fortezza 密钥交换、加密算法和客户证书。

  6. **certificate_verify 和 finished消息:**SSLv3.0 和 TLS 在用 certificate_verifyfinished 消息计算 MD5 和 SHA-1 散列码时,计算的输入有少许差别,但安全性相当。

  7. **加密计算:**TLS 与 SSLv3.0 在计算主密值(master secret)时采用的方式不同。

  8. 填充:用户数据加密之前需要增加的填充字节。

    • 在 SSL 中,填充后的数据长度要达到密文块长度的最小整数倍
    • 而在 TLS 中,填充后的数据长度可以是密文块长度的任意整数倍(但填充的最大长度为255字节),这种方式可以防止基于对报文长度进行分析的攻击。

HTTPS 一定安全可靠吗?

这个问题的场景是这样的:客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服务器」,于是客户端是和「中间人服务器」完成了 TLS 握手,然后这个「中间人服务器」再与真正的服务端完成 TLS 握手。那么这个「中间人服务器」就获取了全部数据。

具体过程如下:

  • 客户端向服务端发起 HTTPS 建立连接请求时,然后被「假基站」转发到了一个「中间人服务器」,接着中间人向服务端发起 HTTPS 建立连接请求,此时客户端与中间人进行 TLS 握手,中间人与服务端进行 TLS 握手;
  • 在客户端与中间人进行 TLS 握手过程中,中间人会发送自己的公钥证书给客户端,客户端验证证书的真伪,然后从证书拿到公钥,并生成一个随机数,用公钥加密随机数发送给中间人,中间人使用私钥解密,得到随机数,此时双方都有随机数,然后通过算法生成对称加密密钥(A),后续客户端与中间人通信就用这个对称加密密钥来加密数据了;
  • 在中间人与服务端进行 TLS 握手过程中,服务端会发送从 CA 机构签发的公钥证书给中间人,从证书拿到公钥,并生成一个随机数,用公钥加密随机数发送给服务端,服务端使用私钥解密,得到随机数,此时双方都有随机数,然后通过算法生成对称加密密钥(B),后续中间人与服务端通信就用这个对称加密密钥来加密数据了;
  • 后续的通信过程中,中间人用对称加密密钥(A)解密客户端的 HTTPS 请求的数据,然后用对称加密密钥(B)加密 HTTPS 请求后,转发给服务端,接着服务端发送 HTTPS 响应数据给中间人,中间人用对称加密密钥(B)解密 HTTPS 响应数据,然后再用对称加密密钥(A)加密后,转发给客户端。

从客户端的角度看,其实并不知道网络中存在中间人服务器这个角色。

那么中间人就可以解开浏览器发起的 HTTPS 请求里的数据,也可以解开服务端响应给浏览器的 HTTPS 响应数据。相当于,中间人能够 “偷看” 浏览器与服务端之间的 HTTPS 请求和响应的数据。

但是要发生这种场景是有前提的,前提是用户点击接受了中间人服务器的证书。

中间人服务器与客户端在 TLS 握手过程中,实际上发送了自己伪造的证书给浏览器,而这个伪造的证书是能被浏览器(客户端)识别出是非法的,于是就会提醒用户该证书存在问题。

如果用户执意点击「继续浏览此网站」,相当于用户接受了中间人伪造的证书,那么后续整个 HTTPS 通信都能被中间人监听了。

所以,这其实并不能说 HTTPS 不够安全,毕竟浏览器都已经提示证书有问题了,如果用户坚决要访问,那不能怪 HTTPS ,得怪自己手贱

抓包工具

为什么抓包工具能截取 HTTPS 数据?

抓包工具 Fiddler 之所以可以明文看到 HTTPS 数据,工作原理与中间人一致的。

对于 HTTPS 连接来说,中间人要满足以下两点,才能实现真正的明文代理:

  • 中间人,作为客户端与真实服务端建立连接这一步不会有问题,因为服务端不会校验客户端的身份;
  • 中间人,作为服务端与真实客户端建立连接,这里会有客户端信任服务端的问题,也就是服务端必须有对应域名的私钥

中间人要拿到私钥只能通过如下方式:

  • 去网站服务端拿到私钥;
  • 去CA处拿域名签发私钥;
  • 己签发证书,且被浏览器信任

不用解释,抓包工具只能使用第三种方式取得中间人的身份。

使用抓包工具进行 HTTPS 抓包的时候,需要在客户端安装 Fiddler 的根证书,这里实际上起认证中心(CA)的作用。

Fiddler 能够抓包的关键是客户端会往系统受信任的根证书列表中导入 Fiddler 生成的证书,而这个证书会被浏览器信任,也就是 Fiddler 给自己创建了一个认证中心 CA。

客户端拿着中间人签发的证书去中间人自己的 CA 去认证,当然认为这个证书是有效的。

如何避免被中间人抓取数据?

我们要保证自己电脑的安全,不要被病毒乘虚而入,而且也不要点击任何证书非法的网站,这样 HTTPS 数据就不会被中间人截取到了。

当然,我们还可以通过 HTTPS 双向认证来避免这种问题。一般我们的 HTTPS 是单向认证,客户端只会验证了服务端的身份,但是服务端并不会验证客户端的身份。如果用了双向认证方式,不仅客户端会验证服务端的身份,而且服务端也会验证客户端的身份。服务端一旦验证到请求自己的客户端为不可信任的,服务端就拒绝继续通信,客户端如果发现服务端为不可信任的,那么也中止通信。

可以参考K8S中的认证使用:👉 https://blog.csdn.net/qq_44766883/article/details/126549242

参考:

最后推荐一个很厉害的网络博主:小林coding,很多内容都是从他那里学的,收益匪浅。

-------------本文结束感谢您的阅读-------------
六经蕴籍胸中久,一剑十年磨在手

欢迎关注我的其它发布渠道