Skip to content
Blog » An Example ECDH Key Exchange with HKDF and Authenticated Encryption in Rust

An Example ECDH Key Exchange with HKDF and Authenticated Encryption in Rust

    I recently created an example project which implements an ephemeral key exchange between a client and server over TCP. The program uses the Ring cryptography library to compute a shared secret using the X25519 algorithm. Two session keys are derived from the output of the key exchange using HKDF and then messages are encrypted and authenticated between the client and server using AES-GCM with a hash transcript to verify that both the client and the server are seeing exactly the same messages in the same order. In this post I walk through how I built the project and explain the reasoning and design considerations at each step.

    Note that the key exchange used in this project is unauthenticated on both sides because the keys are ephemeral and so there is no way for the client to know if it is communicating with the intended server, and vice versa. The means that a man in the middle attacker could intercept the encrypted communications by doing an ephemeral key exchange with the client and server at the same time and forwarding messages between them. In order to authenticate the server to the client we would need to have some root of trust that we can use to trust the server such as using certificates or static public keys which are verified as trusted out of band.

    Create a TCP Server

    I started the project by creating the server which listens on a TCP socket and waits for connections from clients. When a new client connection comes in the server accepts the connection and then proceeds to send and receive messages over the TcpStream. In the example below we simply start the server and then once a connection is created we wait for the client to send a message, print it and then reply to the client with a response message. Note that this example TCP server is single threaded and so would only be able to accept connections from one client at a time.

    Create a TCP Client

    Next we create the TCP client which connects to the server and once connected, sends a request to the server and then waits for the response which it then prints to the console. So far nothing is encrypted and the client and server are both assuming the data can be parsed as UTF-8 strings. The client sends the data over the TcpStream once every 5 seconds.

    Implementing the ECDH Key Exchange

    For this part I created a helper type called EcdhEphemeralKeyExchange which is used by both the client and server programs and which internally uses the Ring cryptography library to implement the key exchange. An enum type Actor is used to determine if the instance is being called from the client or server. The type creates a new SystemRandom instance which is used as a source of entropy to generate the ephemeral keys used in the key exchange. During the key exchange the public keys are stored in the pub_key and peer_pub_key variables which are used later as input into the hash transcript. The run method executes the key change over the TcpStream which is passed in as a parameter by sending and receiving the public keys to and from the peer.

    Using HKDF to Derive Session Keys

    The output of ECDH should not directly be used as a session key to encrypt messages because the bits are not uniformly distributed. For this reason it is best practice to run the ECDH output through a key derivation function such as HKDF which will produce a uniformly distributed output which is cryptographically indistinguishable from random.

    In the code below I’ve added a kdf method to the EcdhEphemeralKeyExchange type which is called just after the key change. In this method we use the HKDF extract and expand functions implemented by the Ring cryptography library to derive the session keys. The HKDF extract function is used to produce the uniformly distributed output and HKDF expand is used to stretch the key material to double the length so that we can create two session keys, one for the client to server communication and the other for the server to client communication. Having two session keys limits the scope of a compromise in the event that one of the session keys are recovered by an attacker.

    Using Authenticated Encryption to Send & Receive Encrypted Messages

    Next we create two helper types AeadEncrypter and AeadDecrypter which handle the encryption and decryption for each session. Both the client and the server will each create an instance of both types, one for each direction of the communication. For example the client will create an AeadEncrypter to send messages to the server and an AeadDecrypter to receive messages from the server. Each type gets constructed with the respective session key. These types both use AES-GCM from the Ring cryptography library which we use for authenticated encryption. AES-GCM requires passing a nonce to each encryption operation in order to randomise the encryption. The nonce must be unique and so we need to take care to never use repeated nonces for the same encryption key. The nonce doesn’t need to be unpredictable so we simply use a counter starting from one which is incremented for each encryption operation.

    In the code below we implement the NonceSequence interface as required by the library which takes the starting index as a parameter and increments the number to be used as the nonce with each call to the encryption and decryption operations.

    The AeadEncrypter type contains a SealingKey which is constructed by passing in the session key and the nonce sequence. This allows us to use the same encryption key for the entire session. If we need to use a new key for another session we create another instance of AeadEncrypter. The encrypt method takes the data to be encrypted and the associated data as parameters. The associated data is not encrypted but rather is used to bind and authenticate some context data to the encryption.

    The AeadDecrypter type is implemented similarly to the AeadEncrypter and contains an OpeningKey type which is constructed using the session key and nonce sequence as input. The decrypt method takes the ciphertext and associated data as parameters. If the associated_data passed in does not match the data used in the encryption operation then the decryption will fail. This prevents attackers from tampering with the ciphertext while it is in transit.

    Hash Transcript

    A hash transcript is used to authenticate each AEAD decryption operation (similar to the design used in Noise Protocol Framework). We append the public keys and every message sent and received to the hash transcript in order to enforce that both the client and server are seeing the exact same messages in the same order. The hash transcript is implemented by using SHA-256 to hash new messages with the previous transcript hash which creates a kind of hash chain.

    Here is the HashTranscript type which has an append method which hashes a new value with the previous hash to produce the updated transcript hash. This type is used by both the client and the server to record the full transcript of messages sent and received and the public keys used in the key exchange. The as_bytes method returns the transcript hash which is passed in as authenticated data to the AeadEncrypter::encrypt and AeadDecrypter::decrypt methods respectively.

    Putting it All Together

    Finally I updated the client and server to use these helper types to run the key exchange, and send and receive the encrypted messages.

    Here is the handle_session function defined in the client. Once we have a connection, we run the key exchange to create the two session keys, then we append the public keys to the HashTranscript. Next, new AeadEncrypter and AeadDecrypter instances are created which are then used to encrypt and decrypt messages every 5 seconds. After each encryption and decryption operation we append each plaintext message to the hash transcript.

    To see how all these code samples fit together you can view the full code example on Github here.

    Running the Code

    To start the server run:

    To start the client run:

    Conclusion

    In this post I showed how to create a simple project which implements a ECDH key exchange using Rust and the Ring cryptography library. We created helper types which use the ring::agreementring::hkdf and ring::aead modules for each of the required cryptography primitives. We covered how to create a tcp client and server, how to implement the ECDH key exchange, how to derive session keys using HKDF and how to encrypt and decrypt using AES-GCM with a hash transcript for message authentication.

    Leave a Reply

    Your email address will not be published. Required fields are marked *