TLS1.3 概述---protocol

tls1.3
TLS1.3的最終版本,在2018年8月發佈,它包含着很多不同以往版本的改進,相對於之前版本安全性以及性能具有極大的提高,同時它也具備了更多的擴展和握手模式,那麼從實現完整TLS1.3結構的角度去學習TLS,我們應該從哪些方面入手呢?

什麼是TLS


TLS代表傳輸層安全性,並且是SSL(安全套接字層)的後繼者。TLS提供了Web瀏覽器和服務器之間的安全通信。連接本身是安全的,因爲使用對稱密碼術對傳輸的數據進行加密。密鑰是爲每個連接唯一生成的,並且基於在會話開始時協商的共享機密(也稱爲TLS握手)。HTTP + TLS = HTTPS

TLS歷史

history

TLS1.2


TLS1.2握手原理

tls1.2
握手的過程主要包括兩部分:

  • 參數協商
    客戶端向服務器發送client Hello消息,裏面包含client所支持的參數(密碼套件等等),還包含一些有用的參數(version、random、sessionId等等),server會從中選取自己支持的密碼套件、版本,通過server Hello發送給client,其中也包含一個隨機數還有一些其他字段。
  • 密鑰交換
    之後server會通過Server Key Exchange消息向client發送自己用於協商的公鑰,用於協商的算法是:ECDHE或DHE,同樣client通過 Client Key Exchange 發送自己的公鑰,這樣雙方都具有了彼此的公鑰,用它們生成臨時私鑰。client在收到server的Certificate消息之後會驗證server的身份,驗證通過纔會發送Client Key Exchange,其中還包含着pre-Masterkey,是對client生成的隨機數加密得來,最後使用三個隨機數生成Masterkey用於會話密鑰。

觀察圖片我們可以看出,TLS1.2整個握手過程需要2個RTT時間,而且每次握手都用到了非對稱加密算法簽名或者解密的操作,比較耗時和耗 CPU,每次都要傳輸證書,證書比較大會消耗帶寬。

  • RTT
    Round-Trip Time,往返時延,在計算機網絡中它也是一個重知要的性能指標,它表示從發送端發道送數據開始,到發送端收到來自接收端的確認版(接收端收到數據後便立即發送確認),總共經歷的時延。

TLS1.2會話恢復

  • SessionID
    將協商好的會話參數緩存在客戶端與服務器中,client下次握手時會帶上上次握手的SessionID,server對其查詢,若存在直接複用。
  • SessionTicket
    server將協商好的會話參數和密鑰加密發送給客戶端,client下次握手會將這個SessionTicket帶上,如果server解密成功就複用上次的會話參數和密鑰。

在TLS1.3中沒有了SessionID這種會話恢復模式,但是在client Hello中還會存在該字段,主要是爲了兼容版本。並且在SessionTicket模式中,添加了Ticket age,指的是會話是存在時間限制的,如果超過了該時間,那麼也就不能進行會話恢復。

sessionid
我們可以看到,使用SessionID恢復會話的時候,需要花費1個RTT的時間,在TLS1.3中一定情況下恢復會話只需要花費0個RTT!

在握手的過程中很多數據都會臨時計算,如果我們把這些數據提前計算出來,然後存入擴展當中,這樣就可以減少握手的時間爲1個RTT,TLS1.3實現的主要思想就是這樣的。

TLS1.3


TLS1.3握手

更快的訪問速度
TLS1.2handshake
這是一張TLS1.2的握手過程圖片,前面也分析過,它需要2個RTT的時間才能完成整個握手過程,下面我們看一下TLS1.3的握手過程圖:

tls1.3extension
我們會發現,其中存在一些以前版本從來沒有出現過的extension,比如:key_share、signature_algorithms等等,這只是一部分,還包含很多擴展,我會一一細說。正因爲這些擴展才使得TLS1.3的握手速度大大提高。

注:

  • +:上一消息的擴展消息
  • *:可選發送
  • {}:用握手層流密鑰加密
  • []:用流密鑰加密

client Hello

當client第一次連接server的時候,它需要向server發送client Hello 消息。

clientHello消息的結構:

  uint16 ProtocolVersion;
      opaque Random[32];

      uint8 CipherSuite[2];    /* Cryptographic suite selector */

      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;

簡單介紹一下比較重要的幾個字段的含義:

  • legacy_version
    在 TLS 以前的版本里,這個字段被用來版本協商和表示 Client 所能支持的 TLS 最高版本號。經驗表明,很多 Server 並沒有正確的實現版本協商,導致了 “version intolerance” —— Sever 拒絕了一些本來可以支持的 ClientHello 消息,只因爲這些消息的版本號高於 Server 能支持的版本號。在TLS1.3中,設置了一個supported_version的擴展來表明client所支持的版本。legacy_version 字段必須設置成 0x0303,這是 TLS 1.2 的版本號。在 TLS 1.3 中的 ClientHello 消息中的 legacy_version 都設置成 0x0303,supported_versions 擴展設置成 0x0304。主要是爲了兼容之前的TLS版本。
  • legacy_session_id
    前面我也提到過,TLS1.3中不再使用SessionID進行會話恢復,這一特性已經和預共享密鑰PSK合併了,設置這個字段的意義,主要也是爲了兼容之前版本,如果 Client 有 TLS 1.3 版本之前的 Server 設置的緩存 Session ID,那麼這個字段要填上這個 ID 值。兼容模式下,這個值必須是非空的,所以如果Client不能提供之前版本的值,那麼需要重新生成一個32字節的值。

還有cipher_suites、legacy_compression_methods,包含的是Client支持的密碼套件和壓縮算法,壓縮算法TLS1.3也已經不再支持了,這個字段主要還是爲了兼容版本,對於每個 ClientHello,該向量必須包含一個設置爲 0 的一個字節,它對應着 TLS 之前版本中的 null 壓縮方法。

supported_groups

這個擴展表明了 Client 支持的用於密鑰交換的命名組。按照優先級從高到低。這個擴展中的 “extension_data” 字段包含一個 “NamedGroupList” 值:

 enum {

          /* Elliptic Curve Groups (ECDHE) */
          secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
          x25519(0x001D), x448(0x001E),

          /* Finite Field Groups (DHE) */
          ffdhe2048(0x0100), ffdhe3072(0x0101), ffdhe4096(0x0102),
          ffdhe6144(0x0103), ffdhe8192(0x0104),

          /* Reserved Code Points */
          ffdhe_private_use(0x01FC..0x01FF),
          ecdhe_private_use(0xFE00..0xFEFF),
          (0xFFFF)
      } NamedGroup;
 struct {
          NamedGroup named_group_list<2..2^16-1>;
      } NamedGroupList;

key_share

這個擴展我覺得是TLS1.3的重大改變,它裏面包含了Client對應於supported_groups中參數的公鑰集,如果使用了曲線,則會表明所使用的曲線以及對應的公鑰。

   struct {
          NamedGroup group;
          opaque key_exchange<1..2^16-1>;
      } KeyShareEntry;
  • group:
    要交換的密鑰的命名組。
  • key_exchange:
    密鑰交換信息。這個字段的內容由特定的組和相應的定義確定。主要包含特定組的公鑰等信息。

在 ClientHello 消息中,“key_share” 擴展中的 “extension_data” 包含 KeyShareClientHello 值:

  struct {
          KeyShareEntry client_shares<0..2^16-1>;
      } KeyShareClientHello;
  • client_shares:
    按照 Client 偏好降序順序提供的 KeyShareEntry 值列表。

在golang中該結構的實現:

type KeyShareEntry struct {
	group 			NamedGroup
	length			uint16
	keyExchange		[]byte
}

type KeyShareClientHello struct {
	length 			uint16
	clientShares	[]KeyShareEntry
}

如果我們只實現橢圓曲線的話,首先需要選擇要使用的橢圓曲線,之後再選取隨機數生成公鑰,將公鑰存入keyExchange字段中,也就是說這個擴展已經將Client用於協商會話密鑰的參數提前計算出來,並存儲了起來,這與之前版本的形式server先選擇參數,然後發給Client,然後Client再計算相比較,極大的節省了時間和握手過程中佔用的CPU。

Client 可以提供與其提供的 support groups 一樣多數量的 KeyShareEntry 的值。每個值都代表了一組密鑰交換參數。例如,Client 可能會爲多個橢圓曲線或者多個 FFDHE 組提供 shares。每個 KeyShareEntry 中的 key_exchange 值必須獨立生成。Client 不能爲相同的 group 提供多個 KeyShareEntry 值。Client 不能爲,沒有出現在 Client 的 “supported_group” 擴展中列出的 group 提供任何 KeyShareEntry 值。Server 會檢查這些規則,如果違反了規則,立即發送 “illegal_parameter” alert 消息中止握手。

當選用PSK密鑰協商模式時,即使在supported_groups中不存在支持的算法也不會終止握手,這時候,server會向Client發送和serverhello具有相同結構的消息:HelloRetryRequest,它的目的主要是想讓Client作出一些改變以使得握手正常進行,在這種情況下,在 HelloRetryRequest 消息中,“key_share” 擴展中的 “extension_data” 字段包含 KeyShareHelloRetryRequest 值。

    struct {
          NamedGroup selected_group;
      } KeyShareHelloRetryRequest;
  • selected_group
    表明server所選擇的NamedGroup中組

Client收到此消息之後也會對其進行驗證,selected_group是否在NamedGroup中出現了,selected_group 沒有在原始的 ClientHello 中的 “key_share” 中出現過。如果上面 的檢查都失敗了,那麼 Client 必須通過 “illegal_parameter” alert 消息來中止握手。否則,在發送新的 ClientHello 時,Client 必須將原始的 “key_share” 擴展替換爲僅包含觸發 HelloRetryRequest 的 selected_group 字段中指示的組,這個組中只包含新的 KeyShareEntry。

在 ServerHello 消息中,“key_share” 擴展中的 “extension_data” 字段包含 KeyShareServerHello 值。

  struct {
          KeyShareEntry server_share;
      } KeyShareServerHello;
  • server_share:
    與 Client 共享的位於同一組的單個 KeyShareEntry 值。

我們再來看一下ECDHE的參數,對於 secp256r1,secp384r1 和 secp521r1,內容是以下結構體的序列化值:

      struct {
          uint8 legacy_form = 4;
          opaque X[coordinate_length];
          opaque Y[coordinate_length];
      } UncompressedPointRepresentation;

對端還要驗證對方的公鑰以確保爲有效的點:

  • 驗證公鑰不是無窮大點
  • 兩個整數x、y中間有正確的間隔
  • x、y是橢圓曲線方程的正確的解

小結

TLS 1.3 中優化握手:

  • client發送clientHello(extension)消息,extension中的support_groups中攜帶client支持的橢圓曲線的類型,並且在擴展key_share中計算出了相對應的公鑰,一起發送給server
  • server收到clientHello後會首先選擇相應的橢圓曲線參數計算自身的公鑰,從key_share擴展中提取相應的公鑰作爲密鑰協商的參數,計算主密鑰,並且把自身計算出的公鑰放到serverHello的擴展key_share中,然後發送serverHello等消息給client,Client從key_share中取出公鑰計算主密鑰。

TLS1.3會話恢復

在本文前面我提到過TLS1.3已經不再使用SessionID進行會話恢復了,現在主要使用的是SessionTicket進行會話恢復,但是又不同於TLS1.2中使用SessionTicket進行會話恢復的過程,也做出了一些改變,或者說是進行了一些更新(PSK)。

tls1.3handshake
會話恢復所花費的時間是1個RTT,這與整個的握手時間是一樣的。在TLS1.3中採用的會話恢復機制是PSK它與SessionTicket有些類似,Client通過PSK發送被server加密的會話緩存參數,如果server解密成功就可以直接複用會話,不需要再重新傳輸證書和協商密鑰了。

密鑰交換模式

  • PSK-Only
  • (EC)DHE
  • PSK with (EC)DHE(暫時還沒出現)

PSK handshake

在使用PSK密鑰交換模式時我們首先要了解幾個ClientHello的其它擴展:

Pre-Shared Key Exchange Modes

爲了使用PSK,client還需要發送Pre-Shared Key Exchange Modes擴展,它的含義是Client 僅支持使用具有這些模式的 PSK,這就限制了在這個 ClientHello 中提供的 PSK 的使用,也限制了 Server 通過 NewSessionTicket 提供的 PSK 的使用。
如果Client提供了 pre_shared_key擴展,那麼就必須提供該擴展

  enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

      struct {
          PskKeyExchangeMode ke_modes<1..255>;
      } PskKeyExchangeModes;
  • psk_ke:
    僅 PSK 密鑰建立。在這種模式下,Server 不能提供 key_share 值
  • psk_dhe_ke:
    PSK 和 (EC)DHE 建立。在這種模式下,Client 和 Server 必須提供 key_share值。

這樣的話就可以進行模式選擇,並作出相應的改變。未來分配的任何值都必須要能保證傳輸的協議消息可以明確的標識 Server 選擇的模式。目前 Server 選擇的值由 ServerHello 中存在的 key_share 表示。

Pre-Shared Key

該擴展是用來協商標識的,該標識是與PSK密鑰相關聯的給定握手所使用的預共享密鑰的標識。或者說是New Session Ticket+binders,由於在TLS1.3中,New Session Ticket可以在握手結束後可能多次發送,所以Pre-Shared Key可能會存儲多組對應的值,下面我們具體來了解一下它的結構。

struct {
          opaque identity<1..2^16-1>;
          uint32 obfuscated_ticket_age;
      } PskIdentity;

      opaque PskBinderEntry<32..255>;

      struct {
          PskIdentity identities<7..2^16-1>;
          PskBinderEntry binders<33..2^16-1>;
      } OfferedPsks;

      struct {
          select (Handshake.msg_type) {
              case client_hello: OfferedPsks;
              case server_hello: uint16 selected_identity;
          };
      } PreSharedKeyExtension;
  • identity:
    一個預共享密鑰的標籤。
  • obfuscated_ticket_age:
    SessionTicket的壽命的混淆版本,爲了防止一些相關連接的被動觀察者。而在TLS1.2中是不存在這樣的字段,即不會標識出客戶端已存在的時間,server收到後主要靠裏面的內容來判斷Ticket是否過期,而在TLS1.3中就增加了這樣一個字段來表示Ticket的壽命,因爲是明文傳輸所以會被觀察者發現,所以給時間加了一些調味品,是New Session Ticket中的ticket_age_add,因爲New Session Ticket本身就是被加密的,所以這個ticket_age_add只有通信兩端才知道。
  • 混淆的方法:
    用 ticket 時間(毫秒爲單位)加上 “ticket_age_add” 字段,最後對 2^32 取模。注意,NewSessionTicket 消息中的 “ticket_lifetime” 字段是秒爲單位,但是 “obfuscated_ticket_age” 是毫秒爲單位。
  • identities:
    Client 願意和 Server 協商的 identities 列表,其內容就是NewSessionTicket中的ticket部分。如果和 early_data 一起發送,第一個標識被用來標識 0-RTT 的。有關early_data後面還會說到。
  • selected_identity:
    server選擇的標識,是server在自己的 Pre-Shared Key擴展中自己設置的選擇的標識,表明正常解析了Client的擴展,其實選擇的話就是一個序號。

0-RTT

前面已經提到過TLS1.3已經將握手時間優化到了1-RTT,對比之前版本的速度已經快了很多,但是TLS1.3最終極的做法是0-RTT,即握手的時間是0-RTT。

Client發送ClientHello消息,除了在PSK模式中提到的那些擴展外,還應該具有一個early_data擴展,同理server發送serverHello中也應該包括該擴展,並表示其願意接受early_data,client發送完early_data後,發送End_Of_Early_Data報文表示client自己發送完了early_data。

如果 Server 提供了 early_data 擴展,Client 必須驗證 Server 的 selected_identity 是否爲 0。如果返回任何其他值,Client 必須使用 “illegal_parameter” alert 消息中止握手。下面我們看一下它的結構:

  struct {} Empty;

      struct {
          select (Handshake.msg_type) {
              case new_session_ticket:   uint32 max_early_data_size;
              case client_hello:         Empty;
              case encrypted_extensions: Empty;
          };
      } EarlyDataIndication;

其中的max_early_data_size字段表明,允許Client發送的最大0-RTT的數據量。

發生錯誤會導致0-RTT降級到1-RTT。

New Session Ticket

Post-Handshake Messages在 Server 接收到 Client 的 Finished 消息以後的任何時刻,它都可以發送 NewSessionTicket 消息。此消息在 ticket 值和從恢復主密鑰派生出來的 PSK 之間創建了唯一的關聯。

   struct {
          uint32 ticket_lifetime;
          uint32 ticket_age_add;
          opaque ticket_nonce<0..255>;
          opaque ticket<1..2^16-1>;
          Extension extensions<0..2^16-2>;
      } NewSessionTicket;
  • ticket_lifetime:
    這個字段表示 ticket 的生存時間,這個時間是以 ticket 發佈時間爲網絡字節順序的 32 位無符號整數表示以秒爲單位的時間。Server 禁止使用任何大於 604800秒(7 天)的值。值爲零表示應立即丟棄 ticket。無論 ticket_lifetime 如何,Client 都不得緩存超過 7 天的 ticket,並且可以根據本地策略提前刪除 ticket。Server 可以將 ticket 視爲有效的時間段短於 ticket_lifetime 中所述的時間段。
  • ticket_age_add:
    安全的生成的隨機 32 位值,用於模糊 Client 在 “pre_shared_key” 擴展中包含的 ticket 的時間。Client 的 ticket age 以模 2 ^ 32 的形式添加此值,以計算出 Client 要傳輸的值。Server 必須爲它發出的每個 ticket 生成一個新值。
  • ticket_nonce:
    每一個 ticket 的值,在本次連接中發出的所有的 ticket 中是唯一的。初始值是0,發送一個則++
  • ticket:
    這個值是被用作 PSK 標識的值

TLS1.3一些其他擴展和機制

降級保護機制

主要通過隨機數來實現,當協商TLS1.2或更老的版本,爲了響應ClientHello在random後8個字節填入特定的隨機值,若爲TLS1.2則後8個字節的值爲:

44 4F 57 4E 47 52 44 01

supported_version

主要功能的話前面也有提到,對Client標明所支持的TLS版本,對Server標明正在使用的TLS版本,如果協商TLS之前的版本,這個擴展必須帶上,若不存在該擴展,server要協商之前的版本,則中止握手,若存在server將禁止使用ClientHello中的legacy_version作爲版本協商的值,只能使用supported_versions中的值。
對於server:

  • 版本<TLS1.3 則設置serverHello.version,不能發送supported_version
  • 版本>=TLS1.3則必須發送supported_version擴展,還要設置serverHello.legacy_version爲0x0303,若擴展存在Client會忽略serverHello.legacy_version,而去讀取supported_version的值。
    struct {
          select (Handshake.msg_type) {
              case client_hello:
                   ProtocolVersion versions<2..254>;

              case server_hello: /* and HelloRetryRequest */
                   ProtocolVersion selected_version;
          };
      } SupportedVersions;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章