In this post I show you how to implement an ECDH key exchange using the agreement
module of the Rust cryptography library Ring. We will go through generating a public/private key pair, exchanging public keys with a peer and then using the agree_ephemeral
function to securely compute a shared secret.
Introduction to ECDH
Elliptic-curve Diffie-Hellman (ECDH) is a key agreement protocol which allows two parties to compute a shared secret over an insecure channel using elliptic curve public/private key pairs. It is based on the earlier Diffie-Hellman protocol which uses modular exponentiation and whose security is based on the difficulty of solving the discrete logarithm problem. For more details on the classical Diffie-Hellman protocol see here. ECDH is faster and considered more secure than classical Diffie-Hellman and therefore ECDH should generally be preferred for modern applications. It is usually used as one step in a larger protocol to establish a shared secret to be used in subsequent operations as a symmetric encryption key.
From a high level, Diffie-Hellman works as follows (for both classical Diffie-Hellman & ECDH). Both parties in the key exchange do the following steps:
- Generate a new private key – This key must be kept secret and never shared.
- Compute a new public key from the private key – There is a special mathematical relationship between the public key and private key which is based on the chosen key agreement algorithm.
- Share the public key with the peer – This peer could be a client or server.
- Receive the peer’s public key and validate – The peer’s public key needs to be validated for correctness before using.
- Use our private key and the peer’s validated public key to produce the shared secret – Due to the mathematics of the protocol our private key combined with our peers public key is equal to our peers private key combined with our public key. To understand the details of why this works you will need to read up on the mathematics of DH/ECDH but I won’t be covering that in this post.
ECDH Usage Guidelines
- ECDH prevents a passive man-in-the-middle (MITM) attacker from being able to obtain the shared secret.
- ECDH by default does not protect against an active MITM attacker from impersonating the peer during the key exchange, so additional authentication mechanisms are needed to authenticate each peer if we need to protect against an active MITM attacker. For example, we could have the server generate a long term public/private key pair and then have the client include a known copy of the public key. During the key exchange the client can check that the public key matches what it expects. This is called an authenticated key exchange. If both sides perform the same form of authentication to the peer than it is called a mutually authenticated key exchange.
- The keys used on either side of a ECDH key exchange can be static (long term) or ephemeral and each configuration has pros and cons. The combinations are listed below with their pros and cons:
- Ephemeral to ephemeral – Most common scenario for key agreement. Provides forward secrecy but no authenticity on either side of the exchange.
- Static to static – Generates a long term shared secret so provides no forward secrecy. Provides authenticity on both sides.
- Ephemeral to static – Provides no forward secrecy. Static side can be authenticated by the ephemeral side.
- The current minimum recommended key length for classical Diffie-Hellman is 2,048-bits and for ECDH is 256-bits. Applications with higher security requirements can use DH keys with 3072-bits or ECDH keys with 384-bits.
- ECDH can be used with a selection of standardised elliptic curves. Ring supports three curves that are considered secure and safe to use; P-256 and P-384 by NIST and X25519 by Daniel J. Bernstein. X25519 is a good default if you are unsure which to use.
- As recommended in RFC 7748 you should apply a KDF to the computed shared secret before using as a session key or other in a real application.
Summary of Types
The ring::agreement
module contains the following types and functions:
struct Algorithm – The type of ECDH algorithm. ECDH_P256
, ECDH_P384
and X25519
algorithms are supported.
struct EphemeralPrivateKey – An ephemeral private key for use (only) with agree_ephemeral
. The signature of agree_ephemeral
ensures that an EphemeralPrivateKey
can be used for at most one key agreement.
struct PublicKey – A public key for key agreement.
struct UnparsedPublicKey – An unparsed, possibly malformed, public key for key agreement.
fn agree_ephemeral – Performs a key agreement with an ephemeral private key and the given public key.
pub fn agree_ephemeral<B: AsRef<[u8]>, R>(
my_private_key: EphemeralPrivateKey,
peer_public_key: &UnparsedPublicKey<B>,
kdf: impl FnOnce(&[u8]) -> R,
) -> Result<R, error::Unspecified>
See the ring::agreement
module documentation here.
Rust Imports
Let’s start by importing the required types into our project.
use ring::error::Unspecified;
use ring::rand::SystemRandom;
use ring::agreement::Algorithm;
use ring::agreement::ECDH_P256;
use ring::agreement::ECDH_P384;
use ring::agreement::X25519;
use ring::agreement::EphemeralPrivateKey;
use ring::agreement::PublicKey;
use ring::agreement::UnparsedPublicKey;
use ring::agreement::agree_ephemeral;
Generate our Public/Private Key Pair
Here we start by creating an instance of SystemRandom which is used as a source of entropy for generating the private key. We then select the key agreement algorithm to use, in this case we are using X25519
. We then call EphemeralPrivateKey::generate
to generate the private key and then we compute the public key from the private key using EphemeralPrivateKey::compute_public_key
.
// Use a rand::SystemRandom as the source of entropy
let rng = SystemRandom::new();
// Select a key agreement algorithm. All agreement algorithms follow the same flow
let alg: &Algorithm = &X25519;
// Generate a private key and public key
let my_private_key: EphemeralPrivateKey = EphemeralPrivateKey::generate(alg, &rng)?;
let my_public_key: PublicKey = my_private_key.compute_public_key()?;
// The EphemeralPrivateKey type doesn't allow us to directly access the private key as designed
println!("my_public_key = {}", hex::encode(my_public_key.as_ref()));
Exchange Public Keys with Peer
In this first example we are simply simulating sending and receiving the public keys with a peer, just to show the steps. We then create an instance of UnparsedPublicKey which will be used to parse and validate the public key later.
// Send our public key to the peer here
// Simulate receiving a public key from the peer
let peer_public_key: PublicKey = {
let peer_private_key = EphemeralPrivateKey::generate(alg, &rng)?;
peer_private_key.compute_public_key()?
};
println!("peer_public_key = {}", hex::encode(peer_public_key.as_ref()));
// The peer public key needs to be parsed before use so wrap it creating as an instance of UnparsedPublicKey
let peer_public_key = UnparsedPublicKey::new(alg, peer_public_key);
Compute the Shared Secret
Then the final step is to run the agree_ephemeral
function which takes our private key, the peer’s public key, the error to return on failure and a lambda which gets called with the computed shared secret passed as a parameter. Note that RFC 7748 recommends applying a KDF to the shared secret before usage.
// run ECDH to agree on a shared secret
agree_ephemeral(my_private_key,
&peer_public_key,
|shared_secret: &[u8]| { // the result of the key agreement is passed to this lambda
println!("shared_secret = {}", hex::encode(shared_secret.as_ref())); // don't print this in production
// As recommended in RFC 7748 we should apply a KDF on the key material here before using in a real application
// We can return the derived key from the kdf here, otherwise we just return () if the key isn't needed outside this scope
})
Running the code gives the following output:
my_public_key = 0b226d3e97b342d800a681cafbb79f0a151122562a306983f48e4841ea8dc714
peer_public_key = df5e6313fec0909914c686b63578b9aeee13ab3f49300c5e692bf3a204ed2475
shared_secret = cb210ba8b6909dd7a4d9c5a0c5e28329f142780cdfb556594e55efe390deb735
Client-Server Key Exchange over TCP
In this second example we start a tcp server in another thread and then connect to it as a tcp client in the main thread. The call to server.join
blocks the main thread until the child thread completes. The ecdh_x25519
function runs the full ECDH protocol as described above except the public keys are exchanged over the tcp stream after the connection is established.
use std::thread;
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
fn main() {
let server = thread::spawn(move || {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
let mut stream = listener.accept().unwrap().0;
ecdh_x25519("server", &mut stream).unwrap();
});
let mut stream = TcpStream::connect("127.0.0.1:8080").unwrap();
ecdh_x25519("client", &mut stream).unwrap();
server.join().unwrap();
}
Running the code gives the following output. As you can see the server_shared_secret
and server_shared_secret
values match as expected:
client_public_key = 648185523469cf6678912c9e9b24b243851b2137ed065729621e4ecba4ef1564
server_public_key = c7a92fe60880181d7c11ac688c98be671dff7892cfc51de10563be41f48d264d
server_peer_public_key = 648185523469cf6678912c9e9b24b243851b2137ed065729621e4ecba4ef1564
client_peer_public_key = c7a92fe60880181d7c11ac688c98be671dff7892cfc51de10563be41f48d264d
server_shared_secret = 3ab034715237b6d34ec14b3f65c66418cd715284febf3f9102553ec8f5aa8a4d
client_shared_secret = 3ab034715237b6d34ec14b3f65c66418cd715284febf3f9102553ec8f5aa8a4d
Full Sample Code
Here is the full code sample for reference. Note that this example is not production ready code due to the usage of calls to unwrap
and the limited error checking in the code that sends and receives the public keys over the TCP streams.
use ring::error::Unspecified;
use ring::rand::SystemRandom;
use ring::agreement::Algorithm;
use ring::agreement::ECDH_P256;
use ring::agreement::ECDH_P384;
use ring::agreement::X25519;
use ring::agreement::EphemeralPrivateKey;
use ring::agreement::PublicKey;
use ring::agreement::UnparsedPublicKey;
use ring::agreement::agree_ephemeral;
use std::thread;
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
fn main() {
let server = thread::spawn(move || {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
let mut stream = listener.accept().unwrap().0;
ecdh_x25519("server", &mut stream).unwrap();
});
let mut stream = TcpStream::connect("127.0.0.1:8080").unwrap();
ecdh_x25519("client", &mut stream).unwrap();
server.join().unwrap();
}
fn ecdh_x25519(actor: &str, stream: &mut TcpStream) -> Result<(), Unspecified> {
// Use a rand::SystemRandom as the source of entropy
let rng = SystemRandom::new();
// Select a key agreement algorithm. All agreement algorithms follow the same flow
let alg: &Algorithm = &X25519;
// Generate a private key and public key
let my_private_key: EphemeralPrivateKey = EphemeralPrivateKey::generate(alg, &rng)?;
let my_public_key: PublicKey = my_private_key.compute_public_key()?;
// The EphemeralPrivateKey doesn't allow us to directly access the private key as designed
println!("{}_public_key = {}", actor, hex::encode(my_public_key.as_ref()));
// Send our public key to the peer here
stream.write_all(my_public_key.as_ref()).unwrap();
// Simulate receiving a public key from the peer
let mut peer_public_key = [0u8; 32];
stream.read_exact(&mut peer_public_key).unwrap();
println!("{}_peer_public_key = {}", actor, hex::encode(peer_public_key.as_ref()));
// The peer public key needs to be parsed before use so wrap it creating as an instance of UnparsedPublicKey
let peer_public_key = UnparsedPublicKey::new(alg, peer_public_key);
// run ECDH to agree on a shared secret
agree_ephemeral(my_private_key,
&peer_public_key,
|shared_secret: &[u8]| { // the result of the key agreement is passed to this lambda
println!("{}_shared_secret = {}", actor, hex::encode(shared_secret.as_ref())); // don't print this in production
// As recommended in RFC 7748 we should apply a KDF on the key material here before using in a real application
// We can return the derived key from the kdf here, otherwise we just return () if the key isn't needed outside this scope
})
)
}
Conclusion
In this post I introduced the ECDH protocol, explained what it is used for and the high level algorithm which allows us to securely compute a shared secret over an insecure medium using public/private key cryptography. Then we covered how to use the agreement
module of the Ring cryptography library to implement ECDH in our applications by using the EphemeralPrivateKey
, PublicKey
and UnparsedPublicKey
types and then calling the agree_ephemeral
function. We went through two examples, the first a very simple example which only simulates the key exchange with the peer and the second which actually connects to the peer over a tcp connection and does the full key exchange.