TLS 1.3 Record Protocol

The TLS record protocol receives messages to be transmitted, fragments the data into manageable blocks, protects the records, and transmits the result. Received data is verified, decrypted, reassembled, and then delivered to higher-level protocols.

TLS records allow multiple higher-layer protocols to be multiplexed over the same record layer. This document specifies four content types: handshake, application_data, alert, and change_cipher_spec. change_cipher_spec records are used only for compatibility purposes.

Implementations may receive an unencrypted record of type change_cipher_spec consisting of the single-byte value 0x01 at any time after sending or receiving the first ClientHello message and before receiving the peer’s Finished message. If such a record is received, it MUST simply be discarded without further processing. Note that this record can appear during the handshake, at a point where the implementation expects records to be protected, so it is necessary to detect this case before attempting to remove protection from the record. An implementation that receives any other change_cipher_spec value, or that receives a protected change_cipher_spec record, MUST abort the handshake with an “unexpected_message” alert. If an implementation detects a change_cipher_spec record received before the first ClientHello message or after the peer’s Finished message, it MUST treat it as an unexpected record type (although a stateless Server may be unable to distinguish these cases from the permitted cases).

Unless some extension has been negotiated, implementations MUST NOT send record types that are not defined in this document. If a TLS implementation receives an unexpected record type, it MUST terminate the connection with an “unexpected_message” alert. New record content type values are assigned by IANA in the TLS ContentType registry, as described in Section 11.

1. Record Layer

The record layer fragments blocks of information into TLSPlaintext records, where each TLSPlaintext contains a block of data of 2^14 bytes or less. Message boundaries are handled differently depending on the underlying ContentType. Any future additional content type MUST specify appropriate rules. Note that these rules are stricter than those enforced in TLS 1.2.

Handshake messages may be coalesced into a single TLSPlaintext record or fragmented across several records, provided that:

  • Handshake messages MUST NOT be interleaved with other record types. That is, if a handshake message is split across two or more records, there cannot be any other records between them.

  • Handshake messages MUST NOT span a key change. Implementations MUST verify that all messages before a key change are aligned with record boundaries; if they are not, they MUST terminate the connection with an “unexpected_message” alert. Because ClientHello, EndOfEarlyData, ServerHello, Finished, and KeyUpdate messages can occur immediately before a key change, implementations MUST align these messages with record boundaries.

Implementations MUST NOT send zero-length fragments of the handshake type, even if those fragments contain padding.

Alert messages MUST NOT be fragmented across records, and multiple alert messages MUST NOT be coalesced into a single TLSPlaintext record. In other words, a record of type alert MUST contain exactly one message.

Application data messages contain data that is opaque to TLS. Application data messages are always protected. Zero-length fragments of application data may be sent, because they can be used as a countermeasure against traffic analysis. Application data fragments may be split across multiple records or coalesced into a single record.

      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:
    The higher-level protocol used to process the enclosed fragment.

  • legacy_record_version:
    For all records generated by TLS 1.3 implementations except the initial ClientHello (i.e., records not generated after a HelloRetryRequest), this MUST be set to 0x0303, although for compatibility purposes it may also be 0x0301. This field is deprecated and MUST be ignored. In some cases, earlier versions of TLS used other values in this field.

  • length:
    The length of TLSPlaintext.fragment in bytes. The length MUST NOT exceed 2 ^ 14 bytes. An endpoint that receives a record longer than this limit MUST terminate the connection with a “record_overflow” alert message.

  • fragment:
    The data being transmitted. The value of this field is transparent and is treated as an independent block to be processed by the higher-level protocol specified by the type field.

This document describes TLS 1.3, which uses version 0x0304. This version value is historical, derived from the use of 0x0301 for TLS 1.0 and 0x0300 for SSL 3.0. To maximize backward compatibility, records containing the initial ClientHello SHOULD have version 0x0301 (corresponding to TLS 1.0), and records containing the second ClientHello or ServerHello MUST have version 0x0303 (corresponding to TLS 1.2). When negotiating earlier versions of TLS, endpoints need to follow the procedures and requirements provided in Appendix D.

When record protection has not yet been used, the TLSPlaintext structure is written directly onto the wire. Once record protection begins, TLSPlaintext records are protected and sent as described in the following section. Note that application data records MUST NOT be written to an unprotected connection. (For details, see Section 2)

II. Record Payload Protection

The record protection function converts a TLSPlaintext structure into a TLSCiphertext structure. The deprotection function and the protection function are inverses of each other. In TLS 1.3, unlike previous versions of TLS, all ciphers are “Authenticated Encryption with Associated Data” (AEAD)[RFC5116]. The AEAD function provides unified encryption and authentication operations, converting plaintext into authenticated ciphertext and back again. Each encrypted record consists of a plaintext header followed by an encrypted body, which itself contains a type and optional padding.

      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 value, containing the byte encoding of a handshake or alert message, or the raw bytes of the application data to be sent.

  • type:
    TLSPlaintext.type value, containing the content type of the record.

  • zeros:
    An arbitrary number of zero-valued bytes may appear in the plaintext after the type field. As long as the total remains within the record size limit, this field gives the sender an opportunity to pad any TLS record by an amount of its choosing. For more details, see [Section 5.4]

  • opaque_type:
    The opaque_type field outside a TLSCiphertext record is always set to the value 23 (application_data), for external compatibility with middleboxes that parse earlier versions of TLS. After decryption, the record’s actual content type is found in TLSInnerPlaintext.type.

  • legacy_record_version:
    The legacy_record_version field is always 0x0303. TLS 1.3 TLSCiphertexts are not generated until after TLS 1.3 has been negotiated, so there is no historical compatibility issue in which other values might be received. Note that the handshake protocol (including the ClientHello and ServerHello messages) authenticates the protocol version, so this value is redundant.

  • length:
    The length of TLSCiphertext.encrypted_record (in bytes), which is the sum of the content and padding lengths, plus the length of the inner content type, plus any expansion added by the AEAD algorithm. The length MUST NOT exceed 2 ^ 14 + 256 bytes. An endpoint that receives a record longer than this MUST terminate the connection with a “record_overflow” alert message.

  • encrypted_record:
    The serialized TLSInnerPlaintext structure in AEAD-encrypted form.

The AEAD algorithm takes as input a single key, nonce, plaintext, and “additional data” to be included in the authentication check, as described in [Section 2.1 of RFC5116]. The key is client_write_key or server_write_key, the nonce is derived from the sequence number and client_write_iv or server_write_iv (see [Section 5.3]), and the input for the additional data is the record header, for example:

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

The plaintext input to the AEAD algorithm is the encoded TLSInnerPlaintext structure. Section 7.3 defines the derivation of traffic keys.

The AEAD output consists of the ciphertext output of the AEAD encryption operation. Because it includes TLSInnerPlaintext.type and any padding provided by the sender, the plaintext length is greater than the corresponding TLSPlaintext.length. The length of the AEAD output is typically greater than the plaintext, but part of it also varies with the AEAD algorithm.

Because a cipher may include padding, the overhead may vary with different plaintext lengths.

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

The encrypted\_record field of TLSCiphertext is set to AEADEncrypted.

For decryption and verification, the cipher takes the key, nonce, additional data, and the AEADEncrypted value as input. The output is the plaintext or an error indicating decryption failure. There is no separate integrity check here.

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

If decryption fails, the receiver MUST terminate the connection with a “bad_record_mac” alert message.

The AEAD algorithms used in TLS 1.3 MUST NOT produce an expansion greater than 255 octets. When receiving a record from the peer, if TLSCiphertext.length is greater than 2 ^ 14 + 256 octets, the connection MUST be terminated with a “record_overflow” message. This limit comes from the fact that the maximum length of TLSInnerPlaintext is 2 ^ 14 octets + 1 octet for ContentType + a maximum AEAD expansion of 255 octets.

III. Per-Record Nonce

Maintain separate 64-bit sequence numbers for reading and writing records. After each record is read or written, the appropriate sequence number is incremented by 1. Each sequence number is set to zero at the start of the connection and whenever the keys are changed; the first record transmitted under a particular traffic key MUST use sequence number 0.

Because the sequence numbers are 64 bits in size, they should not wrap. If a TLS implementation would need to wrap a sequence number, it MUST rekey (Section 4.6.3) or terminate the connection.

Each AEAD algorithm specifies a range of possible lengths for the per-record nonce, from N_MIN bytes to N_MAX bytes of input [RFC5116]. For an AEAD algorithm, the length of the TLS per-record nonce (iv_length) is set to the larger of 8 bytes and N_MIN (see [RFC5116], Section 4). AEAD algorithms for which N_MAX is less than 8 bytes MUST NOT be used with TLS. The per-record nonce for the AEAD construction is formed as follows:

  1. The 64-bit record sequence number is encoded in network byte order and left-padded with zeros to iv_length.
  2. The padded sequence number is XORed with client_write_iv or server_write_iv, depending on the role.

The resulting value (of length iv_length) is used as the per-record nonce.

Note: This differs from the construction in TLS 1.2, which specified a partially explicit nonce.

IV. Record Padding

All encrypted TLS records can be padded, thereby increasing the size of TLSCiphertext. This allows the sender to hide traffic sizes from observers.

When generating a TLSCiphertext record, implementations may choose to add padding. An unpadded record is simply a record with zero-length padding. Padding is a string of zero-valued bytes appended before encryption after the ContentType field. Implementations MUST set all padding octets to zero before encryption.

If the sender needs it, application data records may contain zero-length TLSInnerPlaintext.content. This allows reasonably sized cover traffic to be generated when the presence or absence of activity is sensitive. Implementations MUST NOT send handshake or alert records with zero-length TLSInnerPlaintext.content; if such a message is received, the receiving implementation MUST terminate the connection with an “unexpected_message” alert message.

The padding sent is automatically validated by the record protection mechanism; after TLSCiphertext.encrypted_record is successfully decrypted, the receiving implementation scans the field from the end toward the beginning until it finds a nonzero octet. That nonzero octet is the content type field of the message. This padding scheme was chosen because it allows any encrypted TLS record to be padded to an arbitrary size (from zero up to the TLS record size limit) without introducing a new content type. This design also enforces all-zero padding octets so that padding errors can be detected quickly.

Implementations MUST limit the scan to the plaintext returned by AEAD decryption. If the receiving implementation does not find a nonzero octet in the plaintext, it MUST terminate the connection with an “unexpected_message” alert message.

Padding does not change the overall record size limit: the fully encoded TLSInnerPlaintext MUST NOT exceed 2 ^ 14 + 1 octets. If the maximum fragment length is reduced – for example, by the record_size_limit extension from [RFC8449] – then the reduced limit applies to the complete plaintext, including the content type and padding.

Choosing a reasonable padding strategy, such as when to pad and how much padding to add, is a complex topic and is outside the scope of this specification. If the application-layer protocol above TLS has its own padding, it is preferable to pad application data TLS records within the application layer. However, padding for encrypted handshake or alert records still has to be handled at the TLS layer. Later documents may define padding selection algorithms or mechanisms for requesting padding policies via TLS extensions or by some other means.

V. Limits on Key Usage

There are cryptographic limits on the amount of plaintext that can be safely encrypted under a given set of keys. [AEAD-LIMITS] provides an analysis of these limits under the assumption that the underlying primitives (AES or ChaCha20) have no weaknesses. Before these limits are reached, implementations should update keys as described in Section 4.6.3.

For AES-GCM, up to 2 ^ 24.5 full-size records (about 24 million) may be encrypted on a given connection while maintaining an approximately 2 ^ -57 security margin for authenticated encryption (AE) security. For ChaCha20 / Poly1305, the record sequence number will wrap before the security limit is reached.


References:

RFC 8446

GitHub Repository: Halfrost-Field

Follow: halfrost · GitHub

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