透彻理解TLS1.2

下面内容是整理自《深入浅出HTTPS:从原理到实战》,然后加入了一些实践

通过理论理解TLS/SSL

背景

一些已有的协议通常安全问题,以HTTP为例来说明,HTTP在设计之初就没有考虑安全问题,它的目的就是数据传输和共享,HTTP协议三个安全问题如下:

  1. 数据没加密,是明文传输;而且TCP/IP 的特点同时导致HTTP数据很容易被截获
  2. 无法验证身份。
  3. 数据易篡改:HTTP数据传输过程中,会经过很多节点,这些节点都可以修改原始数据,而对于客户端和服务器来说,没有任何技术来确保接收的数据就是发送者发送的原始数据 (也叫中间人攻击)

而要解决类似上面的这些安全问题,我们就需要TLS/SSL

必要的密码学基础知识

密钥协商算法

请先自行学习对称加密和非对称加密算法。知道对称加密算法速度快,但是安全性差,因为密钥需要分发,容易泄露;而非对称加密算法(公开密钥算法)速度慢,安全性强,因为私钥只在一处地方保留。

在网络通信应用中,密钥的管理和分配是个难题,尤其是生成一个动态密钥更难,而密钥协商算法就可以解决密钥分配、存储、传输等问题。

比如,RSA密钥协商算法流程如下:

  • 客户端初始化连接服务器端,服务器发送RSA密钥对的公钥给客户端。
  • 客户端生成一个随机值,这个值必须是随机的,不能被攻击者猜出,这个值就是会话密钥。
  • 客户端使用服务器RSA密钥对的公钥加密会话密钥,并发送给服务器端,由于攻击者没有服务器的私钥,所以无法解密会话密钥。
  • 服务器端用它的私钥解密出会话密钥。
  • 至此双方完成连接,接下来服务器端和客户端可以使用对称加密算法和会话密钥加密解密数据

实际的TLS协商比这个过程要复杂一些,但是差不多

数字签名

流程主要包括签名生成和签名验证(下面做摘要的一个目的主要是为摘要值固定,比较运算的时候速度会快)

  • 签名生成
    • 发送者对消息计算摘要值
    • 发送者用私钥对摘要做加密,也就是进行签名得到签名值
    • 发送者将原始消息和签名值一同发给接收者
  • 签名验证:
    • 接收者接收到消息后,拆分出消息A和消息签名值B
    • 接收者使用公钥对签名值B做解密,得到摘要值C
    • 接收者对消息A计算摘要得到摘要值D
    • 接收者对摘要值C和签名值D进行比较,如果相同表示签名验证成功,否则就是验证失败

      ​​​​​​​

TLS是什么,主要解决什么问题

要了解安全协议首先要理解密码学算法。

什么是协议呢?协议就是解决方案、标准,能够解决很多普适性问题。我们可以看看TLS/SSL协议在网络层协议中的定位。可以看出,TLS/SSL协议位于应用层协议和TCP之间,构建在TCP之上,由TCP协议保证数据传输的可靠性,任何上层数据到达TCP之前,都经过TLS/SSL协议处理,下层数据进入应用层也会经过TLS/SSL协议处理。对于HTTPS来说,HTTP+TLS/SSL就等于HTTPS,HTTPS拥有HTTP所有的特征,并且HTTP消息由TLS/SSL协议进行安全保护。

TLS协议有四个目标:数据是机密的、互操作性、可扩展性(可以引入新的算法)、效率。

TLS/SSL协议核心就三大步骤:认证、秘钥协商、数据加密。通过这三大步骤,TLS/SSL实现有如下的功能:

  1. 基于PKI的身份认证(身份认证其实不属于TLS/SSL的一部分,只是为了确保密钥协商过程的安全而需要引入的过程,主要就是为了确认服务器传过来的公钥真是服务器的公钥,而这个公钥正是我们密钥协商过程所需要的)
  2. 基于混合加密算法的数据加密
    1. 使用RSA算法(公开密钥算法/非对称加密算法)协商密钥,再使用这个协商密钥做对称加密
  3. 基于数字签名的数据完整性(不可抵赖,由CA可信推导出这个pk就是server的)
    1. 这个其实更多的是再传输公钥的环节上使用的,基于CA的公钥对服务器的证书做数字签名,然后对比数字签名与服务器上证书上的数字签名是否一致,从而来保证报文的一致性/完整性

为了做数据加密,我们需要通过秘钥协商(RSA或者DH密钥协商算法)来生成一个预备主密钥,然后客户端和服务端基于预备主密钥推导处主密钥和密钥块。TLS的流程大致如下

                                 图3-4 TLS协议流程图(使用RSA密码套件)

                      图3-5 TLS协议流程图(使用DHE_RSA密码套件)

在协商算法上,服务器需要提供一对密钥(对外提供公钥,自己保留私钥),这个过程会有一个致命的问题,就是中间人攻击

中间人攻击

主要问题在与服务器传递给客户端的公钥可能被攻击者替换

这里的主要问题就在于客户端没法确认服务端的真实身份,客户端通关一个https://www.example.com接收到一个服务器公钥,但是它没法确认这个公钥就是需要www.example.com的,这个可能就是某个攻击者的。所以我们就需要有一种手段区认证公钥的真正主人,解决方案就是PKI,PKI的核心就是证书。

PKI(public Key Infrastructure)

PKI的主要目的是向客户端提供服务器身份认证,认证的基础就是招一个可信的第三方组织,认证的技术方案就是数字签名技术。第三方组织能够使用数字签名技术管理证书,包括创建证书、存储证书、更新证书、撤销证书。信任背后的密码学技术就是数字签名,数字签名有防篡改、防抵赖、防伪造

PKI并不是TLS/SSL协议的一部分,但是在HTTPS中,必须引入PKI技术才能确保安全。PKI可以确保客户端接收到的服务器公钥确实是服务器的公钥。所以这里就要明白,TLS/SSL只是解决了数据传输安全,身份验证机制需要借助PKI。

PKI有以下几部分组成:

  • 服务器实体
  • 客户端
  • CA机构

客户端基于对CA机构的信任,有办法校验服务器的身份,这个方法就是数字签名技术。大致流程就是CA机构拥有一个密钥对,比如RSA密钥对(与服务器的RSA密钥对没有任何关系),它用私钥对证书(服务器使用自己的RSA密钥生成的)进行数字签名,将签名的证书发送给服务器。浏览器再连接服务器,服务器发送证书给浏览器,浏览器拥有CA机构的公钥(内嵌在浏览器中),然后校验证书的签名,一旦校验成功,就代表这个证书是可信的CA机构签发的。然后再校验证书里面的域名是否是自己访问的域名,确认后,代表校验身份成功,最后客户端从证书中获取服务器的公钥,用来进行密钥协商 (在http的场景下,CA公钥是集成再浏览器里面的)

服务器申请证书的流程大致如下:

  1. 服务器实体希望发布一个HTTPS网站(https://www.example.com)。
  2. 服务器实体生成公开密钥算法的一对密钥,比如一对RSA密钥。
  3. 服务器实体生成一个CSR(Cerificate SigningRequest)文件,CSR是证书签名请求文件,其中包含的重要信息是网站的域名(www.example.com)、RSA密钥对的公钥、营业执照,然后将CSR文件发送给CA机构申请证书。
  4. CA机构收到CSR文件后,核实申请者的身份,最简单的核实就是校验域名(www.example.com)的拥有者是不是证书申请者。
  5. 一旦审核成功,CA机构用自己的密钥对(比如ECDSA密钥对)的私钥签名CSR文件的内容得到签名值,然后将签名值附在CSR文件后面得到证书文件,证书文件中除了包含申请者的信息,还包括CA机构的信息,比如包括CA机构采用的签名算法(本例中就是ECDSA签名算法)、CA机构的名称。
  6. 最终CA机构将证书文件发送给服务器实体。

接下来看看客户端如何校验证书,大体流程如下:

  1. 浏览器向服务器端发送连接请求https://www.example.com。
  2. 服务器接收到请求后,将证书文件和RSA密钥对的公钥发送给浏览器。
  3. 浏览器接收到证书文件,从中判断出是某CA机构签发的证书,并且知道了证书签名算法是ECDSA算法,由于浏览器内置了该CA机构的根证书,根证书包含了CA机构的ECDSA公钥,用于验证签名。
  4. 浏览器一旦验证签名成功,代表该证书确实是合法CA机构签发的。
  5. 浏览器接着校验证书申请者的身份,从证书中取出RSA公钥(注意不是CA机构的公钥)和主机名,假设证书包含的主机也是www.example.com,且连接阶段接收到的RSA公钥等同于证书中包含的RSA公钥,则表示浏览器成功校验了服务器的身份,连接的服务器确实是www.example.com主机的拥有者。

一旦服务器身份校验成功,接下来就是进行密钥协商,协商出密钥块。(这里的协商就客户端只需要服务器的公钥)

PKI的组成如下:

X.509

X.509标准是PKI技术里面的一种标准。X.509标准定义了证书应该包含的内容,而为了让机器和人更好地理解和组织X.509标准,可以采用ASN.1标准来描述X.509标准(或者说证书),ASN.1类似于伪代码,是一种可理解的数据结构。

CSR(Certificate Signing Requst)

CSR文件是服务器实体再向CA机构申请证书之前生成的,生成之后会发送给CA机构。CSR文件包括俩部分内容:

  • 生成证书必需的信息,比如域名、公钥。
  • 服务器实体的证明材料,比如企业的纳税编号等信息(可以简单地如此理解)。
  • 服务器实体还会对上面内容使用私钥做数字签名

使用OpenSSL大致了解

生成私钥公钥对、自签名证书

# 生成私钥公钥对,rsa_private.key包含公钥和私钥(隐性包含公钥)
openssl genpkey -algorithm rsa -out server_priv.key
# 提取公钥
openssl rsa -pubout -in server_priv.key
openssl rsa -pubout -in server_priv.key  -out server_pub.key

上面生成的公钥私钥,我们可以使用它们来做

# 生成一个测试文件
echo "this is a test" > text
# 对文件做加密
#采用公钥对文件进行加密
openssl rsautl -encrypt -in text -inkey server_pub.key -pubin -out text.en
#采用私钥解密文件
openssl rsautl -decrypt -in text.en -inkey server_priv.key 
# 这里需要注意的是,私钥加密在openssl中对应的是-sign这个选项,公钥解密对应的是-verify这个选项,如下:
#用私钥对文件进行加密(签名)
openssl rsautl -sign -in text -inkey server_priv.key -out text.en
#用公钥对文件进行解密(校验)
openssl rsautl -verify -in text.en -inkey server_pub.key -pubin
this is a test

生成证书(证书就是前面的公钥+额外的信息+ca的签名)

# 基于私钥我们生成一个CSR(证书签名请求) ,这里都默认就可以了
openssl req -new -key server_priv.key -out server.csr
# 查看我们输入的内容
openssl req -in server.csr -text -noout
# 把CSR文件发送给CA,注册,我们这里用自签就可以了
openssl x509 -req -days 365 -in server.csr -signkey server_priv.key -out server.crt
# 查看证书的相关信息
openssl x509 -in server.crt -text
# 因为我们是自签名的,所以CA校验也就是,自己校验自己了
openssl verify -CAfile server.crt server.crt

搭建一个TLS服务 (单向认证)

生成CA

openssl genrsa  -out ca.key 4096
# 自签名证书
openssl req -new -x509 -days 365 -key ca.key -out ca.crt

生成服务端秘钥

openssl genrsa -out server.key 2048

生成服务端证书的 CSR

# CN(common name) 必须填写
# 一般为网站域名,cnblogs.com/iiiiher/p/8085698.html
openssl req -new -key server.key -subj "/CN=127.0.0.1" -out server.csr

通过 CSR 向 CA 签发服务端证书, 地址要填写,不然要配置InsecureSkipVerify才可以访问, 对应与SANs

cat >extfile.cnf<<EOF
subjectAltName=@alt_names
[alt_names]
DNS.1 = www.my.com
DNS.2 = www.alone.com
IP.1 = 192.168.0.38
IP.2 = 127.0.0.1
EOF

生成CA证书 (这里一般都是要发给CA机构去生成的,这里只是模拟下过程)

openssl x509 -req -days 365 -in server.csr -CA ../ca.crt -CAkey ../ca.key -set_serial 01 -out server.crt -extfile extfile.cnf

上面我们就把证书都生成好了,接下来启动服务

# server 
package main

import (
    "bufio"
    "crypto/tls"
    "log"
    "net"
)

func main() {
    cert, err := tls.LoadX509KeyPair("./server/server.crt", "./server/server.key")
    if err != nil {
        log.Println(err)
        return
    }

    config := &tls.Config{Certificates: []tls.Certificate{cert}, MaxVersion: tls.VersionTLS12}
    ln, err := tls.Listen("tcp", ":443", config)
    if err != nil {
        log.Println("err: ", err)
        return
    }
    defer ln.Close()
    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)
    for {
        // 要知道,tls的握手验证是在这里发生的,也就是在自己的携程里面做校验的,在这里会去读取证书信息,然后
        // 做握手,具体是izai
        msg, err := r.ReadString('\n')
        if err != nil {
            log.Println(err)
            return
        }

        println(msg)

        n, err := conn.Write([]byte("world\n"))
        if err != nil {
            log.Println(n, err)
            return
        }
    }
}
# client 
package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
)

func main()  {
    caCertBytes, err := ioutil.ReadFile("./ca.crt")
    if err != nil {
        panic("unable to read client.pem")
    }
    clientCertPool := x509.NewCertPool()
    ok := clientCertPool.AppendCertsFromPEM(caCertBytes)
    if !ok {
        panic("failed to parse root certificate")
    }

    conf := &tls.Config{
        RootCAs: clientCertPool,
        //InsecureSkipVerify: true,
    }

    conn, err := tls.Dial("tcp", "127.0.0.1:443", conf)
    //conn, err := tls.Dial("tcp", "192.168.0.38:443", conf)
    if err != nil {
        log.Println("Dial faile: ", err)
        return
    }
    defer conn.Close()

    n, err := conn.Write([]byte("hello\n"))
    if err != nil {
        log.Println(n, err)
        return
    }

    buf := make([]byte, 100)
    n, err = conn.Read(buf)
    if err != nil {
        log.Println(n, err)
        return
    }
    println(string(buf[:n]))
}

搭建一个TLS服务(双向认证)

 生成CA

openssl genrsa  -out ca.key 4096
# 自签名证书
openssl req -new -x509 -days 365 -key ca.key -out ca.crt

生成服务端秘钥

openssl genrsa -out server.key 2048

生成服务端证书的 CSR

# CN(common name) 必须填写
# 一般为网站域名,cnblogs.com/iiiiher/p/8085698.html
openssl req -new -key server.key -subj "/CN=127.0.0.1" -out server.csr

通过 CSR 向 CA 签发服务端证书, 地址要填写,不然要配置InsecureSkipVerify才可以访问, 对应与SANs

cat >extfile.cnf<<EOF
subjectAltName=@alt_names
[alt_names]
DNS.1 = www.my.com
DNS.2 = www.alone.com
IP.1 = 192.168.0.38
IP.2 = 127.0.0.1
EOF

生成CA证书 (这里一般都是要发给CA机构去生成的,这里只是模拟下过程)

openssl x509 -req -days 365 -in server.csr -CA ../ca.crt -CAkey ../ca.key -set_serial 01 -out server.crt -extfile extfile.cnf

生成客户端秘钥

openssl genrsa -out client.key 2048

生成客户端 CSR,一路回车即可

openssl req -new -key client.key  -out client.csr

 通过 CSR 向 CA 签发客户端证书

openssl x509 -req -days 365 -in client.csr -CA ../ca.crt -CAkey ../ca.key -set_serial 01 -out client.crt

启动如下程序

# server : go run server.go
package main

import (
    "bufio"
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "net"
)

func main() {
    cert, err := tls.LoadX509KeyPair("./server/server.crt", "./server/server.key")
    if err != nil {
        log.Println(err)
        return
    }

    caCertBytes, err := ioutil.ReadFile("./ca.crt")
    if err != nil {
        panic("unable to read client.pem")
    }
    clientCertPool := x509.NewCertPool()
    ok := clientCertPool.AppendCertsFromPEM(caCertBytes)
    if !ok {
        panic("failed to parse root certificate")
    }

    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
        ClientAuth: tls.RequireAndVerifyClientCert,
        ClientCAs: clientCertPool,
    }

    ln, err := tls.Listen("tcp", ":443", config)
    if err != nil {
        log.Println("err")
        return
    }
    defer ln.Close()
    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        go handleConn1(conn)
    }
}

func handleConn1(conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)
    for {
        msg, err := r.ReadString('\n')
        if err != nil {
            log.Println(err)
            return
        }

        println(msg)

        n, err := conn.Write([]byte("world\n"))
        if err != nil {
            log.Println(n, err)
            return
        }
    }
}


# client: go run client.go
package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
)

func main()  {
    cert, err := tls.LoadX509KeyPair("./client/client.crt", "./client/client.key")
    if err != nil {
        log.Println(err)
        return
    }
    caCertBytes, err := ioutil.ReadFile("./ca.crt")
    if err != nil {
        panic("unable to read client.pem")
    }
    clientCertPool := x509.NewCertPool()
    ok := clientCertPool.AppendCertsFromPEM(caCertBytes)
    if !ok {
        panic("failed to parse root certificate")
    }

    conf := &tls.Config{
        RootCAs: clientCertPool,
        Certificates: []tls.Certificate{cert},
    }

    conn, err := tls.Dial("tcp", "127.0.0.1:443", conf)
    if err != nil {
        log.Println(err)
        return
    }
    defer conn.Close()

    n, err := conn.Write([]byte("hello\n"))
    if err != nil {
        log.Println(n, err)
        return
    }

    buf := make([]byte, 100)
    n, err = conn.Read(buf)
    if err != nil {
        log.Println(n, err)
        return
    }
    println(string(buf[:n]))
}

TLS的大致流程

在密码学中,不同的密码学算法都有固定的描述形式。和TLS/SSL协议有关的密码学属性主要有5个,分别是digital signing(数字签名)、stream cipherencryption(流加密模式)、block cipher encryption(块加密模式)、authenticated encryption with additional data encryption(AEAD加密模式)、public key encryption(公开密钥操作模式)

TLS/SSL协议位于应用层协议和TCP协议之间,TLS/SSL协议共分为两层:

  1. 接近应用层协议的高层协议是握手协议(TLS HandshakingProtocols)
  2. 接近TCP协议的底层协议是记录层协议(TLS Record Protocol)
    1. 封装和处理所有上层子协议的消息,添加消息头
    2. 对上层的应用层协议进行密码学保护,其他三个子协议只是进行简单的封装处理,添加下消息头(因为这个时候密钥还没协商好)
    3. TLS记录层消息会传递给下层的TCP处理
    4. TLS记录层协议进行密码学保护(机密性和完整性)所要用到的密钥块来源于哪儿?那就是接下来要讲解的握手协议。

TLS握手协议由4个子协议构成,分别是:

  • 握手协议(TLS Handshaking Protocols)
    • 握手协议主要的工作就是客户端和服务器端协商出双方都认可的密码套件,基于密码套件协商出密钥块,TLS记录层协议进行密码学保护所需要的密码块就是握手协议产生的。
  • 警告协议(Alert Protocol)
    • 客户端和服务器端建立一条连接后,会通过握手协议协商密钥块,在协商和认证过程中,可能会产生错误。错误信息由警告协议处理,警告协议有多个错误,某些错误可能是致命的,会直接终止客户端和服务器端的连接。
  • 应用层协议(Application Data Protocol)
    • 应用层协议就是TLS/SSL记录层协议的上层协议,包括HTTP、FTP、SMTP等应用层协议。TLS/SSL协议能够无缝地处理应用层数据,TLS记录层协议密码学保护的主要信息就是应用层协议数据。
  • 密码切换协议(Change Cipher Spec Protocol)
    • Change Cipher Spec协议的作用就是通知TLS记录层协议其加密学所需要的密钥块已经准备好了,一个TLS连接一旦客户端和服务器端发出了ChangeCipher Spec子协议,TLS记录层协议就可以对应用层协议(ApplicationData协议)进行加密保护了。

那么连接状态何时切换呢?客户端和服务器端分别发送ChangeCipherSpec协议消息后,连接状态就会切换。客户端和服务器在没有发送ChangeCipherSpec协议消息之前,所有的握手消息也会由TLS记录层协议处理,但都是明文处理的,没有机密性和完整性保护。

TLS记录协议主要有四部分的逻辑处理:

  • 数据分块
  • 压缩
  • 加密和完整性保护,主要有三种模式:流加密模式、分组模式、AEAD模式
  • 添加消息头

下面是完整握手协议交互流程:

  •  在这个过程不会描述证书的校验逻辑,这个不属于TLS/SSL协议定义的内容

握手协议的主要步骤:

  • 互相交换hello子消息,该消息交换随机值和支持的密码套件列表,协商出密码套件以及对应的算法,检查会话是否可恢复。
  • 交换证书和密码学信息,允许服务器端和客户端互相校验身份,本章主要讲解服务器身份验证
    • 这一步就是发送Server Certificate子消息,这个消息是可选的,根据协商出来的密码套件,服务器选择是要发送证书,在HTTPS服务器一般会发送。(如果没有这个,可能会遇到中间人攻击);证书的目的一般两个:一个是身份认证,另一个是证书中包含服务器的公钥,这个公钥结合密码套件的密钥协商算法协商出预备主密钥
  • 交换必要的密码学参数,客户端和服务器端获得一致的预备主密钥(premaster secret)。
  • 通过预备主密钥和服务器/客户端的随机值生成主密钥(mastersecret)。
  • 握手协议提供加密参数(主要是密码块)给TLS记录层协议。
  • 客户端和服务器端校验对方的Finished子消息,避免握手协议的消息被篡改。
  • 接下来就使用生成的密钥进行应用层的数据加密

学习使用openssl

s_client工具

# s_client是调试TLS/SSL协议的工具,这个命令可以知晓服务的HTTP部署
openssl s_client -connect www.example.com:443 
# -cert会发送客户端自己的证书
# -CApath : s_client s_client工具在接收到服务器发送的证书后,为了校验服务器的真实身份,
#   需要客户端指定根证书进行校验,如果不指定-CApath参数,s_client工具使用系统根证书的目录进行校验,
#   如果想使用其他的根证书目录进行校验,可以通过-CApath指定。
# -CAfile filename : 对于s_client工具来说,也可以通过-CAfile参数指定某个根证书或者多个根证书
#   (包含在一个文件中)进行服务器身份验证,比如执行如下命令
openssl s_client -connect www.example.com:443 -cert <client-crt> -CApath directory
# -state 可以打印各个握手协议的会话信息,了解各个子消息的交互

s_server工具

s_server可以生成一个支持TLS/SSL协议的服务器,如果读者没有构建一个HTTPS网站,但为了学习相关的HTTPS知识,可以使用该命令生成HTTPS协议的服务器。

通过抓包理解TLS1.2

服务部署:https://github.com/khalid-davis/khalid.foundation/tree/master/source_code/tls (代码里面协议特地配置了tls1.2,如果不特殊配置的话,默认是使用TLS1.3版本)

如下是我们的Wireshark截图

流量分为两部分:

  • 握手协议消息,Wireshark会明文显示所有的握手子消息
  • TLS记录层的机密数据

我们首先看看我们的应用层数据,这个数据是放在TLS记录层里面的,如下

可以看到,数据是被加密的,而这个加密所使用的主密钥就是TLS协议再前面的握手协议里面协商出来的。同时我们可以注意到TLS记录层协议消息头包含三个部分:

  • 协议就是应用层协议 (TLS握手协议由四个子协议组成,握手、警告、应用层(编号是0x17,也就是十进制下的23)、密码切换)
  • 版本,版本是TLS 1.2
  • 消息长度,30字节

接下来就是长度为30字节的消息体了(一般就是上层应用报文的加密信息,比如http的话,就是http头加http 体)

上面是服务器收到Server hello后回复的消息,可以看到包含了四个内容,下面是Certificte消息里面的证书内容

  • 版本是V3
  • 证书的序列号、签名算法、签发者、证书的有效期
  • 证书的公钥信息,RSA公钥值;(客户端会使用这个东西和自己本地的CA证书做身份认证(数字签名对比)、确认后,使用这个RSA的公钥来协商出加密用的预备主密钥)

  • 接下来是客户端给服务端发送的消息,Change Cipher Spec告诉服务端说客户端可以使用TLS记录层协议进行密码学保护了,第一套密码学保护的消息就是Finish消息,也及时上面图里面的第三条子消息:Encrypted handshake Message,第一次使用Encrypted

  • 这个就是服务端的Change Cipher Spec和Finished消息,与客户端一样的作用

最后再看一条消息

这个就是我们握手协议里面的警告协议(Alert Protocol),它由警告错误级别和警告协议的详细描述信息组成,应该是user_canceld(90)

通过Go源码理解TLS

示例代码

// server 
package main

import (
    "bufio"
    "crypto/tls"
    "log"
    "net"
)

func main() {
    cert, err := tls.LoadX509KeyPair("./server/server.crt", "./server/server.key")
    if err != nil {
        log.Println(err)
        return
    }

    config := &tls.Config{Certificates: []tls.Certificate{cert}, MaxVersion: tls.VersionTLS12}
    ln, err := tls.Listen("tcp", ":443", config)
    if err != nil {
        log.Println("err: ", err)
        return
    }
    defer ln.Close()
    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Println(err)
            continue
        }
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()
    r := bufio.NewReader(conn)
    for {
        // 要知道,tls的握手验证是在这里发生的,也就是在自己的携程里面做校验的,在这里会去读取证书信息,然后
        // 做握手,具体是izai
        msg, err := r.ReadString('\n')
        if err != nil {
            log.Println(err)
            return
        }

        println(msg)

        n, err := conn.Write([]byte("world\n"))
        if err != nil {
            log.Println(n, err)
            return
        }
    }
}
// client
package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
)

func main()  {
    caCertBytes, err := ioutil.ReadFile("./ca.crt")
    if err != nil {
        panic("unable to read client.pem")
    }
    clientCertPool := x509.NewCertPool()
    ok := clientCertPool.AppendCertsFromPEM(caCertBytes)
    if !ok {
        panic("failed to parse root certificate")
    }

    conf := &tls.Config{
        //RootCAs: clientCertPool,
        InsecureSkipVerify: true,
    }

    conn, err := tls.Dial("tcp", "127.0.0.1:443", conf)
    //conn, err := tls.Dial("tcp", "192.168.0.38:443", conf)
    if err != nil {
        log.Println("Dial faile: ", err)
        return
    }
    defer conn.Close()

    n, err := conn.Write([]byte("hello\n"))
    if err != nil {
        log.Println(n, err)
        return
    }

    buf := make([]byte, 100)
    n, err = conn.Read(buf)
    if err != nil {
        log.Println(n, err)
        return
    }
    println(string(buf[:n]))
}



 

参数配置

InsecureSkipVeriry

看代码文档的定义:

// InsecureSkipVerify controls whether a client verifies the server's
// certificate chain and host name. If InsecureSkipVerify is true, crypto/tls
// accepts any certificate presented by the server and any host name in that
// certificate. In this mode, TLS is susceptible to machine-in-the-middle
// attacks unless custom verification is used. This should be used only for
// testing or in combination with VerifyConnection or VerifyPeerCertificate

其实这个配置就是表明作为客户端的一端要不要去校验服务端证书的证书链和hostname的有效性。最直观的体会就是,如果配置了InsecureSkipVeriry,我们会要求服务器给我们证书,但是不会去校验它是不是我们认可的RootCAs所签发的,也就是来者不拒了。这个字段在实际的环境中,一般要配合VerifyPeerCertificate来使用,也就是你要自己去做校验这个证书的合法性,而不用系统默认的证书去校验。否则很容易导致中间人攻击,应为你没法去校验通过网络发送过来的那个证书是不是真的是你要通讯的server所发送的

ClientAuth

看文档的定义:(https://pkg.go.dev/crypto/tls)


const (
    // NoClientCert indicates that no client certificate should be requested
    // during the handshake, and if any certificates are sent they will not
    // be verified.
    NoClientCert ClientAuthType = iota
    // RequestClientCert indicates that a client certificate should be requested
    // during the handshake, but does not require that the client send any
    // certificates.
    RequestClientCert
    // RequireAnyClientCert indicates that a client certificate should be requested
    // during the handshake, and that at least one certificate is required to be
    // sent by the client, but that certificate is not required to be valid.
    RequireAnyClientCert
    // VerifyClientCertIfGiven indicates that a client certificate should be requested
    // during the handshake, but does not require that the client sends a
    // certificate. If the client does send a certificate it is required to be
    // valid.
    VerifyClientCertIfGiven
    // RequireAndVerifyClientCert indicates that a client certificate should be requested
    // during the handshake, and that at least one valid certificate is required
    // to be sent by the client.
    RequireAndVerifyClientCert
)

通过上面的解释我们可以看到,RequireAnyClientCert就跟我们客户端设置的InsecureSkipVeriry作用是一样的。

源码解析

首先我们要明白,tls的连接是封装了tcp的连接,然后在连接读数据的时候,会先读取tls校验相关的数据,然后对tls做校验,如果失败就直接终端连接。(设计思想是什么)

根据上面的代码,我们先看到tls.Listen的实现

// Listen creates a TLS listener accepting connections on the
// given network address using net.Listen.
// The configuration config must be non-nil and must include
// at least one certificate or else set GetCertificate.
func Listen(network, laddr string, config *Config) (net.Listener, error) {
    // 首先判断作为server是否有提供证书,如果没有证书的,直接返回错误
    if config == nil || len(config.Certificates) == 0 &&
        config.GetCertificate == nil && config.GetConfigForClient == nil {
        return nil, errors.New("tls: neither Certificates, GetCertificate, nor GetConfigForClient set in Config")
    }
    // 调用net包的Listen,可能是tcp或者udp,因为tls是可以叠加在其他的协议上面的; 一般这里就是tcp
    // 这里就是tcp的Listner逻辑了
    l, err := net.Listen(network, laddr)
    if err != nil {
        return nil, err
    }
    // 将上面的tcp 连接封装
    return NewListener(l, config), nil
}
// NewListener creates a Listener which accepts connections from an inner
// Listener and wraps each connection with Server.
// The configuration config must be non-nil and must include
// at least one certificate or else set GetCertificate.
// 这里我们可以看到,tls把tcp的连接做了一个封装tls.listener,但是还是返回了与net.Listen一样的接口net.Listener,从而对外提供了一致的使用接口
func NewListener(inner net.Listener, config *Config) net.Listener {
    l := new(listener)
    l.Listener = inner
    l.config = config
    return l
}

// A Listener is a generic network listener for stream-oriented protocols.
//
// Multiple goroutines may invoke methods on a Listener simultaneously.
type Listener interface {
    // Accept waits for and returns the next connection to the listener.
    Accept() (Conn, error)

    // Close closes the listener.
    // Any blocked Accept operations will be unblocked and return errors.
    Close() error

    // Addr returns the listener's network address.
    Addr() Addr
}

在程序调用conn, err := ln.Accept()时,其实就是调用了tls.listener

// 这里的逻辑还是一样的,先调用原本net里面的函数accept,然后封装一下再放回,但是会实现一样的接口net.Conn
func (l *listener) Accept() (net.Conn, error) {
    c, err := l.Listener.Accept()
    if err != nil {
        return nil, err
    }
    return Server(c, l.config), nil
}
// net.Conn的定义如下
type Conn interface {
    // Read reads data from the connection.
    // Read can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetReadDeadline.
    Read(b []byte) (n int, err error)

    // Write writes data to the connection.
    // Write can be made to time out and return an error after a fixed
    // time limit; see SetDeadline and SetWriteDeadline.
    Write(b []byte) (n int, err error)

    // Close closes the connection.
    // Any blocked Read or Write operations will be unblocked and return errors.
    Close() error

    // LocalAddr returns the local network address.
    LocalAddr() Addr

    // RemoteAddr returns the remote network address.
    RemoteAddr() Addr

    // SetDeadline sets the read and write deadlines associated
    // with the connection. It is equivalent to calling both
    // SetReadDeadline and SetWriteDeadline.
    //
    // A deadline is an absolute time after which I/O operations
    // fail instead of blocking. The deadline applies to all future
    // and pending I/O, not just the immediately following call to
    // Read or Write. After a deadline has been exceeded, the
    // connection can be refreshed by setting a deadline in the future.
    //
    // If the deadline is exceeded a call to Read or Write or to other
    // I/O methods will return an error that wraps os.ErrDeadlineExceeded.
    // This can be tested using errors.Is(err, os.ErrDeadlineExceeded).
    // The error's Timeout method will return true, but note that there
    // are other possible errors for which the Timeout method will
    // return true even if the deadline has not been exceeded.
    //
    // An idle timeout can be implemented by repeatedly extending
    // the deadline after successful Read or Write calls.
    //
    // A zero value for t means I/O operations will not time out.
    SetDeadline(t time.Time) error

    // SetReadDeadline sets the deadline for future Read calls
    // and any currently-blocked Read call.
    // A zero value for t means Read will not time out.
    SetReadDeadline(t time.Time) error

    // SetWriteDeadline sets the deadline for future Write calls
    // and any currently-blocked Write call.
    // Even if write times out, it may return n > 0, indicating that
    // some of the data was successfully written.
    // A zero value for t means Write will not time out.
    SetWriteDeadline(t time.Time) error
}

接下来就是把这个conn给一个其他的协程去处理,在这里我们可以看到,还没有发生tls校验,因为这个是在conn里面处理的,也就是说不会在姆socket里面处理这个,而是会在子socket里面处理。接下来看看handleConn里面的bufio.NewReader人逻辑,我们只要知道这个r.ReadString('\n')最后回去调用前面传入的conn的Read方法就可以了

可以看到,在这里开始做tls的握手协议了

// Handshake runs the client or server handshake
// protocol if it has not yet been run.
//
// Most uses of this package need not call Handshake explicitly: the
// first Read or Write will call it automatically.
//
// For control over canceling or setting a timeout on a handshake, use
// the Dialer's DialContext method.
func (c *Conn) Handshake() error {
    c.handshakeMutex.Lock()
    defer c.handshakeMutex.Unlock()

    if err := c.handshakeErr; err != nil {
        return err
    }
    // 如果已经TLS握手完成了,就直接放回,握手只会在连接的第一次建立那里做,后面就直接接发信息了
    if c.handshakeComplete() {
        return nil
    }

    c.in.Lock()
    defer c.in.Unlock()

    // 调用握手函数
    c.handshakeErr = c.handshakeFn()
    if c.handshakeErr == nil {
        c.handshakes++
    } else {
        // If an error occurred during the handshake try to flush the
        // alert that might be left in the buffer.
        c.flush()
    }

    // 根据握手结果返回内容
    if c.handshakeErr == nil && !c.handshakeComplete() {
        c.handshakeErr = errors.New("tls: internal error: handshake should have had a result")
    }

    return c.handshakeErr
}
 

这个握手函数就是在前面Accept那里,通过Server函数初始化传入的,Client也是类似的,如下

// Server returns a new TLS server side connection
// using conn as the underlying transport.
// The configuration config must be non-nil and must include
// at least one certificate or else set GetCertificate.
func Server(conn net.Conn, config *Config) *Conn {
    c := &Conn{
        conn:   conn,
        config: config,
    }
    c.handshakeFn = c.serverHandshake
    return c
}

// Client returns a new TLS client side connection
// using conn as the underlying transport.
// The config cannot be nil: users must set either ServerName or
// InsecureSkipVerify in the config.
func Client(conn net.Conn, config *Config) *Conn {
    c := &Conn{
        conn:     conn,
        config:   config,
        isClient: true,
    }
    c.handshakeFn = c.clientHandshake
    return c
}

在握手函数里面就是我们前面理论分析的过程了

其他问题

1.  我们在代码里面会添加rootCAs配置来指定我们校验服务器证书的根证书,但是在浏览器里面我们并没有这么做,那么浏览器是怎么知道我们的证书呢?

答:在系统里面都会内置一些证书的,这些是可信第三方,比如windows可以在运行里面输入“certmgr.msc” 进行查看。如果没特别指定,浏览器会去系统读取那些证书

参考

《深入浅出HTTPS:从原理到实战》

  • 8
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值