TLS1.3實現篇

前言

前面我已經寫過一些有關TLS1.3協議的文章,主要是從理論出發去了解TLS1.3協議,爲了更加深入的理解TLS1.3協議,我將嘗試去實現它,目前有部分站點已經開始支持TLS1.3,我們可以利用這些站點來進行測試代碼是否成功實現TLS1.3的部分結構,我現在主要實現了ClientHello的整體結構和部分擴展,但是在進行測試的時候不盡如人意,我們先來看一下測試情況。

  • 測試站點www.github.com

github

我們發現服務器並沒有迴應,具體原因我還沒有找到。

  • 添加TLS1.2cipherSuites測試www.baidu.com

baidu

服務器發送回了ServerHello和Certificate以及ServerHelloDone消息可以說明整體結構編寫沒有問題,可能還存在一些不符合協議的錯誤,我會及時更新改正後的實現到博客,下面我們先來看一下整體結構的實現。

TLS1.3實現-整體架構

下面給出我實現的整體架構

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 等

三者的關係如圖:

record

接口

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截取包

最後我們來看一下,我發送出去的包的樣子吧!

ClientHello

extension

我的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截圖如下:

tls1.3base

再來看一下serverHello

tls1.3serverHello

我們可以看到server選擇的密碼套件和所支持的曲線x25519。終於有點成果了,非常的開心啊!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章