TLS1.3 协议的Golang 实现——ClientHello
前言
撰写本文时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等支撑业务。
下面我们将尽可能的参照TLS1.3定义的结构编写代码,同时利用Wireshark 抓包查看包情况。
从ClientHello开始
TLS1.3 的握手流程由客户端 发送ClientHello 开始,该消息携带密钥协商必要的数据。服务器端收到该消息后回复ServerHello。我们将向一个启用了TLS1.3 协议的站点发送,自己实现的 ClientHello 消息,看能否收到 ServerHello回复。
先看结果
上图是Wireshark 的抓包结果,ali-BF 是我本机电脑,Server是某台启用TLS1.3的网络服务器。
从图中可以看到三次tcp 握手后, 我们发出ClientHello 消息长度348个字节,经过一个ack后成功收到 Server Hello 消息。
编码实现
TLS1.3 包含一系列子协议,如 Record Protocol、Handshake Protocol 、Alert Protocol 、ApplicationData Protocol 等
三者关系如图:
发送一个ClientHello 至少需要实现以下模块
Record 层
ClientHello 是明文传输的,所以是封装在TLSPlaintext 中
// ContentType enum {...} ;
type ContentType byteconst (invalid ContentType = 0changeCipherSpec ContentType = 20alert ContentType = 21handshake ContentType = 22applicationData ContentType = 23
)// TLSPlaintext plaintext on record layer
type TLSPlaintext struct {contentType ContentTypelegacyRecordVersion ProtocolVersion //staticlength uint16fragment 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 uint24handshakeData 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 uint16extensions []extension.Extension
}type ClientHello struct {legacyVersion tls.ProtocolVersionrandom [32]bytelegacySessionId legacySessionIdcipherSuites CipherSuiteVectorlegacyCompressionMethods legacyCompressionMethodsextensions 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]byterand.Read(r[:])extBytes := 0for _, 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 := 0for _, 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 ExtensionTypelength uint16extensionData syntax.Vector
}
下面将列出SupportedVersions 和 KeyShare 的golang 实现,其他Extension 的实现比较相似。
ClientHello 的SupportedVersions 比较简单,只要包含一个ProtocolVersions数组即可。
type SupportedVersions struct {length uint8 // byte count of array protocolVersionsprotocolVersions []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 NamedGrouplength uint16keyExchange []byte
}func generateKeyShareEntry(group NamedGroup) (KeyShareEntry, []byte) {var curve elliptic.Curveswitch group {case SECP256R1:curve = elliptic.P256()breakcase SECP384R1:curve = elliptic.P384()breakcase 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 uint16clientShares []KeyShareEntry
}func generateKeyShareClientHello(enters ...KeyShareEntry) KeyShareClientHello {var l uint16for _, 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] = ksprivateList[i] = priv}kscl := generateKeyShareClientHello(keyShareList...)return generateExtension(keyShare, &kscl), privateList}type UncompressedPointRepresentation struct {legacyForm uint8X []byteY []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.TLS1_3)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的 抓包数据
包含了我们所构建的数据和几个Extenison
Server端的回复:
可以看到 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 uint16cipherSuites []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 byteconst (clientHello HandshakeType = 1serverHello HandshakeType = 2newSessionTicket HandshakeType = 4endOfEarlyData HandshakeType = 5encryptedExtensions HandshakeType = 8certificate HandshakeType = 11certificateRequest HandshakeType = 13certificateVerify HandshakeType = 15finished HandshakeType = 20keyUpdate HandshakeType = 24messageHash 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
TLS1.3 协议的Golang 实现——ClientHello相关推荐
- 安全修复之Web——【中危】启用了不安全的TLS1.0、TLS1.1协议
安全修复之Web--[中危]启用了不安全的TLS1.0.TLS1.1协议 背景 日常我们开发时,会遇到各种各样的奇奇怪怪的问题(踩坑o(╯□╰)o),这个常见问题系列就是我日常遇到的一些问题的记录文章 ...
- TLSNotary中心化预言机(1) TLS1.1协议
TLSNotary是基于TLS1.1协议的流程来实现 1.TLS1.1协议 协议的大体流程是协商密钥生成算法然后通过随机数生成秘钥,最终双向验证秘钥达成握手,详细流程可以参见文末链接. 协议最终的效果 ...
- http服务(nginx、apache)停用不安全的SSL协议、TLS1.0和TLS1.1协议/启用TLS1.3
文章目录 一.http服务停用不安全的TLS1.0和TLS1.1协议 nginx Apache apache要支持TLS1.2 版本要求 工作中遇到问题整理 [error] No SSL protoc ...
- TLS1.3 协议的加密过程
记录一下 TLS1.3 的加密过程,如何协商会话秘钥 TLS 就是 HTTPS 的那一层加密协议,前身是SSL,但是现在SSL已经淘汰了.之前用wireshark 抓包看,现在的网站多是TLS1.2. ...
- tls1.2协议学习笔记
一.概述 从技术上讲,TLS 1.0与SSL 3.0的差异非常微小 TLS:Transport Layer Security SSL:Secure Socket Layer TLS/SSL 协议位于应 ...
- TLS1.3 协议介绍
一. 协议介绍 TLS(Transport Layer Security )的中文全称是安全传输层协议,TLS的设计目标是在传输层之上,构建一个安全传输层. 因为有了TLS的存在,HTTP协议栈增加了 ...
- 【中危】启用了不安全的TLS1.0、TLS1.1协议
文章目录 1. 漏洞描述 2. 补充学习 3. 漏洞检测 4. 漏洞修复 5. 片尾彩蛋 1. 漏洞描述 TLS 1.0是于1996年发布的老版协议,使用的是弱加密算法和系统.比如SHA-1和MD5, ...
- java6不支持tlsv1.2_解决 JDK1.7 不支持 VCenter 6.7 的问题(涉及到Https TLS1.2协议)
解决 JDK1.7 不支持 VCenter 6.7 的问题 问题描述 原项目工程是使用JDK 1.7,可以连接 5.X版本和 6.0版本的 VCenter资源池. 但是,现在VCenter已经升到 6 ...
- 5月11日云栖精选夜读丨清华大学成功卫冕ASC18世界超算总决赛冠军,黑马上海科大斩获AI大奖
原文链接:点击打开链接 摘要: 5月9日,2018 ASC世界大学生超级计算机竞赛(ASC18)总决赛在南昌大学落下帷幕,清华大学成功卫冕总冠军,首次入围总决赛的"黑马"上海科技大 ...
最新文章
- AAAI-19 日程 安排
- Java面试笔试题大汇总一(最全+详细答案)
- 《Java核心技术 卷Ⅱ 高级特性(原书第10版)》一导读
- hdu 1593(数学)
- 样式和主题的区别(Styles and Themes)
- matlab求灰度图像梯度,[求助]如何求图像的梯度
- 为什么互联网35岁是道坎?
- 开发WinRT自定义组件之富文本框
- 行车记录仪数据集_福特自动驾驶数据集公布 总体积达1.6TB或为数据最全的数据集之一...
- 找出最耗资源的sql ----没明白
- 推荐一些2021年整理的PHP毕业设计、毕设参考作品案例
- 贴片铝电解电容封装的说明
- DDA算法和Bresenham算法
- PHP探测手机客户端
- 如何关闭方正软件保护卡
- 网站视频很卡怎么办?有没有免费的视频平台?使用阿里云OSS对象云存储+下行流量包解决网站文件/视频访问慢问题
- 编程猫海龟编辑器 附使用教程
- 计算机作文600字关于科学事业,对科学事业的执着追求作文600字
- 无刷电机噪音产生原因及解决方法
- Mapper method 'dao.xxx' has an unsupported return type
热门文章
- 拿下丰厚的年终奖,却未能拯救总薪酬,2021 年度 IT 薪酬调查报告出炉!
- 抗住 8 亿人买买买!双 11 背后黑科技大曝光
- Hadoop 之父趣事:用儿子的大象玩偶为大数据项目命名
- 程序员饭碗不保了?GPT-3 最强应用发布,动动手指就自动写代码的神器来了!...
- String字符串位置移动
- mysql优化-----多列索引的左前缀规则
- DataGridView使用技巧十一:DataGridView用户输入时,单元格输入值的设定
- ORACLE的impdp和expdp命令
- 云栖小镇不是“镇”,就像中关村不是“村”。小镇是一个符号,就像起建于50年前的硅谷的“谷”,和100年前爱迪生所在的门洛公园。...
- Kafka设计解析(二):Kafka High Availability (上)