https 原理分析进阶-模拟https通信过程

大家好,我是蓝胖子,之前出过一篇https的原理分析 ,完整的介绍了https概念以及通信过程,今天我们就来比较完整的模拟实现https通信的过程,通过这篇文章,你能了解到https核心的概念以及原理,https证书是如何申请的,以及如何用golang实现https通信,https双向认证。

本章代码已经上传到github

代码语言:shell
复制
https://github.com/HobbyBear/codelearning/tree/master/httpsdemo

https原理回顾

在开始之前,让我们来了解下https相关的核心知识,可以作为上篇https原理分析的补充。学习一个东西一定要先知道为什么要用它,我总结了两点:

1,https 第一个好处是使原本的http明文传输变成了密文传输,增加了安全性。

2,https第二好处是采用数字证书来解决了身份认证问题,起码对端通信是经过ca认证的。

那么https又是通过什么技术来实现上述两点的呢?

数字证书原理

我先聊聊数字证书的实现原理,在https的握手阶段,服务端会发送自身的证书给客户端,客户端会去验证这个证书的有效性,有效性是这样保证的:

数字证书上会写明证书的签名算法和证书的签名,如下图所示

image.png

证书经过签名算法中指定的SHA-256算法将证书内容进行hash得到消息摘要,然后再将这个摘要值经过RSA算法用证书颁发机构的私钥进行加密就得到了证书的签名。

而客户端拿到这个证书就会用证书颁发机构的公钥去解密签名,然后按SHA-256算法也对证书内容进行hash,也得到一个消息摘要值,客户端就去比对自己计算的消息摘要和公钥解密签名得到的消息摘要是否一致,一致则说明证书未被篡改并且是证书颁发机构颁发的。

有同学可能会疑惑,证书颁发机构的公钥是从哪里获取的,证书颁发机构的公钥就在颁发机构其自身的证书里,如下图所示。

image.png

https密文加密原理

知道了数字证书的验证原理,我们来看看https通信中涉及到的加密过程,在https的握手阶段,服务端会选择一个与客户端都支持的密钥套件用于后续的加密,密钥套件一般会有如下组件:

  1. 密钥交换算法:用于在客户端和服务器之间安全地交换加密密钥。常见的密钥交换算法有RSA和Diffie-Hellman等。
  2. 对称加密算法:用于对通信数据进行加密和解密。常见的对称加密算法有AES、DES和3DES等。
  3. 摘要算法:用于生成和验证消息的完整性。常见的摘要算法有MD5和SHA-256等。

https采用非对称加密的方式交换密钥,然后使用对称加密的方式对数据进行加密,并且对消息的内容采用摘要算法得到消息摘要,这样对端在解密数据后可以通过相同的消息摘要算法对计算后的消息摘要和传过来的消息摘要进行对比,从而判断数据是否经过篡改。

具体步骤如下:

  1. 客户端向服务器发送一个初始的握手请求,该请求中包含了客户端支持的密码套件列表。
  2. 服务器收到握手请求后,会从客户端提供的密码套件列表中选择一个与自己支持的密码套件相匹配的套件。
  3. 服务器将选定的密码套件信息返回给客户端。
  4. 客户端收到服务器返回的密码套件信息后,会选择一个与服务器相匹配的密码套件。
  5. 客户端生成一个随机的对称加密密钥,并使用服务器的公钥对该密钥进行加密。
  6. 客户端将加密后的对称加密密钥发送给服务器。
  7. 服务器使用自己的私钥对接收到的加密的对称加密密钥进行解密。
  8. 客户端和服务器现在都拥有了相同的对称加密密钥,可以使用该密钥进行加密和解密通信数据。
  9. 客户端和服务器使用对称加密密钥对通信数据进行加密和解密,并使用摘要算法对数据进行完整性验证。

通过以上步骤,客户端和服务器可以建立一个安全的HTTPS连接,并使用密码套件来保护通信的安全性。

模拟证书颁发

接下来,我们就要开始实现下https的通信了,由于只是实验,我们不会真正的去为我的服务器去申请一个数字证书,所以我们暂时在本地用openssl来模拟下证书颁发的逻辑。

模拟根认证ca机构

我们知道证书颁发的机构是ca,而ca根证书是默认信任的,一般内置在浏览器和操作系统里,所以首先来生成一个根证书,并且让系统默认信任它。

先生成ca的私钥

代码语言:shell
复制
openssl genpkey -algorithm RSA -out ca_private.key 

然后生成ca的证书请求

代码语言:shell
复制
openssl req -new -key ca_private.key -out ca_csr.csr

生成ca证书

代码语言:shell
复制
openssl x509 -req -in ca_csr.csr -signkey ca_private.key -out ca_cert.crt

我用的是mac系统,所以我这里演示下mac系统如何添加证书信任,

打开钥匙串应用-> 将证书拖进登录那一栏 -> 右击证书点击显示简介-> 将信任那一栏改为始终信任

image.png

模拟ca机构向服务器颁发证书

生成 服务器自身的私钥

代码语言:shell
复制
openssl genpkey -algorithm RSA -out final_private.key

接着就是生成证书请求,和前面生成证书请求不同,因为目前主流浏览器都要求证书需要设置subjectAltName,如果没有设置SAN会报证书错误。

所以我们要换种方式生成证书请求,首先创建一个文件,比如我创建一个san.txt的文件

代码语言:shell
复制
[req]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = country
stateOrProvinceName = province
localityName = city
organizationName = company name
## 换成自己的域名
commonName = lanpangzi.com  
[v3_req]
subjectAltName = @alt_names
[alt_names]
## 换成自己的域名
DNS.1=*.lanpangzi.com
DNS.2=*.lanpangzi2.com

到时候上述文件只需要更换为自己的域名即可。由于我的域名设置为了.lanpangzi.com 和.lanpangzi2.com,所以我还要改下本地的hosts文件。

代码语言:shell
复制
## /etc/hosts
127.0.0.1 www.lanpangzi2.com
127.0.0.1       www.lanpangzi.com

接着生成服务器证书请求

代码语言:shell
复制
openssl req -new -key final_private.key -out final_csr.csr -config san.txt -sha256

生成服务器证书

代码语言:shell
复制
openssl x509 -req -days 365 -in final_csr.csr -CA ca_cert.crt -CAkey ca_private.key -set_serial 01 -out final_csr.crt -extfile san.txt -extensions v3_req

golang实现https服务验证证书

经过了上述步骤后算是生成了一个由ca机构颁发的证书,然后我们用golang代码实现一个https服务器。需要为https服务器传入证书以及服务器自身的私钥。

代码语言:go
复制
func main() {  
   http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
      fmt.Fprintf(w, "Hello, World!\n")  
   })  
   fmt.Println(http.ListenAndServeTLS(":443",  
      "./final_csr.crt",  
      "./final_private.key", nil))  
}

接着实现下客户端代码

代码语言:go
复制
func main() {  
   client := &http.Client{Transport: tr}  
   resp, err := client.Get("https://www.lanpangzi.com")  
   if err != nil {  
      fmt.Println("Get error:", err)  
      return  
   }  
   defer resp.Body.Close()  
   body, err := ioutil.ReadAll(resp.Body)  
   fmt.Println(string(body))  
}

启动服务端和客户端后能看到服务正常返回了。

代码语言:shell
复制
/private/var/folders/yp/g914gkcd54qdm5d0qyc9ljm00000gn/T/GoLand/___go_build_codelearning_httpsdemo_client
Hello, World!

说明证书配置已经成功,而客户端验证证书的逻辑已经在本文开始讲解了。

golang实现https双向认证

上述代码只是实现了https的单向认证,即客户端对服务端的域名进行认证,在某些情况下,服务端也需要检验客户端是否合法,所以下面我们就来看下如何用golang实现双向认证的。首先我们还是要用ca位客户端颁发一个证书。

模拟ca机构向客户端颁发证书

生成 服务器自身的私钥

代码语言:shell
复制
openssl genpkey -algorithm RSA -out client_private.key

创建一个san_client.txt的文件

代码语言:shell
复制
[req]
default_bits = 4096
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = country
stateOrProvinceName = province
localityName = city
organizationName = company name
## 换成自己的域名
commonName = lanpangziclient.com  
[v3_req]
subjectAltName = @alt_names
[alt_names]
## 换成自己的域名
DNS.1=*.lanpangziclient.com
DNS.2=*.lanpangziclient2.com

到时候上述文件只需要更换为自己的域名即可。由于我的域名设置为了.lanpangzi.com 和.lanpangzi2.com,所以我还要改下本地的hosts文件。

代码语言:shell
复制
## /etc/hosts
127.0.0.1 www.lanpangziclient2.com
127.0.0.1       www.lanpangziclient.com

接着生成服务器证书请求

代码语言:shell
复制
openssl req -new -key client_private.key -out client_csr.csr -config san_client.txt -sha256

生成服务器证书

代码语言:shell
复制
openssl x509 -req -days 365 -in client_csr.csr -CA ca_cert.crt -CAkey ca_private.key -set_serial 01 -out client_csr.crt -extfile san_client.txt -extensions v3_req

服务端和客户端需要做下改动,服务端默认不会去校验客户端身份,但是现在改成强制校验

代码语言:go
复制
func main() {

s := &http.Server{
Addr: ":443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!\n")
}),
TLSConfig: &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
},
}

fmt.Println(s.ListenAndServeTLS("./final_csr.crt",
"./final_private.key"))
}

客户端代码请求时需要带上自己的证书

代码语言:go
复制
func main() {
cliCrt, err := tls.LoadX509KeyPair("./client_csr.crt", "./client_private.key")
if err != nil {
fmt.Println("Loadx509keypair err:", err)
return
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cliCrt},
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://www.lanpangzi.com")
if err != nil {
fmt.Println("Get error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}

这样就完成了一个https的双向认证。