Skip to content

Instantly share code, notes, and snippets.

@Konamiman
Last active August 3, 2024 14:19
Show Gist options
  • Save Konamiman/8fa616cdeb41b64b3bb2869479baa534 to your computer and use it in GitHub Desktop.
Save Konamiman/8fa616cdeb41b64b3bb2869479baa534 to your computer and use it in GitHub Desktop.
TLS 1.3 key derivation in C#

Section 7 of RFC8446 explains how to perform the calculations needed to derive the handshake and application keys for the client and the server on a TLS 1.3 connection, based on a computed shared secret and a hash of the handshake messages exchanged so far. On the other hand, in "The Illustrated TLS 1.3 Connection" we see a pretty detailed dissection of a real TLS connection establishment, including the byte by byte description of each exchanged messages and the calculated keys.

Still, putting all the pieces together in order to come up with working code for the generation of these keys can be a bit challenging. Here I present a TrafficKeys class written in C# (tested in .NET 8) that computes and stores the keys and IVs for a TLS 1.3 connection, together with an usage example that uses the same cipher suite, secrets and handshake hash as in "The Illustrated TLS 1.3 Connection" (and therefore the derived keys are also the same ones). I hope this is useful for you, who very bravely are developing your own TLS 1.3 implementation.

The calculations for exporter, resumption and early data secrets aren't included here, but they shouldn't be hard to implement given the already existing code. Also the UpdateClientKeys and UpdateServerKeys methods are presumed to work correctly, but "The Illustrated TLS 1.3 Connection" doesn't provide example values to test against.

using System;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
class Example
{
public static void Main()
{
// Example shared secret and ClientHello hash taken from
// "The Illustrated TLS 1.3 Connection" site, section "Server Handshake Keys Calc"
// (https://tls13.xargs.org/#server-handshake-keys-calc)
var sharedSecret = "df4a291baa1eb7cfa6934b29b474baad2697e29f1f920dcc77c8a0a088447624";
var helloHash = "e05f64fcd082bdb0dce473adf669c2769f257a1c75a51b7887468b5e0e7a7de4f4d34555112077f16e079019d5a845bd";
// Keys computation for TLS_AES_256_GCM_SHA384.
// See RFC5116, sections 5.1 and 5.2 for keyLength and ivLength
// (https://datatracker.ietf.org/doc/html/rfc5116)
var keys = new TrafficKeys(new HMACSHA384(), SHA384.Create(), keyLength: 32, ivLength: 12);
keys.ComputeHandshakeKeys(ParseHexAsByteArray(sharedSecret), ParseHexAsByteArray(helloHash));
// This should print the same values listed at the end of
// https://tls13.xargs.org/#server-handshake-keys-calc
Console.WriteLine(
$"""
>>> Handshake:
Client key: {ByteArrayToString(keys.ClientKey)}
Server key: {ByteArrayToString(keys.ServerKey)}
Client iv: {ByteArrayToString(keys.ClientIv)}
Server iv: {ByteArrayToString(keys.ServerIv)}
""");
// Now let's compute the traffic keys using the example updated handshake hash
// (https://tls13.xargs.org/#server-application-keys-calc)
var handshakeHash = "fa6800169a6baac19159524fa7b9721b41be3c9db6f3f93fa5ff7e3db3ece204d2b456c51046e40ec5312c55a86126f5";
keys.ComputeApplicationKeys(ParseHexAsByteArray(handshakeHash));
Console.WriteLine(
$"""
>>> Application:
Client key: {ByteArrayToString(keys.ClientKey)}
Server key: {ByteArrayToString(keys.ServerKey)}
Client iv: {ByteArrayToString(keys.ClientIv)}
Server iv: {ByteArrayToString(keys.ServerIv)}
""");
// Simulate a key update for both client and server
keys.UpdateClientKeys();
keys.UpdateServerKeys();
Console.WriteLine(
$"""
>>> Updated keys:
Client key: {ByteArrayToString(keys.ClientKey)}
Server key: {ByteArrayToString(keys.ServerKey)}
Client iv: {ByteArrayToString(keys.ClientIv)}
Server iv: {ByteArrayToString(keys.ServerIv)}
""");
}
/*
* Auxiliary method to parse a string of hex digits into a byte array
*/
static byte[] ParseHexAsByteArray(string bytesAsHex)
{
var value = BigInteger.Parse("0" + bytesAsHex, NumberStyles.AllowHexSpecifier).ToByteArray().Reverse().ToArray();
if(value.Length == (bytesAsHex.Length / 2) + 1) {
value = value.Skip(1).ToArray();
}
return value;
}
/*
* Auxiliary method to convert a byte array to a string of hex digits
*/
static string ByteArrayToString(byte[] data)
{
return BitConverter.ToString(data).Replace("-", "");
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
namespace Konamiman.NestorTls.TlsClient.Cryptography;
/*
* This class holds the keys and IVs for a TLS 1.3 connection,
* and provides methods to (re)calculate them.
*/
internal class TrafficKeys
{
readonly HMAC hmac;
readonly HashAlgorithm hash;
readonly int hashSize;
readonly int keyLength;
readonly int ivLength;
static readonly byte[] empty = [];
byte[] handshakeSecret = null;
readonly byte[] emptyHash;
byte[] clientSecret;
byte[] serverSecret;
public byte[] ClientKey { get; private set; }
public byte[] ServerKey { get; private set; }
public byte[] ClientIv { get; private set; }
public byte[] ServerIv { get; private set; }
public TrafficKeys(HMAC hmacAlgorithm, HashAlgorithm hashAlgorithgm, int keyLength, int ivLength)
{
this.hmac = hmacAlgorithm;
this.hash = hashAlgorithgm;
hashSize = hmacAlgorithm.HashSize / 8; //We get bits, we need bytes
this.keyLength = keyLength;
this.ivLength = ivLength;
emptyHash = hash.ComputeHash(empty);
}
/*
* Compute the handshake traffic secrets according to RFC8446, section 7.1
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.1)
*/
public void ComputeHandshakeKeys(byte[] sharedSecret, byte[] handshakeHash)
{
var earlySecret = Extract(empty, Enumerable.Repeat<byte>(0, hashSize).ToArray());
var derivedSecret = ExpandLabel(earlySecret, "derived", emptyHash, hashSize);
handshakeSecret = Extract(derivedSecret, sharedSecret);
clientSecret = ExpandLabel(handshakeSecret, "c hs traffic", handshakeHash, hashSize);
serverSecret = ExpandLabel(handshakeSecret, "s hs traffic", handshakeHash, hashSize);
ComputeKeysFromSecrets();
}
/*
* Compute the application traffic secrets according to RFC8446, section 7.1
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.1)
*/
public void ComputeApplicationKeys(byte[] handshakeHash)
{
if(handshakeSecret is null) {
throw new InvalidOperationException($"{nameof(ComputeApplicationKeys)} can't be invoked before ${nameof(ComputeHandshakeKeys)}");
}
var derivedSecret = ExpandLabel(handshakeSecret, "derived", emptyHash, hashSize);
var masterSecret = Extract(derivedSecret, Enumerable.Repeat<byte>(0, hashSize).ToArray());
clientSecret = ExpandLabel(masterSecret, "c ap traffic", handshakeHash, hashSize);
serverSecret = ExpandLabel(masterSecret, "s ap traffic", handshakeHash, hashSize);
ComputeKeysFromSecrets();
}
/*
* Update the client application traffic keys according to RFC8446, section 7.2
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.2)
*/
public void UpdateClientKeys()
{
clientSecret = ExpandLabel(clientSecret, "traffic upd", empty, hashSize);
ComputeKeysFromSecrets(forServer: false);
}
/*
* Update the server application traffic keys according to RFC8446, section 7.2
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.2)
*/
public void UpdateServerKeys()
{
serverSecret = ExpandLabel(serverSecret, "traffic upd", empty, hashSize);
ComputeKeysFromSecrets(forClient: false);
}
/*
* Derive the handshake or application traffic keys according to RFC8446, section 7.3
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.3)
*/
private void ComputeKeysFromSecrets(bool forClient = true, bool forServer = true)
{
if(forClient) {
ClientKey = ExpandLabel(clientSecret, "key", empty, keyLength);
ClientIv = ExpandLabel(clientSecret, "iv", empty, ivLength);
}
if(forServer) {
ServerKey = ExpandLabel(serverSecret, "key", empty, keyLength);
ServerIv = ExpandLabel(serverSecret, "iv", empty, ivLength);
}
}
/*
* "Extract" function as per RFC5869, section 2.2
* (https://datatracker.ietf.org/doc/html/rfc5869#section-2.2)
*/
private byte[] Extract(byte[] salt, byte[] ikm)
{
hmac.Key = salt;
return hmac.ComputeHash(ikm);
}
/*
* "Expand" function as per RFC5869, section 2.3
* (https://datatracker.ietf.org/doc/html/rfc5869#section-2.3)
*/
private byte[] Expand(byte[] prk, byte[] info, int length)
{
hmac.Key = prk;
var steps = (int)Math.Ceiling((decimal)length / hashSize);
var result = new List<byte>();
var singleByte = new byte[] { 1 };
var previous = empty;
for(var i = 0; i < steps; i++) {
previous = hmac.ComputeHash(previous.Concat(info).Concat(singleByte).ToArray());
result.AddRange(previous);
singleByte[0]++;
}
return result.Take(length).ToArray();
}
/*
* "Expand label" function as per RFC8446, section 7.1
* (https://datatracker.ietf.org/doc/html/rfc8446#section-7.1)
*/
byte[] ExpandLabel(byte[] secret, string label, byte[] context, int length)
{
/*
struct {
uint16 length = Length;
opaque label<7..255> = "tls13 " + Label;
opaque context<0..255> = Context;
} HkdfLabel;
*/
var labelBytes = Encoding.ASCII.GetBytes("tls13 " + label);
var hkdfLabel = new byte[] {
(byte)(length >> 8), (byte)(length & 0xFF), //fixed-size "uint16 length"
(byte)labelBytes.Length, //size indicator for "label<7..255>"
}.Concat(labelBytes)
.Concat(new byte[] { (byte)context.Length }) //size indicator for "context<0..255>"
.Concat(context)
.ToArray();
return Expand(secret, hkdfLabel, length).Take(length).ToArray();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment