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.
| 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
| 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.
| 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.
| 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.