QUIC DTLS TLS 1.3 TLS 1.2

The Illustrated DTLS Connection

Every byte explained and reproduced

In this demonstration a client connects to a server, negotiates a DTLS 1.3 connection, 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 created from the private key as explained on the 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 Hello Datagram
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
DTLS Record Header 16 fe fd 00 00 00 00 00 00 00 00 00 9d
Each DTLS record starts with a type, some sequence info, and a length.
  • 16 - TLS record type 22 (Handshake)
  • fe fd - Protocol version (DTLS 1.2, see below)
  • 00 00 - key epoch (incremented each time the encryption keys are updated)
  • 00 00 00 00 00 00 - DTLS record sequence number 0
  • 00 9d - length of following data in this record (0x9D (157) bytes)

DTLS versions are encoded by breaking the protocol version into parts and then putting each part into a byte with the ones-complement value (thus "1.3" becomes {1,3} which becomes the bytes 0xFE 0xFC). This complement technique keeps DTLS versions distinct from TLS versions.

Because middleboxes have been created and deployed that do not allow protocol versions that they do not recognize, all DTLS 1.3 sessions indicate version DTLS 1.2 (0xFE 0xFD) in unencrypted records.

TLS Handshake Header 01 00 00 91
Each TLS handshake record starts with a type and a length.
  • 01 - handshake record type 1 (ClientHello)
  • 00 00 91 - 0x91 (145) bytes of client hello data is in this handshake record.
Handshake Reconstruction Data 00 00 00 00 00 00 00 91
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 00 - handshake message sequence number 0
  • 00 00 00 - fragment offset of 0 bytes
  • 00 00 91 - fragment length of 0x91 (145) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Legacy Client Version fe fd
DTLS versions are encoded by breaking the protocol version into parts and then putting each part into a byte with the ones-complement value (thus "1.3" becomes {1,3} which becomes the bytes 0xFE 0xFC). This complement technique keeps DTLS versions distinct from TLS versions.

Because middleboxes have been created and deployed that do not allow protocol versions that they do not recognize, all DTLS 1.3 sessions indicate version DTLS 1.2 (0xFE 0xFD) in this field. Therefore this field is no longer used in version negotiation, which uses the "Supported Versions" extension below instead.

Client Random e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff
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.
Legacy Session ID 00
This is a legacy field and is not used in DTLS 1.3.
  • 00 - 0 bytes of session ID follow
Legacy Cookie 00
This is a legacy field and is not used in DTLS 1.3.
  • 00 - 0 bytes of round-trip confirmation 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 (and therefore DTLS) supported compression, which was found to leak information about the encrypted data allowing it to be read (see CRIME).

DTLS 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 byte of compression methods
  • 00 - assigned value for "null" compression
Extensions Length 00 61
The client has provided a list of optional extensions which the server can use to take action or enable new features.
  • 00 61 - the extensions will take 0x61 (97) 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 - 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 - Supported Versions 00 2b 00 03 02 fe fc
The client indicates its support of DTLS 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 DTLS version follows
  • fe fc - assigned value for DTLS 1.3
Extension - Signature Algorithms 00 0d 00 20 00 1e 06 03 05 03 04 03 02 03 08 06 08 0b 08 05 08 0a 08 04 08 09 06 01 05 01 04 01 03 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 20 - 0x20 (32) bytes of "Signature Algorithms" extension data follows
  • 00 1e - 0x1E (30) bytes of data are in the following list of algorithms
  • 06 03 - assigned value for ECDSA-SECP512r1-SHA512
  • 05 03 - assigned value for ECDSA-SECP384r1-SHA384
  • 04 03 - assigned value for ECDSA-SECP256r1-SHA256
  • 02 03 - assigned value for ECDSA-SHA1
  • 08 06 - assigned value for RSA-PSS-RSAE-SHA512
  • 08 0b - assigned value for RSA-PSS-PSS-SHA512
  • 08 05 - assigned value for RSA-PSS-RSAE-SHA384
  • 08 0a - assigned value for RSA-PSS-PSS-SHA384
  • 08 04 - assigned value for RSA-PSS-RSAE-SHA256
  • 08 09 - assigned value for RSA-PSS-PSS-SHA256
  • 06 01 - assigned value for RSA-PKCS1-SHA512
  • 05 01 - assigned value for RSA-PKCS1-SHA384
  • 04 01 - assigned value for RSA-PKCS1-SHA256
  • 03 01 - assigned value for SHA224-RSA
  • 02 01 - assigned value for RSA-PKCS1-SHA1
Extension - Encrypt-then-MAC 00 16 00 00
The client indicates it can support EtM, which prevents certain vulnerabilities in earlier versions of TLS and DTLS. In DTLS 1.3 this mechanism is always used, so this extension will have no effect in this session.
  • 00 16 - assigned value for extension "Encrypt-then-MAC"
  • 00 00 - 0 bytes of extension data follows
Extension - Supported Groups 00 0a 00 04 00 02 00 1d
The client has indicated that it supports elliptic curve (EC) cryptography for one curve type. 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 04 - 4 bytes of "supported group" extension data follows
  • 00 02 - 2 bytes of data are in the curves list
  • 00 1d - assigned value for the curve "x25519"
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 created from the private key as explained on the 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 Hello Datagram
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
DTLS Record Header 16 fe fd 00 00 00 00 00 00 00 00 00 62
Each DTLS record starts with a type, some sequence info, and a length.
  • 16 - TLS record type 22 (Handshake)
  • fe fd - Protocol version (DTLS 1.2, see below)
  • 00 00 - key epoch (incremented each time the encryption keys are updated)
  • 00 00 00 00 00 00 - DTLS record sequence number 0
  • 00 62 - length of following data in this record (0x62 (98) bytes)

DTLS versions are encoded by breaking the protocol version into parts and then putting each part into a byte with the ones-complement value (thus "1.3" becomes {1,3} which becomes the bytes 0xFE 0xFC). This complement technique keeps DTLS versions distinct from TLS versions.

Because middleboxes have been created and deployed that do not allow protocol versions that they do not recognize, all DTLS 1.3 sessions indicate version DTLS 1.2 (0xFE 0xFD) in unencrypted records.

TLS Handshake Header 02 00 00 56
Each TLS handshake record starts with a type and a length.
  • 02 - handshake record type 2 (ServerHello)
  • 00 00 56 - 0x56 (86) bytes of client hello data is in this handshake record.
Handshake Reconstruction Data 00 00 00 00 00 00 00 56
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 00 - handshake message sequence number 0
  • 00 00 00 - fragment offset of 0 bytes
  • 00 00 56 - fragment length of 0x56 (86) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Server Version (Legacy) fe fd
DTLS versions are encoded by breaking the protocol version into parts and then putting each part into a byte with the ones-complement value (thus "1.3" becomes {1,3} which becomes the bytes 0xFE 0xFC). This complement technique keeps DTLS versions distinct from TLS versions.

Because middleboxes have been created and deployed that do not allow protocol versions that they do not recognize, all DTLS 1.3 sessions indicate version DTLS 1.2 (0xFE 0xFD) in this field. Therefore this field is no longer used in version negotiation, which uses the "Supported Versions" extension below instead.

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.
Legacy Session ID 00
This is a legacy field and is not used in DTLS 1.3.
  • 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 fe fc
The server indicates the negotiated DTLS version of 1.3.
  • 00 2b - assigned value for extension "Supported Versions"
  • 00 02 - 2 bytes of "Supported Versions" extension data follows
  • fe fc - assigned value for DTLS 1.3
Server Handshake Keys Calc
The server now has the information needed to calculate the keys used to encrypt the rest of the handshake. 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 DTLS-only bytes in the records, which are bytes 0-12 and 17-24. Ignoring these bytes allows implementations to share code between TLS and DTLS implementations. This "hello_hash" is aee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7:
$ (cat captures/caps/record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';
   cat captures/caps/record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/') \
   | openssl sha256

aee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7
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: "key", ctx: "", len: 16)
server_key = HKDF-Expand-Label(key: server_secret, label: "key", ctx: "", len: 16)
client_iv = HKDF-Expand-Label(key: client_secret, label: "iv", ctx: "", len: 12)
server_iv = HKDF-Expand-Label(key: server_secret, label: "iv", ctx: "", len: 12)
client_sn_key = HKDF-Expand-Label(key: client_secret, label: "sn", ctx: "", len: 16)
server_sn_key = HKDF-Expand-Label(key: server_secret, label: "sn", ctx: "", len: 16)
I've created an HKDF tool to perform these operations on the command line. The key derivation process is reproduced below:
$ hello_hash=aee8eba0d2ee87052fbbc6864c1514c5a927d6f0ffb4f7954c7f379d95f1b1d7
$ shared_secret=df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
$ zero_key=0000000000000000000000000000000000000000000000000000000000000000
$ early_secret=$(./hkdf-dtls extract 00 $zero_key)
$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')
$ derived_secret=$(./hkdf-dtls expandlabel $early_secret "derived" $empty_hash 32)
$ handshake_secret=$(./hkdf-dtls extract $derived_secret $shared_secret)
$ csecret=$(./hkdf-dtls expandlabel $handshake_secret "c hs traffic" $hello_hash 32)
$ ssecret=$(./hkdf-dtls expandlabel $handshake_secret "s hs traffic" $hello_hash 32)
$ client_handshake_key=$(./hkdf-dtls expandlabel $csecret "key" "" 16)
$ server_handshake_key=$(./hkdf-dtls expandlabel $ssecret "key" "" 16)
$ client_handshake_iv=$(./hkdf-dtls expandlabel $csecret "iv" "" 12)
$ server_handshake_iv=$(./hkdf-dtls expandlabel $ssecret "iv" "" 12)
$ client_sn_key=$(./hkdf-dtls expandlabel $csecret "sn" "" 16)
$ server_sn_key=$(./hkdf-dtls expandlabel $ssecret "sn" "" 16)
$ echo client_key: $client_handshake_key
$ echo client_iv: $client_handshake_iv
$ echo server_key: $server_handshake_key
$ echo server_iv: $server_handshake_iv
$ echo client_sn_key: $client_sn_key
$ echo server_sn_key: $server_sn_key

client_key: 6caa2633d5e48f10051e69dc45549c97
client_iv: 106dc6e393b7a9ea8ef29dd7
server_key: 004e03e64ab6cba6b542775ec230e20a
server_iv: 6d9924be044ee97c624913f2
client_sn_key: beed6218676635c2cb46a45694144fec
server_sn_key: 7173fac51194e775001d625ef69d7c9f
From this we get the following encryption keys and IVs:
  • client handshake key: 6caa2633d5e48f10051e69dc45549c97
  • client handshake IV: 106dc6e393b7a9ea8ef29dd7
  • server handshake key: 004e03e64ab6cba6b542775ec230e20a
  • server handshake IV: 6d9924be044ee97c624913f2
  • client record number key: beed6218676635c2cb46a45694144fec
  • server record number key: 7173fac51194e775001d625ef69d7c9f
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: 6caa2633d5e48f10051e69dc45549c97
  • client handshake IV: 106dc6e393b7a9ea8ef29dd7
  • server handshake key: 004e03e64ab6cba6b542775ec230e20a
  • server handshake IV: 6d9924be044ee97c624913f2
  • client record number key: beed6218676635c2cb46a45694144fec
  • server record number key: 7173fac51194e775001d625ef69d7c9f
Server Encrypted Extensions Datagram

The connection (including the handshake) is encrypted from this point on. The encryption of handshake data is new in DTLS 1.3.

Any extensions that aren't needed for negotiating encryption are given in this encrypted record so they can be hidden from eavesdroppers and middleboxes.

Header Info Byte 2e

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2E have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB10Encryption epoch 2 - handshake keys
Record Number 79 fa 00 00

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from handshake keys calc step above
$ key=7173fac51194e775001d625ef69d7c9f
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=ee9dcff3f8679a4859fe68377fb34ada
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

79fa

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 2f
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 2f - Record length of 0x2F (47) bytes
Encrypted Data ee 9d cf f3 f8 67 9a 48 59 fe 68 37 7f b3 4a da 85 df 87 9c 67 3e 50 1d 7a 4e 8f 19 50 e0 fc
This data is encrypted with the server handshake key.
Auth Tag f6 7f e4 42 e7 d7 d2 b8 a3 d5 fa 59 57 4f fd 00
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server handshake key and the server handshake IV that were generated during the "Server 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 5-byte record header, as authenticated data that must match for the 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 "Server Handshake Keys Calc" step
$ key=004e03e64ab6cba6b542775ec230e20a
$ iv=6d9924be044ee97c624913f2
### from this record
$ recdata=2e0000002f
$ authtag=f67fe442e7d7d2b8a3d5fa59574ffd00
$ 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  08 00 00 12 00 01 00 00  00 00 00 12 00 10 00 0a  |................|
00000010  00 0c 00 0a 00 17 00 1d  00 18 00 19 01 00 16     |...............|
Handshake Header 08 00 00 12
Each handshake message starts with a type and a length.
  • 08 - handshake message type 0x08 (encrypted extensions)
  • 00 00 12 - 0x12 (18) bytes of handshake message data within
Handshake Reconstruction Data 00 01 00 00 00 00 00 12
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 01 - handshake message sequence number 1
  • 00 00 00 - fragment offset of 0 bytes
  • 00 00 12 - fragment length of 0x12 (18) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Extensions Length 00 10
The server has provided a list of extensions to apply to the connection.
  • 00 10 - the extensions will take 0x10 (16) bytes of data
Extension - Supported Groups 00 0a 00 0c 00 0a 00 17 00 1d 00 18 00 19 01 00
The server replies with the elliptic curve algorithms it supports. To make this extension more generic for other cryptography types it calls these "supported groups" instead of "supported curves".
  • 00 0a - assigned value for extension "Supported Groups"
  • 00 0c - 0xC (12) bytes of "supported group" extension data follows
  • 00 0a - 0xA (10) bytes of data are in the curves list
  • 00 17 - assigned value for curve "secp256r1"
  • 00 1d - assigned value for curve "x25519"
  • 00 18 - assigned value for curve "secp384r1"
  • 00 19 - assigned value for curve "secp521r1"
  • 01 00 - assigned value for group "ffdhe2048"
Record Type 16
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 16 - type is 22 (handshake record)
Server Certificate Datagram
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.
Header Info Byte 2e

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2E have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB10Encryption epoch 2 - handshake keys
Record Number ed 2b 00 01

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from handshake keys calc step above
$ key=7173fac51194e775001d625ef69d7c9f
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=d3777e1adf9e98c8c4ffa072c2c3b6bb
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

ed2a

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 03 4b
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 03 4b - Record length of 0x34B (843) bytes
Encrypted Data d3 77 7e 1a df 9e 98 c8 c4 ff a0 72 c2 c3 b6 bb cd 9f bd 2d 1f 34 3c 5d f9 54 d9 97 a2 cd 1d 33 42 a3 3d 3f 6a 85 e1 21 42 3c e0 02 ea 33 5e 37 7e 7a 21 5b 8a 9e cc 5e 26 7c 60 a2 bc 79 4e d1 d8 1f 39 8b ce df a3 68 fb db 7c a3 67 a0 46 65 5d 61 e4 86 67 62 fa ac fd a4 9d 0f 3a 39 71 86 d8 32 e4 81 87 d0 76 ea 8d e5 32 12 87 be 9b fd a2 15 19 da 58 e0 c4 80 56 99 7e 49 2e df e4 76 6b 2c d5 1e a1 2b c2 f6 d5 50 5b 80 e5 1a 64 5d a9 b0 7f bf 7a 01 b8 4d 5b a7 22 b2 e1 7d d9 52 8c 28 63 cd 63 a7 35 b5 4c d8 23 95 87 84 1a 59 2f be 57 5b 2d e1 8a 6c 99 f7 82 a9 56 e2 8c e7 69 67 42 67 3d 7e e7 37 f4 6e 9c ba a2 89 2d 97 21 ef cc c9 1f 16 72 26 a5 be 4c 9c d8 6b 97 fe f3 32 3f d1 92 f4 60 e8 ef 8b 91 3b bf 9f 97 05 63 85 d4 c3 ec 2b 2b dc 2e c4 8a 66 8c f6 f1 0d b3 fe 00 91 97 fa b9 8d 7c 2a 88 15 ac 5a 4e d3 aa 08 94 b9 f9 f9 95 12 43 0d f2 1f 13 4c 49 34 40 73 f9 af 32 8e 35 c2 e1 6b 91 3f 4e 61 33 21 e4 a7 9b d2 d3 38 47 32 1e 61 5d 58 94 09 b1 65 f9 c2 b0 18 80 4f 3c 33 40 e4 0a d5 f5 9a 26 46 0a 12 0f 2d 55 fc 8b ca 47 22 74 fd b9 06 09 a2 18 70 e1 cc 41 aa d0 24 fa 48 a8 6f 07 8f 90 8b c6 26 18 c4 c3 2f 0c fc fb b5 95 a7 d2 93 f4 ba ab 93 ff 35 f0 de 10 71 17 1e 4c 51 0d 75 dd 29 f5 0d 3d e8 1c ae 9e 1c 56 ed 60 9c 1b c7 27 5e ac 1d 69 33 df 08 93 dd 0e 3c 5c 7f d3 65 14 26 b3 e4 c3 ca 6d 46 1d 82 0a df ff 75 fb 7b 15 8b e9 89 30 89 da c9 30 a0 15 f8 9c b4 ef 22 7a b9 e4 3d f0 14 7a 25 07 59 e3 e0 1b 5d b7 48 0c 52 7a 1d 4b 8a 09 c4 ac 05 fc c6 d6 40 15 d6 af 2c 3e 52 15 03 a8 2f b9 02 5c 61 98 18 ca 31 fb 24 03 63 0a c0 6a b7 11 90 53 a7 02 86 24 0b 3f 8e 43 96 61 ad 95 48 7a a5 72 d7 08 60 8d d0 d4 fe 27 bb cf 1e df 50 3a 54 05 46 0b 9e 10 f6 93 4a 41 a8 cf b7 0b 60 90 6f 7e 66 d6 53 15 61 ef 08 ad e3 de 45 77 a7 77 6b f6 56 bb 48 5c ee 28 2c 83 7a a8 bc e0 6a e6 06 a1 71 d7 54 96 36 fe d8 3e 24 bf 9f 10 5b 7d 1d 02 da 30 86 ce 24 49 af a2 d0 ec 26 18 5d 0c 1f 05 2f 88 cd 9d 55 eb 12 4b da e3 66 7f 59 79 97 95 f9 27 50 b9 ca 70 55 66 86 6a 99 24 a2 46 a4 71 90 4b 2d 69 dc 17 cb fe 50 a5 62 ff 26 ff 9e 40 4d 7b 2a 11 67 0c 27 56 3f 3e 37 99 3c c6 e6 73 43 6d c3 a8 51 21 4d 6d 27 86 2b 64 5d cb 0b f4 d4 c7 44 0f 6a d4 83 ef 9d 58 fa b4 7d 24 4b d6 cf a6 8f 12 e9 aa ae cd 2d 52 8e 85 66 f9 7f 50 56 cf 8e fc 7d 1e 55 fb ee 1b e8 7f 7f 89 73 7c 8a fa 20 e4 96 37 0d 25 f7 52 99 e5 91 8c b9 4b a5 b5 ef db 84 7d 9c a5 44 a5 38 65 a3 6d 69 1e be 8b e8 e2 da 08 c1 7b e9 02 38 0d b9 a3 d7 04 91 b8 98 f8 c5 88 e7 44 64 8e b9 37 70 53 0c 83 ce cf
This data is encrypted with the server handshake key.
Auth Tag a4 30 70 21 45 22 93 8c 0e 66 82 9e f1 33 34 9b
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server handshake key and the server handshake IV that were generated during the "Server 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 5-byte record header, as authenticated data that must match for the 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 "Server Handshake Keys Calc" step
$ key=004e03e64ab6cba6b542775ec230e20a
$ iv=6d9924be044ee97c624913f2
### from this record
$ recdata=2e0001034b
$ authtag=a43070214522938c0e66829ef133349b
$ 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  0b 00 03 2e 00 02 00 00  00 00 03 2e 00 00 03 2a  |...............*|
00000010  00 03 25 30 82 03 21 30  82 02 09 a0 03 02 01 02  |..a4..!0...?....|
00000020  02 08 15 5a 92 ad c2 04  8f 90 30 0d 06 09 2a 86  |...Z.??...0...*.|
00000030  48 86 f7 0d 01 01 0b 05  00 30 22 31 0b 30 09 06  |H.?......0"1.0..|
00000040  03 55 04 06 13 02 55 53  31 13 30 11 06 03 55 04  |.U....US1.0...U.|
00000050  0a 13 0a 45 78 61 6d 70  6c 65 20 43 41 30 1e 17  |...Example CA0..|
00000060  0d 31 38 31 30 30 35 30  31 33 38 31 37 5a 17 0d  |.181005013817Z..|
00000070  31 39 31 30 30 35 30 31  33 38 31 37 5a 30 2b 31  |191005013817Z0+1|
00000080  0b 30 09 06 03 55 04 06  13 02 55 53 31 1c 30 1a  |.0...U....US1.0.|
00000090  06 03 55 04 03 13 13 65  78 61 6d 70 6c 65 2e 75  |..U....example.u|
... snip ...
Handshake Header 0b 00 03 2e
Each handshake message starts with a type and a length.
  • 0b - handshake message type 0x0b (certificate)
  • 00 03 2e - 0x32E (814) bytes of handshake message data within
Handshake Reconstruction Data 00 02 00 00 00 00 03 2e
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 02 - handshake message sequence number 2
  • 00 00 00 - fragment offset of 0 bytes
  • 00 03 2e - fragment length of 0x32E (814) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

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
Record Type 16
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 16 - type is 22 (handshake record)
Server Cert Verify Datagram
The server provides information that ties the public key generated during Server Key Exchange Generation to the ownership of the certificate's private key.
Header Info Byte 2e

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2E have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB10Encryption epoch 2 - handshake keys
Record Number a4 3e 00 02

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from handshake keys calc step above
$ key=7173fac51194e775001d625ef69d7c9f
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=83bedfea0f4aa578453af4f4a4be4106
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

a43c

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 01 21
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 01 21 - Record length of 0x121 (289) bytes
Encrypted Data 83 be df ea 0f 4a a5 78 45 3a f4 f4 a4 be 41 06 9b eb e5 9c e4 93 3b f2 f2 ff 35 36 f0 e6 11 45 9f 7a fc 07 14 1e 4a 80 e4 b1 10 f2 c5 48 24 4e 83 42 cd 13 46 26 f0 d6 bc 12 2c 6e e3 cc 81 64 e3 e1 1f b8 bc 7b 58 ff 8d ef af 99 c9 26 81 f7 42 64 cc 29 5d f2 69 b4 63 af e5 78 53 ba 86 04 bd 8e ef 74 91 a0 fc 5a 5d df c2 2b 87 f7 cc 55 94 fd 2b 13 69 68 ab 07 ce 1d 84 33 07 df 9f 41 37 27 11 0f e0 5a c6 df 33 7c 44 4c 9a 2d 8b 28 30 b3 50 48 13 72 dd a1 4b e3 04 63 cb 94 16 f8 15 b7 29 b8 20 be b9 1e df 34 f8 b2 29 fa 71 4d fa 58 68 61 c5 25 15 aa d2 8e 98 52 90 d2 a7 e1 97 df 5a 4f 73 20 4d 95 2c a3 e2 34 af 34 fa e6 5a 3a 34 c1 33 8b 52 dd b7 8e 87 a9 14 95 21 2c 8e da ed 59 6e 0b 4b ad 18 65 66 8d 5a 33 9f d7 61 31 43 bc b8 5d 96 10 41 22 f6 17 e5 39 3b 4c ba 44 d0 86 e5 32 c7 39 e8 15 ea dc 2a 84 07 c4 72
This data is encrypted with the server handshake key.
Auth Tag bd f0 f6 f0 06 0d b4 71 19 71 38 7c 21 89 39 4f
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server handshake key and the server handshake IV that were generated during the "Server 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 2. The process also takes as input the 5-byte record header, as authenticated data that must match for the 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 "Server Handshake Keys Calc" step
$ key=004e03e64ab6cba6b542775ec230e20a
$ iv=6d9924be044ee97c624913f2
### from this record
$ recdata=2e00020121
$ authtag=bdf0f6f0060db4711971387c2189394f
$ 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  0f 00 01 04 00 03 00 00  00 00 01 04 08 04 01 00  |................|
00000010  2c 76 3d 6a d3 d8 af 7f  a3 7d a6 d8 d9 0e 73 7c  |,v=j?د.?}???.s||
00000020  ea 53 ee 7a ff a5 61 48  74 cc 68 48 9c 73 a2 f3  |?S?z??aHt?hH.s??|
00000030  a0 43 cb ba e6 c2 7a 41  91 0e de 9a df c7 22 23  |?C˺??zA..?.??"#|
00000040  58 26 12 ec 96 79 fe 1f  9f a5 f4 a4 b6 12 f8 6f  |X&.?.y?..????.?o|
... snip ...
Handshake Header 0f 00 01 04
Each 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 within
Handshake Reconstruction Data 00 03 00 00 00 00 01 04
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 03 - handshake message sequence number 3
  • 00 00 00 - fragment offset of 0 bytes
  • 00 01 04 - fragment length of 0x104 (260) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Signature 08 04 01 00 2c 76 3d 6a d3 d8 af 7f a3 7d a6 d8 d9 0e 73 7c ea 53 ee 7a ff a5 61 48 74 cc 68 48 9c 73 a2 f3 a0 43 cb ba e6 c2 7a 41 91 0e de 9a df c7 22 23 58 26 12 ec 96 79 fe 1f 9f a5 f4 a4 b6 12 f8 6f 40 88 49 a3 29 f7 63 e0 4f be 95 9a 91 e8 d1 8d 4a ba 79 29 57 6f a0 24 ec b2 37 d6 33 78 e9 8e e5 9d c9 59 49 b2 63 b3 06 53 0a 2e 6f b9 b2 2f a2 3c 64 32 33 43 03 89 33 01 fd 60 e2 05 82 6e b9 ec 41 4f ec 5f 9a 0d 6f 8f 3d 89 a0 9f 14 8e 0f 05 03 49 bc 1e 17 97 d9 28 1e ed f6 e7 66 9c e2 56 ae 79 d4 ee 8c 96 56 0d cf 07 6c 2a 45 a4 ee e8 d2 79 71 0f 0c e7 03 4a 3f 5c aa 94 41 4e ae df 61 08 48 66 e4 9e 81 88 3e e2 1a 12 59 3c cb 96 dd 11 76 9e 34 0f 1e 6c c2 14 b0 57 95 e5 4a fc 94 79 84 5e 4d f2 bf 96 9f bb 21 8c b9 c4 b8 34 a8 51 be 34 75 a1 45 2f 4b 33 55 4f 9d 65
Because the server is generating ephemeral keys for each session (optional in TLS 1.2, mandatory in TLS 1.3) the session is not inherently tied to the certificate as it was in previous versions of TLS, when the certificate's public/private key were used for key exchange.

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.
  • 08 04 - reserved value for RSA-PSS-RSAE-SHA256 signature
  • 01 00 - 0x100 (256) bytes of signature data follows
  • 2c 76 3d ... 4f 9d 65 - a signature over this handshake's hash
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
### cleartext record headers, DTLS-only record headers,
### or 1-byte decrypted record trailers
$ handshake_hash=$((
   cat record-chello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
   cat record-shello | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
   cat record-encext | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
   cat record-cert   | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
   )| 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 "2c 76 3d 6a d3 d8 af 7f a3 7d a6 d8 d9 0e 73 7c ea 53 ee 7a
  ff a5 61 48 74 cc 68 48 9c 73 a2 f3 a0 43 cb ba e6 c2 7a 41 91 0e
  de 9a df c7 22 23 58 26 12 ec 96 79 fe 1f 9f a5 f4 a4 b6 12 f8 6f
  40 88 49 a3 29 f7 63 e0 4f be 95 9a 91 e8 d1 8d 4a ba 79 29 57 6f
  a0 24 ec b2 37 d6 33 78 e9 8e e5 9d c9 59 49 b2 63 b3 06 53 0a 2e
  6f b9 b2 2f a2 3c 64 32 33 43 03 89 33 01 fd 60 e2 05 82 6e b9 ec
  41 4f ec 5f 9a 0d 6f 8f 3d 89 a0 9f 14 8e 0f 05 03 49 bc 1e 17 97
  d9 28 1e ed f6 e7 66 9c e2 56 ae 79 d4 ee 8c 96 56 0d cf 07 6c 2a
  45 a4 ee e8 d2 79 71 0f 0c e7 03 4a 3f 5c aa 94 41 4e ae df 61 08
  48 66 e4 9e 81 88 3e e2 1a 12 59 3c cb 96 dd 11 76 9e 34 0f 1e 6c
  c2 14 b0 57 95 e5 4a fc 94 79 84 5e 4d f2 bf 96 9f bb 21 8c b9 c4
  b8 34 a8 51 be 34 75 a1 45 2f 4b 33 55 4f 9d 65" | 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
Record Type 16
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 16 - type is 22 (handshake record)
Server Handshake Finished Datagram
To verify that the handshake was successful and not tampered with, the server calculates verification data that client will agree on. The verification data is built from a hash of all handshake messages.
Header Info Byte 2e

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2E have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB10Encryption epoch 2 - handshake keys
Record Number 0b b8 00 03

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from handshake keys calc step above
$ key=7173fac51194e775001d625ef69d7c9f
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=a44135732a099823b8a5f61a2b35ce92
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

0bbb

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 3d
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 3d - Record length of 0x3D (61) bytes
Encrypted Data a4 41 35 73 2a 09 98 23 b8 a5 f6 1a 2b 35 ce 92 1a 89 ab b1 52 f8 76 cd 26 79 7d c3 ed 73 d9 17 b2 99 c1 69 28 b9 cf 9e 58 d1 cd 58 68
This data is encrypted with the server handshake key.
Auth Tag 6b 8b 90 ce 9f e6 45 4e 0c ef 9e fc 40 f2 39 7a
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server handshake key and the server handshake IV that were generated during the "Server 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 3. The process also takes as input the 5-byte record header, as authenticated data that must match for the 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 "Server Handshake Keys Calc" step
$ key=004e03e64ab6cba6b542775ec230e20a
$ iv=6d9924be044ee97c624913f2
### from this record
$ recdata=2e0003003d
$ authtag=6b8b90ce9fe6454e0cef9efc40f2397a
$ recordnum=3
### 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  14 00 00 20 00 04 00 00  00 00 00 20 1d 89 aa 62
00000010  e5 f8 8a 0f c9 52 88 47  15 d8 ac b3 79 86 59 af
00000020  b9 e7 78 9a 8d b2 b3 81  6b a4 52 46 16
... snip ...
Handshake Header 14 00 00 20
Each handshake message starts with a type and a length.
  • 14 - handshake message type 0x14 (finished)
  • 00 00 20 - 0x20 (32) bytes of handshake message data within
Handshake Reconstruction Data 00 04 00 00 00 00 00 20
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 04 - handshake message sequence number 4
  • 00 00 00 - fragment offset of 0 bytes
  • 00 00 20 - fragment length of 0x20 (32) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Verify Data 1d 89 aa 62 e5 f8 8a 0f c9 52 88 47 15 d8 ac b3 79 86 59 af b9 e7 78 9a 8d b2 b3 81 6b a4 52 46
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 (Client Hello to Server Certificate Verify).
finished_key = HKDF-Expand-Label(key: server_secret, label: "finished", ctx: "", len: 32)
finished_hash = SHA256(Client Hello ... Server Cert Verify)
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
### cleartext record headers, DTLS-only record headers,
### or 1-byte decrypted record trailers
$ fin_hash=$((
    cat record-chello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
    cat record-shello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
    cat record-encext  | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
    cat record-cert    | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
    cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  ) | openssl sha256)
$ sht_secret=8ad7990b9d249bcbaa0805d8d3f3ad2259e75f3a42c5d84db3ea3c6ee57b3d38
$ fin_key=$(./hkdf-dtls expandlabel $sht_secret "finished" "" 32)
$ echo $fin_hash | xxd -r -p \
    | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key

1d89aa62e5f88a0fc952884715d8acb3798659afb9e7789a8db2b3816ba45246
Record Type 16
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 16 - type is 22 (handshake record)
Client Handshake Finished Datagram
To verify that the handshake was successful and not tampered with, the client calculates verification data that the server will agree on, and encrypts it with the client handshake key. The verification data is built from a hash of all handshake messages.
Header Info Byte 2e

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2E have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB10Encryption epoch 2 - handshake keys
Record Number c2 48 00 00

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "client record number key" from handshake keys calc step above
$ key=beed6218676635c2cb46a45694144fec
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=8a2cd52d5000f8786afb47cdf0b8f2b8
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

c248

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 3d
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 3d - Record length of 0x3D (61) bytes
Encrypted Data 8a 2c d5 2d 50 00 f8 78 6a fb 47 cd f0 b8 f2 b8 13 42 b0 0c 43 dc e6 4b 1d 01 94 d2 e2 01 f6 81 75 09 78 52 8b be 26 af 79 61 24 01 c0
This data is encrypted with the client handshake key.
Auth Tag 07 a2 c5 f7 5f 7c ff b7 46 5b c0 1d 23 d8 51 1f
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the client handshake key and the client handshake IV that were generated during the "Client 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 5-byte record header, as authenticated data that must match for the 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 "Client Handshake Keys Calc" step
$ key=6caa2633d5e48f10051e69dc45549c97
$ iv=106dc6e393b7a9ea8ef29dd7
### from this record
$ recdata=2e0000003d
$ authtag=07a2c5f75f7cffb7465bc01d23d8511f
$ 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  14 00 00 20 00 01 00 00  00 00 00 20 6f 28 01 39
00000010  6b 0e 90 eb b4 a3 ba 38  4a bc fc 6b 24 20 1a bd
00000020  81 b3 16 b2 39 1d a3 78  37 7f ac f5 16
Handshake Header 14 00 00 20
Each handshake message starts with a type and a length.
  • 14 - handshake message type 0x14 (finished)
  • 00 00 20 - 0x20 (32) bytes of handshake message data within
Handshake Reconstruction Data 00 01 00 00 00 00 00 20
Because UDP does not guarantee delivery or ordering, and because UDP datagrams might be smaller than handshake records that need to be sent, values must be provided to support record re-construction in case of data loss, reordering, or fragmentation.
  • 00 01 - handshake message sequence number 1
  • 00 00 00 - fragment offset of 0 bytes
  • 00 00 20 - fragment length of 0x20 (32) bytes

In this case the entire handshake record fits within a single UDP datagram, indicated by offset of zero and length of the full handshake record length.

Verify Data 6f 28 01 39 6b 0e 90 eb b4 a3 ba 38 4a bc fc 6b 24 20 1a bd 81 b3 16 b2 39 1d a3 78 37 7f ac f5
The verify_data is built using the client_secret from the "Client Handshake Keys Calc" step and a SHA256 hash of every handshake record before this point (Client Hello to Server Handshake Finished).
finished_key = HKDF-Expand-Label(key: client_secret, label: "finished", ctx: "", len: 32)
finished_hash = SHA256(Client Hello ... Server Handshake Finished)
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
### cleartext record headers, DTLS-only record headers,
### or 1-byte decrypted record trailers
$ fin_hash=$((
    cat record-chello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
    cat record-shello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/s';
    cat record-encext  | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
    cat record-cert    | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
    cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
    cat record-sfin    | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  ) | openssl sha256)
$ cht_secret=33e472fb8d821b0193314626bebee307ccbd1aeb3d3a17ba468888ffc5246da1
$ fin_key=$(./hkdf-dtls expandlabel $cht_secret "finished" "" 32)
$ echo $fin_hash | xxd -r -p \
    | openssl dgst -sha256 -mac HMAC -macopt hexkey:$fin_key

6f2801396b0e90ebb4a3ba384abcfc6b24201abd81b316b2391da378377facf5
Record Type 16
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 16 - type is 22 (handshake record)
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 Client Hello to Server Handshake Finished
We calculate the SHA256 hash of all handshake messages to this point (Client Hello through Server Finished). The hash input does not include cleartext record headers, DTLS-only record headers, or 1-byte decrypted record trailers. This "handshake_hash" is 77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b:
$ (
  cat record-chello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';
  cat record-shello  | perl -0777 -pe 's/.{13}(.{4}).{8}/$1/';
  cat record-encext  | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  cat record-cert    | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  cat record-cverify | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  cat record-sfin    | perl -0777 -pe 's/(.{4}).{8}(.*).$/$1$2/s';
  )| openssl sha256

77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b
We then feed the hash and the handshake secret into a set of key derivation operations, designed to ensure the integrity of the handshake process and 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_application_key = HKDF-Expand-Label(key: client_secret, label: "key", ctx: "", len: 16)
server_application_key = HKDF-Expand-Label(key: server_secret, label: "key", ctx: "", len: 16)
client_application_iv = HKDF-Expand-Label(key: client_secret, label: "iv", ctx: "", len: 12)
server_application_iv = HKDF-Expand-Label(key: server_secret, label: "iv", ctx: "", len: 12)
I've created an HKDF tool to perform these operations on the command line.
$ handshake_hash=77ff5eee528abc269960b0ea316eb8578dc8325d86ec1336ffe4b2941e26d82b
$ handshake_secret=d0d1397bb3c445d37f26f7ed00c83b73d2f67540de3761465ffe524f8f944e12
$ zero_key=0000000000000000000000000000000000000000000000000000000000000000
$ empty_hash=$(openssl sha256 < /dev/null | sed -e 's/.* //')
$ derived_secret=$(./hkdf-dtls expandlabel $handshake_secret "derived" $empty_hash 32)
$ master_secret=$(./hkdf-dtls extract $derived_secret $zero_key)
$ csecret=$(./hkdf-dtls expandlabel $master_secret "c ap traffic" $handshake_hash 32)
$ ssecret=$(./hkdf-dtls expandlabel $master_secret "s ap traffic" $handshake_hash 32)
$ client_application_key=$(./hkdf-dtls expandlabel $csecret "key" "" 16)
$ server_application_key=$(./hkdf-dtls expandlabel $ssecret "key" "" 16)
$ client_application_iv=$(./hkdf-dtls expandlabel $csecret "iv" "" 12)
$ server_application_iv=$(./hkdf-dtls expandlabel $ssecret "iv" "" 12)
$ client_sn_key=$(./hkdf-dtls expandlabel $csecret "sn" "" 16)
$ server_sn_key=$(./hkdf-dtls expandlabel $ssecret "sn" "" 16)
$ echo client_key: $client_application_key
$ echo client_iv: $client_application_iv
$ echo server_key: $server_application_key
$ echo server_iv: $server_application_iv
$ echo client_sn_key: $client_sn_key
$ echo server_sn_key: $server_sn_key

client_key: 9ba90dbce8857bc1fcb81d41a0465cfe
client_iv: 682219974631fa0656ee4eff
server_key: 2b65fffbbc8189474aa2003c43c32d4d
server_iv: 582f5a11bdaf973fe3ffeb4e
client_sn_key: 5cb5bd8bac29777c650c0dde22d16d47
server_sn_key: 57ba02596c6a1352d7fe8416c7e17d5a
From this we get the following key data:
  • server application key: 2b65fffbbc8189474aa2003c43c32d4d
  • server application IV: 582f5a11bdaf973fe3ffeb4e
  • client application key: 9ba90dbce8857bc1fcb81d41a0465cfe
  • client application IV: 682219974631fa0656ee4eff
  • client record number key: 5cb5bd8bac29777c650c0dde22d16d47
  • server record number key: 57ba02596c6a1352d7fe8416c7e17d5a
Client Application Keys Calc
The client now 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: 2b65fffbbc8189474aa2003c43c32d4d
  • server application IV: 582f5a11bdaf973fe3ffeb4e
  • client application key: 9ba90dbce8857bc1fcb81d41a0465cfe
  • client application IV: 682219974631fa0656ee4eff
  • client record number key: 5cb5bd8bac29777c650c0dde22d16d47
  • server record number key: 57ba02596c6a1352d7fe8416c7e17d5a
Server ACK Datagram

Each peer must respond to or acknowledge data received from the other peer or it will be assumed lost and sent again.

In this record the server acknowledges receipt of the Client Handshake Finished record.

Header Info Byte 2f

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2F have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB11Encryption epoch 3 - first application keys
Record Number 31 50 00 00

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from application keys calc step above
$ key=57ba02596c6a1352d7fe8416c7e17d5a
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=ea80ab8e08c93895418d243571ea6de7
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

3150

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 23
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 23 - Record length of 0x23 (35) bytes
Encrypted Data ea 80 ab 8e 08 c9 38 95 41 8d 24 35 71 ea 6d e7 d8 63 ee
This data is encrypted with the server application key.
Auth Tag 84 23 0b b6 04 3c b3 84 df 94 b6 da 28 5a 3b c4
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server application key and the server application IV that were generated during the "Server 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 5-byte record header, as authenticated data that must match for the 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 "Server Application Keys Calc" step
$ key=2b65fffbbc8189474aa2003c43c32d4d
$ iv=582f5a11bdaf973fe3ffeb4e
### from this record
$ recdata=2f00000023
$ authtag=84230bb6043cb384df94b6da285a3bc4
$ 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  00 10 00 00 00 00 00 00  00 02 00 00 00 00 00 00  |................|
00000010  00 00 1a                                          |...|
ACK Length 00 10
Each ACK message starts with a payload length.
  • 00 10 - 0x10 (16) bytes of ACK data within
Record Acknowledgement 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 00
The server acknowledges a record that it received.
  • 00 00 00 00 00 00 02 - record epoch 2 (handshake keys)
  • 00 00 00 00 00 00 00 00 - record number 0
Record Type 1a
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 1a - type is 26 (ACK record)
Client Application Data Datagram
The client sends the data "ping".
Header Info Byte 2f

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2F have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB11Encryption epoch 3 - first application keys
Record Number 68 3f 00 00

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "client record number key" from application keys calc step above
$ key=5cb5bd8bac29777c650c0dde22d16d47
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=7d72278b6c649f1e7b56b3cad411faf7
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

683f

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 15
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 15 - Record length of 0x15 (21) bytes
Encrypted Data 7d 72 27 8b 6c
This data is encrypted with the client application key.
Auth Tag 64 9f 1e 7b 56 b3 ca d4 11 fa f7 bd 51 8b fb 15
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the client application key and the client application IV that were generated during the "Client 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 5-byte record header, as authenticated data that must match for the 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 "Client Application Keys Calc" step
$ key=9ba90dbce8857bc1fcb81d41a0465cfe
$ iv=682219974631fa0656ee4eff
### from this record
$ recdata=2f00000015
$ authtag=649f1e7b56b3cad411faf7bd518bfb15
$ 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  70 69 6e 67 17                                    |ping.|
Application Data 70 69 6e 67
The bytes "ping".
Record Type 17
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 17 - type is 23 (application data)
Server Application Data Datagram
The server replies with the data "pong".
Header Info Byte 2f

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2F have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB11Encryption epoch 3 - first application keys
Record Number a2 58 00 01

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from application keys calc step above
$ key=57ba02596c6a1352d7fe8416c7e17d5a
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=f5bd33f27b72780e351fa00703fb9f65
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

a259

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 15
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 15 - Record length of 0x15 (21) bytes
Encrypted Data f5 bd 33 f2 7b
This data is encrypted with the server application key.
Auth Tag 72 78 0e 35 1f a0 07 03 fb 9f 65 8c 68 9f 95 ae
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server application key and the server application IV that were generated during the "Server 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 1. The process also takes as input the 5-byte record header, as authenticated data that must match for the 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 "Server Application Keys Calc" step
$ key=2b65fffbbc8189474aa2003c43c32d4d
$ iv=582f5a11bdaf973fe3ffeb4e
### from this record
$ recdata=2f00010015
$ authtag=72780e351fa00703fb9f658c689f95ae
$ 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  70 6f 6e 67 17                                    |pong.|
Application Data 70 6f 6e 67
The bytes "pong".
Record Type 17
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 17 - type is 23 (application data)
Server Alert Datagram
The server sends an "alert" to indicate an orderly shutdown.
Header Info Byte 2f

An encrypted DTLS packet starts with the "Unified Header". This first byte of the header gives information on the structure and decryption of the rest of the header and packet.

The bits in the value 0x2F have the following meaning:

Value Meaning
MSB001Fixed bits
0Connection ID field not present in header
1Sequence number in header is 2 bytes long
1Length field is present in header
LSB11Encryption epoch 3 - first application keys
Record Number 69 0c 00 02

The record number of the packet is encrypted, to prevent middleware from interpreting or interfering with the sequencing of packets.

This encryption is applied by encrypting a sample of each packet's payload with the "record number key", then XOR'ing certain bits and bytes in each packet with the resulting data.

An example of how to compute record number encryption:
### "server record number key" from application keys calc step above
$ key=57ba02596c6a1352d7fe8416c7e17d5a
### sample is taken from 16 bytes of payload starting 5 bytes into the record
$ sample=dd8cd07daa964fd1ab508825378fc96f
$ echo $sample | xxd -r -p | openssl aes-128-ecb -K $key | head -c 2 | xxd -p

690e

### the above bytes are xor'd one-for-one into the bytes of the record number
Record Length 00 13
Each record is assumed to consume the remainder of the datagram unless this optional length is given. This allows implementations to send several TLS records in a single datagram (though this connection does not take advantage of this).
  • 00 13 - Record length of 0x13 (19) bytes
Encrypted Data dd 8c d0
This data is encrypted with the server application key.
Auth Tag 7d aa 96 4f d1 ab 50 88 25 37 8f c9 6f a8 b1 e8
This is the AEAD authentication tag that protects the integrity of the encrypted data and the record header.
Decryption
This data is encrypted using the server application key and the server application IV that were generated during the "Server 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 1. The process also takes as input the 5-byte record header, as authenticated data that must match for the 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 "Server Application Keys Calc" step
$ key=2b65fffbbc8189474aa2003c43c32d4d
$ iv=582f5a11bdaf973fe3ffeb4e
### from this record
$ recdata=2f00020013
$ authtag=7daa964fd1ab508825378fc96fa8b1e8
$ 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  01 00 15
Alert 01 00
The server sends a "close notify" alert, indicating an orderly shutdown of the connection.
  • 01 - alert level 1 (warning) - unused
  • 00 - alert number 0 (close_notify)
Record Type 15
Each encrypted DTLS 1.3 record has a final byte which indicates its actual record type.
  • 15 - type is 21 (alert)

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]