https 原理分析进阶-模拟https通信过程
阅读原文时间:2023年08月22日阅读:5

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

本章代码已经上传到github

https://github.com/HobbyBear/codelearning/tree/master/httpsdemo

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

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

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

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

数字证书原理

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

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

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

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

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

https密文加密原理

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

  1. 密钥交换算法:用于在客户端和服务器之间安全地交换加密密钥。常见的密钥交换算法有RSA和Diffie-Hellman等。

  2. 对称加密算法:用于对通信数据进行加密和解密。常见的对称加密算法有AES、DES和3DES等。

  3. 摘要算法:用于生成和验证消息的完整性。常见的摘要算法有MD5和SHA-256等。

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

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

模拟根认证ca机构

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

先生成ca的私钥

openssl genpkey -algorithm RSA -out ca_private.key

然后生成ca的证书请求

openssl req -new -key ca_private.key -out ca_csr.csr

生成ca证书

openssl x509 -req -in ca_csr.csr -signkey ca_private.key -out ca_cert.crt

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

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

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

生成 服务器自身的私钥

openssl genpkey -algorithm RSA -out final_private.key

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

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

[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文件。

## /etc/hosts
127.0.0.1 www.lanpangzi2.com
127.0.0.1       www.lanpangzi.com

接着生成服务器证书请求

openssl req -new -key final_private.key -out final_csr.csr -config san.txt -sha256

生成服务器证书

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

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

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))
}

接着实现下客户端代码

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))
}

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

/private/var/folders/yp/g914gkcd54qdm5d0qyc9ljm00000gn/T/GoLand/___go_build_codelearning_httpsdemo_client
Hello, World!

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

golang实现https双向认证

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

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

生成 服务器自身的私钥

openssl genpkey -algorithm RSA -out client_private.key

创建一个san_client.txt的文件

[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文件。

## /etc/hosts
127.0.0.1 www.lanpangziclient2.com
127.0.0.1       www.lanpangziclient.com

接着生成服务器证书请求

openssl req -new -key client_private.key -out client_csr.csr -config san_client.txt -sha256

生成服务器证书

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

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

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"))
}

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

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的双向认证。