Skip to content

# TLS Handshake Inspector

Step through a TLS 1.3 handshake over one fixed vector: decode what the cipher suite commits both sides to, and walk the HKDF key schedule with every derived secret in view. No network is touched at any point.

Rendered when the site was built, by the same code the browser runs. No JavaScript required, and no network at any point: the handshake below is a fixed vector held in the library this page is built from. With JavaScript on, this is replaced by the interactive stepper, which reveals one message at a time and opens one schedule node at a time.

## The handshake, step by step

Seven messages, TLS_AES_128_GCM_SHA256 negotiated. The first two travel in the clear and everything after them is encrypted under the handshake keys, which is the change TLS 1.3 made over TLS 1.2.

The seven handshake messages
Step | Message | Direction | Protection | Bytes | What happens
--- | --- | --- | --- | --- | ---
1 | ClientHello | C -> S | in the clear | 174 | The client offers everything it can do, and guesses a key share.
2 | ServerHello | S -> C | in the clear | 122 | The server picks one suite and returns its key share.
3 | EncryptedExtensions | S -> C | encrypted with handshake keys | 6 | The server's remaining answers, now under the handshake keys.
4 | Certificate | S -> C | encrypted with handshake keys | 45 | The server presents its certificate chain, encrypted.
5 | CertificateVerify | S -> C | encrypted with handshake keys | 72 | The server signs the transcript, proving it holds the private key.
6 | Finished (server) | S -> C | encrypted with handshake keys | 36 | The server MACs the transcript with its finished key.
7 | Finished (client) | C -> S | encrypted with handshake keys | 36 | The client MACs the transcript in turn, and the handshake is done.

- ### 1. ClientHello

  TLS 1.3's speed comes from a guess. The client does not wait to be told which group the server wants: it picks one, sends its public key with the very first message, and is usually right. The host name goes out in the clear here, which is the one part of a TLS 1.3 handshake an observer can still read.

  What it commits to: the client cannot later claim it offered a different set of suites or groups: every byte of this message is inside the transcript that both Finished messages sign.

  ClientHello: fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 1 (0x01) | The one byte that says which handshake message this is.
  length | 170 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  legacy_version | 0x0303 | Frozen at "TLS 1.2" forever. Middleboxes rejected anything else, so the real version moved into an extension.
  random | 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f | 32 fresh bytes from the client. Pinned to a ramp in this vector.
  cipher_suites | 0x1301, 0x1302, 0x1303 | The suites the client will accept, best first. The server picks one.
  supported_versions | 0x0304 (TLS 1.3) | The extension that actually negotiates the version, because legacy_version could not be moved.
  supported_groups | 0x001d (x25519) | Which Diffie-Hellman groups the client can use.
  key_share | x25519, 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f | The client's public key, sent before the server has said which group it wants. Guessing right saves a whole round trip.
  signature_algorithms | 0x0403, 0x0804 | Which signatures the client will accept on the server's certificate.
  server_name | example.invalid | The host being asked for, in the clear. This is the one field of a TLS 1.3 handshake that still leaks which site is being visited.

  Transcript hash through this message
    b73c4bb9b58ea046da272417b4ae31351fbaf5295aeb3d2465e4ad28eceb56af

- ### 2. ServerHello

  One suite, one group, one key share. The moment this message lands, both sides can compute the ECDHE shared secret, and from it the Handshake Secret and everything underneath. This is the last message anyone on the path can read.

  What it commits to: the suite named here fixes the AEAD and the hash for the rest of the connection, and the hash fixes the size of every secret in the key schedule.

  ServerHello: fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 2 (0x02) | The one byte that says which handshake message this is.
  length | 118 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  legacy_version | 0x0303 | Frozen, exactly as in the ClientHello.
  random | 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f | The server's 32 bytes. Pinned to a ramp in this vector.
  cipher_suite | 0x1301 (TLS_AES_128_GCM_SHA256) | The single suite chosen from the client's list. This fixes the AEAD and the hash for the whole connection.
  supported_versions | 0x0304 (TLS 1.3) | The server agreeing to TLS 1.3, in the extension rather than the field.
  key_share | x25519, 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f | The server's public key. With this the shared secret exists at both ends and the handshake stops being readable.

  Transcript hash through this message
    091d7eab961aedd8efd99e344015afdcf887e90d3a03c6daa1d3374a14009c6b

- ### 3. EncryptedExtensions

  Everything the server has to say that is not the certificate: the protocol chosen by ALPN, whether the server name was accepted, and so on. In TLS 1.2 all of this was in the clear. Here it is the first encrypted message of the connection.

  What it commits to: nothing new about keys: this message exists to move answers off the readable part of the handshake.

  EncryptedExtensions: fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 8 (0x08) | The one byte that says which handshake message this is.
  length | 2 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  extensions | none in this vector | A real connection carries ALPN, the server-name acknowledgement, and similar answers here.

  Transcript hash through this message
    c9ec5a5a2bdea736f5a2b323c0171125747f0f9f60d5b4660972615da193adf0

- ### 4. Certificate

  The chain is encrypted, so an observer cannot tell which site was reached from the certificate alone. The certificate in this vector is a placeholder: reading a real one is a different job, and there is a tool for it.

  What it commits to: the server is now claiming an identity. The claim is worth nothing until the next message proves it holds the matching private key.

  Certificate: fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 11 (0x0b) | The one byte that says which handshake message this is.
  length | 41 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  certificate_request_context | empty | Non-empty only when the certificate answers a request made later on an open connection.
  certificate_list | 1 entry, 32 bytes | A placeholder in this vector. Take a real chain apart in the Certificate Decoder instead.

  Transcript hash through this message
    e35778e8fcb0930abba1198e3fbe746ab645c28aa9dd7d8ba0bdfe5b6ce02349

- ### 5. CertificateVerify

  The signature is over the whole handshake so far, not over a nonce the client chose. That means it proves two things at once: the server holds the certificate's private key, and it saw exactly the messages the client sent.

  What it commits to: the identity is now proved for this transcript only. The same signature cannot be replayed onto a different handshake, because a different handshake has a different transcript hash.

  CertificateVerify: fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 15 (0x0f) | The one byte that says which handshake message this is.
  length | 68 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  algorithm | 0x0403 (ecdsa_secp256r1_sha256) | Which signature scheme was used, chosen from the client's list.
  signature | 64 bytes (placeholder) | Over a fixed context string plus the transcript hash so far. Pinned here: producing a real one needs a private key, which this page has no business holding.

  Transcript hash through this message
    4f0c760aa1452be2ac406db4169ca622a9529c1c380f1488c7a6dd2157f6deb0

- ### 6. Finished (server)

  An HMAC over the transcript under the server finished key. Only a party that derived the same schedule from the same messages can produce it, so it closes the handshake against any change on the path that got this far. With it, the application secrets become derivable.

  What it commits to: the server's half of the handshake is complete and both sides agree on every byte exchanged so far.

  Finished (server): fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 20 (0x14) | The one byte that says which handshake message this is.
  length | 32 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  verify_data | 32 bytes, computed | HMAC of the transcript hash under this side's finished key. This one is not pinned: it is derived from the key schedule on this page, so a wrong derivation could not produce a consistent trace.

  Transcript hash through this message
    a163c65ca4b431037883e6f18c577e79d105d992a66fbd355940fd528230bfb3

- ### 7. Finished (client)

  The client's matching HMAC, under its own finished key. After this the connection is open: application data flows under the application traffic keys, and the resumption secret is fixed so the server can issue a ticket for next time.

  What it commits to: both sides have proved they derived the same schedule. Anything after this is application data.

  Finished (client): fields
  Field | Value | What it does
  --- | --- | ---
  HandshakeType | 20 (0x14) | The one byte that says which handshake message this is.
  length | 32 bytes | A three-byte length. The four header bytes plus this is the whole message, and every byte of it goes into the transcript hash.
  verify_data | 32 bytes, computed | HMAC of the transcript hash under this side's finished key. This one is not pinned: it is derived from the key schedule on this page, so a wrong derivation could not produce a consistent trace.

  Transcript hash through this message
    053908e9b66d8b1251455d9b292919b3cf30cab64a6bbe5908a2d854e7fc9b08

## The key schedule

RFC 8446 §7.1, run for real over the vector above. Every arrow reads downwards: a node is derived from the node above it, never the other way about. 21 nodes, and the two constants at the top are the ones every TLS 1.3 stack computes identically because a full handshake has no pre-shared key to mix in.

    TLS 1.3 key schedule (TLS_AES_128_GCM_SHA256, HKDF-SHA-256)
    Early Secret [extract]
        = 33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a
      +-- Derived (early) [expand]
          = 6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba
        +-- Handshake Secret [extract]
            = 68ebe593efe91a9762eb5e1994936dd566dc5039760c3e2d4e58724b6b8aac3f
          +-- client_handshake_traffic_secret [expand]
              = b668e55e5b90a554e228b32a7e9993b2d256aeb31bfe7188a6cc63fef41d650f
            +-- client handshake write key [expand]
                = 1cc8914229bfe84ad3607db4bd709e06
            +-- client handshake write IV [expand]
                = de79c4ccaee297f3cde08987
            +-- client finished key [expand]
                = 3b9875e00ae60e70e74b30467ec46ebebc69bbf6b75d713af3635244c8fb378d
          +-- server_handshake_traffic_secret [expand]
              = 8e80abbae21490892e50ed64d49279c80cef6d474dbc4d23815cfc1fb831a281
            +-- server handshake write key [expand]
                = d9004a408b71080389fae2ce54427cf6
            +-- server handshake write IV [expand]
                = aa3f6546c7db775b8ec16236
            +-- server finished key [expand]
                = b9eb8f26569e5c256ff43bf75e6107996a638c4b8eafe67396d1abf8255bd307
          +-- Derived (handshake) [expand]
              = d731f5738beb92107be34d5a2d82c74015d533bacbe3a76a7f86e6aca3be67fd
            +-- Master Secret [extract]
                = 2fabb717fc05145bb76f914fb49ea788b696edbc9f07482cc0447e13ac8ea77d
              +-- client_application_traffic_secret_0 [expand]
                  = 7fc0acb341206c22ce894cf4f7d42f3ebbb8984f60a5d57246e0770229b4b110
                +-- client application write key [expand]
                    = 145e1ba6147c71c2c30f64a0de100126
                +-- client application write IV [expand]
                    = 228bead61f3f2ac89af50590
              +-- server_application_traffic_secret_0 [expand]
                  = 640a34d23ac813ba0647727ab07cacc88710f2ce5e7ad79736127713b3786cc7
                +-- server application write key [expand]
                    = c65d5fb0c387ccb342f09ab0516e973e
                +-- server application write IV [expand]
                    = 688498b1f2ead690f4ff9047
              +-- exporter_master_secret [expand]
                  = a2ad90d8fc4923c9014cae6c356e6ee1913f26d9df1d605ff9d46053ce3ec017
              +-- resumption_master_secret [expand]
                  = a8aadcddac462c50f76bde3a1896bd326127c2efbbba650524183a6eeb23b6bd

The key schedule, node by node
Secret | HKDF step | Derived from | Derivation | Value | Available | What it is for
--- | --- | --- | --- | --- | --- | ---
Early Secret | extract | nothing: this is the root | HKDF-Extract(salt = 32 zero bytes, IKM = PSK) | 33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a | before anything is sent | The top of the tree. A full handshake has no pre-shared key, so both inputs are zeros and this value is a fixed constant every TLS 1.3 stack in the world computes identically. It is not a secret, and it protects nothing on its own: it exists so that the resumption case and the full case can share one schedule.
Derived (early) | expand | Early Secret | HKDF-Expand-Label(Early Secret, "derived", Transcript-Hash(""), 32) | 6f2615a108c702c5678f54fc9dbab69716c076189c48250cebeac3576c3611ba | before anything is sent | A spacer. HKDF-Extract wants a salt, and reusing the Early Secret directly would let one stage's output be another stage's salt unchanged. Expanding it under "derived" first keeps the stages cryptographically separate.
Handshake Secret | extract | Derived (early) | HKDF-Extract(salt = Derived (early), IKM = ECDHE shared secret) | 68ebe593efe91a9762eb5e1994936dd566dc5039760c3e2d4e58724b6b8aac3f | after the ServerHello | The first value in the schedule that an eavesdropper cannot compute. Everything above it is public arithmetic on zeros; this is where the key exchange finally mixes in, which is why the handshake stays readable on the wire right up until the ServerHello and not one byte longer.
client_handshake_traffic_secret | expand | Handshake Secret | HKDF-Expand-Label(Handshake Secret, "c hs traffic", Transcript-Hash(ClientHello..ServerHello), 32) | b668e55e5b90a554e228b32a7e9993b2d256aeb31bfe7188a6cc63fef41d650f | after the ServerHello | The client's secret for the rest of the handshake. It is bound to the transcript, so if anything on the path had edited the ClientHello or the ServerHello the two sides would derive different secrets here and the handshake would fail rather than continue quietly.
client handshake write key | expand | client_handshake_traffic_secret | HKDF-Expand-Label(client_handshake_traffic_secret, "key", "", 16) | 1cc8914229bfe84ad3607db4bd709e06 | after the ServerHello | The AEAD key that actually encrypts records in this direction. Note the empty context: the traffic secret above it is already transcript-bound, so binding again would buy nothing.
client handshake write IV | expand | client_handshake_traffic_secret | HKDF-Expand-Label(client_handshake_traffic_secret, "iv", "", 12) | de79c4ccaee297f3cde08987 | after the ServerHello | The base nonce. It is not used as-is: the record's sequence number is exclusive-ORed into its low bytes, so every record under one key gets a different nonce without any of them being sent.
client finished key | expand | client_handshake_traffic_secret | HKDF-Expand-Label(client_handshake_traffic_secret, "finished", "", 32) | 3b9875e00ae60e70e74b30467ec46ebebc69bbf6b75d713af3635244c8fb378d | after the ServerHello | The key for this side's Finished message. Finished is an HMAC over the transcript under this key, which is how each side proves it derived the same schedule from the same messages.
server_handshake_traffic_secret | expand | Handshake Secret | HKDF-Expand-Label(Handshake Secret, "s hs traffic", Transcript-Hash(ClientHello..ServerHello), 32) | 8e80abbae21490892e50ed64d49279c80cef6d474dbc4d23815cfc1fb831a281 | after the ServerHello | The server's half of the same pair. One label apart from the client's, and that single differing byte is the whole reason the two directions never share a key.
server handshake write key | expand | server_handshake_traffic_secret | HKDF-Expand-Label(server_handshake_traffic_secret, "key", "", 16) | d9004a408b71080389fae2ce54427cf6 | after the ServerHello | The AEAD key that actually encrypts records in this direction. Note the empty context: the traffic secret above it is already transcript-bound, so binding again would buy nothing.
server handshake write IV | expand | server_handshake_traffic_secret | HKDF-Expand-Label(server_handshake_traffic_secret, "iv", "", 12) | aa3f6546c7db775b8ec16236 | after the ServerHello | The base nonce. It is not used as-is: the record's sequence number is exclusive-ORed into its low bytes, so every record under one key gets a different nonce without any of them being sent.
server finished key | expand | server_handshake_traffic_secret | HKDF-Expand-Label(server_handshake_traffic_secret, "finished", "", 32) | b9eb8f26569e5c256ff43bf75e6107996a638c4b8eafe67396d1abf8255bd307 | after the ServerHello | The key for this side's Finished message. Finished is an HMAC over the transcript under this key, which is how each side proves it derived the same schedule from the same messages.
Derived (handshake) | expand | Handshake Secret | HKDF-Expand-Label(Handshake Secret, "derived", Transcript-Hash(""), 32) | d731f5738beb92107be34d5a2d82c74015d533bacbe3a76a7f86e6aca3be67fd | after the ServerHello | The same spacer trick one stage down, separating the handshake keys from the application keys. Learning a handshake traffic secret does not hand anyone the application ones.
Master Secret | extract | Derived (handshake) | HKDF-Extract(salt = Derived (handshake), IKM = 32 zero bytes) | 2fabb717fc05145bb76f914fb49ea788b696edbc9f07482cc0447e13ac8ea77d | after the ServerHello | An extract with nothing new to extract. The shape is kept so that all three stages look the same, which is what makes the schedule easy to reason about: every stage is an extract, then some labelled expands, then a spacer.
client_application_traffic_secret_0 | expand | Master Secret | HKDF-Expand-Label(Master Secret, "c ap traffic", Transcript-Hash(ClientHello..server Finished), 32) | 7fc0acb341206c22ce894cf4f7d42f3ebbb8984f60a5d57246e0770229b4b110 | after the server Finished | The client's secret for real traffic. The trailing zero in its RFC name is a generation counter: a long-lived connection can rotate to secret 1, 2 and onwards without another key exchange.
client application write key | expand | client_application_traffic_secret_0 | HKDF-Expand-Label(client_application_traffic_secret_0, "key", "", 16) | 145e1ba6147c71c2c30f64a0de100126 | after the server Finished | The AEAD key that actually encrypts records in this direction. Note the empty context: the traffic secret above it is already transcript-bound, so binding again would buy nothing.
client application write IV | expand | client_application_traffic_secret_0 | HKDF-Expand-Label(client_application_traffic_secret_0, "iv", "", 12) | 228bead61f3f2ac89af50590 | after the server Finished | The base nonce. It is not used as-is: the record's sequence number is exclusive-ORed into its low bytes, so every record under one key gets a different nonce without any of them being sent.
server_application_traffic_secret_0 | expand | Master Secret | HKDF-Expand-Label(Master Secret, "s ap traffic", Transcript-Hash(ClientHello..server Finished), 32) | 640a34d23ac813ba0647727ab07cacc88710f2ce5e7ad79736127713b3786cc7 | after the server Finished | The server's half of the application pair, one label away from the client's again.
server application write key | expand | server_application_traffic_secret_0 | HKDF-Expand-Label(server_application_traffic_secret_0, "key", "", 16) | c65d5fb0c387ccb342f09ab0516e973e | after the server Finished | The AEAD key that actually encrypts records in this direction. Note the empty context: the traffic secret above it is already transcript-bound, so binding again would buy nothing.
server application write IV | expand | server_application_traffic_secret_0 | HKDF-Expand-Label(server_application_traffic_secret_0, "iv", "", 12) | 688498b1f2ead690f4ff9047 | after the server Finished | The base nonce. It is not used as-is: the record's sequence number is exclusive-ORed into its low bytes, so every record under one key gets a different nonce without any of them being sent.
exporter_master_secret | expand | Master Secret | HKDF-Expand-Label(Master Secret, "exp master", Transcript-Hash(ClientHello..server Finished), 32) | a2ad90d8fc4923c9014cae6c356e6ee1913f26d9df1d605ff9d46053ce3ec017 | after the server Finished | The root of the exporter interface: a way for a protocol running over TLS to ask for key material of its own that is tied to this exact connection. It never protects TLS records itself.
resumption_master_secret | expand | Master Secret | HKDF-Expand-Label(Master Secret, "res master", Transcript-Hash(ClientHello..client Finished), 32) | a8aadcddac462c50f76bde3a1896bd326127c2efbbba650524183a6eeb23b6bd | after the client Finished | The one secret bound to the whole transcript, client Finished included, because it outlives the connection: a ticket issued from it lets the next connection start from a pre-shared key instead of from zeros.

## Cipher suites

TLS 1.3 threw the old registry away rather than pruning it. Five suites remain, and a name now says only which AEAD protects the records and which hash drives the key schedule. The key exchange is always (EC)DHE and the certificate type is negotiated separately, so neither appears in the name any more.

The five TLS 1.3 cipher suites
Suite | Code point | AEAD | Hash | Key | Nonce | Tag
--- | --- | --- | --- | --- | --- | ---
TLS_AES_128_GCM_SHA256 | 0x1301 | AES-128-GCM | SHA-256 | 16 B | 12 B | 16 B
TLS_AES_256_GCM_SHA384 | 0x1302 | AES-256-GCM | SHA-384 | 32 B | 12 B | 16 B
TLS_CHACHA20_POLY1305_SHA256 | 0x1303 | ChaCha20-Poly1305 | SHA-256 | 32 B | 12 B | 16 B
TLS_AES_128_CCM_SHA256 | 0x1304 | AES-128-CCM | SHA-256 | 16 B | 12 B | 16 B
TLS_AES_128_CCM_8_SHA256 | 0x1305 | AES-128-CCM with a short tag | SHA-256 | 16 B | 12 B | 8 B

- ### TLS_AES_128_GCM_SHA256

  TLS_AES_128_GCM_SHA256: field by field
  Part | Value | What it means
  --- | --- | ---
  Code point | 0x1301 | The two bytes that actually travel. The ClientHello offers a list of these and the ServerHello echoes back exactly one.
  Prefix | TLS_ | Every TLS 1.3 name begins here and then goes straight to a cipher. In TLS 1.2 the next words were the key exchange and the certificate type; in TLS 1.3 they are gone from the name because they are no longer negotiated by it.
  AEAD | AES-128-GCM | AES in Galois/Counter Mode. The mandatory-to-implement suite, and the one almost every connection uses, because chips have had AES instructions for years and GCM authenticates as it encrypts.
  Write key | 16 bytes | Derived per direction from that side's traffic secret, with the label "key". The client and the server never share a key.
  Nonce | 12 bytes | Derived with the label "iv", then combined with the record's sequence number so no two records under one key ever reuse a nonce.
  Tag | 16 bytes | Appended to every record. A record whose tag does not check is not decrypted and not retried: the connection fails.
  Hash | SHA-256 | Two jobs at once: it is the hash inside HKDF for the whole key schedule, and it is the Transcript-Hash that binds every secret to the messages sent so far.
  Secret size | 32 bytes | Every secret in the key schedule is one hash output long, so the hash sets the size of the whole tree.
  Key exchange | not named here | (EC)DHE always, agreed by the supported_groups and key_share extensions instead. TLS 1.3 removed static RSA key transport, so every connection has forward secrecy whether or not anyone asked for it.
  Authentication | not named here | Agreed by signature_algorithms and proved by CertificateVerify. The suite has nothing to say about which certificate the server presents.

- ### TLS_AES_256_GCM_SHA384

  TLS_AES_256_GCM_SHA384: field by field
  Part | Value | What it means
  --- | --- | ---
  Code point | 0x1302 | The two bytes that actually travel. The ClientHello offers a list of these and the ServerHello echoes back exactly one.
  Prefix | TLS_ | Every TLS 1.3 name begins here and then goes straight to a cipher. In TLS 1.2 the next words were the key exchange and the certificate type; in TLS 1.3 they are gone from the name because they are no longer negotiated by it.
  AEAD | AES-256-GCM | The same mode with a 256-bit key, and the only registered suite that moves the hash up to SHA-384 as well, so its whole key schedule works in 48-byte secrets rather than 32-byte ones.
  Write key | 32 bytes | Derived per direction from that side's traffic secret, with the label "key". The client and the server never share a key.
  Nonce | 12 bytes | Derived with the label "iv", then combined with the record's sequence number so no two records under one key ever reuse a nonce.
  Tag | 16 bytes | Appended to every record. A record whose tag does not check is not decrypted and not retried: the connection fails.
  Hash | SHA-384 | Two jobs at once: it is the hash inside HKDF for the whole key schedule, and it is the Transcript-Hash that binds every secret to the messages sent so far.
  Secret size | 48 bytes | Every secret in the key schedule is one hash output long, so the hash sets the size of the whole tree.
  Key exchange | not named here | (EC)DHE always, agreed by the supported_groups and key_share extensions instead. TLS 1.3 removed static RSA key transport, so every connection has forward secrecy whether or not anyone asked for it.
  Authentication | not named here | Agreed by signature_algorithms and proved by CertificateVerify. The suite has nothing to say about which certificate the server presents.

- ### TLS_CHACHA20_POLY1305_SHA256

  TLS_CHACHA20_POLY1305_SHA256: field by field
  Part | Value | What it means
  --- | --- | ---
  Code point | 0x1303 | The two bytes that actually travel. The ClientHello offers a list of these and the ServerHello echoes back exactly one.
  Prefix | TLS_ | Every TLS 1.3 name begins here and then goes straight to a cipher. In TLS 1.2 the next words were the key exchange and the certificate type; in TLS 1.3 they are gone from the name because they are no longer negotiated by it.
  AEAD | ChaCha20-Poly1305 | A stream cipher with a separate authenticator. It is fast in software, which is why phones and embedded devices without AES instructions prefer it.
  Write key | 32 bytes | Derived per direction from that side's traffic secret, with the label "key". The client and the server never share a key.
  Nonce | 12 bytes | Derived with the label "iv", then combined with the record's sequence number so no two records under one key ever reuse a nonce.
  Tag | 16 bytes | Appended to every record. A record whose tag does not check is not decrypted and not retried: the connection fails.
  Hash | SHA-256 | Two jobs at once: it is the hash inside HKDF for the whole key schedule, and it is the Transcript-Hash that binds every secret to the messages sent so far.
  Secret size | 32 bytes | Every secret in the key schedule is one hash output long, so the hash sets the size of the whole tree.
  Key exchange | not named here | (EC)DHE always, agreed by the supported_groups and key_share extensions instead. TLS 1.3 removed static RSA key transport, so every connection has forward secrecy whether or not anyone asked for it.
  Authentication | not named here | Agreed by signature_algorithms and proved by CertificateVerify. The suite has nothing to say about which certificate the server presents.

- ### TLS_AES_128_CCM_SHA256

  TLS_AES_128_CCM_SHA256: field by field
  Part | Value | What it means
  --- | --- | ---
  Code point | 0x1304 | The two bytes that actually travel. The ClientHello offers a list of these and the ServerHello echoes back exactly one.
  Prefix | TLS_ | Every TLS 1.3 name begins here and then goes straight to a cipher. In TLS 1.2 the next words were the key exchange and the certificate type; in TLS 1.3 they are gone from the name because they are no longer negotiated by it.
  AEAD | AES-128-CCM | Counter with CBC-MAC. It reuses a block cipher a constrained device already has, instead of asking it for the extra multiplication GCM needs.
  Write key | 16 bytes | Derived per direction from that side's traffic secret, with the label "key". The client and the server never share a key.
  Nonce | 12 bytes | Derived with the label "iv", then combined with the record's sequence number so no two records under one key ever reuse a nonce.
  Tag | 16 bytes | Appended to every record. A record whose tag does not check is not decrypted and not retried: the connection fails.
  Hash | SHA-256 | Two jobs at once: it is the hash inside HKDF for the whole key schedule, and it is the Transcript-Hash that binds every secret to the messages sent so far.
  Secret size | 32 bytes | Every secret in the key schedule is one hash output long, so the hash sets the size of the whole tree.
  Key exchange | not named here | (EC)DHE always, agreed by the supported_groups and key_share extensions instead. TLS 1.3 removed static RSA key transport, so every connection has forward secrecy whether or not anyone asked for it.
  Authentication | not named here | Agreed by signature_algorithms and proved by CertificateVerify. The suite has nothing to say about which certificate the server presents.

- ### TLS_AES_128_CCM_8_SHA256

  TLS_AES_128_CCM_8_SHA256: field by field
  Part | Value | What it means
  --- | --- | ---
  Code point | 0x1305 | The two bytes that actually travel. The ClientHello offers a list of these and the ServerHello echoes back exactly one.
  Prefix | TLS_ | Every TLS 1.3 name begins here and then goes straight to a cipher. In TLS 1.2 the next words were the key exchange and the certificate type; in TLS 1.3 they are gone from the name because they are no longer negotiated by it.
  AEAD | AES-128-CCM with a short tag | The same as CCM but with the tag cut to 8 bytes, for links where 8 bytes a record is worth saving. A shorter tag is a weaker forgery bound, so this is a deliberate trade rather than a free win.
  Write key | 16 bytes | Derived per direction from that side's traffic secret, with the label "key". The client and the server never share a key.
  Nonce | 12 bytes | Derived with the label "iv", then combined with the record's sequence number so no two records under one key ever reuse a nonce.
  Tag | 8 bytes | Appended to every record. A record whose tag does not check is not decrypted and not retried: the connection fails.
  Hash | SHA-256 | Two jobs at once: it is the hash inside HKDF for the whole key schedule, and it is the Transcript-Hash that binds every secret to the messages sent so far.
  Secret size | 32 bytes | Every secret in the key schedule is one hash output long, so the hash sets the size of the whole tree.
  Key exchange | not named here | (EC)DHE always, agreed by the supported_groups and key_share extensions instead. TLS 1.3 removed static RSA key transport, so every connection has forward secrecy whether or not anyone asked for it.
  Authentication | not named here | Agreed by signature_algorithms and proved by CertificateVerify. The suite has nothing to say about which certificate the server presents.

## What is pinned

Everything not in this table is computed by the code that built the page. These are the values a real handshake would take from outside, and each says why it could not be derived here.

The pinned inputs of the worked handshake
Input | Value | Why it is pinned
--- | --- | ---
Client random | 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f | 32 bytes the client draws fresh for every connection. A ramp here, so it is obvious this is a teaching vector.
Server random | 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f | The server's own 32 bytes, drawn the same way.
Legacy session id | 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f | A leftover from TLS 1.2 session resumption. TLS 1.3 does not use it; the client sends something and the server echoes it so middleboxes see a familiar shape.
Client key share | 606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f | The client's X25519 public key. Generating one needs curve arithmetic, which this module does not do.
Server key share | 808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f | The server's X25519 public key, sent in its ServerHello.
ECDHE shared secret | a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf | What both sides compute from their own private key and the other side's share. Deriving it really would need X25519; this module implements the key schedule, not the curve, so it is pinned and fed in as the schedule's one secret input.
Certificate and signature | 32 bytes of certificate, 64 of signature | Placeholders. Reading a real certificate chain is the Certificate Decoder's job, not this page's.

Either side of this page has a tool of its own: the Packet Analyzer takes a real captured record apart down to its bytes, and the Certificate Decoder reads the chain this walk only carries a placeholder for. The primitives underneath, HKDF and SHA-256 included, are workbenches in the Cryptography Lab, and the connection this all rides on is built in the TCP Handshake Animator.

About this lab3 paragraphs

The handshake is one fixed vector, not a live connection. Nothing here reaches a network, and nothing here will: reading a real server's handshake would need a proxy that saw which site you were visiting, which is exactly the thing this site does not build.

What is derived is really derived. The key schedule runs on the same HKDF the Cryptography Lab uses, and the transcript hashes are taken over the actual message bytes on this page, so both Finished messages check out against the schedule beside them. The values a real handshake takes from outside (the two randoms, the key shares, the shared secret, the certificate) are pinned to obvious byte ramps and each one says so.

The negotiation and the schedule are the subject. Taking a captured record apart byte by byte is the Packet Analyzer's job, and reading a real certificate chain is the Certificate Decoder's; both are linked below. Session resumption, 0-RTT and the TLS 1.2 comparison are out of scope here.