前言
前面我已經寫過一些有關TLS1.3協議的文章,主要是從理論出發去了解TLS1.3協議,爲了更加深入的理解TLS1.3協議,我將嘗試去實現它,目前有部分站點已經開始支持TLS1.3,我們可以利用這些站點來進行測試代碼是否成功實現TLS1.3的部分結構,我現在主要實現了ClientHello的整體結構和部分擴展,但是在進行測試的時候不盡如人意,我們先來看一下測試情況。
- 測試站點www.github.com
我們發現服務器並沒有迴應,具體原因我還沒有找到。
- 添加TLS1.2cipherSuites測試www.baidu.com
服務器發送回了ServerHello和Certificate以及ServerHelloDone消息可以說明整體結構編寫沒有問題,可能還存在一些不符合協議的錯誤,我會及時更新改正後的實現到博客,下面我們先來看一下整體結構的實現。
TLS1.3實現-整體架構
下面給出我實現的整體架構
通過這張圖就可以清晰的瞭解到TLS1.3實現的基本流程,首先是實現ClientHello的結構以及裏面包含的擴展,然後實現handshake的整體結構,獲取ClientHello的數據放入handshakeData中,之後實現TLSPlaintext的結構,獲取handshake的數據放入fragment字段,最後封裝數據採用大端字節編碼,將數據發送給服務器,其中TLSPlaintext屬於Record層,它會把數據分割成可處理的塊,每個塊的大小不能超過2^14字節。
協議之間的關係
TLS1.3 包含一系列子協議,如 Record Protocol、Handshake Protocol 、Alert Protocol 、ApplicationData Protocol 等
三者的關係如圖:
接口
type Serializable interface {
GetSize() int
Serialize() []byte
SerializeInto([]byte)
}
這是實現的一個用於序列化數據的接口,所有字段都需要實現這三個方法。
ClientHello
前面已經提到過,ClientHello的數據返回給handshakeData
整體結構代碼
type ClientHello struct {
legacyVersion ProtocolVersion
random ClientRandom
legacySessionID legacySessionId
cipherSuites CipherSuites
legacyCompressionMethods legacyCompressionMethods
extensions Extensions
}
其中每個字段都需要重新定義結構體,並且要實現接口中的方法用於序列化和組織數據。
type ClientRandom struct {
gmt_unix_time uint32
random_bytes []byte
}
func NewClientRandom()ClientRandom{
var random = ClientRandom{
gmt_unix_time: uint32(time.Now().Unix()),
random_bytes: make([]byte,28),
}
rand.Read(random.random_bytes)
return random
}
func (random ClientRandom) GetSize() int {
return 32
}
func (random ClientRandom) SerializeInto(buf []byte) {
binary.BigEndian.PutUint32(buf[0:4], random.gmt_unix_time)
copy(buf[4:31],random.random_bytes)
}
func (random ClientRandom) Serialize() []byte {
obj := make([]byte,random.GetSize())
random.SerializeInto(obj)
return obj
}
random是32字節的隨機數,前4個字節用於顯示當前的unix時間,uint32是表示佔32位的無符號整數,所以正好是4字節。以此爲例說一下三個方法的含義:
- GetSize:主要功能是返回當前字段所佔的字節數
- Serialize:將序列化爲byte類型的字段數據返回
- SerializeInto:主要功能是序列化數據爲字節類型,本例中的binary.BigEndian.PutUint32()是將uint型數據轉換成byte類型。
ClientHello中其他字段的實現方式與其類似,就不再贅述了,主要是搞清楚數據的結構以及所佔的字節數,我建議大家用wireshark截取包之後參照裏面的字段大小,這樣更加準確、快捷。
New結構體的代碼
func NewClientHello(cp []CipherSuite,exts ...Extension) ClientHello{
var NewRandom ClientRandom
NewRandom = NewClientRandom()
//rand_bytes := make([]byte,32)
//rand.Read(rand_bytes)
return ClientHello{
legacyVersion: TLS12,
random: NewRandom,
legacySessionID: NewlegacySessionId(nil),
cipherSuites: NewCipherSuites(cp),
legacyCompressionMethods: NewlegacyCompressionMethods([]legacyCompressionMethod{0}),
extensions: NewExtensions(exts...),
}
}
legacyVersion設置爲0x0303,是爲了兼容版本,除此之外還有legacySessionID、legacyCompressionMethods都是爲了兼容其他版本。
cipherSuites是Client所支持的密碼套件
const (
CIPHER_SUITE_UNKNOWN CipherSuite = 0x0000
TLS_AES_128_GCM_SHA256 CipherSuite = 0x1301
TLS_AES_256_GCM_SHA384 CipherSuite = 0x1302
TLS_CHACHA20_POLY1305_SHA256 CipherSuite = 0x1303
TLS_AES_128_CCM_SHA256 CipherSuite = 0x1304
TLS_AES_256_CCM_8_SHA256 CipherSuite = 0x1305
)
組織結構體數據
func (hello ClientHello) GetSerialization() NestedSerializable {
return NewNestedSerializable([]Serializable{
hello.legacyVersion,
hello.random,
hello.legacySessionID,
hello.cipherSuites,
hello.legacyCompressionMethods,
hello.extensions,
})
}
主要是通過對每個字段的聯合組織,然後將它們放到一個數組中去,組織成一個整體的數據。
Extensions
Extension
type Extension struct {
extensionType ExtensionType
length ExtensionSize
extensionData Serializable
}
func NewExtension(ext_type ExtensionType,ext_data Serializable) Extension {
length := ext_data.GetSize()
return Extension{
extensionType: ext_type,
length: ExtensionSize(length),
extensionData: ext_data,
}
}
其中extensionData我採用了接口類型,所以其它擴展也需要組織數據,即每一個字段都要實現前面提到的三個方法。
extensions
type Extensions struct {
length uint16
Extensions []Extension
}
func NewExtensions(exts ...Extension)Extensions{
l := 0
for _,ext := range exts{
l += ext.GetSize()
}
return Extensions{
uint16(l),
exts,
}
}
請注意lenth指的是字段Extensions的長度,而不是length和Extensions的長度和。
轉成byte
func (ext Extensions) SerializeInto(buf []byte) {
binary.BigEndian.PutUint16(buf[0:2],ext.length)
var start int = 2
for _,ext := range ext.Extensions {
var end int = start + ext.GetSize()
copy(buf[start:end],ext.Serialize())
start = end
}
}
我們要遍歷數組中的所有extension然後把它們轉換成byte
其它的我就不詳細說了,實現過程都是類似的,要特別注意的就是字段的長度要搞清楚,再下手。
SupportedGroup
這個擴展實現起來比較簡單,主要難點就是對裏面數組的處理,也就是NamedGroups
type SupportedGroup struct {
length SgSize
Group NamedGroups
}
func NewSupportedGroup(group NamedGroups) SupportedGroup {
l := NewSgSize(group.GetSize())
return SupportedGroup{
length: l,
Group: group,
}
}
因爲它需要實現三個方法,所以需要重新定義,不然只需要給出一個數組就可以瞭如下:
type SupportedGroup struct {
length SgSize
Group []NamedGroup
}
擴展中要有生成對應擴展的函數:
func NewSupportedGroupExtension(group []NamedGroup) Extension {
sg := NewNamedGroups(group)
ssg := NewSupportedGroup(sg)
return NewExtension(supported_groups ,ssg.GetSerializetion())
}
首先生成NamedGroups,然後用其生成SupportedGroup,最後調用NewExtension函數生成新的Extension。
KeyShare
TLS1.3主要的擴展之一KeyShare,它裏面的生成的公鑰對應於SupportedGroup中的曲線,本文實現的主要是橢圓曲線:P-256、P-384 、P-521。
type KeyShare struct {
length KsSize
shares KeyShareEntrys
}
func NewKeyShare(share KeyShareEntrys)KeyShare {
l := 0
l = share.GetSize()
return KeyShare{
length: KsSize(l),
shares: share,
}
}
最主要的部分是KeyShareEntry
type KeyShareEntry struct {
group NamedGroup
length uint16
keyExchange []byte
}
func NewKeyShareEntry(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 {
panic(err)
}
nu := NewUncompressedPointRepresentation(x.Bytes(),y.Bytes())
ks := KeyShareEntry{
group: group,
length: uint16(nu.GetSize()),
keyExchange: nu.Serialize(),
}
return ks,priv
}
這個結構我是參考別人實現的過程實現的,主要的功能就是生成對應曲線的公鑰並返回。
其中的UncompressedPointRepresentation結構如下:
type UncompressedPointRepresentation struct {
legacyForm uint8
X []byte
Y []byte
}
func NewUncompressedPointRepresentation(x,y []byte) UncompressedPointRepresentation {
return UncompressedPointRepresentation{
legacyForm: 4,
X: x,
Y: y,
}
}
我在前一篇博客裏面對應的也有詳細的介紹。
其它的擴展實現過程與這兩個比較相似,參考實現即可。
Handshake
結構
handshakeData是接口類型,其中的值對應ClientHello的值,而且它的值又對應TLSPlaintext的值,所以其實現結構也與ClientHello類似。
type Handshake struct {
msgType HandshakeType
length HandshakeSize
handshakeData Serializable
}
func NewHandshake(msg_type HandshakeType,data Serializable)Handshake{
size := NewHandshakeSize(data.GetSize())
return Handshake{
msgType: msg_type,
length: size,
handshakeData: data,
}
}
handshakeSize
值得強調的是,handshakeSize的大小是3個字節24位,因爲handshakeData是接口類型,所以可以返回數據的長度,是int類型的,需要轉換成字節類型存儲在handshakeSize中,所以需要進行位運算轉換成byte。
type HandshakeSize [3]byte
func NewHandshakeSize(num int)HandshakeSize {
return [3]byte{
uint8(num >> 16),
uint8(num >> 8),
uint8(num)}
}
TLSPlaintext
結構
handshake中的數據封裝到字段fragment中,然後打包傳輸到服務器,我們來看一下它的結構:
type TLSPlaintext struct {
ContentType ContentType
legacyRecordVersion ProtocolVersion
length ContentSize
fragment Serializable
}
func NewTLSPlaintext(contentType ContentType,fragment Serializable)TLSPlaintext{
length := NewContentSize(fragment.GetSize())
return TLSPlaintext{
ContentType: contentType,
legacyRecordVersion: TLS12,
length: length,
fragment: fragment,
}
}
其中的legacyRecordVersion字段有好幾張中說法,有的說設置成0x0301,有的說設置成0x0303兼容版本,具體我還沒搞明白。
ContentSize
type ContentSize [2]byte
func NewContentSize(num int) ContentSize {
var ret [2]byte
binary.BigEndian.PutUint16(ret[0:2], uint16(num))
return ret
}
類比handshakeSize,同樣要進行類型的轉換。
小結
這樣就基本實現了ClientHello的整體結構,編碼成大端字節發送給服務器就可以了,就是將組合成的TLSPlaintext數據編碼發送即可,我們來看一下實現代碼:
func firstClientHello(){
fmt.Println("正在發送ClientHello")
//extension
servername := tls.NewServerNameListExtension([]tls.ServerName{"baidu.com"})
supportedVersion := tls.NewSupportedVersionsExtension([]tls.ProtocolVersion{
tls.TLS13,
tls.TLS12,
tls.TLS11,
tls.TLS10,
})
supportedGroup := tls.NewSupportedGroupExtension([]tls.NamedGroup{
tls.Secp256r1,
tls.Secp384r1,
tls.Secp521r1})
keyshare := tls.NewKeyShareExtension([]tls.NamedGroup{
tls.Secp256r1,
tls.Secp384r1,
tls.Secp521r1})
signaturealgorithms := tls.NewSignatureAlgorithmsExtension([]tls.SignatureScheme{
tls.Ecdsa_secp256r1_sha256,
tls.Ecdsa_secp384r1_sha384,
tls.Ecdsa_secp521r1_sha512})
//body
ClientHelloBody := tls.NewClientHello([]tls.CipherSuite{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_128_CCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_AES_256_CCM_8_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA},supportedVersion,signaturealgorithms,servername,supportedGroup,keyshare)
ClientHandshake := tls.NewHandshake(tls.HandshakeTypeClientHello,ClientHelloBody.GetSerialization())
ClientHandshakeMessage := tls.NewTLSPlaintext(tls.RecordTypeHandshake,ClientHandshake.GetSerialization())
getConnect("tcp","www.baidu.com:443",ClientHandshakeMessage.GetSerialization().Serialize())
fmt.Println("發送成功!")
}
有關TLS數據結構的實現
- 基本數字類型是無符號字節,所有較大的數字數據類型均由固定長度的一系列字節組成。
- 均以網絡字節(大端)順序存儲
- 存在一些定長數組、可變長數組、枚舉類型等數據結構
枚舉類型
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;
在golang中可以寫成
type HandshakeType uint8
const (
HandshakeTypeClientHello HandshakeType = 1
HandshakeTypeServerHello HandshakeType = 2
HandshakeTypeNewSessionTicket HandshakeType = 4
HandshakeTypeEndOfEarlyData HandshakeType = 5
HandshakeTypeHelloRetryRequest HandshakeType = 6
HandshakeTypeEncryptedExtensions HandshakeType = 8
HandshakeTypeCertificate HandshakeType = 11
HandshakeTypeCertificateRequest HandshakeType = 13
HandshakeTypeCertificateVerify HandshakeType = 15
HandshakeTypeServerConfiguration HandshakeType = 17
HandshakeTypeFinished HandshakeType = 20
HandshakeTypeKeyUpdate HandshakeType = 24
HandshakeTypeMessageHash HandshakeType = 254
)
其它的枚舉像:ExtensionType、ContentType等都與其類似。
可變長數組類型
CipherSuite cipher_suites<2..2^16-2>;
uint8 CipherSuite[2];
這種數據類型包含兩部分,head+body,也可以理解爲head是body的長度,而body就是存儲的數據,在golang中我們可以表示成:
type CipherSuites struct {
length uint16
ciphersuites []CipherSuite
}
type CipherSuite uint16
因爲head是2所以佔兩個字節也就是16位,又因爲是無符號的類型,所以我們可以用uint16來表示。它這個可變長的意思就是裏面可能存在多個CipherSuites。
定長數組
opaque Random[32]
表示Random類型佔用了32個字節,其中opaque表示不透明的數據結構,可以理解爲byte數組。
在golang中實現如下:
type ClientRandom [32]byte
但是考慮到random的結構包括unix時間所以結構是:
type ClientRandom struct {
gmt_unix_time uint32
random_bytes []byte
}
其中的random_bytes爲28個字節長度。
還有一些其他的數據結構,請大家自行解決吧。
wireshark截取包
最後我們來看一下,我發送出去的包的樣子吧!
我的blog
StrideMaxZZ,歡迎大家訪問!
更新
前面提到過我測試支持TLS1.3的站點時沒有成功,今天我找到原因了,在建立net連接的時候要defer一下斷開連接,因爲後續還需要客戶端迴應,看一下代碼:
func getConnect(network string,address string,message []byte) {
conn,err := net.Dial(network, address)
if err != nil {
panic(err)
}
err = binary.Write(conn, binary.BigEndian, message)
if err != nil {
panic(err)
}
defer conn.Close()
}
這樣的話就可以看到server的迴應了。wireshark截圖如下:
再來看一下serverHello
我們可以看到server選擇的密碼套件和所支持的曲線x25519。終於有點成果了,非常的開心啊!