TLS1.3 协议的Golang 实现——ClientHello

简介: # 前言 撰写本文时[TLS1.3 RFC](https://tools.ietf.org/html/draft-ietf-tls-tls13-28) 已经发布到28版本。以前写过一点密码学及TLS 相关的文章,为了更深入理解TLS1.3协议,这次将尝试使用Go语言实现它。网络上已有部分站点支持TLS1.3,Chrome浏览器通过设置可支持TLS1.3 (draft23),利用这些条件可验证,

前言

撰写本文时TLS1.3 RFC 已经发布到28版本。以前写过一点密码学及TLS 相关的文章,为了更深入理解TLS1.3协议,这次将尝试使用Go语言实现它。网络上已有部分站点支持TLS1.3,Chrome浏览器通过设置可支持TLS1.3 (draft23),利用这些条件可验证,我们实现的协议是否标准。

完整的实现TLS1.3 工作量很大,概念和细节非常多(感觉又在挖坑)。本文首先会从ClientHello开始,后续可能会考虑 :Authentication、Cryptographic Computations、0-RTT 。

5G 未来

每次基础设施的升级都是时代变革的前奏。 在移动互联网2G/3G时代,很多创新都约束在了Wifi 下;移动互联网进入4G时代后,爆发了各种直播、短视频等创新。现在的IOT和移动互联网上半场略有相似,待5G成熟万物互联后,相信也会爆发出一系列的创新。

网络互连后信息的安全传输,也是不容忽视的问题。TLS1.3 相对于之前的版本,修正了很多安全陷阱,降低了握手的次数,提高了效率。

私有化

TLS1.3 中有不少设计是为了向下兼容TLS1.2 或TLS1.1 (毕竟是公开的网络协议),如果想要建立一个私有化的安全层,只要按照标准里的要点,可以剔除其中兼容性的设计,优化包结构减少数据传输量。例如 参考以太坊的RLP编码方式,将长度和数据结合在一起,就可以进一步减少包大小。

利用TLS1.3建立一个安全高效的 “安全会话层”,
1、往下可以增加连接管理模块、心跳控制模块,如:实现多级TCP 通道快速重连重发等,解决弱网状态下的各种问题。
2、往上可以服务各种不同的网络类型如p2p或经典C/S,
3、更上层可以服务一些中间件如: RPC 、MQTT等支撑业务。

connect.001.jpeg

下面我们将尽可能的参照TLS1.3定义的结构编写代码,同时利用Wireshark 抓包查看包情况。

从ClientHello开始

TLS1.3 的握手流程由客户端 发送ClientHello 开始,该消息携带密钥协商必要的数据。服务器端收到该消息后回复ServerHello。我们将向一个启用了TLS1.3 协议的站点发送,自己实现的 ClientHello 消息,看能否收到 ServerHello回复。

先看结果

image.png

上图是Wireshark 的抓包结果,ali-BF 是我本机电脑,Server是某台启用TLS1.3的网络服务器。

从图中可以看到三次tcp 握手后, 我们发出ClientHello 消息长度348个字节,经过一个ack后成功收到 Server Hello 消息。

编码实现

TLS1.3 包含一系列子协议,如 Record Protocol、Handshake Protocol 、Alert Protocol 、ApplicationData Protocol 等
三者关系如图:
20180504150105.png

发送一个ClientHello 至少需要实现以下模块
20180504151618.png

Record 层

ClientHello 是明文传输的,所以是封装在TLSPlaintext 中

// ContentType enum {...} ;
type ContentType byte

const (
    invalid          ContentType = 0
    changeCipherSpec ContentType = 20
    alert            ContentType = 21
    handshake        ContentType = 22
    applicationData  ContentType = 23
)

// TLSPlaintext plaintext on record layer
type TLSPlaintext struct {
    contentType         ContentType
    legacyRecordVersion ProtocolVersion //static
    length              uint16
    fragment            syntax.Vector
}

type TLSCiphertext TLSPlaintext

legacyRecordVersion 的值为0x0303 为了兼容TLS1.2版。

定义一个接口用于序列化,后面所有的Struct 都会实现该接口

type Encoder interface {
    //Encode coding object into the Writer,
    Encode(w io.Writer)  error

    //ByteCount return the byte length of all Object
    ByteCount() int
}

type Vector Encoder

序列化TLSPlaintext


func generateTLSPlaintext(contentType ContentType, fragment syntax.Vector) TLSPlaintext {
    return TLSPlaintext{
        contentType:         contentType,
        legacyRecordVersion: TLS1_2,
        length:              uint16(fragment.ByteCount()),
        fragment:            fragment,
    }
}

func (t *TLSPlaintext) Encode(w io.Writer) error {

    if uint16(t.length) > 2<<14 {
        return errors.New("overflow fragment")
    }
    err := syntax.Encode(w,
        syntax.WriteTo(t.contentType),
        syntax.WriteTo(t.legacyRecordVersion),
        syntax.WriteTo(t.length))
    if err != nil {
        return err
    }
    err = t.fragment.Encode(w)
    return err
}

func (t *TLSPlaintext) ByteCount() int {
    return t.fragment.ByteCount() + 5
}

本文的主要目的是深入浅出的学习TLS1.3协议,因此在实现上并不是很关注性能和效率问题及部分异常情况。

Handshake

TLS1.3 支持三种方式的密钥协商:PSK-Only、(EC)DHE, 和PSK with (EC)DHE ,本文主要是关注 ECDHE。
Handshake 对应了TLSPlaintext 的fragment ,因为其实现了Vector 接口


type Handshake struct {
    msgType       HandshakeType /* handshake type */
    length        uint24
    handshakeData syntax.Vector
}

func generateHandshake(msgType HandshakeType, data syntax.Vector) Handshake {
    l := data.ByteCount()
    return Handshake{
        msgType:       msgType,
        length:        uint24{byte(l >> 16), byte(l >> 8), byte(l)},
        handshakeData: data,
    }
}

ClientHello

ClientHello 对应Handshake的handshakeData 。

type extensionsVector struct {
    length     uint16
    extensions []extension.Extension
}

type ClientHello struct {
    legacyVersion            tls.ProtocolVersion
    random                   [32]byte
    legacySessionId          legacySessionId
    cipherSuites             CipherSuiteVector
    legacyCompressionMethods legacyCompressionMethods
    extensions               extensionsVector
}

legacyVersion 、legacySessionIdlegacyCompressionMethods 的定义是为了兼容旧版本,因此需要的是random 、 cipherSuites 和 extensions.

CipherSuite 指明了Client所能支持的加密套件 例如:TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384等,只支持 AEAD 的加密算法套件。

func generateClientHello(cipherSuites []CipherSuite, exts ...extension.Extension) ClientHello {
    var r [32]byte
    rand.Read(r[:])
    extBytes := 0
    for _, ext := range exts {
        extBytes += ext.ByteCount()
    }
    return ClientHello{
        legacyVersion:            tls.TLS1_2,
        random:                   r,
        legacySessionId:          legacySessionId{0, nil},
        cipherSuites:             NewCipherSuite(cipherSuites...),
        legacyCompressionMethods: generateLegacyCompressionMethods([]byte{0}),
        extensions:               generateExtensions(exts...),
    }
}

func generateExtensions(exts ...extension.Extension) extensionsVector {
    l := 0
    for _, ext := range exts {
        l += ext.ByteCount()
    }
    return extensionsVector{
        length:     uint16(l),
        extensions: exts,
    }
}

func NewClientHelloHandshake(cipherSuites []CipherSuite, exts ...extension.Extension) Handshake {
    cl := generateClientHello(cipherSuites, exts...)
    return generateHandshake(clientHello, &cl)
}

各种Extension

ClientHello 主要是通过Extension 传递密钥协商必要的素材, 第一次ClientHello 至少需要包含以下5个Extension:
1、ServerName : 所请求的主机名

通常情况下一台服务器会寄宿多个站点,即同一个IP 多个Web服务器。由于TLS 层还未完成握手,此时还没有http的请求(host head),无法知道具体站点。后续握手时服务器端将难以确定要发送的证书。

虽然采用通用名和subjectAltName的方式可以支持多个域名,但是无法得知未来所有的域名,一旦有变更还得重新申请证书。因此在ClientHello中添加ServerName扩展用于指明要访问的主机,查看详情RFC6066

这样做有个缺点,ClientHello 是明文传输,中间人可以明确探知该流量的目的地。像WhatApp 和 Signal 就采用一种叫“域前置” 的技术去绕过该问题。

2、SupportedVersions :所能支持的TLS 版本如:TLS1.1、TLS1.2、TLS1.3等

用于协商最终采用的TLS 版本,在ClientHello 所能支持的列表,把最优先支持的放在第一位。

3、SignatureAlgorithms : 所支持的签名算法

如:ECDSA_SECP256R1_SHA256、ECDSA_SECP384R1_SHA384等

4、SupportedGroups:用于密钥交换

本文主要关注椭圆曲线 如:SECP256R1、SECP384R1、SECP521R1等

5、KeyShare:密钥协商时交换的公钥

每一个SupportedGroup 需要有对应的 KeyShare。

Extension Golang实现

Extension 基本是才有 Request/Response 方式通讯,客户端发送Request、服务器端通过Response 的方式回复所选。

type Extension struct {
    extensionType ExtensionType
    length        uint16
    extensionData syntax.Vector
}

下面将列出SupportedVersions 和 KeyShare 的golang 实现,其他Extension 的实现比较相似。

ClientHello 的SupportedVersions 比较简单,只要包含一个ProtocolVersions数组即可。

type SupportedVersions struct {
    length           uint8 // byte count of array protocolVersions
    protocolVersions []tls.ProtocolVersion
}

func generateSupportedVersions(protocolVersions ...tls.ProtocolVersion) SupportedVersions {
    return SupportedVersions{
        length:           uint8(len(protocolVersions) * 2),
        protocolVersions: protocolVersions,
    }
}

//NewSupportedVersionsExtension create a supported versions extension
func NewSupportedVersionsExtension(protocolVersions ...tls.ProtocolVersion) Extension {
    sv := generateSupportedVersions(protocolVersions...)
    return generateExtension(supportedVersions, &sv)
}

本文将以ECC 的P-256、P-384 、P-521曲线 作为实现,说明如果生成对应的KeyShare

type KeyShareEntry struct {
    group       NamedGroup
    length      uint16
    keyExchange []byte
}

func generateKeyShareEntry(group NamedGroup) (KeyShareEntry, []byte) {
    var curve elliptic.Curve
    switch group {
    case SECP256R1:
        curve = elliptic.P256()
        break
    case SECP384R1:
        curve = elliptic.P384()
        break
    case SECP521R1:
        curve = elliptic.P521() 
        break
    }

    priv, x, y, err := elliptic.GenerateKey(curve, rand.Reader)
    if err != nil {
        return KeyShareEntry{}, nil
    }

    nu := generateUncompressedPointRepresentation(x.Bytes(), y.Bytes())
    buffer := new(bytes.Buffer)
    nu.Encode(buffer)

    ks := KeyShareEntry{
        group:       group,
        length:      uint16(len(nu.X)+len(nu.Y)) + 1,
        keyExchange: buffer.Bytes(),
    }
    return ks, priv
}

type KeyShareClientHello struct {
    length       uint16
    clientShares []KeyShareEntry
}

func generateKeyShareClientHello(enters ...KeyShareEntry) KeyShareClientHello {
    var l uint16
    for _, k := range enters {
        l += k.length + 4
    }
    return KeyShareClientHello{
        length:       l,
        clientShares: enters,
    }
}

func NewKeyShareClientExtension(groups ...NamedGroup) (Extension, [][]byte) {

    keyShareList := make([]KeyShareEntry, len(groups))
    privateList := make([][]byte, len(groups))
    for i, g := range groups {
        ks, priv := generateKeyShareEntry(g)
        keyShareList[i] = ks
        privateList[i] = priv
    }

    kscl := generateKeyShareClientHello(keyShareList...)
    return generateExtension(keyShare, &kscl), privateList

}

type UncompressedPointRepresentation struct {
    legacyForm uint8
    X          []byte
    Y          []byte
}

func generateUncompressedPointRepresentation(x, y []byte) UncompressedPointRepresentation {
    return UncompressedPointRepresentation{
        legacyForm: 4,
        X:          x,
        Y:          y,
    }
}

发送ClientHello

组合上面的各种类型,构建ClientHello ,编码后发送给远端服务器(真实存在的站点),TLS1.3 采用的是大端字节序。

func firstClientHello(conn net.Conn, host string) {
    supportedVersion := extension.NewSupportedVersionsExtension(tls.TLS1_3)
    supportedGroup := extension.NewSupportedGroupExtension(extension.SECP256R1, extension.SECP384R1)
    keyShare, _ := extension.NewKeyShareClientExtension(extension.SECP256R1, extension.SECP384R1)
    signatureScheme := extension.NewSignatureSchemeExtension(extension.ECDSA_SECP256R1_SHA256, extension.ECDSA_SECP384R1_SHA384)
    serverName := extension.NewServerNameExtension(host)

    clientHelloHandshake := handshake.NewClientHelloHandshake([]handshake.CipherSuite{
        handshake.TLS_AES_128_GCM_SHA256,
        handshake.TLS_AES_128_CCM_SHA256,
        handshake.TLS_AES_256_GCM_SHA384}, serverName, supportedVersion, signatureScheme, supportedGroup, keyShare)

    clientHelloRecord := tls.NewHandshakeRecord(&clientHelloHandshake)
    clientHelloRecord.Encode(outputBuffer)
    _, err := outputBuffer.WriteTo(conn)
    if err != nil {
        fmt.Println(err)
        return
    }
}

查看Wireshark的 抓包数据
20180507115326.png
包含了我们所构建的数据和几个Extenison

Server端的回复:
20180507115434.png
可以看到 Server 选择了 AES_256_GCM_SHA384 加密套件、KeyShare 选择了 p-256曲线。同时发现服务器端已开始传输部分加密的数据 “ApplicationData” 。


读懂TLS1.3的数据结构

全英文的 TLS1.3 RFC 阅读起来很吃力。 为了能较好的理解协议(以及其引用的一系列RFC )需要先了解其标记语言

Presentation Language

TLS1.3 定义了一些Presentation Language 来描述数据的结构和序列化方式。

1、类型的别名 T T'
T' 为T的类型别名
如:uint16 ProtocolVersion , ProtocolVersion 就是 uint16 的别名,它表明ProtocolVersion在传输中占有2个字节。golang 中的定义:

type ProtocolVersion uint16

2、定长数组类型 T T'[n]
T'[] 表示为T的数组,需要注意的是n并不代表T的个数,而是T'类型占用多少个byte
如: opaque Random[32] ,表示 Random 类型占用了32 个字节。opaque 表示不透明的数据结构,可以理解为byte数组

3、可变长度数组类型 T T'
T'<> 包含两部分: head+body,
head的值表示body 占用了多少个字节,
body的值为真实负载,即T的数组。 head 本身所占的字节数由 决定。
如:
CipherSuite cipher_suites<2..2^16-2>
表示 CipherSuite 类型的数组,其head 占用 2byte, uint16可以容下2^16个字节。

在golang 中可以这样表示

type CipherSuiteVector struct {
    length       uint16
    cipherSuites []CipherSuite
}

4、枚举类型 enum { e1(v1), e2(v2), ... , en(vn) [[, (n)]] } Te;
e1表示枚举类型的值。最后的 [[,(n)]] n 表示最大值, 由此可以推论出 枚举类型 Te 占用的字节数
如:

enum {
          client_hello(1),
          server_hello(2),
          new_session_ticket(4),
          end_of_early_data(5),
          encrypted_extensions(8),
          certificate(11),
          certificate_request(13),
          certificate_verify(15),
          finished(20),
          key_update(24),
          message_hash(254),
          (255)
      } HandshakeType;

HandshakeType 类型 占一个byte 2^8
在golang 中可以这样定义

// HandshakeType alies
type HandshakeType byte

const (
    clientHello         HandshakeType = 1
    serverHello         HandshakeType = 2
    newSessionTicket    HandshakeType = 4
    endOfEarlyData      HandshakeType = 5
    encryptedExtensions HandshakeType = 8
    certificate         HandshakeType = 11
    certificateRequest  HandshakeType = 13
    certificateVerify   HandshakeType = 15
    finished            HandshakeType = 20
    keyUpdate           HandshakeType = 24
    messageHash         HandshakeType = 254
)

5、常量表示
在TLS 1.3 中协议中有些字段必须设置为固定值,主要是为了兼容旧版本,因此需要定义常量的表示。

struct {
          T1 f1 = 8;  /* T.f1 must always be 8 */
          T2 f2;
      } T;

这里 T.f1 的值固定为8
例如:

struct {
          ProtocolVersion legacy_version = 0x0303;    /* TLS v1.2 */
          Random random;
          opaque legacy_session_id<0..32>;
          CipherSuite cipher_suites<2..2^16-2>;
          opaque legacy_compression_methods<1..2^8-1>;
          Extension extensions<8..2^16-1>;
      } ClientHello;

ClientHello.legacy_version 的值固定为 0x0303 ,为了向下兼容。

6、变量定义

struct {
          T1 f1;
          T2 f2;
          ....
          Tn fn;
          select (E) {
              case e1: Te1 [[fe1]];
              case e2: Te2 [[fe2]];
              ....
              case en: Ten [[fen]];
          };
      } Tv;

Tv.E 是变量,跟进具体的情况取值。
例如

struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* and HelloRetryRequest */
                   ProtocolVersion selected_version;
          };
} SupportedVersions;

这里 如果是 在ClientHello 里 SupportedVersions 则是一个 Vector 类型,而在ServerHello 里则是 一个ProtocolVersion

目录
相关文章
|
6月前
|
物联网 Go 网络性能优化
使用Go语言(Golang)可以实现MQTT协议的点对点(P2P)消息发送。MQTT协议本身支持多种消息收发模式
使用Go语言(Golang)可以实现MQTT协议的点对点(P2P)消息发送。MQTT协议本身支持多种消息收发模式【1月更文挑战第21天】【1月更文挑战第104篇】
469 1
|
6月前
|
NoSQL Go Redis
Golang实现redis系列-(3)封装RESP协议
Golang实现redis系列-(3)封装RESP协议
54 0
|
网络协议 Shell Go
Socks5代理协议与Golang实现
本文主要讲解了SOCKS5协议以及golang实现相关内容
258 1
|
网络协议 Go 存储
golang 自定义封包协议(转的)
package protocol import ( "bytes" "encoding/binary" ) const ( ConstHeader = "jackluo" ConstHeaderLength = 7 ...
2428 0
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
94 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
60 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
89 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
78 4
|
2月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
45 3
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
46 4
Golang语言goroutine协程篇