TLS 1.3 // RFC 8446
TLS wraps every byte after a TCP connection in a typed record. Inside those records, ClientHello and ServerHello negotiate a version, cipher suite, key share and ALPN protocol; extensions carry SNI, groups, signatures and certificates. This page decodes the plaintext handshake and the record layer, and shows exactly where the encrypted boundary begins.
01Record layer
five-byte header · typed fragmentsTLS does not hand TCP a naked handshake message. Every message is first wrapped in a record: one content type, a legacy version, a two-byte length and the fragment. A handshake message may span several records, and one record may hold several handshake messages. The record layer is also where AEAD protection is applied.
| Content type | Code | Fragment | TLS 1.3 note |
|---|---|---|---|
| ChangeCipherSpec | 20 | 1 byte, value 0x01 | Compatibility only. It is not a key change in TLS 1.3. |
| Alert | 21 | Level + description | Fatal alerts close the connection. Some alerts are encrypted. |
| Handshake | 22 | One or more handshake messages | Plaintext only until ServerHello; later handshake records are encrypted inside application data. |
| Application Data | 23 | AEAD-protected bytes | Also carries encrypted handshake messages after ServerHello. |
| Heartbeat | 24 | Heartbeat message | Rarely used; historically a source of memory-disclosure bugs. |
TLS 1.3 records use 0x0303 in the legacy record version for compatibility,
and the initial ClientHello record may use 0x0301. The real version is
negotiated in the supported_versions extension. Do not infer TLS 1.3 from the
record header alone.
02Handshake messages
one type byte · three length bytes
Every handshake message begins with a one-byte type and a 24-bit length. The length counts
the body only, so the total message is 4 + length bytes. Handshake messages
are ordered: the client speaks first, the server answers, and Finished proves that both
sides derived the same keys.
| Type | Name | Direction | Purpose |
|---|---|---|---|
1 | ClientHello | C → S | Version, random, session ID, cipher suites, key share and extensions. |
2 | ServerHello | S → C | Selected version, cipher suite, server random and key share. |
4 | NewSessionTicket | S → C | Resumption lifetime, nonce, ticket and early-data size. |
5 | EndOfEarlyData | C → S | Marks the end of 0-RTT data. |
8 | EncryptedExtensions | S → C | Extensions that must not be visible before keys exist, including ALPN. |
11 | Certificate | S → C / C → S | Certificate chain and per-certificate extensions. |
13 | CertificateRequest | S → C | Request for client authentication. |
15 | CertificateVerify | S → C / C → S | Signature over the handshake transcript. |
20 | Finished | Both | HMAC over the transcript with the finished key. |
24 | KeyUpdate | Both | Requests a new application traffic secret. |
In TLS 1.3 only ClientHello and ServerHello are plaintext handshake messages. Everything after ServerHello — EncryptedExtensions, Certificate, CertificateVerify and Finished — is sent as Application Data records protected with handshake traffic keys. A capture without keys shows ciphertext, not a parseable Certificate message.
03ClientHello & ServerHello
the two plaintext messages worth knowing by heartThe hello messages carry the negotiation. ClientHello lists what the client supports; ServerHello selects one version, one cipher suite and one key share. Both keep the TLS 1.2 field shapes so middleboxes see a familiar structure, then move the real decisions into extensions.
| Field | Width | ClientHello | ServerHello |
|---|---|---|---|
| legacy_version | 2 B | 0x0303 in TLS 1.3 | 0x0303 in TLS 1.3 |
| random | 32 B | Client random, later mixed into the key schedule | Server random, later mixed into the key schedule |
| legacy_session_id | 1 + 0–32 B | Compatibility session ID; may be empty | Echoes the client value |
| cipher_suites | 2 + 2n B | List of offered suites | One selected cipher suite |
| legacy_compression_methods | 1 + n B | Must include 0 (null) | 0 |
| extensions | 2 + n B | SNI, groups, signatures, ALPN, versions, key share | supported_versions, key_share, pre_shared_key |
04Extensions
type · length · data · repeated everywhereExtensions are the extension mechanism that made TLS 1.3 possible without changing the hello envelope. Each one is a four-byte header and an opaque body whose shape depends on the extension type and the message that carries it. The same type can mean different things in ClientHello, ServerHello, EncryptedExtensions and Certificate.
| Code | Extension | Carries | Seen in |
|---|---|---|---|
0 | server_name | Host name (SNI); empty acknowledgement in EncryptedExtensions | ClientHello, EncryptedExtensions |
10 | supported_groups | Named groups: x25519, secp256r1, FFDHE | ClientHello, EncryptedExtensions |
13 | signature_algorithms | Signature schemes the peer accepts | ClientHello, CertificateRequest |
16 | ALPN | Protocol names such as h2, http/1.1 | ClientHello, EncryptedExtensions |
43 | supported_versions | Version list or the selected version | ClientHello, ServerHello |
51 | key_share | Ephemeral public keys per group | ClientHello, ServerHello |
41 | pre_shared_key | PSK identities and binders | ClientHello, ServerHello |
45 | psk_key_exchange_modes | psk_ke or psk_dhe_ke | ClientHello |
28 | record_size_limit | Largest record the peer will accept | ClientHello, EncryptedExtensions |
5 | status_request | OCSP stapling request or response | ClientHello, Certificate |
23 | extended_master_secret | Empty marker for EMS | ClientHello, ServerHello |
0xfe0d | encrypted_client_hello | ECHConfig and encrypted SNI | ClientHello |
05TLS 1.3 key schedule
HKDF-Extract · Derive-Secret · Expand-LabelTLS 1.3 replaces the TLS 1.2 PRF cascade with a small HKDF-based schedule. Two inputs matter: the ECDHE shared secret and the PSK (zero when there is no resumption). Each stage mixes in a transcript hash, so a change to any handshake byte changes every later traffic secret.
HKDF-Extract(0, PSK)salt is zero; zero PSK when resumption is not usedDerive-Secret(early, "c e traffic", ClientHello)protects 0-RTT dataDerive-Secret(early, "derived", "")feeds the next HKDF-Extract as its saltHKDF-Extract(derived early, ECDHE)ECDHE is the input keying material (IKM), not the saltDerive-Secret(handshake, "derived", "")feeds the final HKDF-Extract as its saltDerive-Secret(handshake, "c hs traffic" / "s hs traffic", transcript)protects EncryptedExtensions through FinishedHKDF-Extract(derived handshake, 0)IKM is a string of Hash.length zero bytesDerive-Secret(master, "c ap traffic" / "s ap traffic", transcript)protects application data and KeyUpdate| Secret | Derived from | Protects | Notes |
|---|---|---|---|
| early secret | PSK or zeros | — | Only meaningful with PSK resumption. |
| client_early_traffic_secret | early secret + ClientHello transcript | 0-RTT application data | Replayable; servers should limit what 0-RTT can do. |
| handshake secret | derived early secret (salt) + ECDHE (IKM) | — | PSK resumption normally uses psk_dhe_ke for forward secrecy; psk_ke is allowed but weaker. |
| client/server handshake traffic | handshake secret + transcript | EncryptedExtensions, Certificate, CertificateVerify, Finished | This is why the certificate is invisible without keys. |
| master secret | derived handshake secret (salt) + zeros (IKM) | — | Exists so application keys are separated from handshake keys. |
| client/server application traffic | master secret + transcript | Application Data records | KeyUpdate derives the next generation from the current secret. |
| exporter / resumption secrets | master secret + transcript | Outside TLS records | Used for channel bindings, EAP, and session tickets. |
TLS 1.3 builds the per-record nonce by XORing the 64-bit record sequence number into the static IV derived from the traffic secret. The sequence number resets to zero when keys change. Reusing a nonce with the same key breaks AEAD catastrophically, which is why KeyUpdate exists and why the record sequence number must never wrap.
06Alerts
two bytes · level and description
An alert is a two-byte record fragment. The level is 1 for warning or
2 for fatal. In TLS 1.3 most alerts are fatal, and alerts sent after the
handshake keys are installed are encrypted like application data.
| Code | Alert | Typical cause |
|---|---|---|
0 | close_notify | Clean shutdown. It prevents truncation attacks. |
20 | bad_record_mac | AEAD authentication failed or keys disagree. |
22 | record_overflow | Record fragment larger than the negotiated limit. |
40 | handshake_failure | No common cipher suite, group or version. |
42 | bad_certificate | Certificate could not be parsed or verified. |
47 | illegal_parameter | A field was well-formed but not allowed in context. |
50 | decode_error | A message could not be decoded. |
51 | decrypt_error | Signature or Finished verification failed. |
70 | protocol_version | No mutually supported protocol version. |
109 | missing_extension | A required extension was absent. |
112 | unrecognized_name | SNI name is not served by this endpoint. |
120 | no_application_protocol | ALPN found no overlap. |
07Encoder
ClientHello · ServerHello · Alert · Application DataBuild a syntactically valid TLS record. The key share is illustrative unless you paste a real public key; the point is the encoding, not a usable handshake. Every length is derived from the fields you enter, and the output can be fed straight back into the decoder. ALPN selection for TLS 1.3 belongs in EncryptedExtensions, not ServerHello, so it is not offered here; the ClientHello form still shows the offered protocol list.
TLS parameters
RFC 8446The example key share is deterministic filler, not a valid point or key for the selected group. Replace it with a real public key of the group's size if you need a handshake that a peer would accept. Lengths, extension framing and the record header are exact.
wire output
waitingno bytes built yet
It does not generate a private key, sign the transcript, derive traffic keys or encrypt anything. It builds the plaintext record and handshake structures so you can inspect the layout and feed the bytes to a decoder.
08Decoder
record stream · handshake · TCP · IPv4 · IPv6 · EthernetPaste raw TLS records, a bare handshake message, a TCP payload or a complete packet. The decoder walks the record layer, reassembles handshake messages that span records, expands ClientHello/ServerHello/EncryptedExtensions, and reports the point at which the bytes became opaque ciphertext.
parse bytes
RFC 8446 · records · extensions09Wire notes
what TLS 1.3 removed, and what still bitesRSA key transport, static DH, CBC-mode suites, compression, renegotiation and custom
DHE groups are gone from TLS 1.3. A ClientHello that offers only 0x0303 is a
TLS 1.2 client even if the record layer looks modern.
- SNI is not authentication. The server_name extension selects a certificate; the certificate and CertificateVerify signature are what prove the endpoint.
- ALPN is negotiated, not implied by the port. TCP/443 can carry HTTP/1.1, HTTP/2 or another protocol. Read the ALPN result in EncryptedExtensions before assuming HTTP/2.
- 0-RTT is replayable. Early data is encrypted with keys derived before the handshake completes. Servers must restrict it to idempotent operations.
- Record boundaries are not message boundaries. A single handshake message can span records, and one record can contain several handshake messages. A decoder must buffer across records.
- Encrypted records reveal lengths. TLS 1.3 hides content but not record sizes or timing. Padding and record_size_limit reduce, but do not remove, traffic analysis.
- The certificate chain can be large. A Certificate message may exceed one record; the decoder concatenates handshake fragments before parsing.
- Middlebox compatibility mode. Many TLS 1.3 clients send a non-empty legacy_session_id and a dummy ChangeCipherSpec record so older middleboxes keep working. Neither changes the keys.
- DoT, DoH and DoQ are different transports. DoT is TLS over TCP/853, DoH is HTTP over TLS, and DoQ is QUIC. The TLS handshake is similar; the framing above it is not.