Skip to content
Blog » ECDH Key Agreement in Rust using Ring

ECDH Key Agreement in Rust using Ring

    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:

    1. Generate a new private key – This key must be kept secret and never shared.
    2. 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.
    3. Share the public key with the peer – This peer could be a client or server.
    4. Receive the peer’s public key and validate – The peer’s public key needs to be validated for correctness before using.
    5. 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.

    Leave a Reply

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