Decrypting a QUIC Initial packet by hand
QUIC encrypts its handshake. The keys for the first packets are not secret, though: they are derived from a value the packet carries in the clear, so anyone can work them out.
The value everything comes from
A client's first packet carries a Destination Connection ID in the clear, in the long header. The client picks it at random; after a Retry it uses the Connection ID the server chose instead, and the keys are derived again from that. It is the only input. Both sides run it through HKDF with a salt that is written in the standard, so the Initial keys are reproducible by anyone holding the packet.
This is deliberate. Initial protection helps only against someone who cannot see the traffic: from off the path, nobody can make a packet the endpoints will accept. It gives no confidentiality and no authenticity against anyone who can see the packets, because they can derive the same keys, read what is sent and make new packets that open correctly. The real secrets arrive later, from the TLS handshake inside.
The derivation
Using RFC 9001’s own example, where the client chose the Connection ID
8394c8f03e515708:
- Extract. HKDF-Extract with the version 1 salt
38762cf7f55934b34d179ae6a4c80cadccbb7f0aand the Connection ID as the input key material. That gives the initial secret. - Split it in two. HKDF-Expand-Label with
client inandserver ingives one secret per direction, so each side protects its own packets. - Three keys per direction. From each secret, expand
quic key,quic ivandquic hp. The first two open the payload; the third unmasks the header.
For that Connection ID the client’s three values are:
| Label | Value |
|---|---|
quic key | 1f369613dd76d5467730efcbe3b1a22d |
quic iv | fa044b2f42a3fd3b46fb255c |
quic hp | 9f50449e04a0e810283a1e9933adedd2 |
and the server’s, from the same Connection ID:
| Label | Value |
|---|---|
quic key | cf3a5331653c364c88f0f379b6067e37 |
quic iv | 0ac1493ca1905853b0bba03e |
quic hp | c206b8d9b9f0f37644430b490eeaa314 |
The cipher is AES-128-GCM, and header protection is a single AES-128 block used as a keystream.
QUIC version 2 (RFC 9369) derives its Initial keys the same way, but with a different salt and its own
labels, quicv2 key, quicv2 iv and quicv2 hp, so the same Connection ID gives
different keys under each version.
Unmasking the header
QUIC masks a few header bits and the packet number so they cannot be used to follow a connection around. To
undo it, take a 16-byte sample starting four bytes after the start of the packet-number field. The
packet number's length is not known yet, so the sample's position assumes the longest one, four bytes. Encrypt
the sample with AES-128 in ECB mode under the quic hp key, and use the first five bytes of the result
as the mask. On a long header the low four bits of the first mask byte are XORed into the low four bits of the first byte, which include the
packet number's length (on a short header, the low five); the next one to four mask bytes are XORed into the packet number itself. That is why the first
byte has to be unmasked first.
Only then can you build the AEAD’s associated data, because that is the header after unmasking.
Opening the payload
The nonce is the quic iv with the packet number exclusive-ORed into its right-hand end. The
associated data is the unmasked header, the ciphertext is the rest of the packet, and the last 16 bytes are the
tag. AES-GCM either returns the plaintext or fails; there is no partial answer.
For an Initial packet that proves less than it seems. The keys are public, so a packet that opens is only consistent with keys anyone can derive: anyone who can see the traffic could have made it. Who sent it is settled by the TLS handshake that follows, not by the Initial keys.
Inside you will find CRYPTO frames carrying a TLS ClientHello. The client pads every UDP datagram that carries an Initial packet to at least 1,200 bytes (RFC 9000 section 14.1), for two reasons: it shows the path can carry datagrams that large, and it gives the server room to answer, because until the client's address is validated a server may send no more than three times what it has received.
The RFC's packet, all the way through
RFC 9001 Appendix A.2 publishes a complete protected client Initial: 1,200 bytes, Connection ID
8394c8f03e515708, packet number 2. Here is every step, using the client keys above.
| Step | Value |
|---|---|
| The protected header, as sent (22 bytes; the packet number starts at byte 18) | c000000001088394c8f03e5157080000449e7b9aec34 |
| The sample: 16 bytes from byte 22, four after the start of the packet number | d1b1c98dd7689fb8ec11d242b123dc9b |
AES-128-ECB of the sample under quic hp, first five bytes: the mask | 437b9aec36 |
First byte: c0 with its low four bits XORed with 3 | c3: a long header, packet number 4 bytes |
Packet number: 7b9aec34 XOR 7b9aec36 | 00000002 |
| The unprotected header, which is the associated data | c300000001088394c8f03e5157080000449e00000002 |
The nonce: quic iv with the packet number XORed into its last bytes | fa044b2f42a3fd3b46fb255e |
| AES-128-GCM opens the remaining 1,178 bytes (1,162 of ciphertext and a 16-byte tag); the plaintext starts | 060040f1010000ed0303 |
That plaintext is a CRYPTO frame (type 06) at offset 0, 241 bytes long (40f1), holding a
TLS ClientHello (type 01, 237 bytes, legacy version 0303). The other 917 bytes of the
payload are PADDING, which is zero bytes. This site's tests check every value in the table: against the RFC's
printed values where it prints them, and the nonce, which it does not print, from its key and packet number.
Try it
The in-page demo is not here yet.
It will let you paste a QUIC Initial packet and watch each step above happen in your browser, with nothing leaving your computer. It is not built yet because this site deliberately runs no scripts at all, and adding one is a decision we would rather take openly than quietly.
Meanwhile the numbers above are RFC 9001’s own Appendix A vectors, so any HKDF implementation will reproduce them in a few lines.
We are building a tool that does this for you, for the traffic your own programs send. It is not finished. Leave your email address and we will tell you when it is.