前言
撰寫本文時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 ObjectByteCount() 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.TLS13)
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