TLS1.3实现篇

我是研究僧i 提交于 2020-04-21 05:17:19

前言

前面我已经写过一些有关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。终于有点成果了,非常的开心啊!

发布了23 篇原创文章 · 获赞 1 · 访问量 1606
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!