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.
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:
The public key is created from the private key as explained on the X25519 site. The public key calculated is:202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f
The public key calculation can be confirmed at the command line:358072d6365880d1aeea329adf9121383851ed21a28e3b75e965d0d2cd166254
### 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
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
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 (
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.
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 (
This list is presented in descending order of the client's preference.
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:
The public key is created from the private key as explained on the X25519 site. The public key calculated is:909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf
The public key calculation can be confirmed with command line tools:9fd7ad6dcff4298dd3f96d5b1b2af910a0535b1488d7f8fabb349a982880b615
### 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
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
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 (
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.
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 (
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:
I've provided a tool to perform this calculation:df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
$ 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
$ (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
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)
$ 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
I've provided a tool to perform this calculation:df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624
$ 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
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.
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 2 - handshake keys |
### "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
### 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 |...............|
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.
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 2 - handshake keys |
### "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
### 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 ...
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.
$ 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 ...
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 2 - handshake keys |
### "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
### 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 ...
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.
### 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
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 2 - handshake keys |
### "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
### 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 ...
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.
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)
### 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
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 2 - handshake keys |
### "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
### 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
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.
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)
### 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
$ (
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
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)
$ 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
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.
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 3 - first application keys |
### "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
### 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 |...|
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 3 - first application keys |
### "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
### 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.|
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 3 - first application keys |
### "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
### 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.|
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 | |
---|---|---|
MSB | Fixed bits | |
Connection ID field not present in header | ||
Sequence number in header is 2 bytes long | ||
Length field is present in header | ||
LSB | Encryption epoch 3 - first application keys |
### "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
### 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
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.