record類型
const ( recordTypeChangeCipherSpec recordType = 20 // changecipherspec表明發送端已取得用以生成連接參數的足夠信息。內容隨密碼套件不同 recordTypeAlert recordType = 21 // alert協議類型 recordTypeHandshake recordType = 22 // 握手協議類型 recordTypeApplicationData recordType = 23 // 應用數據協議類型 ) |
record head長5個字節
|--record type--|--version--|--data length--|
record type: 1個字節
version: 2個字節
data length: 2個字節 最大值16KB
一條record的長度是可變的, 參考: https://blog.csdn.net/liujiyong7/article/details/65632819
record是TLS 加解密的最小單元,至少要收到一個完整的record,才能加解密。
record 協議會執行以下動作
1. 分片,將應用層的數據進行分片
2. 生成序列號,防重放
3. 壓縮, 可選,握手協議會進行協商,但是不建議壓縮
4. 算HMAC, 計算數據HMAC,防止篡改,僞造
5. 發給tcp/ip, 下層協議,也可以是udp
HANDSHAKE協議
第一次Read或Write方法會自動調用Handshake()方法
完整的握手
ClientHello
type clientHelloMsg struct { raw []byte // 原始數據 vers uint16 // 協議版本,指示客戶端支持的最大協議版本 random []byte // 隨機數 32字節 sessionId []byte // 會話ID,第一次爲空,服務端藉助會話ID恢復會話,需要服務器端緩存。 cipherSuites []uint16 // 客戶端支持的加密套件列表 compressionMethods []uint8 // 客戶端支持的壓縮方法,默認爲null nextProtoNeg bool // 擴展NPN 是否支持次協議協商 serverName string // 擴展SNI 服務器名稱,通常爲域名,默認爲目標地址主機名。支持SNI擴展需要的字段。 ocspStapling bool // 擴展status_request 是否支持ocsp staping。全稱在線證書狀態檢查協議 (rfc6960),用來向 CA 站點查詢證書狀態 scts bool // 擴展SCT。是否支持SCT supportedCurves []CurveID // 擴展ellipic curve 列出支持的橢圓曲線名稱 https://tools.ietf.org/html/rfc4492#section-5.5.1 supportedPoints []uint8 // 擴展Point Formats 對橢圓曲線頂點進行可選壓縮 https://tools.ietf.org/html/rfc4492#section-5.5.2 默認不壓縮 ticketSupported bool // 擴展Sessionticket.是否支持會話ticket sessionTicket []uint8 // 擴展Sessionticket 會話ticket,區別於sessionId的新的會話恢復機制,這種機制不需要服務器端緩存。 signatureAndHashes []signatureAndHash // 擴展SignatureAlgorithms 簽名和散列算法 secureRenegotiation []byte // 擴展RenegotiationInfo 如果請求重新協商,就會發起一次新的握手。 secureRenegotiationSupported bool // 擴展RenegotiationInfo 是否支持renegotiation_info擴展 安全重新協商 alpnProtocols []string // 擴展ALPN 應用層協議協商。 } |
常用的擴展
支持ECC需要兩個擴展 ellipic curve,point formats
SNI擴展用來實現安全虛擬主機。單個服務器可能配有多個證書,服務端使用SNI來區分請求使用的是哪個證書。
SPDY使用NPN擴展協商使用何種應用層協議
HTTP2的協議協商過程使用ALPN擴展
ALPN是由客戶端給服務器發送一個協議清單,由服務器來選擇一個。NPN正好相反
加密套件
使用openssl ciphers -V | column -t 查看本機支持的cipher suite列表。如下所示:
0xC0 , 0x30 - ECDHE-RSA-AES256-GCM-SHA384 TLSv1. 2 Kx=ECDH Au=RSA Enc=AESGCM( 256 ) Mac=AEAD 0xC0 , 0x2C - ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1. 2 Kx=ECDH Au=ECDSA Enc=AESGCM( 256 ) Mac=AEAD 0xC0 , 0x28 - ECDHE-RSA-AES256-SHA384 TLSv1. 2 Kx=ECDH Au=RSA Enc=AES( 256 ) Mac=SHA384 0xC0 , 0x24 - ECDHE-ECDSA-AES256-SHA384 TLSv1. 2 Kx=ECDH Au=ECDSA Enc=AES( 256 ) Mac=SHA384 |
Kx表示密鑰交換算法 Au表示認證算法 Enc表示加密算法 Mac表示消息認證碼算法
每一種cipher suite由一個uint16整數標示
golang的默認cipher suites (如果監測到AES-GSM硬件,會優先提供aes-gcm的加密套件)
topCipherSuites = []uint16{ TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, } |
EC參數填充
supportedCurves的值默認填充以下參數:
var defaultCurvePreferences = []CurveID{X25519, CurveP256, CurveP384, CurveP521} // 23 24 25 29
https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 從中可查到是secp256r1 secp384r1 secp521r1 x25519
supportedPoints 默認爲0 不壓縮
ServerHello
消息的意義是將服務器選擇的參數傳回客戶端,結構與clientHello相似,每個字段只包含一個選項
type serverHelloMsg struct { raw []byte // 原始數據 vers uint16 // 服務端選擇的版本號 通常爲client server都支持的版本中最高的。 common.go:mutualVersion() random []byte // 服務端生成的隨機數 32字節 sessionId []byte // cipherSuite uint16 // 服務端選擇的加密套件 通常爲client server都支持的套件中,最靠前的。所以套件的順序是有講究的。 compressionMethod uint8 // 選擇的壓縮方法 只會選擇不壓縮。如果客戶端不支持不壓縮,會報錯 nextProtoNeg bool // client支持,則server支持 nextProtos []string // 服務端支持的應用層協議 ocspStapling bool // scts [][]byte // 簽名的證書時間戳? ticketSupported bool // secureRenegotiation []byte // secureRenegotiationSupported bool // 與client一致 alpnProtocol string // 服務端選擇的應用層協議,client server都支持的協議中,最靠前的。如果client端爲空,則填充爲服務器支持的協議 } |
根據ClientHello,填充ServerHello消息,主要的代碼在 handshake_server.go:readClientHello()
EC參數填充
preferredCurves := c.config.curvePreferences() for _, curve := range hs.clientHello.supportedCurves { for _, supported := range preferredCurves { // 默認X25519, CurveP256, CurveP384, CurveP521, if supported == curve { supportedCurve = true break Curves } } } ...... var preferenceList, supportedList []uint16 // c.config.PreferServerCipherSuites本字段控制服務端是選擇客戶端最期望的密碼組合還是服務端最期望的密碼組合。 // 如果本字段爲真,服務端會優先選擇CipherSuites字段中靠前的密碼組合使用。 if c.config.PreferServerCipherSuites { preferenceList = c.config.cipherSuites() supportedList = hs.clientHello.cipherSuites } else { preferenceList = hs.clientHello.cipherSuites supportedList = c.config.cipherSuites() } for _, id := range preferenceList { if hs.setCipherSuite(id, supportedList, c.vers) { //選擇一個優先的ciphersuite break } } |
Certificate
可選。攜帶X.509證書鏈,證書鏈以ASN.1 DSR編碼的一系列證書。主證書必須第一個發送,中間證書按照正確的順序跟在後面,根證書需省略。
證書需與選擇的算法套件一致。
type certificateMsg struct { raw []byte certificates [][]byte } |
ServerKeyExchange
攜帶密鑰交換的額外數據,消息內容對不同的協商算法套件存在差異。某些場景下不需要發送
type serverKeyExchangeMsg struct { raw []byte // 對於ECDHE, 填充serverECDHParams key []byte } |
func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) { if ka.curveid == X25519 { // 選擇的橢圓曲線是X25519 var scalar, public [32]byte if _, err := io.ReadFull(config. rand (), scalar[:]); err != nil { return nil, err } curve25519.ScalarBaseMult(& public , &scalar) // 生成公鑰 私鑰 ka.privateKey = scalar[:] // 私鑰自己保存 ecdhePublic = public [:] // 公鑰填充進ECDHParams參數 } else { // 選擇其他的橢圓曲線 curve, ok := curveForCurveID(ka.curveid) if !ok { return nil, errors.New( "tls: preferredCurves includes unsupported curve" ) } var x, y *big.Int var err error ka.privateKey, x, y, err = elliptic.GenerateKey(curve, config. rand ()) // 生成私鑰 公鑰對 if err != nil { return nil, err } ecdhePublic = elliptic.Marshal(curve, x, y) } serverECDHParams := make([]byte, 1+2+1+len(ecdhePublic)) // 計算長度,分配內存 serverECDHParams[0] = 3 // named curve // 選擇的橢圓曲線 serverECDHParams[1] = byte(ka.curveid >> 8) // 保存橢圓曲線高8位 serverECDHParams[2] = byte(ka.curveid) // 保存橢圓曲線低8位 serverECDHParams[3] = byte(len(ecdhePublic)) // 公鑰 ecdhePublic 長度 copy(serverECDHParams[4:], ecdhePublic) // 公鑰 ... skx := new (serverKeyExchangeMsg) sigAndHashLen := 0 if ka.version >= VersionTLS12 { sigAndHashLen = 2 } skx.key = make([]byte, len(serverECDHParams)+sigAndHashLen+2+len(sig)) copy(skx.key, serverECDHParams) k := skx.key[len(serverECDHParams):] if ka.version >= VersionTLS12 { k[0] = sigAndHash.hash // hash k[1] = sigAndHash.signature // 簽名 k = k[2:] } k[0] = byte(len(sig) >> 8) // 簽名長度高8位 k[1] = byte(len(sig)) // 簽名長度低8位 copy(k[2:], sig) // 簽名 } |
ServerHelloDone
表明服務器已經將所有握手消息發送完畢
type serverHelloDoneMsg struct {} |
ClientKeyExchange
攜帶客戶端爲密鑰交換提供的所有信息。內容隨不同的密碼套件會不同。
type clientKeyExchangeMsg struct { raw []byte ciphertext []byte } |
Finished
type finishedMsg struct { raw []byte verifyData []byte } |
客戶端身份驗證
CertificateRequest
服務器使用本消息對客戶端進行身份認證
type certificateRequestMsg struct { raw []byte // hasSignatureAndHash indicates whether this message includes a list // of signature and hash functions. This change was introduced with TLS // 1.2. hasSignatureAndHash bool certificateTypes []byte signatureAndHashes []signatureAndHash certificateAuthorities [][]byte } |
CertificateVerify
客戶端使用本消息證明自己的私鑰與之前發送的客戶端證書中的公鑰對應
type certificateVerifyMsg struct { raw []byte hasSignatureAndHash bool signatureAndHash signatureAndHash signature []byte } |
密鑰交換
ECDHE
Elliptic curve Diffie-Hellman 橢圓曲線密鑰交換,如名稱所示,基於橢圓曲線加密。
數學原理
安全性建立在離散對數計算很困難的基礎上
Diffie-Hellman
兩個基本概念:
- 本原根:如果整數 a 是素數 p 的本原根,則 a, a^2, …, a^(p-1) 在 mod p 下都不相同。
- 離散對數:對任意整數 b 和素數 p 的本原根 a,存在唯一的指數 i 滿足: b ≡ a^i mod p (0≤i≤p-1)
則稱 i 是 b 的以 a 爲底的模 p 的離散對數
a,p是公開的,已知i,求b比較容易。已知b,求i非常困難
示例:
假設 client 和 server 需要協商密鑰,p=2579,則本原根 a = 2。
1, Client 選擇隨機數 Kc = 123 做爲自己的私鑰,計算 Yc = a^Kc mod p = 2^123 mod 2579 = 2400,把 Yc 作爲公鑰發送給 server。
2, Server 選擇隨機數 Ks = 293 作爲私鑰,計算 Ys = a^Ks mod p = s^293 mod 2579 = 968,把 Ys 作爲公鑰發送給 client。
3, Client 計算共享密鑰:secrect = Ys^Kc mod (p) = 968^123 mod(2579) = 434
4, Server 計算共享密鑰:secrect = Yc^Ks mod(p) =2400^293 mod(2579) =434
上述公式中的 Ys,Yc,P, a, 都是公開信息,可以被中間者查看,只有 Ks,Kc 作爲私鑰沒有公開,當私鑰較小時,通過窮舉攻擊能夠計算出共享密鑰,但是當私鑰非常大時,窮舉攻擊肯定是不可行的。
ECC
http://www.8btc.com/introduction
橢圓曲線簡化定義爲 y^2 = x^3+ax+b
觀察以上橢圓曲線圖中的四個點,定義一個阿貝爾羣
• 羣中的元素是一條橢圓曲線上的點;
• 單位元爲無窮遠點O;
• 點P的逆元是其關於x-軸的對稱點;
• 加法,滿足以下規則: 對於3個處在同一直線上的非零點 P, Q 和 R, 它們的和 P + Q + R = 0.
考慮上圖,在切線的情況下, P+Q+R =0, 切點P=Q, 所以P+P=-R
定義乘法
nP = P+P+P...+P n個P相加
簡寫爲Q=nP
已知n,P求解Q,比較容易
已知Q,P,求解n,非常困難
加密使用的橢圓曲線公式 y^2 mod p = (x^3+ax +b) mod p
1.挑選基點 G = (x, y),G 的階爲 n。n 爲滿足 nG = 0 的最小正整數。
2.Client 選擇私鑰 Kc (0 <Kc<n ),產生公鑰 Yc =Kc *G
3.server 選擇私鑰 Ks 併產生公鑰 Ys =Ks*G
4.client 計算共享密鑰 K = Kc*Ys ,server 端計算共享密鑰 Ks*Yc ,這兩者的結果是一樣的
Kc*Ys = Kc*(Ks*G) = Ks*(Kc*G) = Ks*Yc
協商過程
tls利用兩個擴展elliptic_curves, point_formats 實現EC功能
elliptic_curves在clientHello消息中列出支持的橢圓曲線名稱
1, 瀏覽器發送 client_hello,包含一個隨機數 random1,同時需要有 2 個擴展:
a) Elliptic_curves:客戶端支持的曲線類型和有限域參數。現在使用最多的是 256 位的素數域,參數定義如上節所述。
b) Ec_point_formats:支持的曲線點格式,默認都是 uncompressed。
2, 服務端回覆 server_hello,包含一個隨機數 random2 及 ECC 擴展。
3, 服務端回覆 certificate,攜帶了證書公鑰。
4, 服務端生成 ECDH 臨時公鑰,同時回覆 server_key_exchange,包含三部分重要內容:
a) ECC 相關的參數。
b) ECDH 臨時公鑰。
c) ECC 參數和公鑰生成的簽名值,用於客戶端校驗。
5, 瀏覽器接收 server_key_exchange 之後,使用證書公鑰進行簽名解密和校驗,獲取服務器端的 ECDH 臨時公鑰,生成會話所需要的共享密鑰。
至此,瀏覽器端完成了密鑰協商。
6, 瀏覽器生成 ECDH 臨時公鑰和 client_key_exchange 消息,跟 RSA 密鑰協商不同的是,這個消息不需要加密了。
7, 服務器處理 client_key_exchang 消息,獲取客戶端 ECDH 臨時公鑰。
8, 服務器生成會話所需要的共享密鑰。
9, Server 端密鑰協商過程結束。
GOLANG實現
func (hs *serverHandshakeState) establishKeys() error { c := hs.c // 生成密鑰協商需要的密鑰,這6個值,在加密過程中會使用 clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV := keysFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.clientHello.random, hs.hello.random, hs.suite.macLen, hs.suite.keyLen, hs.suite.ivLen) var clientCipher, serverCipher interface{} var clientHash, serverHash macFunction if hs.suite.aead == nil { clientCipher = hs.suite.cipher(clientKey, clientIV, true /* for reading */ ) clientHash = hs.suite.mac(c.vers, clientMAC) serverCipher = hs.suite.cipher(serverKey, serverIV, false /* not for reading */ ) serverHash = hs.suite.mac(c.vers, serverMAC) } else { clientCipher = hs.suite.aead(clientKey, clientIV) serverCipher = hs.suite.aead(serverKey, serverIV) } c.in.prepareCipherSpec(c.vers, clientCipher, clientHash) c.out.prepareCipherSpec(c.vers, serverCipher, serverHash) return nil } |
c.vers tls version handshake_server.go readClientHello() 142 協商最大的版本號
hs.suite 加密套件 handshake_server.go readClientHello() 265
hs.masterSecret 主密鑰 handshake_server.go doFullHandshake 498
hs.clientHello.random clientHello傳進來的隨機數
hs.hello.random serverHello 隨機數
hs.suite.macLen echde_ecdsa_aes128_gcm_sha256 0 macLen keyLen ivLen都在cipher_suite.go中定義
hs.suite.keyLen echde_ecdsa_aes128_gcm_sha256 16
hs.suite.ivLen echde_ecdsa_aes128_gcm_sha256 4
ALERT協議
以簡單的通知機制告知對端通信出現異常。通常會攜帶close_notify異常,在連接關閉時使用。
需要關閉從client往server發送ALERT,以避免server端過多timeout
加密
AES128-GCM-SHA256
分組加密
type serverHandshakeState struct { c *Conn // tls連接 clientHello *clientHelloMsg // ClientHello消息 hello *serverHelloMsg // ServeHello 消息 suite *cipherSuite // 加密套件 ellipticOk bool // ecdsaOk bool // rsaDecryptOk bool // rsaSignOk bool // sessionState *sessionState // finishedHash finishedHash // masterSecret []byte // 主密鑰 certsFromClient [][]byte // 客戶端證書 cert *Certificate // 證書 cachedClientHelloInfo *ClientHelloInfo // } |
chacha20-poly1305
流加密
參考資料
1.https://github.com/WeMobileDev/article
2.https://blog.helong.info/blog/2015/09/07/tls-protocol-analysis-and-crypto-protocol-design/
3.http://www.vuln.cn/6521 TLS擴展
4.http://blog.csdn.net/u010129119/article/details/54090814 tls 1.3規範
5.https://tlswg.github.io/tls13-spec/draft-ietf-tls-tls13.html tls 1.3最新草案
7.https://tools.ietf.org/html/rfc5246 tls 1.2 rfc
8.http://blog.csdn.net/t0mato_/article/details/53160772 AES-GCM
9.http://blog.csdn.net/fw0124/article/details/8472560 分組加密的四種模式