天天看點

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

前言

撰寫本文時

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等支撐業務。

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

下面我們将盡可能的參照TLS1.3定義的結構編寫代碼,同時利用Wireshark 抓包檢視包情況。

從ClientHello開始

TLS1.3 的握手流程由用戶端 發送ClientHello 開始,該消息攜帶密鑰協商必要的資料。伺服器端收到該消息後回複ServerHello。我們将向一個啟用了TLS1.3 協定的站點發送,自己實作的 ClientHello 消息,看能否收到 ServerHello回複。

先看結果

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

上圖是Wireshark 的抓包結果,ali-BF 是我本機電腦,Server是某台啟用TLS1.3的網絡伺服器。

從圖中可以看到三次tcp 握手後, 我們發出ClientHello 消息長度348個位元組,經過一個ack後成功收到 Server Hello 消息。

編碼實作

TLS1.3 包含一系列子協定,如 Record Protocol、Handshake Protocol 、Alert Protocol 、ApplicationData Protocol 等

三者關系如圖:

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

發送一個ClientHello 至少需要實作以下子產品

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

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的 抓包資料

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

包含了我們所建構的資料和幾個Extenison

Server端的回複:

TLS1.3 協定的Golang 實作——ClientHello前言5G 未來從ClientHello開始讀懂TLS1.3的資料結構

可以看到 Server 選擇了 AES_256_GCM_SHA384 加密套件、KeyShare 選擇了 p-256曲線。同時發現伺服器端已開始傳輸部分加密的資料 “ApplicationData” 。

讀懂TLS1.3的資料結構

全英文的

閱讀起來很吃力。 為了能較好的了解協定(以及其引用的一系列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

繼續閱讀