ProtocolHTTPS

TLS 1.3 Record Protocol

TLS 记录协议接收要传输的消息,将数据分段为可管理的块,保护记录并传输结果。收到的数据经过验证,解密,重新组装,然后交付给更上层的协议。

TLS 记录允许同一层记录层上复用多个更高层的协议。本文档指定了 4 种内容类型:handshake,application_data,alert 和 change_cipher_spec。change_cipher_spec 记录仅用于兼容性目的。

实现方可能会在发送或接收第一个 ClientHello 消息之后,和,在接收到对等方的 Finished 消息之前的任何时间接收由单字节值 0x01 组成的未加密的类型 change_cipher_spec 的记录,如果接收到了这种记录,则必须简单地丢弃它而不进行进一步处理。请注意,此记录可能出现在握手中,这时候实现方是期望保护记录的,因此有必要在尝试对记录进行去除保护之前检测此情况。接收到任何其他 change_cipher_spec 值或接收到受保护的 change_cipher_spec 记录的实现方必须使用 "unexpected_message" alert 消息中止握手。如果实现方检测到在第一个 ClientHello 消息之前或在对等方的 Finished 消息之后收到的 change_cipher_spec 记录,则必须将其视为意外记录类型(尽管无状态的 Server 可能无法将这些情况与允许的情况区分开)

除非协商了某些扩展,否则实现方绝不能发送本文档中未定义的记录类型。如果 TLS 实现方收到意外的记录类型,它必须使用 "unexpected_message" alert 消息终止连接。新记录内容类型值由 IANA 在 TLS ContentType 注册表中分配,具体见第 11 节。

一. Record Layer

记录层将信息块分段为 TLSPlaintext 记录,TLSPlaintext 中包含 2^14 字节或更少字节块的数据。根据底层 ContentType 的不同,消息边界的处理方式也不同。任何未来的新增的内容类型必须指定适当的规则。请注意,这些规则比 TLS 1.2 中强制执行的规则更加严格。

握手消息可以合并为单个 TLSPlaintext 记录,或者在几个记录中分段,前提是:

  • 握手消息不得与其他记录类型交错。也就是说,如果握手消息被分成两个或多个记录,则它们之间不能有任何其他记录。

  • 握手消息绝不能跨越密钥更改。实现方必须验证密钥更改之前的所有消息是否与记录边界对齐; 如果没有,那么他们必须用 "unexpected_message" alert 消息终止连接。因为 ClientHello,EndOfEarlyData,ServerHello,Finished 和 KeyUpdate 消息可以在密钥更改之前立即发生,所以实现方必须将这些消息与记录边界对齐。

实现方绝不能发送握手类型的零长度片段,即使这些片段包含填充。

Alert 消息禁止在记录之间进行分段,并且多条 alert 消息不得合并为单个 TLSPlaintext 记录。换句话说,具有 alert 类型的记录必须只包含一条消息。

应用数据消息包含对 TLS 不透明的数据。应用数据消息始终受到保护。可以发送应用数据的零长度片段,因为它们可能作为流量分析对策使用。应用数据片段可以拆分为多个记录,也可以合并为一个记录。

      enum {
          invalid(0),
          change_cipher_spec(20),
          alert(21),
          handshake(22),
          application_data(23),
          (255)
      } ContentType;

      struct {
          ContentType type;
          ProtocolVersion legacy_record_version;
          uint16 length;
          opaque fragment[TLSPlaintext.length];
      } TLSPlaintext;
  • type:
    用于处理封闭片段的更高级协议。

  • legacy_record_version:
    对于除初始 ClientHello 之外的 TLS 1.3 实现生成的所有记录(即,在 HelloRetryRequest 之后未生成的记录),必须将其设置为 0x0303,其中出于兼容性目的,它也可以是0x0301。该字段已弃用,必须忽略它。在某些情况下,以前版本的 TLS 将在此字段中使用其他值。

  • length:
    TLSPlaintext.fragment 的长度(以字节为单位)。长度不得超过 2 ^ 14 字节。接收超过此长度的记录的端点必须使用 "record_overflow" alert 消息终止连接。

  • fragment:
    正在传输的数据。此字段的值是透明的,它并被视为一个独立的块,由类型字段指定的更高级别协议处理。

本文档描述了使用版本 0x0304 的 TLS 1.3。此版本的值是历史的,源自对 TLS 1.0 使用 0x0301 和对 SSL 3.0 使用 0x0300。为了最大化向后兼容性,包含初始 ClientHello 的记录应该具有版本 0x0301(对应 TLS 1.0),包含第二个 ClientHello 或 ServerHello 的记录必须具有版本 0x0303(对应 TLS 1.2)。在协商 TLS 的早期版本时,端点需要遵循附录 D 中提供的过程和要求。

当尚未使用记录保护时,TLSPlaintext 结构是直接写入传输线路中的。一旦记录保护开始,TLSPlaintext 记录将受到保护并按照下节部分描述的那样进行发送。请注意,应用数据记录不得写入未受保护的连接中。(有关详细信息,请参阅第 2 节)

二. Record Payload Protection

记录保护功能将 TLSPlaintext 结构转换为 TLSCiphertext 结构。去除保护功能和保护功能互为逆过程。在 TLS 1.3 中,与先前版本的 TLS 相反,所有密码都是“具有关联数据的认证加密”(AEAD)[RFC5116]。 AEAD 功能提供统一的加密和认证操作,将明文转换为经过认证的密文,然后再返回。每个加密记录由一个明文标题后跟一个加密的主体组成,该主体本身包含一个类型和可选的填充。

      struct {
          opaque content[TLSPlaintext.length];
          ContentType type;
          uint8 zeros[length_of_padding];
      } TLSInnerPlaintext;

      struct {
          ContentType opaque_type = application_data; /* 23 */
          ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
          uint16 length;
          opaque encrypted_record[TLSCiphertext.length];
      } TLSCiphertext;
  • content:
    TLSPlaintext.fragment 值,包含握手或警报消息的字节编码,或要发送的应用数据的原始字节。

  • type:
    TLSPlaintext.type 值,包含记录的内容类型。

  • zeros:
    在类型字段之后的明文中可以出现任意长度的零值字节。只要总数保持在记录大小限制范围内,这个字段为发件人提供了按所选的量去填充任何 TLS 记录的机会。更多详细信息,请参见[第 5.4 节]

  • opaque_type:
    TLSCiphertext 记录外部的 opaque_type 字段始终设置为值23(application_data),以便与解析以前版本的 TLS 的中间件向外兼容。解密后,在 TLSInnerPlaintext.type 中找到记录的实际内容类型。

  • legacy_record_version:
    legacy_record_version 字段始终为 0x0303。TLS 1.3 TLSCiphertexts 在协商 TLS 1.3 之后才生成,因此没有历史兼容性问题可能会收到其他值。请注意,握手协议(包括 ClientHello 和 ServerHello 消息)会对协议版本进行身份验证,因此该值是多余的。

  • length:
    TLSCiphertext.encrypted_record 的长度(以字节为单位),它是内容和填充的长度之和,加上内部内容类型的长度加上 AEAD 算法添加的任何扩展。长度不得超过 2 ^ 14 + 256 字节。接收超过此长度的记录的端点必须使用 "record_overflow" alert 消息终止连接。

  • encrypted_record:
    AEAD 加密形式的序列化 TLSInnerPlaintext 结构。

AEAD 算法将单个密钥,随机数,明文和要包含在认证检查中的“附加数据”作为输入,如[RFC5116的第2.1节]所述。key 是 client_write_key 或 server_write_key,nonce 是从序列号和 client_write_iv 或 server_write_iv 中派生出来的(参见[第5.3节]),附加数据的输入是记录头,例如:

      additional_data = TLSCiphertext.opaque_type ||
                        TLSCiphertext.legacy_record_version ||
                        TLSCiphertext.length

作为输入 AEAD 算法的明文是编码后的 TLSInnerPlaintext 结构。第7.3节定义了流量密钥的派生。

AEAD 输出包括 AEAD 加密操作的密文输出。由于包含 TLSInnerPlaintext.type 和发送方提供的任何填充,明文的长度大于相应的 TLSPlaintext.length。AEAD 输出的长度通常大于明文,但是其中一部分也会随着 AEAD 算法的变化而变化。

由于密码可能包含填充,因此开销量可能随着明文的不同长度而变化。

      AEADEncrypted =
          AEAD-Encrypt(write_key, nonce, additional_data, plaintext)

TLSCiphertext 的 encrypted_record 字段设置为 AEADEncrypted。

为了解密和验证,密码会把密钥,随机数,附加数据和 AEADEncrypted 值作为输入。输出的是明文或表示解密失败的错误。这里没有单独的完整性检查。

      plaintext of encrypted_record =
          AEAD-Decrypt(peer_write_key, nonce,
                       additional_data, AEADEncrypted)

如果解密失败,接收方必须使用 "bad_record_mac" alert 消息终止连接。

TLS 1.3 中使用的 AEAD 算法不得产生大于 255 个八位字节的扩展。从对端的 TLSCiphertext.length 接收记录,如果 TLSCiphertext.length 大于 2 ^ 14 + 256 个八位字节,则必须用 "record_overflow" 消息终止连接。这个限制源自于:TLSInnerPlaintext 长度的最大值是 2 ^ 14 个八位字节 + 1 个八字节的 ContentType + 255 个八位字节的最大 AEAD 扩展。

三. Per-Record Nonce

分别维护 64 位序列号以读取和写入记录。在读取或写入每个记录之后,适当的序列号加 1。每个序列号在连接开始时和每次更改密钥时都设置为零; 在特定流量密钥下传输的第一条记录必须使用序列号 0。

因为序列号的大小是 64 位,所以它们不应该 wrap。如果 TLS 实现方需要 wrap 序列号,它必须重新生成密钥(第4.6.3节)或终止连接。

每个 AEAD 算法将为 per-record 的随机数指定一系列可能的长度,从 N_MIN 字节到输入的 N_MAX 字节[RFC5116]。 对于 AEAD 算法,TLS 的 per-record 随机数(iv_length)的长度设置为 8 字节和 N_MIN 中的较大者(参见[RFC5116],第4节)。 其中 N_MAX 小于 8 个字节的 AEAD 算法不得与 TLS 一起使用。AEAD 结构的 per-record 随机数形成如下:

  1. 64 位的记录序列号是按网络字节顺序编码,并在左边用零填充到 iv_length。
  2. 填充的序列号要和 client_write_iv 或者 server_write_iv 进行异或(取决于角色)

得到的值(长度为 iv_length)被用作 per-record 的随机数。

注意:这与 TLS 1.2 中的结构不同了,TLS 1.2 指定了部分显式的随机数。

四. Record Padding

所有加密的 TLS 记录都可以被填充,从而扩大 TLSCiphertext 的大小。这种做法允许发送者隐藏来自观察者的流量大小。

生成 TLSCiphertext 记录时,实现方可以选择填充。未填充的记录只是填充长度为零的记录。填充是在加密之前附加到 ContentType 字段的一串零值字节。实现方必须在加密之前将填充的八位字节全部设置为零。

如果发送者需要,应用数据记录可以包含零长度 TLSInnerPlaintext.content。 这允许在对 activity 存在或者不存在敏感的情况下,产生合理大小的覆盖流量。实现方绝不能发送具有零长度 TLSInnerPlaintext.content 的握手和 alert 记录; 如果收到这样的消息,接收实现方必须用 "unexpected_message" alert 消息终止连接。

发送的填充由记录保护机制自动验证;在成功解密 TLSCiphertext.encrypted_record后,接收实现方从末端向前开始扫描字段,直到找到非零八位字节。该非零八位字节是消息的内容类型 type 字段。选择此填充方案是因为它允许以任意大小(从零到 TLS 记录大小限制)填充任何加密的 TLS 记录,而不引入新的内容类型。该设计还强制执行全零填充八位字节,以便快速检测填充错误。

实现方必须将扫描范围限制为只扫描从 AEAD 解密返回的明文。如果接收实现方在明文中没有找到非零八位字节,它必须以 "unexpected_message" alert 消息终止连接。

填充并不会改变整体记录大小限制:完整编码的 TLSInnerPlaintext 不得超过 2 ^ 14 + 1 个八位字节。如果最大片段长度减少 - 例如,来自 [RFC8449] 的 record_size_limit 扩展 - 那么减少的限制适用于完整的纯文本,包括内容类型和填充。

选择一个合理的填充策略,比如何时填充,填充多少,这是一个复杂的主题,超出了本规范的范围。如果 TLS 之上的应用层协议具有自己的填充,则最好在应用层内填充应用数据 TLS 记录。但是,加密握手或 alert 记录的填充仍必须在 TLS 层处理。稍后的文档可以定义填充选择算法或通过 TLS 扩展或一些其他手段定义填充策略的请求机制。

五. Limits on Key Usage

明文数量存在一些加密限制,这些限制可以在给定的一组密钥下安全加密。[AEAD-LIMITS] 在假设基础原语(AES 或 ChaCha20)没有弱点的情况下提供了对这些限制的分析。在达到这些限制之前,实现方应该按照第 4.6.3 节中的描述进行密钥更新。

对于 AES-GCM,在给定连接上可以加密多达 2 ^ 24.5 个全尺寸记录(大约 2400 万个),同时为认证加密(AE)安全性保持大约 2 ^ -57 的安全余量。对于 ChaCha20 / Poly1305,记录序列号将在达到安全限制之前就会 wrap。


Reference:

RFC 8446

GitHub Repo:Halfrost-Field

Follow: halfrost · GitHub

Source: https://halfrost.com/TLS_1.3_Record_Protocol/

支付宝扫码打赏 微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章

一缕殇流化隐半边冰霜's Picture
一缕殇流化隐半边冰霜

我是于德志 (@halfrost),一名来自中国的 iOS 开发者,已退役 acmer 。现居上海。从 2016 年开始写博客记录自己技术成长的一点一滴,到年底,成为简书推荐作者,2016 年度掘金最优秀的 10 佳原创作者。吾笃信:天道酬勤,勤能补拙。地道酬实,实能不弱。人道酬德,德能补寡。 目前就职于 饿了么 。

Shanghai「上海」 https://halfrost.com

Subscribe to Halfrost's Field | 冰霜之地

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!

Comments