QUIC X25519 TLS 1.3 TLS 1.2

The Illustrated QUIC Connection

Every byte explained and reproduced

QUIC is a secure UDP-based stream protocol that forms the basis of HTTP/3.

In this demonstration a client connects to a server, negotiates a QUIC connection with TLS encryption, sends "ping", receives "pong", then terminates the connection. Click below to begin exploring.

Client Key Exchange Generation

The connection begins with the client generating a private/public keypair for key exchange. Key exchange is a technique where two parties can agree on the same number without an eavesdropper being able to tell what the number is.

An explanation of the key exchange can be found on my X25519 site, but doesn't need to be understood in depth for the rest of this page.

The private key is chosen by selecting an integer between 0 and 2256-1. The client does this by generating 32 bytes (256 bits) of random data. The private key selected is:

202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
The public key is chosen as explained on X25519 site. The public key calculated is:
358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254
The public key calculation can be confirmed at the command line:
### requires openssl 1.1.0 or higher
$ openssl pkey -noout -text < client-ephemeral-private.key

X25519 Private-Key:
priv:
    20:21:22:23:24:25:26:27:28:29:2a:2b:2c:2d:2e:
    2f:30:31:32:33:34:35:36:37:38:39:3a:3b:3c:3d:
    3e:3f
pub:
    35:80:72:d6:36:58:80:d1:ae:ea:32:9a:df:91:21:
    38:38:51:ed:21:a2:8e:3b:75:e9:65:d0:d2:cd:16:
    62:54
At this point nothing has been sent over the network. Continue the connection below.
Client Initial Keys Calc
Next, the client continues to prepare for the connection by generating the encryption keys for the Initial packets. Because key exchange between client and server has not taken place there is limited security in these keys - any observer can derive the keys and read the traffic like the server will. Encrypting the Initial packets prevents certain kinds of attacks such as request forgery attacks.

The client begins by generating 8 bytes of random data, in this case the bytes:
0001020304050607
The client then derives encryption keys using the following process:
initial_salt = 38762cf7f55934b34d179ae6a4c80cadccbb7f0a
initial_random = (random bytes from client given above)
initial_secret = HKDF-Extract(salt: initial_salt, key: initial_random)
client_secret = HKDF-Expand-Label(key: initial_secret, label: "client in", ctx: "", len: 32)
server_secret = HKDF-Expand-Label(key: initial_secret, label: "server in", ctx: "", len: 32)
client_key = HKDF-Expand-Label(key: client_secret, label: "quic key", ctx: "", len: 16)
server_key = HKDF-Expand-Label(key: server_secret, label: "quic key", ctx: "", len: 16)
client_iv = HKDF-Expand-Label(key: client_secret, label: "quic iv", ctx: "", len: 12)
server_iv = HKDF-Expand-Label(key: server_secret, label: "quic iv", ctx: "", len: 12)
client_hp_key = HKDF-Expand-Label(key: client_secret, label: "quic hp", ctx: "", len: 16)
server_hp_key = HKDF-Expand-Label(key: server_secret, label: "quic hp", ctx: "", len: 16)
The use of the magic constant "38762cf7f55934b34d179ae6a4c80cadccbb7f0a" as the initial salt is interesting, as it is not derived from mathematical constants or cryptographic principles. It's the value of the first SHA-1 collision, co-discovered by Google researchers (QUIC itself was initially created, sponsored, and deployed by Google).

This has introduced two new cryptographic concepts from TLS 1.3:
  • HKDF-Extract - given a salt and some bytes of key material create 256 bits (32 bytes) of new key material, with the input key material's entropy evenly distributed in the output.
  • HKDF-Expand-Label - given the inputs of key material, label, and context data, create a new key of the requested length.
I've created an HKDF tool to perform HKDF operations on the command line. The key derivation process is reproduced at the command line below:
$ init_salt=38762cf7f55934b34d179ae6a4c80cadccbb7f0a
$ init_dcid=0001020304050607
$ init_secret=$(./hkdf extract $init_salt $init_dcid)
$ csecret=$(./hkdf expandlabel $init_secret "client in" "" 32)
$ ssecret=$(./hkdf expandlabel $init_secret "server in" "" 32)
$ client_init_key=$(./hkdf expandlabel $csecret "quic key" "" 16)
$ server_init_key=$(./hkdf expandlabel $ssecret "quic key" "" 16)
$ client_init_iv=$(./hkdf expandlabel $csecret "quic iv" "" 12)
$ server_init_iv=$(./hkdf expandlabel $ssecret "quic iv" "" 12)
$ client_init_hp=$(./hkdf expandlabel $csecret "quic hp" "" 16)
$ server_init_hp=$(./hkdf expandlabel $ssecret "quic hp" "" 16)
$ echo ckey: $client_init_key
$ echo civ: $client_init_iv
$ echo chp: $client_init_hp
$ echo skey: $server_init_key
$ echo siv: $server_init_iv
$ echo shp: $server_init_hp

ckey: b14b918124fda5c8d79847602fa3520b
civ: ddbc15dea80925a55686a7df
chp: 6df4e9d737cdf714711d7c617ee82981
skey: d77fc4056fcfa32bd1302469ee6ebf90
siv: fcb748e37ff79860faa07477
shp: 440b2725e91dc79b370711ef792faa3d
From this we get the following encryption keys and IVs:
  • client initial key: b14b918124fda5c8d79847602fa3520b
  • client initial IV: ddbc15dea80925a55686a7df
  • server initial key: d77fc4056fcfa32bd1302469ee6ebf90
  • server initial IV: fcb748e37ff79860faa07477
We also get the following "header protection keys", which will be explained below:
  • client initial header protection key: 6df4e9d737cdf714711d7c617ee82981
  • server initial header protection key: 440b2725e91dc79b370711ef792faa3d
At this point there is still no data sent over the network. The first data is sent below.
UDP Datagram 1 - Client hello
Client Initial Packet
The session begins with the client sending an "Initial" packet. This packet contains the "ClientHello" TLS record, used to begin the TLS 1.3 encrypted session.
Packet Header Byte cd c0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from calc step above
$ key=6df4e9d737cdf714711d7c617ee82981
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=ed78716be9711ba498b7ed868443bb2e
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

ed9895bb15

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xC0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
00Packet type: Initial
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 08 00 01 02 03 04 05 06 07
The client has not yet received a connection ID chosen by the server. Instead it uses this field to provide the 8 bytes of random data for deriving Initial encryption keys, as explained in "Initial Keys Calc" above.
  • 08 - 8 bytes of connection ID follows
  • 00 01 ... 06 07 - the connection ID
Source Connection ID 05 63 5f 63 69 64
The client uses this field to indicate its chosen connection ID to the server.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Token 00
The client can use this field in some scenarios to provide a token requested by the server, such as to prove that its connection attempt is not spoofed. In this case, there is no token to provide, and the field is empty.
  • 00 - 0 bytes of token data follows
Packet Length 41 03
The client indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer. The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x103, or 259 bytes.
Packet Number 98 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Initial" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 1c 36 a7 ed 78 71 6b e9 71 1b a4 98 b7 ed 86 84 43 bb 2e 0c 51 4d 4d 84 8e ad cc 7a 00 d2 5c e9 f9 af a4 83 97 80 88 de 83 6b e6 8c 0b 32 a2 45 95 d7 81 3e a5 41 4a 91 99 32 9a 6d 9f 7f 76 0d d8 bb 24 9b f3 f5 3d 9a 77 fb b7 b3 95 b8 d6 6d 78 79 a5 1f e5 9e f9 60 1f 79 99 8e b3 56 8e 1f dc 78 9f 64 0a ca b3 85 8a 82 ef 29 30 fa 5c e1 4b 5b 9e a0 bd b2 9f 45 72 da 85 aa 3d ef 39 b7 ef af ff a0 74 b9 26 70 70 d5 0b 5d 07 84 2e 49 bb a3 bc 78 7f f2 95 d6 ae 3b 51 43 05 f1 02 af e5 a0 47 b3 fb 4c 99 eb 92 a2 74 d2 44 d6 04 92 c0 e2 e6 e2 12 ce f0 f9 e3 f6 2e fd 09 55 e7 1c 76 8a a6 bb 3c d8 0b bb 37 55 c8 b7 eb ee 32 71 2f 40 f2 24 51 19 48 70 21 b4 b8 4e 15 65 e3 ca 31 96 7a c8 60 4d 40 32 17 0d ec 28 0a ee fa 09 5d 08
This data is encrypted with the client "Initial" traffic key.
Auth Tag b3 b7 24 1e f6 64 6a 6c 86 e5 c6 2c e0 8b e0 99
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client "Initial" traffic key and IV that were generated during the "Initial Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 24 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Initial Keys Calc" step
$ key=b14b918124fda5c8d79847602fa3520b
$ iv=ddbc15dea80925a55686a7df
### from this record
$ recdata=c00000000108000102030405060705635f63696400410300
$ authtag=b3b7241ef6646a6c86e5c62ce08be099
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  06 00 40 ee 01 00 00 ea  03 03 00 01 02 03 04 05  |..@.............|
00000010  06 07 08 09 0a 0b 0c 0d  0e 0f 10 11 12 13 14 15  |................|
... snip ...
CRYPTO frame header 06 00 40 ee
Each QUIC packet contains a list of one or more frames in its payload. This packet contains only one frame, a CRYPTO frame.

CRYPTO frames create a single stream of bytes used by TLS to establish a secure connection.
  • 06 - frame type "CRYPTO"
  • 00 - variable length integer, offset of the crypto stream data being provided (0 bytes)
  • 40 ee - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data length of 0xEE (238) bytes
ClientHello TLS Record 01 00 00 ea 03 03 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 00 00 06 13 01 13 02 13 03 01 00 00 bb 00 00 00 18 00 16 00 00 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 00 0a 00 08 00 06 00 1d 00 17 00 18 00 10 00 0b 00 09 08 70 69 6e 67 2f 31 2e 30 00 0d 00 14 00 12 04 03 08 04 04 01 05 03 08 05 05 01 08 06 06 01 02 01 00 33 00 26 00 24 00 1d 00 20 35 80 72 d6 36 58 80 d1 ae ea 32 9a df 91 21 38 38 51 ed 21 a2 8e 3b 75 e9 65 d0 d2 cd 16 62 54 00 2d 00 02 01 01 00 2b 00 03 02 03 04 00 39 00 31 03 04 80 00 ff f7 04 04 80 a0 00 00 05 04 80 10 00 00 06 04 80 10 00 00 07 04 80 10 00 00 08 01 0a 09 01 0a 0a 01 03 0b 01 19 0f 05 63 5f 63 69 64
This record is represented in detail below.
TLS: ClientHello
The encrypted session begins with the client saying "Hello". The client provides information including the following:
  • client random data (used later in the handshake)
  • a list of cipher suites that the client supports
  • a public key for key exchange
  • protocol versions that the client can support
TLS Handshake Header 01 00 00 ea
Each TLS handshake message starts with a type and a length.
  • 01 - handshake message type 0x01 (client hello)
  • 00 00 ea - 0xEA (234) bytes of client hello data follows
Client Version 03 03
A protocol version of "3,3" (meaning TLS 1.2) is given. Because middleboxes have been created and widely deployed that do not allow protocol versions that they do not recognize, all TLS 1.3 sessions indicate version TLS 1.2 in this field. This field is no longer used, instead version negotiation is performed using the "Supported Versions" extension below.

The unusual version number ("3,3" representing TLS 1.2) is due to TLS 1.0 being a minor revision of the SSL 3.0 protocol. Therefore TLS 1.0 is represented by "3,1", TLS 1.1 is "3,2", and so on.
Client Random 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
The client provides 32 bytes of random data. This data will be used later in the session. In this example we've made the random data a predictable string.
Session ID 00
This is a legacy field and is not used in QUIC.
  • 00 - 0 bytes of session ID follow
Cipher Suites 00 06 13 01 13 02 13 03
The client provides an ordered list of which cipher suites it will support for encryption. The list is in the order preferred by the client, with highest preference first.
  • 00 06 - 6 bytes of cipher suite data
  • 13 01 - assigned value for TLS_AES_128_GCM_SHA256
  • 13 02 - assigned value for TLS_AES_256_GCM_SHA384
  • 13 03 - assigned value for TLS_CHACHA20_POLY1305_SHA256
Compression Methods 01 00
Previous versions of TLS supported compression, which was found to leak information about the encrypted data allowing it to be read (see CRIME).

TLS 1.3 no longer allows compression, so this field is always a single entry with the "null" compression method which performs no change to the data.
  • 01 - 1 bytes of compression methods
  • 00 - assigned value for "null" compression
Extensions Length 00 bb
The client has provided a list of optional extensions which the server can use to take action or enable new features.
  • 00 bb - the extensions will take 0xBB (187) bytes of data
Each extension will start with two bytes that indicate which extension it is, followed by a two-byte content length field, followed by the contents of the extension.
Extension - Server Name 00 00 00 18 00 16 00 00 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74
The client has provided the name of the server it is contacting, also known as SNI (Server Name Indication).

Without this extension a HTTPS server would not be able to provide service for multiple hostnames (virtual hosts) on a single IP address because it couldn't know which hostname's certificate to send until after the TLS session was negotiated and the HTTP request was made.
  • 00 00 - assigned value for extension "server name"
  • 00 18 - 0x18 (24) bytes of "server name" extension data follows
  • 00 16 - 0x16 (22) bytes of first (and only) list entry follows
  • 00 - list entry is type 0x00 "DNS hostname"
  • 00 13 - 0x13 (19) bytes of hostname follows
  • 65 78 61 ... 6e 65 74 - "example.ulfheim.net"
Extension - Supported Groups 00 0a 00 08 00 06 00 1d 00 17 00 18
The client has indicated that it supports elliptic curve (EC) cryptography for three curve types. To make this extension more generic for other cryptography types it calls these "supported groups" instead of "supported curves".

This list is presented in descending order of the client's preference.
  • 00 0a - assigned value for extension "supported groups"
  • 00 08 - 8 bytes of "supported group" extension data follows
  • 00 06 - 6 bytes of data are in the curves list
  • 00 1d - assigned value for the curve "x25519"
  • 00 17 - assigned value for the curve "secp256r1"
  • 00 18 - assigned value for the curve "secp384r1"
Extension - ALPN 00 10 00 0b 00 09 08 70 69 6e 67 2f 31 2e 30
Application Layer Protocol Negotiation, or ALPN, is used by QUIC to negotiate supported protocols and versions between server and client.
Some example applications might be "http/1.1", "h2" (HTTP/2), or "h3" (HTTP/3).
  • 00 10 - assigned value for extension "Application Layer Protocol Negotiation"
  • 00 0b - 0xB (11) bytes of "ALPN" extension data follows
  • 00 09 - 9 bytes of "ALPN" protocol data follows
  • 08 - 8 bytes of a protocol name follows
  • 70 69 ... 2e 30 - the string "ping/1.0"
Extension - Signature Algorithms 00 0d 00 14 00 12 04 03 08 04 04 01 05 03 08 05 05 01 08 06 06 01 02 01
This extension indicates which signature algorithms the client supports. This can influence the certificate that the server presents to the client, as well as the signature that is sent by the server in the CertificateVerify record.

This list is presented in descending order of the client's preference.
  • 00 0d - assigned value for extension "Signature Algorithms"
  • 00 14 - 0x14 (20) bytes of "Signature Algorithms" extension data follows
  • 00 12 - 0x12 (18) bytes of data are in the following list of algorithms
  • 04 03 - assigned value for ECDSA-SECP256r1-SHA256
  • 08 04 - assigned value for RSA-PSS-RSAE-SHA256
  • 04 01 - assigned value for RSA-PKCS1-SHA256
  • 05 03 - assigned value for ECDSA-SECP384r1-SHA384
  • 08 05 - assigned value for RSA-PSS-RSAE-SHA384
  • 05 01 - assigned value for RSA-PKCS1-SHA384
  • 08 06 - assigned value for RSA-PSS-RSAE-SHA512
  • 06 01 - assigned value for RSA-PKCS1-SHA512
  • 02 01 - assigned value for RSA-PKCS1-SHA1
Extension - Key Share 00 33 00 26 00 24 00 1d 00 20 35 80 72 d6 36 58 80 d1 ae ea 32 9a df 91 21 38 38 51 ed 21 a2 8e 3b 75 e9 65 d0 d2 cd 16 62 54
The client sends one or more ephemeral public keys using algorithm(s) that it thinks the server will support. This allows the rest of the handshake after the ClientHello and ServerHello messages to be encrypted, unlike previous protocol versions where the handshake was sent in the clear.
  • 00 33 - assigned value for extension "Key Share"
  • 00 26 - 0x26 (38) bytes of "Key Share" extension data follows
  • 00 24 - 0x24 (36) bytes of key share data follows
  • 00 1d - assigned value for x25519 (key exchange via curve25519)
  • 00 20 - 0x20 (32) bytes of public key follows
  • 35 80 ... 62 54 - public key from the step "Client Key Exchange Generation"
Extension - PSK Key Exchange Modes 00 2d 00 02 01 01
The client indicates the modes available for establishing keys from pre-shared keys (PSKs). Since we do not use PSKs in this session, this extension has no effect.
  • 00 2d - assigned value for extension "PSK Key Exchange Modes"
  • 00 02 - 2 bytes of "PSK Key Exchange Modes" extension data follows
  • 01 - 1 bytes of exchange modes follow
  • 01 - assigned value for "PSK with (EC)DHE key establishment"
Extension - Supported Versions 00 2b 00 03 02 03 04
The client indicates its support of TLS 1.3. For compatibility reasons this is put into an extension instead of the Client Version field above.
  • 00 2b - assigned value for extension "Supported Versions"
  • 00 03 - 3 bytes of "Supported Versions" extension data follows
  • 02 - 2 bytes of TLS version follows
  • 03 04 - assigned value for TLS 1.3
Extension - QUIC Transport Parameters 00 39 00 31 03 04 80 00 ff f7 04 04 80 a0 00 00 05 04 80 10 00 00 06 04 80 10 00 00 07 04 80 10 00 00 08 01 0a 09 01 0a 0a 01 03 0b 01 19 0f 05 63 5f 63 69 64
The client's configuration values for the QUIC connection are given here. They are put into this record instead of the headers of the Initial packet because all data in TLS records is protected from tampering by malicious actors.

The following QUIC parameters are set in the data below:
  • max_udp_payload_size: 65527
  • initial_max_data: 10485760
  • initial_max_stream_data_bidi_local: 1048576
  • initial_max_stream_data_bidi_remote: 1048576
  • initial_max_stream_data_uni: 1048576
  • initial_max_streams_bidi: 10
  • initial_max_streams_uni: 10
  • ack_delay_exponent: 3
  • initial_source_connection_id: "c_cid"
A full listing and explanation of the bytes follows:
  • 00 39 - assigned value for extension "QUIC Transport Parameters"
  • 00 31 - 0x31 (49) bytes of "QUIC Transport Parameters" extension data follows
  • 03 - assigned value for "max_udp_payload_size"
  • 04 - 4 bytes of "max_udp_payload_size" data follows
  • 80 00 ff f7 - a variable length integer with value 0xfff7 (65527)
  • 04 - assigned value for "initial_max_data"
  • 04 - 4 bytes of "initial_max_data" data follows
  • 80 a0 00 00 - a variable length integer with value 0xa00000 (10485760)
  • 05 - assigned value for "initial_max_stream_data_bidi_local"
  • 04 - 4 bytes of "initial_max_stream_data_bidi_local" data follows
  • 80 10 00 00 - a variable length integer with value 0x100000 (1048576)
  • 06 - assigned value for "initial_max_stream_data_bidi_remote"
  • 04 - 4 bytes of "initial_max_stream_data_bidi_remote" data follows
  • 80 10 00 00 - a variable length integer with value 0x100000 (1048576)
  • 07 - assigned value for "initial_max_stream_data_uni"
  • 04 - 4 bytes of "initial_max_stream_data_uni" data follows
  • 80 10 00 00 - a variable length integer with value 0x100000 (1048576)
  • 08 - assigned value for "initial_max_streams_bidi"
  • 01 - 1 bytes of "initial_max_streams_bidi" data follows
  • 0a - a variable length integer with value 0xA (10)
  • 09 - assigned value for "initial_max_streams_uni"
  • 01 - 1 bytes of "initial_max_streams_uni" data follows
  • 0a - a variable length integer with value 0xA (10)
  • 0a - assigned value for "ack_delay_exponent"
  • 01 - 1 bytes of "ack_delay_exponent" data follows
  • 03 - a variable length integer with value 3
  • 0b - assigned value for "GREASE", a technique for preventing middleboxes from disallowing new extensions, by pre-reserving extension values and injecting them randomly into connections
  • 01 - 1 bytes of "GREASE" data follows
  • 19 - a variable length integer with value 0x19 (25)
  • 0f - assigned value for "initial_source_connection_id"
  • 05 - 5 bytes of "initial_source_connection_id" data follows
  • 63 5f 63 69 64 - a copy of the source connection ID from the packet header: "c_cid"
Padding
Any datagram sent by the client that contains an Initial packet must be padded to a length of 1200 bytes. This library does it by appending nul bytes to the datagram.
Padding Bytes 00 00 00 00 00 00 00 00 ... snip ... 00 00 00 00 00 00 00 00
Padding this packet to a size of 1200 bytes serves two purposes:
  • Path MTU validation - Any IPv4 host or router is allowed to drop packets that exceed their MTU limit, to a minimum of 576 bytes. The vast majority of the internet has a much higher MTU (typically 1500 bytes). A higher packet size will increase throughput and performance. Given these realities QUIC chooses a minimum size constraint of 1200 bytes, which should traverse the vast majority of real networks (including tunneled networks) without being dropped for size.
    To prevent a scenario where a connection is established successfully with smaller packets but then starts timing out once larger packets are sent, the initial packets are padded to a length of 1200 bytes to prove that the end-to-end path will allow packets of that size.
  • Amplification Attack Mitigation - There is a class of network attack in which an attacker can send a small amount of traffic to an innocent third party which replies with a much larger amount of traffic directed at the target. In the case of QUIC this could be done with IP address spoofing, and would cause QUIC servers to reply to small Initial datagrams with much larger Handshake responses.
    To help mitigate this, QUIC servers are forbidden from replying to a client with more than 3 times the traffic that was sent to it, until the server has received some proof from the client that it's at the given address (such as round-trip data originally from the server). Adding padding to this Initial datagram gives the server a "byte budget" to perform handshake responses without exceeding this 3x limit.
Server Key Exchange Generation

The server creates its own private/public keypair for key exchange. Key exchange is a technique where two parties can agree on the same number without an eavesdropper being able to tell what the number is.

An explanation of the key exchange can be found on my X25519 site, but doesn't need to be understood in depth for the rest of this page.

The private key is chosen by selecting an integer between 0 and 2256-1. The server does this by generating 32 bytes (256 bits) of random data. The private key selected is:

909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
The public key is chosen as explained on X25519 site. The public key calculated is:
9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615
The public key calculation can be confirmed with command line tools:
### requires openssl 1.1.0 or higher
$ openssl pkey -noout -text < server-ephemeral-private.key

X25519 Private-Key:
priv:
    90:91:92:93:94:95:96:97:98:99:9a:9b:9c:9d:9e:
    9f:a0:a1:a2:a3:a4:a5:a6:a7:a8:a9:aa:ab:ac:ad:
    ae:af
pub:
    9f:d7:ad:6d:cf:f4:29:8d:d3:f9:6d:5b:1b:2a:f9:
    10:a0:53:5b:14:88:d7:f8:fa:bb:34:9a:98:28:80:
    b6:15
Server Initial Keys Calc
Next, the server performs its own calculation of the Initial traffic keys. It gets the 8 bytes of random data from the "Destination Connection ID" field from the client's first Initial packet:
0001020304050607
and computes the same keys using the method shown in "Client Initial Keys Calc":
  • client initial key: b14b918124fda5c8d79847602fa3520b
  • client initial IV: ddbc15dea80925a55686a7df
  • client initial header protection key: 6df4e9d737cdf714711d7c617ee82981
  • server initial key: d77fc4056fcfa32bd1302469ee6ebf90
  • server initial IV: fcb748e37ff79860faa07477
  • server initial header protection key: 440b2725e91dc79b370711ef792faa3d
UDP Datagram 2 - Server hello and handshake
Server Initial Packet
The server responds with an "Initial" packet in return. This packet contains the "ServerHello" TLS record, used to continue the TLS 1.3 encrypted session negotiation.
Packet Header Byte cd c0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from calc step above
$ key=440b2725e91dc79b370711ef792faa3d
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=d5d9c823d07c616882ca770279249864
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

4d3acc3988

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xC0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
00Packet type: Initial
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 63 5f 63 69 64
The client's chosen connection ID is repeated back to it.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Source Connection ID 05 73 5f 63 69 64
The server uses this field to indicate its chosen connection ID to the client.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Token 00
This field is unused in server responses.
  • 00 - 0 bytes of token data follows
Packet Length 40 75
The server indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer.

The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x75, or 117 bytes.
Packet Number 3a 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Initial" packet sent by the server.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 83 68 55 d5 d9 c8 23 d0 7c 61 68 82 ca 77 02 79 24 98 64 b5 56 e5 16 32 25 7e 2d 8a b1 fd 0d c0 4b 18 b9 20 3f b9 19 d8 ef 5a 33 f3 78 a6 27 db 67 4d 3c 7f ce 6c a5 bb 3e 8c f9 01 09 cb b9 55 66 5f c1 a4 b9 3d 05 f6 eb 83 25 2f 66 31 bc ad c7 40 2c 10 f6 5c 52 ed 15 b4 42 9c 9f 64 d8 4d 64 fa 40 6c
This data is encrypted with the server "Initial" traffic key.
Auth Tag f0 b5 17 a9 26 d6 2a 54 a9 29 41 36 b1 43 b0 33
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the server "Initial" traffic key and IV that were generated during the "Initial Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 21 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### From the "Initial Keys Calc" step
$ key=d77fc4056fcfa32bd1302469ee6ebf90
$ iv=fcb748e37ff79860faa07477
### from this record
$ recdata=c00000000105635f63696405735f63696400407500
$ authtag=f0b517a926d62a54a9294136b143b033
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 00 42 40 00 00 06 00  40 5a 02 00 00 56 03 03  |..B@....@Z...V..|
00000010  70 71 72 73 74 75 76 77  78 79 7a 7b 7c 7d 7e 7f  |pqrstuvwxyz{|}~.|
... snip ...
ACK frame 02 00 42 40 00 00
The server acknowledges receipt of the client's Initial packet 0.
  • 02 - frame type "ACK"
  • 00 - largest_acknowledged: largest packet being acknowledged
  • 42 40 - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 64 * 8 = 512 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 00 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
CRYPTO frame header 06 00 40 5a
CRYPTO frames create a single stream of bytes used by TLS to establish a secure connection.
  • 06 - frame type "CRYPTO"
  • 00 - variable-length integer, offset of the crypto stream data being provided (0 bytes)
  • 40 5a - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data length of 238 bytes
ClientHello TLS Record 02 00 00 56 03 03 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 00 13 01 00 00 2e 00 33 00 24 00 1d 00 20 9f d7 ad 6d cf f4 29 8d d3 f9 6d 5b 1b 2a f9 10 a0 53 5b 14 88 d7 f8 fa bb 34 9a 98 28 80 b6 15 00 2b 00 02 03 04
This record is represented in detail below.
TLS: ServerHello
The server says "Hello" back. The server provides information including the following:
  • server random data (used later in the handshake)
  • a selected cipher suite
  • a public key for key exchange
  • the negotiated protocol version
TLS Handshake Header 02 00 00 56
Each TLS handshake message starts with a type and a length.
  • 02 - handshake message type 0x02 (server hello)
  • 00 00 56 - 0x56 (86) bytes of server hello data follows
Server Version 03 03
A protocol version of "3,3" (meaning TLS 1.2) is given. Because middleboxes have been created and widely deployed that do not allow protocol versions that they do not recognize, all TLS 1.3 sessions indicate version TLS 1.2 in this field. This field is no longer used, instead version negotiation is performed using the "Supported Versions" extension below.

The unusual version number ("3,3" representing TLS 1.2) is due to TLS 1.0 being a minor revision of the SSL 3.0 protocol. Therefore TLS 1.0 is represented by "3,1", TLS 1.1 is "3,2", and so on.
Server Random 70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f
The server provides 32 bytes of random data. This data will be used later in the session. In this example we've made the random data a predictable string.
Session ID 00
This is a legacy field and is not used in QUIC.
  • 00 - 0 bytes of session ID follow
Cipher Suite 13 01
The server has selected cipher suite 0x1301 (TLS_AES_128_GCM_SHA256) from the list of options given by the client.
Compression Method 00
The server has selected compression method 0x00 ("Null", which performs no compression) from the list of options given by the client.
Extensions Length 00 2e
The server has returned a list of extensions to the client. Because the server is forbidden from replying with an extension that the client did not send in its hello message, the server knows that the client will understand and support all extensions listed.
  • 00 2e - the extensions will take 0x2E (46) bytes of data
Extension - Key Share 00 33 00 24 00 1d 00 20 9f d7 ad 6d cf f4 29 8d d3 f9 6d 5b 1b 2a f9 10 a0 53 5b 14 88 d7 f8 fa bb 34 9a 98 28 80 b6 15
The server sends a public key using the algorithm of the public key sent by the client. Once this is sent encryption keys can be calculated and the rest of the handshake will be encrypted, unlike previous protocol versions where the handshake was sent in the clear.
  • 00 33 - assigned value for extension "Key Share"
  • 00 24 - 0x24 (36) bytes of "Key Share" extension data follows
  • 00 1d - assigned value for x25519 (key exchange via curve25519)
  • 00 20 - 0x20 (32) bytes of public key follows
  • 9f d7 ... b6 15 - public key from the step "Server Key Exchange Generation"
Extension - Supported Versions 00 2b 00 02 03 04
The server indicates the negotiated TLS version of 1.3.
  • 00 2b - assigned value for extension "Supported Versions"
  • 00 02 - 2 bytes of "Supported Versions" extension data follows
  • 03 04 - assigned value for TLS 1.3
Server Handshake Keys Calc
The server now has the information needed to calculate the keys used to encrypt Handshake packets. It uses the following information in this calculation: First, the server finds the shared secret, which is the result of the key exchange that allows the client and server to agree on a number. The server multiplies the client's public key by the server's private key using the curve25519() algorithm. The 32-byte result is found to be:
df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
I've provided a tool to perform this calculation:
$ cc -o curve25519-mult curve25519-mult.c
$ ./curve25519-mult server-ephemeral-private.key \
                    client-ephemeral-public.key | hexdump

0000000 df 4a 29 1b aa 1e b7 cf a6 93 4b 29 b4 74 ba ad
0000010 26 97 e2 9f 1f 92 0d cc 77 c8 a0 a0 88 44 76 24
It then calculates the SHA256 hash of all handshake messages to this point (ClientHello and ServerHello). The hash does not include the 6-byte CRYPTO frame headers. This "hello_hash" is ff788f9ed09e60d8142ac10a8931cdb6a3726278d3acdba54d9d9ffc7326611b:
$ cat crypto_clienthello crypto_serverhello | openssl sha256
ff788f9ed09e60d8142ac10a8931cdb6a3726278d3acdba54d9d9ffc7326611b
We then feed the hash and the shared secret into a set of key derivation operations, designed to protect against known and possible attacks:
early_secret = HKDF-Extract(salt=00, key=00...)
empty_hash = SHA256("")
derived_secret = HKDF-Expand-Label(key: early_secret, label: "derived", ctx: empty_hash, len: 32)
handshake_secret = HKDF-Extract(salt: derived_secret, key: shared_secret)
client_secret = HKDF-Expand-Label(key: handshake_secret, label: "c hs traffic", ctx: hello_hash, len: 32)
server_secret = HKDF-Expand-Label(key: handshake_secret, label: "s hs traffic", ctx: hello_hash, len: 32)
client_key = HKDF-Expand-Label(key: client_secret, label: "quic key", ctx: "", len: 16)
server_key = HKDF-Expand-Label(key: server_secret, label: "quic key", ctx: "", len: 16)
client_iv = HKDF-Expand-Label(key: client_secret, label: "quic iv", ctx: "", len: 12)
server_iv = HKDF-Expand-Label(key: server_secret, label: "quic iv", ctx: "", len: 12)
I've created an HKDF tool to perform these operations on the command line. The key derivation process is reproduced below:
$ hello_hash=ff788f9ed09e60d8142ac10a8931cdb6a3726278d3acdba54d9d9ffc7326611b
$ shared_secret=df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
$ zero_key=0000000000000000000000000000000000000000000000000000000000000000
$ early_secret=$(./hkdf extract 00 $zero_key)
$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')
$ derived_secret=$(./hkdf expandlabel $early_secret "derived" $empty_hash 32)
$ handshake_secret=$(./hkdf extract $derived_secret $shared_secret)
$ csecret=$(./hkdf expandlabel $handshake_secret "c hs traffic" $hello_hash 32)
$ ssecret=$(./hkdf expandlabel $handshake_secret "s hs traffic" $hello_hash 32)
$ client_handshake_key=$(./hkdf expandlabel $csecret "quic key" "" 16)
$ server_handshake_key=$(./hkdf expandlabel $ssecret "quic key" "" 16)
$ client_handshake_iv=$(./hkdf expandlabel $csecret "quic iv" "" 12)
$ server_handshake_iv=$(./hkdf expandlabel $ssecret "quic iv" "" 12)
$ client_handshake_hp=$(./hkdf expandlabel $csecret "quic hp" "" 16)
$ server_handshake_hp=$(./hkdf expandlabel $ssecret "quic hp" "" 16)
$ echo ckey: $client_handshake_key
$ echo civ: $client_handshake_iv
$ echo chp: $client_handshake_hp
$ echo skey: $server_handshake_key
$ echo siv: $server_handshake_iv
$ echo shp: $server_handshake_hp

ckey: 30a7e816f6a1e1b3434cf39cf4b415e7
civ: 11e70a5d1361795d2bb04465
chp: 84b3c21cacaf9f54c885e9a506459079
skey: 17abbf0a788f96c6986964660414e7ec
siv: 09597a2ea3b04c00487e71f3
shp: 2a18061c396c2828582b41b0910ed536
From this we get the following encryption keys and IVs:
  • client handshake key: 30a7e816f6a1e1b3434cf39cf4b415e7
  • client handshake IV: 11e70a5d1361795d2bb04465
  • client handshake header protection key: 84b3c21cacaf9f54c885e9a506459079
  • server handshake key: 17abbf0a788f96c6986964660414e7ec
  • server handshake IV: 09597a2ea3b04c00487e71f3
  • server handshake header protection key: 2a18061c396c2828582b41b0910ed536
Server Handshake Packet
The server follows up with a "Handshake" packet. This packet contains TLS 1.3 handshake records from the server.
Packet Header Byte ed e0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from calc step above
$ key=2a18061c396c2828582b41b0910ed536
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=296209dff2d02d3d50af692176dd4d50
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

ddb7ce7613

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xE0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
10Packet type: Handshake
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 63 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Source Connection ID 05 73 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Packet Length 44 14
The server indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer.

The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x414, or 1044 bytes.
Packet Number b7 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Handshake" packet sent by the server.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data dd 73 ae 29 62 09 df f2 d0 2d 3d 50 af 69 21 76 dd 4d 50 9f e8 cb 1b 46 e4 5b 09 36 4d 81 5f a7 a5 74 8e 21 80 da d2 b7 b6 68 ca b8 6f bd c2 98 8c 45 cb b8 51 dd cf 16 01 b7 80 d7 48 b9 ee 64 1e bc be 20 12 6e 32 26 7e 66 4d 2f 37 cf 53 b7 53 d1 24 71 7c 2e 13 c4 8a 09 e3 42 8b 11 dc 73 ba eb d4 98 e8 ca f5 be ce fe a7 60 d0 e7 a5 cd b7 6b 52 bc b1 92 29 97 3e 5d 09 aa 05 5e 9c 97 18 dc 58 14 54 77 5c 58 ec dd 5e e7 e7 72 78 f5 60 10 70 40 41 62 a7 9e e8 c5 96 45 d6 ca 24 a2 00 18 6a e9 9c e4 7e ac e1 cf c9 52 7b 24 ae 8b c6 cc db ac b7 9b 81 c9 1a 26 95 47 07 ba 35 cb a0 ca e9 af f4 18 c6 e0 8d a6 50 61 63 a3 9f 19 b6 76 a6 6a c1 74 e3 29 5f 1a b9 ea 73 83 a9 c2 85 d7 3e 95 75 8d c9 bd 8d a9 07 34 a9 fe df d7 e1 f7 4d 2b 69 c7 0b f7 39 a4 8c 5a 5d 0a fa 0b fa 16 03 47 1b 0c 61 a9 ca de 12 0b 39 86 a6 ce 02 95 be 82 28 c6 92 70 13 b0 6d a5 8d 31 99 62 31 b9 e3 15 0b b5 82 70 96 0e 61 cb c6 69 8a 2f 13 79 a2 25 84 65 da 73 25 b3 49 c6 cd 55 d1 05 fd 54 85 fd 0a c7 9a 1d f1 db ba 7f 85 b4 9b 72 36 5b fa b9 d5 78 e0 1d cb ff 85 15 a6 32 fd 70 01 38 2e d9 0f 6c dc b1 7d b9 9a 33 fa 11 81 f6 f6 1a 89 e7 83 cf b0 42 fc 0f 2f 67 cd b6 0e 89 f2 63 88 56 81 ae 64 5a 1c 7a b1 59 0e b2 f8 46 9f 46 0f 04 e0 9f ea 2a 3a 41 1b 49 86 63 01 0b 3c 38 2a 3f 25 83 7c 2c 70 86 af 5a 9a d2 90 cf 3c cf 1a c6 eb 0f 44 55 35 e8 b0 0a 55 7c 87 a5 3d 93 07 14 62 a0 bc 22 61 4e 5c 3a e0 84 17 b7 20 a7 36 c1 ad 48 ea 37 75 cd 0f 00 9f 0c 57 50 0e 0b b2 e7 e9 c5 3f 83 69 9a 47 e5 f1 3b b2 07 72 ab 23 50 64 24 b7 6f 6e f9 6a 61 c9 17 22 6e 6e 04 8d e6 f8 24 26 ca 63 ea bf 3b 59 43 af 0b 5f 0d 12 3d 9a f0 45 bb 35 7c ad bd 10 92 ad 0a 1d 75 51 16 2a 3b 4b 48 6c 27 1e 00 24 4b 23 d8 ad ec 81 c9 2e 31 23 9c 75 af 41 cb 07 98 08 57 1b 48 ac b5 07 33 3f fb f1 a4 86 d8 05 3e dc c8 62 b6 a9 bf d3 6a 09 cd db a3 29 1b 9b 8b a1 58 49 34 59 80 5c e2 41 da f5 c1 30 85 99 fc 0e 6e 6e a7 10 30 33 b2 94 cc 7a 5f db 2d 46 54 f1 d4 40 78 25 eb c3 75 ab df b2 cc a1 ab f5 a2 41 34 3d ec 3b 16 5d 32 0a f8 4b c1 fa 21 11 2e fd b9 d4 5c 6c fc 7b 8a 64 42 ff 59 3d 09 21 93 36 fa 07 56 d9 e4 5b ab 4f a6 33 94 a2 a8 80 3d f4 67 8e 79 21 6f df 13 1f 55 82 2f 9e ad 69 4a b7 5e e2 54 96 e6 b7 8c 3b 09 04 66 58 e2 c4 27 dd c4 53 8a f8 de 2a cb 81 39 8b 74 82 83 37 f2 69 cb 03 1d 99 7a 5c f6 3e 11 ab 05 0a a8 ae e1 f0 79 62 dd d7 51 5a b6 0e 19 2e 40 3c 30 03 11 e9 e4 b9 b7 0f 16 15 02 9d 07 fe 1c 23 19 39 02 71 49 f4 fd 29 72 02 3a 55 de 29 35 65 05 fb e7 49 90 8c 62 aa 33 eb 25 9a 39 9b f7 11 b9 2b 61 6c b7 48 de 73 c8 bf ad d5 d4 3e 2d ae 91 6a 7b a0 db 61 df cd 6f af 95 76 08 26 2b 68 34 e3 31 85 b8 d5 59 8f 87 e6 99 2a ac f5 76 96 ad d5 55 8a 7d 96 94 38 1f 5d 7d 65 9d a2 de 95 1b 60 74 78 f6 1d a2 08 a2 4a 07 ba 8d a0 02 58 fa 7f 2f e1 0d ef 61 83 26 7f 5d 38 e0 4c 94 23 00 b9 c8 74 e8 98 3c 1b e1 4e 16 08 ff dc a6 7d 7e 45 13 cc 0c b9 ca b8 1d 63 19 dd 10 74 b2 17 e5 19 54 65 13 1e 06 dd 0b af ab a8 4e b5 2c 22 a4 a8 c6 12 a4 05 fe 6c 87 42 32 e4 a9 34 61 1b c7 3c 56 fe 70 b2 cb 7a 59 6c 1f 53 c7 29 b6 64 3c bd 70 d5 30 fe 31 96 06 9f c0 07 8e 89 fb b7 0d c1 b3 8a b4 e1 77 0c
This data is encrypted with the server "Handshake" traffic key.
Auth Tag 8f fb 53 31 6d 67 3a 32 b8 92 59 b5 d3 3e 94 ad
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the server "Handshake" traffic key and IV that were generated during the "Handshake Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 20 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### From the "Handshake Keys Calc" step
$ key=17abbf0a788f96c6986964660414e7ec
$ iv=09597a2ea3b04c00487e71f3
### from this record
$ recdata=e00000000105635f63696405735f636964441400
$ authtag=8ffb53316d673a32b89259b5d33e94ad
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  06 00 43 ff 08 00 00 56  00 54 00 10 00 0b 00 09  |..C....V.T......|
00000010  08 70 69 6e 67 2f 31 2e  30 00 39 00 41 00 08 00  |.ping/1.0.9.A...|
... snip ...
CRYPTO frame header 06 00 43 ff
CRYPTO frames create a single stream of bytes used by TLS to establish a secure connection.
  • 06 - frame type "CRYPTO"
  • 00 - variable length integer, offset of the crypto stream data being provided (0 bytes)
  • 43 ff - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data length of 0x3ff (1023) bytes
EncryptedExtensions TLS Record 08 00 00 56 00 54 00 10 00 0b 00 09 08 70 69 6e 67 2f 31 2e 30 00 39 00 41 00 08 00 01 02 03 04 05 06 07 01 04 80 01 d4 c0 03 04 80 00 ff f7 04 04 80 50 00 00 05 04 80 08 00 00 06 04 80 08 00 00 07 04 80 08 00 00 08 01 02 09 01 02 0a 01 03 0b 01 19 0f 05 73 5f 63 69 64
This record is represented in detail below.
Certificate TLS Record 0b 00 03 2e 00 00 03 2a 00 03 25 30 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 30 22 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 13 30 11 06 03 55 04 0a 13 0a 45 78 61 6d 70 6c 65 20 43 41 30 1e 17 0d 31 38 31 30 30 35 30 31 33 38 31 37 5a 17 0d 31 39 31 30 30 35 30 31 33 38 31 37 5a 30 2b 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 1c 30 1a 06 03 55 04 03 13 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 30 82 01 22 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 82 01 0f 00 30 82 01 0a 02 82 01 01 00 c4 80 36 06 ba e7 47 6b 08 94 04 ec a7 b6 91 04 3f f7 92 bc 19 ee fb 7d 74 d7 a8 0d 00 1e 7b 4b 3a 4a e6 0f e8 c0 71 fc 73 e7 02 4c 0d bc f4 bd d1 1d 39 6b ba 70 46 4a 13 e9 4a f8 3d f3 e1 09 59 54 7b c9 55 fb 41 2d a3 76 52 11 e1 f3 dc 77 6c aa 53 37 6e ca 3a ec be c3 aa b7 3b 31 d5 6c b6 52 9c 80 98 bc c9 e0 28 18 e2 0b f7 f8 a0 3a fd 17 04 50 9e ce 79 bd 9f 39 f1 ea 69 ec 47 97 2e 83 0f b5 ca 95 de 95 a1 e6 04 22 d5 ee be 52 79 54 a1 e7 bf 8a 86 f6 46 6d 0d 9f 16 95 1a 4c f7 a0 46 92 59 5c 13 52 f2 54 9e 5a fb 4e bf d7 7a 37 95 01 44 e4 c0 26 87 4c 65 3e 40 7d 7d 23 07 44 01 f4 84 ff d0 8f 7a 1f a0 52 10 d1 f4 f0 d5 ce 79 70 29 32 e2 ca be 70 1f df ad 6b 4b b7 11 01 f4 4b ad 66 6a 11 13 0f e2 ee 82 9e 4d 02 9d c9 1c dd 67 16 db b9 06 18 86 ed c1 ba 94 21 02 03 01 00 01 a3 52 30 50 30 0e 06 03 55 1d 0f 01 01 ff 04 04 03 02 05 a0 30 1d 06 03 55 1d 25 04 16 30 14 06 08 2b 06 01 05 05 07 03 02 06 08 2b 06 01 05 05 07 03 01 30 1f 06 03 55 1d 23 04 18 30 16 80 14 89 4f de 5b cc 69 e2 52 cf 3e a3 00 df b1 97 b8 1d e1 c1 46 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 03 82 01 01 00 59 16 45 a6 9a 2e 37 79 e4 f6 dd 27 1a ba 1c 0b fd 6c d7 55 99 b5 e7 c3 6e 53 3e ff 36 59 08 43 24 c9 e7 a5 04 07 9d 39 e0 d4 29 87 ff e3 eb dd 09 c1 cf 1d 91 44 55 87 0b 57 1d d1 9b df 1d 24 f8 bb 9a 11 fe 80 fd 59 2b a0 39 8c de 11 e2 65 1e 61 8c e5 98 fa 96 e5 37 2e ef 3d 24 8a fd e1 74 63 eb bf ab b8 e4 d1 ab 50 2a 54 ec 00 64 e9 2f 78 19 66 0d 3f 27 cf 20 9e 66 7f ce 5a e2 e4 ac 99 c7 c9 38 18 f8 b2 51 07 22 df ed 97 f3 2e 3e 93 49 d4 c6 6c 9e a6 39 6d 74 44 62 a0 6b 42 c6 d5 ba 68 8e ac 3a 01 7b dd fc 8e 2c fc ad 27 cb 69 d3 cc dc a2 80 41 44 65 d3 ae 34 8c e0 f3 4a b2 fb 9c 61 83 71 31 2b 19 10 41 64 1c 23 7f 11 a5 d6 5c 84 4f 04 04 84 99 38 71 2b 95 9e d6 85 bc 5c 5d d6 45 ed 19 90 94 73 40 29 26 dc b4 0e 34 69 a1 59 41 e8 e2 cc a8 4b b6 08 46 36 a0 00 00
This record is represented in detail below.
CertificateVerify TLS Record (fragment) 0f 00 01 04 08 04 01 00 0a 99 af 32 a9 e4 06 d7 25 f9 a9 39 6d e5 af 37 56 b7 a8 f6 e4 da d5 85 ab c3 f8 7c 6d 1f c1 5f 5f 00 ab a8 dc a9 d0 5c db 51 d3 c9 35 43 36 56 d8 8b 74 32 00 5e e7 e0 48 03 b2 47 57 44 d7 55 5c f3 de 48 9c c2 16 a4 85 a7 28 b2 18 90 e8 7a a9 41 5d 19 e6 3a 6a 77 9b 9c db b1 28 a8 04 c4 28 b8 27 fa 65 df cd 95 2a ce 54
This CRYPTO frame only contains the first part of this record. It is represented in detail in the next datagram.
TLS: EncryptedExtensions
Any extensions that aren't needed for negotiating encryption keys are listed here to hide them from eavesdroppers and middleboxes.
TLS Handshake Header 08 00 00 56
Each TLS handshake message starts with a type and a length.
  • 08 - handshake message type 0x08 (encrypted extensions)
  • 00 00 56 - 0x56 (86) bytes of handshake message data follows
Extensions Length 00 54
  • 00 54 - 0x54 (84) bytes of extension data follows
Extension - ALPN 00 10 00 0b 00 09 08 70 69 6e 67 2f 31 2e 30
Application Layer Protocol Negotiation, or ALPN, is used by QUIC to negotiate supported protocols and versions between server and client.
The server indicates it has chosen "ping/1.0" for the application protocol. It was the only option given by the client.
  • 00 10 - assigned value for extension "Application Layer Protocol Negotiation"
  • 00 0b - 0xB (11) bytes of "ALPN" extension data follows
  • 00 09 - 9 bytes of "ALPN" protocol data follows
  • 08 - 8 bytes of a protocol name follows
  • 70 69 ... 2e 30 - the string "ping/1.0"
Extension - QUIC Transport Parameters 00 39 00 41 00 08 00 01 02 03 04 05 06 07 01 04 80 01 d4 c0 03 04 80 00 ff f7 04 04 80 50 00 00 05 04 80 08 00 00 06 04 80 08 00 00 07 04 80 08 00 00 08 01 02 09 01 02 0a 01 03 0b 01 19 0f 05 73 5f 63 69 64
The server's configuration values for the QUIC connection are given here.

The following QUIC parameters are set in the data below:
  • initial_destination_connection_id: 0001020304050607
  • max_idle_timeout: 120000ms (2 minutes)
  • max_udp_payload_size: 65527
  • initial_max_data: 5242880
  • initial_max_stream_data_bidi_local: 524288
  • initial_max_stream_data_bidi_remote: 524288
  • initial_max_stream_data_uni: 524288
  • initial_max_streams_bidi: 2
  • initial_max_streams_uni: 2
  • ack_delay_exponent: 3
A full listing and explanation of the bytes follows:
  • 00 39 - assigned value for extension "QUIC Transport Parameters"
  • 00 41 - 0x41 (65) bytes of "QUIC Transport Parameters" extension data follows
  • 00 - assigned value for "initial_destination_connection_id"
  • 08 - 8 bytes of "initial_destination_connection_id" data follows
  • 00 01 ... 06 07 - the initial connection ID given by the client (used for Initial keys)
  • 01 - assigned value for "max_idle_timeout"
  • 04 - 4 bytes of "max_idle_timeout" data follows
  • 80 01 d4 c0 - a variable length integer with value 0x1d4c0 (120000)
  • 03 - assigned value for "max_udp_payload_size"
  • 04 - 4 bytes of "max_udp_payload_size" data follows
  • 80 00 ff f7 - a variable length integer with value 0xfff7 (65527)
  • 04 - assigned value for "initial_max_data"
  • 04 - 4 bytes of "initial_max_data" data follows
  • 80 50 00 00 - a variable length integer with value 0x500000 (5242880)
  • 05 - assigned value for "initial_max_stream_data_bidi_local"
  • 04 - 4 bytes of "initial_max_stream_data_bidi_local" data follows
  • 80 08 00 00 - a variable length integer with value 0x80000 (524288)
  • 06 - assigned value for "initial_max_stream_data_bidi_remote"
  • 04 - 4 bytes of "initial_max_stream_data_bidi_remote" data follows
  • 80 08 00 00 - a variable length integer with value 0x80000 (524288)
  • 07 - assigned value for "initial_max_stream_data_uni"
  • 04 - 4 bytes of "initial_max_stream_data_uni" data follows
  • 80 08 00 00 - a variable length integer with value 0x80000 (524288)
  • 08 - assigned value for "initial_max_streams_bidi"
  • 01 - 1 bytes of "initial_max_streams_bidi" data follows
  • 02 - a variable length integer with value 2
  • 09 - assigned value for "initial_max_streams_uni"
  • 01 - 1 bytes of "initial_max_streams_uni" data follows
  • 02 - a variable length integer with value 2
  • 0a - assigned value for "ack_delay_exponent"
  • 01 - 1 bytes of "ack_delay_exponent" data follows
  • 03 - a variable length integer with value 3
  • 0b - assigned value for "GREASE", a technique for preventing middleboxes from disallowing new extensions, by pre-reserving extension values and injecting them randomly into connections
  • 01 - 1 bytes of "GREASE" data follows
  • 19 - a variable length integer with value 25
  • 0f - assigned value for "initial_source_connection_id"
  • 05 - 5 bytes of "initial_source_connection_id" data follows
  • 73 5f 63 69 64 - a copy of the source connection ID from the packet header: "s_cid"
TLS: Certificate
The server sends one or more certificates:
  • the certificate for this host, containing the hostname, a public key, and a signature from a third party asserting that the owner of the certificate's hostname holds the private key for this certificate
  • an optional list of further certificates, each of which signs the previous certificate, and which form a chain of trust leading from the host certificate to a trusted certificate that has been pre-installed on the client
In an effort to keep this example small we only send a host certificate. Certificates are in a binary format called DER which you can explore here.
TLS Handshake Header 0b 00 03 2e
Each TLS handshake message starts with a type and a length.
  • 0b - handshake message type 0x0B (certificate)
  • 00 03 2e - 0x32E (814) bytes of certificate payload follow
Request Context 00
This record is empty because this certificate was not sent in response to a Certificate Request.
  • 00 - 0 bytes of request context follows
Certificates Length 00 03 2a
  • 00 03 2a - 0x32A (810) bytes of certificates follow
Certificate Length 00 03 25
The length of the first (and only) certificate.
  • 00 03 25 - 0x325 (805) bytes of certificate follows
Certificate 30 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 30 22 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 13 30 11 06 03 55 04 0a 13 0a 45 78 61 6d 70 6c 65 20 43 41 30 1e 17 0d 31 38 31 30 30 35 30 31 33 38 31 37 5a 17 0d 31 39 31 30 30 35 30 31 33 38 31 37 5a 30 2b 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 1c 30 1a 06 03 55 04 03 13 13 65 78 61 6d 70 6c 65 2e 75 6c 66 68 65 69 6d 2e 6e 65 74 30 82 01 22 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 82 01 0f 00 30 82 01 0a 02 82 01 01 00 c4 80 36 06 ba e7 47 6b 08 94 04 ec a7 b6 91 04 3f f7 92 bc 19 ee fb 7d 74 d7 a8 0d 00 1e 7b 4b 3a 4a e6 0f e8 c0 71 fc 73 e7 02 4c 0d bc f4 bd d1 1d 39 6b ba 70 46 4a 13 e9 4a f8 3d f3 e1 09 59 54 7b c9 55 fb 41 2d a3 76 52 11 e1 f3 dc 77 6c aa 53 37 6e ca 3a ec be c3 aa b7 3b 31 d5 6c b6 52 9c 80 98 bc c9 e0 28 18 e2 0b f7 f8 a0 3a fd 17 04 50 9e ce 79 bd 9f 39 f1 ea 69 ec 47 97 2e 83 0f b5 ca 95 de 95 a1 e6 04 22 d5 ee be 52 79 54 a1 e7 bf 8a 86 f6 46 6d 0d 9f 16 95 1a 4c f7 a0 46 92 59 5c 13 52 f2 54 9e 5a fb 4e bf d7 7a 37 95 01 44 e4 c0 26 87 4c 65 3e 40 7d 7d 23 07 44 01 f4 84 ff d0 8f 7a 1f a0 52 10 d1 f4 f0 d5 ce 79 70 29 32 e2 ca be 70 1f df ad 6b 4b b7 11 01 f4 4b ad 66 6a 11 13 0f e2 ee 82 9e 4d 02 9d c9 1c dd 67 16 db b9 06 18 86 ed c1 ba 94 21 02 03 01 00 01 a3 52 30 50 30 0e 06 03 55 1d 0f 01 01 ff 04 04 03 02 05 a0 30 1d 06 03 55 1d 25 04 16 30 14 06 08 2b 06 01 05 05 07 03 02 06 08 2b 06 01 05 05 07 03 01 30 1f 06 03 55 1d 23 04 18 30 16 80 14 89 4f de 5b cc 69 e2 52 cf 3e a3 00 df b1 97 b8 1d e1 c1 46 30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00 03 82 01 01 00 59 16 45 a6 9a 2e 37 79 e4 f6 dd 27 1a ba 1c 0b fd 6c d7 55 99 b5 e7 c3 6e 53 3e ff 36 59 08 43 24 c9 e7 a5 04 07 9d 39 e0 d4 29 87 ff e3 eb dd 09 c1 cf 1d 91 44 55 87 0b 57 1d d1 9b df 1d 24 f8 bb 9a 11 fe 80 fd 59 2b a0 39 8c de 11 e2 65 1e 61 8c e5 98 fa 96 e5 37 2e ef 3d 24 8a fd e1 74 63 eb bf ab b8 e4 d1 ab 50 2a 54 ec 00 64 e9 2f 78 19 66 0d 3f 27 cf 20 9e 66 7f ce 5a e2 e4 ac 99 c7 c9 38 18 f8 b2 51 07 22 df ed 97 f3 2e 3e 93 49 d4 c6 6c 9e a6 39 6d 74 44 62 a0 6b 42 c6 d5 ba 68 8e ac 3a 01 7b dd fc 8e 2c fc ad 27 cb 69 d3 cc dc a2 80 41 44 65 d3 ae 34 8c e0 f3 4a b2 fb 9c 61 83 71 31 2b 19 10 41 64 1c 23 7f 11 a5 d6 5c 84 4f 04 04 84 99 38 71 2b 95 9e d6 85 bc 5c 5d d6 45 ed 19 90 94 73 40 29 26 dc b4 0e 34 69 a1 59 41 e8 e2 cc a8 4b b6 08 46 36 a0
The certificate is in ASN.1 DER encoding. The details of this format and the content of this binary payload are documented on another page. The certificate can be converted to the binary data in this message at the command line:
$ openssl x509 -outform der < server.crt | hexdump

0000000 30 82 03 21 30 82 02 09 a0 03 02 01 02 02 08 15
0000010 5a 92 ad c2 04 8f 90 30 0d 06 09 2a 86 48 86 f7
... snip ...
Certificate Extensions 00 00
The server can provide extension data for the certificate.
  • 00 00 - 0 bytes of extension data follows
UDP Datagram 3 - Server handshake finished
Server Handshake Packet
The server continues with another "Handshake" packet. This packet contains the rest of the server's TLS 1.3 handshake records.
Packet Header Byte e5 e0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from calc step above
$ key=2a18061c396c2828582b41b0910ed536
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=19681c3f0f102a30f5e647a3399abf54
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

e54e8fcd38

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xE0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
10Packet type: Handshake
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 63 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Source Connection ID 05 73 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Packet Length 40 cf
The server indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer.

The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0xcf, or 207 bytes.
Packet Number 4f 01

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x01, indicating it is packet 1, or the second "Handshake" packet sent by the server.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 44 20 f9 19 68 1c 3f 0f 10 2a 30 f5 e6 47 a3 39 9a bf 54 bc 8e 80 45 31 34 99 6b a3 30 99 05 62 42 f3 b8 e6 62 bb fc e4 2f 3e f2 b6 ba 87 15 91 47 48 9f 84 79 e8 49 28 4e 98 3f d9 05 32 0a 62 fc 7d 67 e9 58 77 97 09 6c a6 01 01 d0 b2 68 5d 87 47 81 11 78 13 3a d9 17 2b 7f f8 ea 83 fd 81 a8 14 ba e2 7b 95 3a 97 d5 7e bf f4 b4 71 0d ba 8d f8 2a 6b 49 d7 d7 fa 3d 81 79 cb db 86 83 d4 bf a8 32 64 54 01 e5 a5 6a 76 53 5f 71 c6 fb 3e 61 6c 24 1b b1 f4 3b c1 47 c2 96 f5 91 40 29 97 ed 49 aa 0c 55 e3 17 21 d0 3e 14 11 4a f2 dc 45 8a e0 39 44 de 51 26 fe 08 d6 6a 6e f3 ba
This data is encrypted with the server "Handshake" traffic key.
Auth Tag 2e d1 02 5f 98 fe a6 d6 02 49 98 18 46 87 dc 06
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the server "handshake" traffic key and IV that were generated during the "Handshake Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 1. The process also takes as input the 20 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### From the "Handshake Keys Calc" step
$ key=17abbf0a788f96c6986964660414e7ec
$ iv=09597a2ea3b04c00487e71f3
### from this record
$ recdata=e00000000105635f63696405735f63696440cf01
$ authtag=2ed1025f98fea6d6024998184687dc06
$ recordnum=1
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  06 43 ff 40 b9 46 1e 8a  23 40 58 98 8e 7f 26 4d  |.C.@.F..#@X...&M|
00000010  7a b6 a5 1a 21 c6 29 79  b7 a6 79 f4 a0 87 70 85  |z...!.)y..y...p.|
... snip ...
CRYPTO frame header 06 43 ff 40 b9
CRYPTO frames create a single stream of bytes used by TLS to establish a secure connection.
  • 06 - frame type "CRYPTO"
  • 43 ff - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data offset of 0x3ff (1023) bytes
  • 40 b9 - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data length of 0xb9 (185) bytes
CertificateVerify TLS Record (fragment) 46 1e 8a 23 40 58 98 8e 7f 26 4d 7a b6 a5 1a 21 c6 29 79 b7 a6 79 f4 a0 87 70 85 6e 92 6d 37 1b 2e 89 16 9a a1 90 b8 03 63 6b b1 0c 0f b9 05 98 3d 2b 50 0a ad 26 83 df be 15 6e cc f6 66 de 1a 5a d4 5d 77 38 d5 e7 8b d1 7b c3 e6 d2 5f 9a d4 af ba 8f 81 de 9f 4d 55 72 11 8e 08 55 1a 4b b9 4b 56 a9 70 e8 04 c6 82 67 45 4b 51 7f c8 38 6c 9b ae 3a 77 cc cb 7f 29 0f 6e 58 fb a1 26 f0 53 33 a1 1f 8a b0 89 2e 6e 7a 89 58 53 82 d3 6e ef 25 29 cf 5b 7b
This CRYPTO frame contains the remainder of this TLS record. It is represented in detail below.
Handshake Finished TLS Record 14 00 00 20 06 8f cb 60 6a a1 c8 aa 35 4d 7b 60 64 a3 32 8c f3 76 bc d9 f3 20 0e 68 ac e3 de 2e e9 fc ac cb
This record is represented in detail below.
TLS: CertificateVerify
The server provides information that ties the ephemeral public key generated during Server Key Exchange Generation to the ownership of the certificate's private key.
TLS Handshake Header 0f 00 01 04
Each TLS handshake message starts with a type and a length.
  • 0f - handshake message type 0x0f (certificate verify)
  • 00 01 04 - 0x104 (260) bytes of handshake message data follows
Signature Algorithm 08 04
The server indicates the signature type:
  • 08 04 - assigned value for "rsa_pss_rsae_sha256"
Signature Length 01 00
The server indicates that 0x100 (256) bytes of signature follow.
Signature 0a 99 af 32 a9 e4 06 d7 25 f9 a9 39 6d e5 af 37 56 b7 a8 f6 e4 da d5 85 ab c3 f8 7c 6d 1f c1 5f 5f 00 ab a8 dc a9 d0 5c db 51 d3 c9 35 43 36 56 d8 8b 74 32 00 5e e7 e0 48 03 b2 47 57 44 d7 55 5c f3 de 48 9c c2 16 a4 85 a7 28 b2 18 90 e8 7a a9 41 5d 19 e6 3a 6a 77 9b 9c db b1 28 a8 04 c4 28 b8 27 fa 65 df cd 95 2a ce 54 46 1e 8a 23 40 58 98 8e 7f 26 4d 7a b6 a5 1a 21 c6 29 79 b7 a6 79 f4 a0 87 70 85 6e 92 6d 37 1b 2e 89 16 9a a1 90 b8 03 63 6b b1 0c 0f b9 05 98 3d 2b 50 0a ad 26 83 df be 15 6e cc f6 66 de 1a 5a d4 5d 77 38 d5 e7 8b d1 7b c3 e6 d2 5f 9a d4 af ba 8f 81 de 9f 4d 55 72 11 8e 08 55 1a 4b b9 4b 56 a9 70 e8 04 c6 82 67 45 4b 51 7f c8 38 6c 9b ae 3a 77 cc cb 7f 29 0f 6e 58 fb a1 26 f0 53 33 a1 1f 8a b0 89 2e 6e 7a 89 58 53 82 d3 6e ef 25 29 cf 5b 7b
Because the server is generating ephemeral keys for each session the session is not inherently tied to the certificate as it was in previous versions of TLS.

To prove that the server owns the server certificate (giving the certificate validity in this TLS session), it signs a hash of the handshake messages using the certificate's private key. The signature can be proven valid by the client by using the certificate's public key.

The signing process can't be reproduced byte-for-byte at the command line because the signing tool introduces random or changing data into the signature.

We can verify the signature using the server's certificate at the command line:
### find the hash of the conversation to this point, excluding
### 5-byte record headers or 1-byte wrapped record trailers
$ handshake_hash=$(cat crypto_clienthello crypto_serverhello \
  crypto_extensions crypto_cert | openssl sha256)

### build the data that was signed:
### 1. add 64 space characters
$ echo -n '                                ' > /tmp/tosign
$ echo -n '                                ' >> /tmp/tosign
### 2. add this fixed string
$ echo -n 'TLS 1.3, server CertificateVerify' >> /tmp/tosign
### 3. add a single null character
$ echo -en '\0' >> /tmp/tosign
### 4. add hash of handshake to this point
$ echo $handshake_hash | xxd -r -p >> /tmp/tosign

### copy the signature that we want to verify
$ echo "0a 99 af 32 a9 e4 06 d7 25 f9 a9 39 6d e5 af 37 56 b7 a8 f6
  e4 da d5 85 ab c3 f8 7c 6d 1f c1 5f 5f 00 ab a8 dc a9 d0 5c db 51
  d3 c9 35 43 36 56 d8 8b 74 32 00 5e e7 e0 48 03 b2 47 57 44 d7 55
  5c f3 de 48 9c c2 16 a4 85 a7 28 b2 18 90 e8 7a a9 41 5d 19 e6 3a
  6a 77 9b 9c db b1 28 a8 04 c4 28 b8 27 fa 65 df cd 95 2a ce 54 46
  1e 8a 23 40 58 98 8e 7f 26 4d 7a b6 a5 1a 21 c6 29 79 b7 a6 79 f4
  a0 87 70 85 6e 92 6d 37 1b 2e 89 16 9a a1 90 b8 03 63 6b b1 0c 0f
  b9 05 98 3d 2b 50 0a ad 26 83 df be 15 6e cc f6 66 de 1a 5a d4 5d
  77 38 d5 e7 8b d1 7b c3 e6 d2 5f 9a d4 af ba 8f 81 de 9f 4d 55 72
  11 8e 08 55 1a 4b b9 4b 56 a9 70 e8 04 c6 82 67 45 4b 51 7f c8 38
  6c 9b ae 3a 77 cc cb 7f 29 0f 6e 58 fb a1 26 f0 53 33 a1 1f 8a b0
  89 2e 6e 7a 89 58 53 82 d3 6e ef 25 29 cf 5b 7b" | xxd -r -p > /tmp/sig
### extract the public key from the certificate
$ openssl x509 -pubkey -noout -in server.crt > server.pub

### verify the signature
$ cat /tmp/tosign | openssl dgst -verify server.pub -sha256 \
    -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 -signature /tmp/sig

Verified OK
TLS: Handshake Finished
To verify that the handshake was successful and not tampered with, the server creates verification data that client will confirm. The verification data is built from a hash of all handshake messages.
TLS Handshake Header 14 00 00 20
Each TLS handshake message starts with a type and a length.
  • 14 - handshake message type 0x14 (finished)
  • 00 00 20 - 0x20 (32) bytes of handshake finished data follows
Verify Data 06 8f cb 60 6a a1 c8 aa 35 4d 7b 60 64 a3 32 8c f3 76 bc d9 f3 20 0e 68 ac e3 de 2e e9 fc ac cb
The verify_data is built using the server_secret from the "Server Handshake Keys Calc" step and a SHA256 hash of every handshake record before this point (ClientHello to CertificateVerify).
finished_key = HKDF-Expand-Label(key: server_secret, label: "finished", ctx: "", len: 32)
finished_hash = SHA256(ClientHello ... CertificateVerify)
verify_data = HMAC-SHA256(key: finished_key, msg: finished_hash)
We can use the HKDF tool to reproduce this on the command line.
### find the hash of the conversation to this point, excluding
### QUIC frame headers
$ fin_hash=$(cat crypto_clienthello crypto_serverhello \
  crypto_extensions crypto_cert crypto_certverify | openssl sha256)
$ sht_secret=88ad8d3b0986a71965a28d108b0f40ffffe629284a6028c80ddc5dc083b3f5d1
$ fin_key=$(./hkdf expandlabel $sht_secret "finished" "" 32)
$ echo $fin_hash | xxd -r -p \
    | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key

068fcb606aa1c8aa354d7b6064a3328cf376bcd9f3200e68ace3de2ee9fcaccb
Client Handshake Keys Calc
The client now has the information to calculate the keys that used to encrypt the rest of the handshake. It uses the following information in this calculation: First, the client finds the shared secret, which is the result of the key exchange that allows the client and server to agree on a number. The client multiplies the server's public key by the client's private key using the curve25519() algorithm. The properties of elliptic curve multiplication will cause this to result in the same number found by the server in its multiplication. The 32-byte result is found to be:
df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
I've provided a tool to perform this calculation:
$ cc -o curve25519-mult curve25519-mult.c
$ ./curve25519-mult client-ephemeral-private.key \
                    server-ephemeral-public.key | hexdump

0000000 df 4a 29 1b aa 1e b7 cf a6 93 4b 29 b4 74 ba ad
0000010 26 97 e2 9f 1f 92 0d cc 77 c8 a0 a0 88 44 76 24
Since the shared secret above is the same number calculated by the server in "Server Handshake Keys Calc", the rest of the calculation is identical and the same values are found:
  • client handshake key: 30a7e816f6a1e1b3434cf39cf4b415e7
  • client handshake IV: 11e70a5d1361795d2bb04465
  • client handshake header protection key: 84b3c21cacaf9f54c885e9a506459079
  • server handshake key: 17abbf0a788f96c6986964660414e7ec
  • server handshake IV: 09597a2ea3b04c00487e71f3
  • server handshake header protection key: 2a18061c396c2828582b41b0910ed536
UDP Datagram 4 - Acks
Client Initial Packet
The client sends one more "Initial" packet, containing an ACK for the server's last "Initial" packet.
Packet Header Byte cf c0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from handshake keys calc step above
$ key=6df4e9d737cdf714711d7c617ee82981
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=ed1f7b0555cdb783fbdf5b52724b7d29
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

8f57c29e79

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xC0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
00Packet type: Initial
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 73 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Source Connection ID 05 63 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Token 00
A token is not needed here, and is not sent.
  • 00 - 0 bytes of token data follows
Packet Length 40 17
The client indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer. The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x17, or 23 bytes.
Packet Number 56 01

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x01, indicating it is packet 1, or the second "Initial" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 6e 1f 98 ed 1f 7b
This data is encrypted with the client "Initial" traffic key.
Auth Tag 05 55 cd b7 83 fb df 5b 52 72 4b 7d 29 f0 af e3
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client "Initial" traffic key and IV that were generated during the "Initial Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 1. The process also takes as input the 21 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Initial Keys Calc" step
$ key=b14b918124fda5c8d79847602fa3520b
$ iv=ddbc15dea80925a55686a7df
### from this record
$ recdata=c00000000105735f63696405635f63696400401701
$ authtag=0555cdb783fbdf5b52724b7d29f0afe3
$ recordnum=1
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 00 40 81 00 00                                 |..@...|
ACK frame 02 00 40 81 00 00
The server acknowledges receipt of the client's Initial packet 0.
  • 02 - frame type "ACK"
  • 00 - largest_acknowledged: largest packet being acknowledged
  • 40 81 - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 129 * 8 = 1,032 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 00 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
Client Handshake Packet
The client sends a "Handshake" packet, containing an ACK for the server's last "Handshake" packet.
Packet Header Byte ee e0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from handshake keys calc step above
$ key=84b3c21cacaf9f54c885e9a506459079
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=c6cc12512d7eda141ec057b804d30feb
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

5e8c3ee850

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xE0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
10Packet type: Handshake
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 73 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Source Connection ID 05 63 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Packet Length 40 16
The client indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer. The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x16, or 22 bytes.
Packet Number 8c 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Handshake" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data b1 95 1f c6 cc
This data is encrypted with the client "Handshake" traffic key.
Auth Tag 12 51 2d 7e da 14 1e c0 57 b8 04 d3 0f eb 51 5b
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client "Handshake" traffic key and IV that were generated during the "Handshake Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 20 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Handshake Keys Calc" step
$ key=30a7e816f6a1e1b3434cf39cf4b415e7
$ iv=11e70a5d1361795d2bb04465
### from this record
$ recdata=e00000000105735f63696405635f636964401600
$ authtag=12512d7eda141ec057b804d30feb515b
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 00 20 00 00                                    |.. ..|
ACK frame 02 00 20 00 00
The client acknowledges receipt of the server's Handshake packet 0.
  • 02 - frame type "ACK"
  • 00 - largest_acknowledged: largest packet being acknowledged
  • 20 - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 32 * 8 = 256 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 00 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
Padding
Any datagram sent by the client that contains an Initial packet must be padded to a length of 1200 bytes. This library does it by appending nul bytes to the datagram.
Padding Bytes 00 00 00 00 00 00 00 00 ... snip ... 00 00 00 00 00 00 00 00
Padding this packet to a size of 1200 bytes serves two purposes:
  • Path MTU validation - Any IPv4 host or router is allowed to drop packets that exceed their MTU limit, to a minimum of 576 bytes. The vast majority of the internet has a much higher MTU (typically 1500 bytes). A higher packet size will increase throughput and performance. Given these realities QUIC chooses a minimum size constraint of 1200 bytes, which should traverse the vast majority of real networks (including tunneled networks) without being dropped for size.
    To prevent a scenario where a connection is established successfully with smaller packets but then starts timing out once larger packets are sent, the initial packets are padded to a length of 1200 bytes to prove that the end-to-end path will allow packets of that size.
  • Amplification Attack Mitigation - There is a class of network attack in which an attacker can send a small amount of traffic to an innocent third party which replies with a much larger amount of traffic directed at the target. In the case of QUIC this could be done with IP address spoofing, and would cause QUIC servers to reply to small Initial datagrams with much larger Handshake responses.
    To help mitigate this, QUIC servers are forbidden from replying to a client with more than 3 times the traffic that was sent to it, until the server has received some proof from the client that it's at the given address (such as round-trip data originally from the server). Adding padding to this Initial datagram gives the server a "byte budget" to perform handshake responses without exceeding this 3x limit.
Server Application Keys Calc
The server now has the information to calculate the keys used to encrypt application traffic. It uses the following information in this calculation:
  • The handshake secret (from "Server Handshake Key Calc")
  • The SHA256 hash of every handshake message from ClientHello to ServerHandshakeFinished
The hash input does not include the QUIC frame headers. This "handshake_hash" is b965185af5034eda0ea13ab424dde193afcb42451823a96921ae9d2dad9594ef:
$ cat crypto_clienthello crypto_serverhello crypto_extensions \
  crypto_cert crypto_certverify crypto_s_finished  | openssl sha256

b965185af5034eda0ea13ab424dde193afcb42451823a96921ae9d2dad9594ef
We then feed the hash and the handshake secret into a set of key derivation operations, designed to protect against known and possible attacks:
empty_hash = SHA256("")
derived_secret = HKDF-Expand-Label(key: handshake_secret, label: "derived", ctx: empty_hash, len: 32)
master_secret = HKDF-Extract(salt: derived_secret, key: 00...)
client_secret = HKDF-Expand-Label(key: master_secret, label: "c ap traffic", ctx: handshake_hash, len: 32)
server_secret = HKDF-Expand-Label(key: master_secret, label: "s ap traffic", ctx: handshake_hash, len: 32)
client_key = HKDF-Expand-Label(key: client_secret, label: "quic key", ctx: "", len: 16)
server_key = HKDF-Expand-Label(key: server_secret, label: "quic key", ctx: "", len: 16)
client_iv = HKDF-Expand-Label(key: client_secret, label: "quic iv", ctx: "", len: 12)
server_iv = HKDF-Expand-Label(key: server_secret, label: "quic iv", ctx: "", len: 12)
client_hp_key = HKDF-Expand-Label(key: client_secret, label: "quic hp", ctx: "", len: 16)
server_hp_key = HKDF-Expand-Label(key: server_secret, label: "quic hp", ctx: "", len: 16)
I've created an HKDF tool to perform these operations on the command line.
$ handshake_hash=b965185af5034eda0ea13ab424dde193afcb42451823a96921ae9d2dad9594ef
$ handshake_secret=fb9fc80689b3a5d02c33243bf69a1b1b20705588a794304a6e7120155edf149a
$ zero_key=0000000000000000000000000000000000000000000000000000000000000000
$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')
$ derived_secret=$(./hkdf expandlabel $handshake_secret "derived" $empty_hash 32)
$ master_secret=$(./hkdf extract $derived_secret $zero_key)
$ csecret=$(./hkdf expandlabel $master_secret "c ap traffic" $handshake_hash 32)
$ ssecret=$(./hkdf expandlabel $master_secret "s ap traffic" $handshake_hash 32)
$ client_data_key=$(./hkdf expandlabel $csecret "quic key" "" 16)
$ server_data_key=$(./hkdf expandlabel $ssecret "quic key" "" 16)
$ client_data_iv=$(./hkdf expandlabel $csecret "quic iv" "" 12)
$ server_data_iv=$(./hkdf expandlabel $ssecret "quic iv" "" 12)
$ client_data_hp=$(./hkdf expandlabel $csecret "quic hp" "" 16)
$ server_data_hp=$(./hkdf expandlabel $ssecret "quic hp" "" 16)
$ echo skey: $server_data_key
$ echo siv: $server_data_iv
$ echo shp: $server_data_hp
$ echo ckey: $client_data_key
$ echo civ: $client_data_iv
$ echo chp: $client_data_hp

skey: fd8c7da9de1b2da4d2ef9fd5188922d0
siv: 02f6180e4f4aa456d7e8a602
shp: b7f6f021453e52b58940e4bba72a35d4
ckey: e010a295f0c2864f186b2a7e8fdc9ed7
civ: eb3fbc384a3199dcf6b4c808
chp: 8a6a38bc5cc40cb482a254dac68c9d2f
From this we get the following key data:
  • server application key: fd8c7da9de1b2da4d2ef9fd5188922d0
  • server application IV: 02f6180e4f4aa456d7e8a602
  • server application header protection key: b7f6f021453e52b58940e4bba72a35d4
  • client application key: e010a295f0c2864f186b2a7e8fdc9ed7
  • client application IV: eb3fbc384a3199dcf6b4c808
  • client application header protection key: 8a6a38bc5cc40cb482a254dac68c9d2f
Client Application Keys Calc
The client has the information to calculate the keys used to encrypt application traffic. It performs the same calculation shown in "Server Application Keys Calc" and finds the same values:
  • server application key: fd8c7da9de1b2da4d2ef9fd5188922d0
  • server application IV: 02f6180e4f4aa456d7e8a602
  • server application header protection key: b7f6f021453e52b58940e4bba72a35d4
  • client application key: e010a295f0c2864f186b2a7e8fdc9ed7
  • client application IV: eb3fbc384a3199dcf6b4c808
  • client application header protection key: 8a6a38bc5cc40cb482a254dac68c9d2f
UDP Datagram 5 - Client handshake finished, "ping"
Client Handshake Packet
The client sends a "Handshake" packet, containing the client's "Handshake Finished" TLS record, completing the handshake process.
Packet Header Byte e0 e0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from handshake keys calc step above
$ key=84b3c21cacaf9f54c885e9a506459079
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=9da7e61daa07732aa10b5fbd11a00a62
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

b0b3b06690

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xE0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
10Packet type: Handshake
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 73 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Source Connection ID 05 63 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Packet Length 40 3f
The client indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer. The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x3f, or 63 bytes.
Packet Number b2 01

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x01, indicating it is packet 1, or the second "Handshake" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 5e 1e 45 9d a7 e6 1d aa 07 73 2a a1 0b 5f bd 11 a0 0a 62 0b f5 e1 27 e3 7b 81 bb 10 f1 1c 31 2e 7f 9c 04 a4 3c d5 30 f3 d9 81 d5 02 3a bd
This data is encrypted with the client "Handshake" traffic key.
Auth Tag 5e 98 f2 2d c6 f2 59 79 91 9b ad 30 2f 44 8c 0a
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client "Handshake" traffic key and IV that were generated during the "Handshake Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 20 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Handshake Keys Calc" step
$ key=30a7e816f6a1e1b3434cf39cf4b415e7
$ iv=11e70a5d1361795d2bb04465
### from this record
$ recdata=e00000000105735f63696405635f636964403f01
$ authtag=5e98f22dc6f25979919bad302f448c0a
$ recordnum=1
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 01 40 46 00 01 06 00  40 24 14 00 00 20 50 ff  |..@F....@$... P.|
00000010  b0 c1 a4 25 c6 41 89 1c  98 3d 12 67 26 02 6d 3d  |...%.A...=.g&.m=|
00000020  b2 8e a3 51 0b dc 20 54  fc d6 37 ed ca cc        |...Q.. T..7...|
ACK frame 02 01 40 46 00 01
The client acknowledges receipt of the server's Handshake packet 1.
  • 02 - frame type "ACK"
  • 01 - largest_acknowledged: largest packet being acknowledged
  • 40 46 - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 70 * 8 = 560 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 01 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
CRYPTO frame header 06 00 40 24
Each QUIC packet contains a list of one or more frames in its payload. This packet contains only one frame, a CRYPTO frame.

CRYPTO frames create a single stream of bytes used by TLS to establish a secure connection.
  • 06 - frame type "CRYPTO"
  • 00 - variable length integer, offset of the crypto stream data being provided (0 bytes)
  • 40 24 - variable length integer (first two bits indicate 2-byte integer) showing crypto stream data length of 0x24 (36) bytes
Handshake Finished TLS Record 14 00 00 20 50 ff b0 c1 a4 25 c6 41 89 1c 98 3d 12 67 26 02 6d 3d b2 8e a3 51 0b dc 20 54 fc d6 37 ed ca cc
This record is represented in detail below.
TLS: Handshake Finished
To verify that the handshake was successful and not tampered with, the client creates verification data that the server will confirm. The verification data is built from a hash of all handshake messages.
TLS Handshake Header 14 00 00 20
Each TLS handshake message starts with a type and a length.
  • 14 - handshake message type 0x14 (finished)
  • 00 00 20 - 0x20 (32) bytes of handshake finished data follow
Verify Data 50 ff b0 c1 a4 25 c6 41 89 1c 98 3d 12 67 26 02 6d 3d b2 8e a3 51 0b dc 20 54 fc d6 37 ed ca cc
The verify_data is built using the client_secret from the "Handshake Keys Calc" step and a SHA256 hash of every handshake record before this point (Client Hello to Server Finished).
finished_key = HKDF-Expand-Label(key: client_secret, label: "finished", ctx: "", len: 32)
finished_hash = SHA256(ClientHello ... ServerFinished)
verify_data = HMAC-SHA256(key: finished_key, msg: finished_hash)
We can use the HKDF tool to reproduce this on the command line.
### find the hash of the conversation to this point, excluding
### QUIC frame headers
$ fin_hash=$(cat crypto_clienthello crypto_serverhello crypto_extensions \
  crypto_cert crypto_certverify crypto_s_finished | openssl sha256)
$ cht_secret=b8902ab5f9fe52fdec3aea54e9293e4b8eabf955fcd88536bf44b8b584f14982
$ fin_key=$(./hkdf expandlabel $cht_secret "finished" "" 32)
$ echo $fin_hash | xxd -r -p \
    | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key

50ffb0c1a425c641891c983d126726026d3db28ea3510bdc2054fcd637edcacc
Client Application Packet
The client sends its first post-handshake packet, containing stream data with the contents "ping".
Packet Header Byte 4e 40

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "short" format packets such as this one, the protected sections are the lower 5 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from application keys calc step above
$ key=8a6a38bc5cc40cb482a254dac68c9d2f
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=e66e8ee950ba8b8ed10cba39a06ab7b0
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

4e1e62a65d

### first byte of result is xor'd into lower 5 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0x40 have the following meaning:
Val Meaning
MSB0Short header format
1Fixed bit (always set)
0"Spin" bit, optionally used to allow observers to measure RTT, but unused by this library
00Reserved (always unset)
0Key phase bit, used to signal when key rotation occurs
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
Destination Connection ID 73 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.

Note that the connection ID length is not given. The peer must know the length, either by always using the same length, or by embedding its own length encoding in the ID.
  • 73 5f 63 69 64 - the connection ID "s_cid"
Packet Number 1e 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Application" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data cc 91 70 e6 6e 8e e9 50 ba
This data is encrypted with the client "Application" traffic key.
Auth Tag 8b 8e d1 0c ba 39 a0 6a b7 b0 67 0a 50 ef 68 e6
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client key and IV that were generated during the "Application Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 16 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Application Keys Calc" step
$ key=e010a295f0c2864f186b2a7e8fdc9ed7
$ iv=eb3fbc384a3199dcf6b4c808
### from this record
$ recdata=40735f63696400
$ authtag=8b8ed10cba39a06ab7b0670a50ef68e6
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  0f 00 00 40 04 70 69 6e  67                       |...@.ping|
STREAM frame type and flags 0f
The client indicates it is sending data with a "STREAM" frame. Streams are the mechanism for all application data sent in a QUIC connection, and are analogous to an individual TCP connection.

The frame type is a number in the range 0x8 through 0xf (in binary: 0b00001xxx) with the variable bits acting as flags that give additional information about the stream frame:
Bitmask Meaning
0x4OFF: An "Offset" field is present in this frame (otherwise the offset is 0)
0x2LEN: A "Length" field is present in this frame (otherwise consume all data in the frame)
0x1FIN: This frame contains the final data of this stream, and the sender is done writing to it
In this case, the sender indicates all three: there will be an offset field, a length field, and this is the final data for this stream.
Stream ID 00
The client gives the ID number of the stream. ID numbers increase sequentially, with the last two bits indicating the stream type and direction:
Bitmask Meaning
0x2Indicates whether the stream is bi-directional (0) or uni-directional (1).
0x1Indicates whether the stream was opened by client (0) or server (1).
In this case, the stream ID of 0 indicates it is the first bi-directional stream opened by the client.
Stream Offset 00
A variable-length integer indicating the offset of the stream data. In this case, a single-byte integer showing an offset of 0.
Stream Length 40 04
A variable-length integer indicating the length of the stream data. In this case, the first two bits (0 1) indicate a two-byte integer, and the remaining bits give the length of 4 bytes.
Stream Data 70 69 6e 67
The application data "ping".
UDP Datagram 6 - "pong"
Server Handshake Packet
The server sends a "Handshake" packet, containing an ACK for the client's last "Handshake" packet.
Packet Header Byte e5 e0

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "long" format packets such as this one, the protected sections are the lower 4 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from handshake keys calc step above
$ key=2a18061c396c2828582b41b0910ed536
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=169e6f1b817e4623e1acbe1db3899b00
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

a5a6f88ece

### first byte of result is xor'd into lower 4 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0xE0 have the following meaning:
Val Meaning
MSB1Long header format
1Fixed bit (always set)
10Packet type: Handshake
00Reserved (always unset)
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
QUIC Version 00 00 00 01
The version of QUIC is given: version 1.
Destination Connection ID 05 63 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.
  • 05 - 5 bytes of connection ID follows
  • 63 5f 63 69 64 - the connection ID "c_cid"
Source Connection ID 05 73 5f 63 69 64
The source connection ID is given.
  • 05 - 5 bytes of connection ID follows
  • 73 5f 63 69 64 - the connection ID "s_cid"
Packet Length 40 16
The server indicates how many bytes of encrypted payload are in the packet. This field is a variable length integer - the first two bits of the first byte indicate how many total bytes are in the integer. The first byte starts with the two bits "0 1" (0x4), which indicate two bytes. The remaining bits give the number 0x16, or 22 bytes.
Packet Number a4 02

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x02, indicating it is packet 2, or the third "Handshake" packet sent by the server.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 87 5b 25 16 9e
This data is encrypted with the server "Handshake" traffic key.
Auth Tag 6f 1b 81 7e 46 23 e1 ac be 1d b3 89 9b 00 ec fb
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the server "Handshake" traffic key and IV that were generated during the "Handshake Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 20 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Handshake Keys Calc" step
$ key=17abbf0a788f96c6986964660414e7ec
$ iv=09597a2ea3b04c00487e71f3
### from this record
$ recdata=e00000000105635f63696405735f636964401602
$ authtag=6f1b817e4623e1acbe1db3899b00ecfb
$ recordnum=2
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 01 1c 00 01                                    |.....|
ACK frame 02 01 1c 00 01
The server acknowledges receipt of the client's Handshake packet 0.
  • 02 - frame type "ACK"
  • 01 - largest_acknowledged: largest packet being acknowledged
  • 1c - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 28 * 8 = 224 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (1) optionally provided below
  • 01 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
Server Application Packet
The server sends its first post-handshake packet, containing stream data with the reply "pong".
Packet Header Byte 49 40

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "short" format packets such as this one, the protected sections are the lower 5 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from application keys calc step above
$ key=b7f6f021453e52b58940e4bba72a35d4
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=4057c883e94d9c296baa8ca0ea6e3a21
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

09cd79a059

### first byte of result is xor'd into lower 5 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0x40 have the following meaning:
Val Meaning
MSB0Short header format
1Fixed bit (always set)
0"Spin" bit, optionally used to allow observers to measure RTT, but unused by this library
00Reserved (always unset)
0Key phase bit, used to signal when key rotation occurs
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
Destination Connection ID 63 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.

Note that the connection ID length is not given. The peer must know the length, either by always using the same length, or by embedding its own length encoding in the ID.
  • 63 5f 63 69 64 - the connection ID "c_cid"
Packet Number cd 00

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x00, indicating it is packet 0, or the first "Application" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 9a 64 12 40 57 c8 83 e9 4d 9c 29 6b aa 8c a0
This data is encrypted with the client "Application" traffic key.
Auth Tag ea 6e 3a 21 fa af 99 af 2f e1 03 21 69 20 57 d2
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client key and IV that were generated during the "Application Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 16 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Application Keys Calc" step
$ key=fd8c7da9de1b2da4d2ef9fd5188922d0
$ iv=02f6180e4f4aa456d7e8a602
### from this record
$ recdata=40635f63696400
$ authtag=ea6e3a21faaf99af2fe10321692057d2
$ recordnum=0
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 00 12 00 00 1e 0f 00  00 40 04 70 6f 6e 67     |.........@.pong|
ACK frame 02 00 12 00 00
The server acknowledges receipt of the client's Application packet 0.
  • 02 - frame type "ACK"
  • 00 - largest_acknowledged: largest packet being acknowledged
  • 12 - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 18 * 8 = 144 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 00 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
HANDSHAKE_DONE frame 1e
The server confirms to the client that the handshake is complete. This frame has no content other than its type.
  • 1e - frame type "HANDSHAKE_DONE"
STREAM frame type and flags 0f
The server indicates it is sending data with a "STREAM" frame. Streams are the mechanism for all application data sent in a QUIC connection, and are analogous to an individual TCP connection.

The frame type is a number in the range 0x8 through 0xf (in binary: 0b00001xxx) with the variable bits acting as flags that give additional information about the stream frame:
Bitmask Meaning
0x4OFF: An "Offset" field is present in this frame (otherwise the offset is 0)
0x2LEN: A "Length" field is present in this frame (otherwise consume all data in the frame)
0x1FIN: This frame contains the final data of this stream, and the sender is done writing to it
In this case, the sender indicates all three: there will be an offset field, a length field, and this is the final data for this stream.
Stream ID 00
The client gives the ID number of the stream. ID numbers increase sequentially, with the last two bits indicating the stream type and direction:
Bitmask Meaning
0x2Indicates whether the stream is bi-directional (0) or uni-directional (1).
0x1Indicates whether the stream was opened by client (0) or server (1).
In this case, the stream ID of 0 indicates it is the first bi-directional stream opened by the client.
Stream Offset 00
A variable-length integer indicating the offset of the stream data. In this case, a single-byte integer showing an offset of 0.
Stream Length 40 04
A variable-length integer indicating the length of the stream data. In this case, the first two bits (0 1) indicate a two-byte integer, and the remaining bits give the length of 4 bytes.
Stream Data 70 6f 6e 67
The application data "pong".
UDP Datagram 7 - Acks
Client Application Packet
The client acknowledges the data from the server.
Packet Header Byte 5a 40

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "short" format packets such as this one, the protected sections are the lower 5 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "client header protection key" from application keys calc step above
$ key=8a6a38bc5cc40cb482a254dac68c9d2f
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=90588b44b10d7cd32b03e34502802f25
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

1ac9ce3a7a0

### first byte of result is xor'd into lower 5 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0x40 have the following meaning:
Val Meaning
MSB0Short header format
1Fixed bit (always set)
0"Spin" bit, optionally used to allow observers to measure RTT, but unused by this library
00Reserved (always unset)
0Key phase bit, used to signal when key rotation occurs
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
Destination Connection ID 73 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.

Note that the connection ID length is not given. The peer must know the length, either by always using the same length, or by embedding its own length encoding in the ID.
  • 73 5f 63 69 64 - the connection ID "s_cid"
Packet Number c8 01

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x01, indicating it is packet 1, or the second "Application" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 67 e0 b4 90 58
This data is encrypted with the client "Application" traffic key.
Auth Tag 8b 44 b1 0d 7c d3 2b 03 e3 45 02 80 2f 25 a1 93
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client key and IV that were generated during the "Application Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 16 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Application Keys Calc" step
$ key=e010a295f0c2864f186b2a7e8fdc9ed7
$ iv=eb3fbc384a3199dcf6b4c808
### from this record
$ recdata=40735f63696401
$ authtag=8b44b10d7cd32b03e34502802f25a193
$ recordnum=1
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  02 00 0b 00 00                                    |.....|
ACK frame 02 00 0b 00 00
The client acknowledges receipt of the server's Application packet 0.
  • 02 - frame type "ACK"
  • 00 - largest_acknowledged: largest packet being acknowledged
  • 0b - ack_delay: variable-length integer giving the amount of time this ack was delayed in sending, in microseconds. Multiply by 2ack_delay_exponent, giving a value of 11 * 8 = 88 µseconds
  • 00 - ack_range_count: the number of additional ack ranges (0) optionally provided below
  • 00 - first_ack_range: variable length integer giving the number of packets before largest_acknowledged that are also being acknowledged
UDP Datagram 8 - Close connection
Server Application Packet
The server, having confirmed that the client has received all pending data, shuts down the connection.
Packet Header Byte 54 40

The packet begins with a header byte, which has header protection applied. Header protection is used to hide packet numbers and other information from outside observers.

Header protection is applied by encrypting a sample of each packet's payload with the "header protection key", then XOR'ing certain bits and bytes in each packet with the resulting data. For "short" format packets such as this one, the protected sections are the lower 5 bits of this byte, and the bytes of the Packet Number (seen later).

An example of how to compute header protection:
### "server header protection key" from application keys calc step above
$ key=b7f6f021453e52b58940e4bba72a35d4
### sample is taken from 16 bytes of payload starting
### 4 bytes past the first byte of the packet number
$ sample=ffeb17b67ec27f97e50d271dc702d92c
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 5 | xxd -p

f494fdfbb6

### first byte of result is xor'd into lower 5 bits of this byte,
### remaining bytes are xor'd one-for-one into the bytes of
### the packet number (which in this packet is only one byte)
The bits in the unprotected byte 0x40 have the following meaning:
Val Meaning
MSB0Short header format
1Fixed bit (always set)
0"Spin" bit, optionally used to allow observers to measure RTT, but unused by this library
00Reserved (always unset)
0Key phase bit, used to signal when key rotation occurs
LSB00Packet Number field length (indicates the "Packet Number"
field below will have length of one byte)
Destination Connection ID 63 5f 63 69 64
The destination connection ID is given. This field allows packets for a connection to be recognized by recipients even if the sender's network address or NAT translation has changed, making it more resilient than the underlying network connection.

Note that the connection ID length is not given. The peer must know the length, either by always using the same length, or by embedding its own length encoding in the ID.
  • 63 5f 63 69 64 - the connection ID "c_cid"
Packet Number 95 01

This byte has header protection applied, see Packet Header Byte for details.

This byte has the unprotected value of 0x01, indicating it is packet 1, or the second "Application" packet sent by the client.

This data is also potentially truncated. The sending endpoint calculates the spread between the highest packet number sent and the lowest unacknowledged packet number, doubles that spread for safety, rounds up, then figures the number of bytes it can remove from the high bits of the packet number to unambiguously represent a number between those two ends. The encoded packet number is then truncated to that number of bytes, and the receiving endpoint fills in the full number based on the packet numbers it has most recently seen. Because our example conversation sends so few packets (fewer than 64), this truncation won't occur in this document. See RFC 9000 for details.
Encrypted Data 18 c4 a5 ff eb 17 b6 7e c2 7f 97 e5 0d 27 1d c7 02 d9 2c ef b0
This data is encrypted with the client "Application" traffic key.
Auth Tag 68 8b e9 fd 7b 30 2d 9e b4 7c df 1f c4 cd 9a ac
This is the AEAD authentication tag that confirms the integrity of the encrypted data and the packet header. It is produced by the encryption algorithm, and consumed by the decryption algorithm.
Decryption
This data is encrypted using the client key and IV that were generated during the "Application Keys Calc" step. The IV will be modified by XOR'ing it by the count of records that have already been encrypted with this key, which in this case is 0. The process also takes as input the 16 bytes of header at the beginning of this packet, as authenticated data that must match for decryption to succeed.

Because the openssl command line tool does not yet support AEAD ciphers, I've written command line tools to both decrypt and encrypt this data.
### from the "Application Keys Calc" step
$ key=fd8c7da9de1b2da4d2ef9fd5188922d0
$ iv=02f6180e4f4aa456d7e8a602
### from this record
$ recdata=40635f63696401
$ authtag=688be9fd7b302d9eb47cdf1fc4cd9aac
$ recordnum=1
### may need to add -I and -L flags for include and lib dirs
$ cc -o aes_128_gcm_decrypt aes_128_gcm_decrypt.c -lssl -lcrypto
$ cat /tmp/msg1 \
  | ./aes_128_gcm_decrypt $iv $recordnum $key $recdata $authtag \
  | hexdump -C

00000000  1c 00 00 11 67 72 61 63  65 66 75 6c 20 73 68 75  |....graceful shu|
00000010  74 64 6f 77 6e                                    |tdown|
CONNECTION_CLOSE frame 1c 00 00 11 67 72 61 63 65 66 75 6c 20 73 68 75 74 64 6f 77 6e
The server sends an indication to close the connection.
  • 1c - frame type "CONNECTION_CLOSE"
  • 00 - error code: "No Error"
  • 00 - frame type which triggered this error (0 = unknown)
  • 11 - variable-length integer giving the length of the reason for shutdown
  • 67 72 .. 77 6e - 0x11 (17) bytes giving the reason for shutdown: "graceful shutdown"

The code for this project, including packet captures, can be found on GitHub.

You may also be interested in a breakdown of TLS 1.3.

If you found this page useful or interesting let me know via Twitter @XargsNotBombs.

[print]