前面我已经写过一些有关TLS1.3协议的文章,主要是从理论出发去了解TLS1.3协议,为了更加深入的理解TLS1.3协议,我将尝试去实现它,目前有部分站点已经开始支持TLS1.3,我们可以利用这些站点来进行测试代码是否成功实现TLS1.3的部分结构,我现在主要实现了ClientHello的整体结构和部分扩展,但是在进行测试的时候不尽如人意,我们先来看一下测试情况。
我们发现服务器并没有回应,具体原因我还没有找到。
服务器发送回了ServerHello和Certificate以及ServerHelloDone消息可以说明整体结构编写没有问题,可能还存在一些不符合协议的错误,我会及时更新改正后的实现到博客,下面我们先来看一下整体结构的实现。
下面给出我实现的整体架构
通过这张图就可以清晰的了解到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的数据返回给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字节。以此为例说一下三个方法的含义:
ClientHello中其他字段的实现方式与其类似,就不再赘述了,主要是搞清楚数据的结构以及所占的字节数,我建议大家用wireshark截取包之后参照里面的字段大小,这样更加准确、快捷。
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,
})
}
主要是通过对每个字段的联合组织,然后将它们放到一个数组中去,组织成一个整体的数据。
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我采用了接口类型,所以其它扩展也需要组织数据,即每一个字段都要实现前面提到的三个方法。
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的长度和。
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
其它的我就不详细说了,实现过程都是类似的,要特别注意的就是字段的长度要搞清楚,再下手。
这个扩展实现起来比较简单,主要难点就是对里面数组的处理,也就是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。
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,
}
}
我在前一篇博客里面对应的也有详细的介绍。
其它的扩展实现过程与这两个比较相似,参考实现即可。
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的大小是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)}
}
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兼容版本,具体我还没搞明白。
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("发送成功!")
}
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个字节长度。
还有一些其他的数据结构,请大家自行解决吧。
最后我们来看一下,我发送出去的包的样子吧!
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。终于有点成果了,非常的开心啊!