Skip to content
Blog » Deriving Cryptographic Keys with HKDF in Rust using Ring

Deriving Cryptographic Keys with HKDF in Rust using Ring

    Ring is a popular Rust cryptography library which has support for generating cryptographic keys from a single input key using HKDF, the HMAC-based Extract-and-Expand Key Derivation Function. As you will see in this post, the ring::hkdf module supports securely generating pseudorandom bytes using the Salt::extract and Prk::expand methods implemented in the library.

    Introduction to HKDF

    The IETF defined the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) in RFC 5869. It is a type of key derivation function (KDF) that is based on HMAC (see my post on HMACs in Rust here). Cryptography experts designed KDFs as a primitive to take an input key and generate one or more cryptographically strong keys. The keys generated are pseudorandom, derived deterministically from the input and contain no additional entropy.

    HKDF as defined in the RFC has two stages; HKDF-Extract and then HKDF-Expand. The first stage takes the input key and extracts from it a fixed-length pseudorandom key. The second stage expands the generated pseudorandom key into several additional pseudorandom keys (the output of the KDF).

    HKDF-Extract

    The RFC document defines the the HKDF-Extract function as follows:

    HKDF-Extract(salt, IKM) -> PRK

    salt – You can set the salt as an optional random value to provide domain separation between different calls to HKDF. It is recommend to use a salt to increase the security of HKDF.

    IKM (Input Keying Material) – This is the input key which is used as the initial source of entropy to the HKDF.

    PRK (Pseudo-Random Key) – This is the fixed length pseudorandom key returned as output. This key’s length is equal to the length of the hash function used by the chosen HMAC algorithm (see my post on hash functions in Rust here).

    HKDF-Expand

    The RFC document defines the the HKDF-Expand function as follows:

    HKDF-Expand(PRK, info, L) -> OKM

    PRK (Pseudo-Random Key) – This is the fixed length pseudorandom key which was returned from the call to HKDF-Extract.

    info – This is an optional string of context data. Provide this parameter to create domain separation between different calls to HKDF-Extract. Changing the value in the info parameter will change the OKM value produced. This can be useful when using the same input key in multiple contexts.

    L – The length of the output keying material in bytes. This parameter defines how much key material to return in the HKDF-Expand step.

    OKM (Output Key Material) – This is the final output key material which is returned as a single output. Note that this final output key can be up to 255 times longer than the length of the hash function used by the HMAC. You can create multiple output keys from this value if required.

    HKDF Usage Guidelines

    • HKDF requires a strong input key with enough entropy to generate secure keys. This means the input should be both random (unpredictable) and long. Using input key material with a shorter length can make the system vulnerable to brute force attacks.
    • Insecure keys can result from a weak hash function, hence it is advisable to avoid older legacy hash functions like SHA1.
    • You should not use HKDF for other purposes such as message authentication or directly deriving keys from passwords, although you can use it in combination with password hashing algorithms.

    Summary of Types

    The ring::hkdf module contains the following types and functions:

    trait KeyType – A type that defines a len method which determines the length of the final output key.

    struct Algorithm – The type of HKDF algorithm to be used. HKDF_SHA1_FOR_LEGACY_USE_ONLY, HKDF_SHA256, HKDF_SHA384 and HKDF_SHA512 HKDF algorithms are supported. Algorithm implements the KeyType trait so it can be passed in place of KeyType.

    struct Salt – Created by supplying an algorithm and salt value. Supports extracting the input key and returning a Prk (pseudorandom key).

    struct Prk – Represents a pseudorandom key. Supports expanding the pseudorandom key into the output key. Implements the expand function which returns Okm (output key material).

    struct Okm – Represents the output key material and defines a fill method which consumes the Okm and copies the key material into a buffer.

    fn Salt::extract – This is the implementation of the HKDF-Extract function as defined in the RFC document.

    pub fn extract(&self, secret: &[u8]) -> Prk

    fn Prk::expand – This is the implementation of the HKDF-Expand function as defined in the RFC document.

    pub fn expand<'a, L: KeyType>(
        &'a self, 
        info: &'a [&'a [u8]], 
        len: L
    ) -> Result<Okm<'a, L>, error::Unspecified>

    Rust Imports

    Let’s start by importing the required types into our project.

    use ring::digest::SHA256_OUTPUT_LEN;
    use ring::hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY;
    use ring::hkdf::HKDF_SHA256;
    use ring::hkdf::HKDF_SHA384;
    use ring::hkdf::HKDF_SHA512;
    use ring::hkdf::KeyType;
    use ring::hkdf::Algorithm;
    use ring::hkdf::Salt;
    use ring::hkdf::Prk;
    use ring::hkdf::Okm;

    Create an Instance of Salt

    We first create an instance of the Salt struct passing in the HKDF algorithm and salt value. Creating the Salt instance is relatively expensive so instances having the same salt value should be reused where possible instead of re-constructing. The salt value is optional but it is best practice to use a randomly generated salt to improve the security of the HKDF.

    let salt = Salt::new(HKDF_SHA256, b"salt bytes");

    Extract the Pseudorandom Key

    Next we call the Salt::extract method passing in the input key material to generate a Prk instance (the pseudorandom key). This is the HKDF-Extract operation.

    let input_key_material = b"secret key"; // use a long randomly generated key
    println!("Input key material: {}", hex::encode(input_key_material)); // don't print this in production
    
    let pseudo_rand_key: Prk = salt.extract(input_key_material);

    Expand the Pseudorandom Key

    Then we call the Prk::expand method passing in some application specific context data and an instance of KeyType which is used to provide the length of the output key to be returned. In this case we are passing in the HKDF_SHA256 Algorithm instance which implements KeyType and returns SHA256_OUTPUT_LEN in the call to KeyType::len. The call will only fail if KeyType::len is over 255 times larger than the size of the Prk. This means we can generate up to 255 keys equal in length to the pseudorandom key.

    Then we use the Okm::fill method to get the output key bytes. Note that the fill method consumes the Okm instance and therefore it can only be called once. This design prevents reuse of the output key.

    let context_data = &["context one".as_bytes()];
    let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
    
    let mut result = [0u8; SHA256_OUTPUT_LEN];
    output_key_material.fill(&mut result).unwrap();
    println!("Derived output key material: {}", hex::encode(result)); // don't print this in production

    After running the code we get the following output:

    Input key material: 736563726574206b6579
    Derived output key material: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e

    Supplying Context Data with Info

    Note that it is possible to issue multiple calls to Prk::expand using the same instance of Prk but because the function is deterministic, using the same context data in separate calls will produce the same output key material. For this reason we can reuse the Prk and supply different context data in the info parameter in separate calls to Prk::expand to generate different keys from the same Prk instance but make sure that you are aware of the security implications of doing so.

    let context_data = &["context one".as_bytes()];
    let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
    
    let mut result = [0u8; SHA256_OUTPUT_LEN];
    output_key_material.fill(&mut result).unwrap();
    println!("Derived output key material: {}", hex::encode(result)); // don't print this in production
    
    // second call with different context data produces a different output
    let context_data = &["context two".as_bytes()];
    let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
    
    let mut result = [0u8; SHA256_OUTPUT_LEN];
    output_key_material.fill(&mut result).unwrap();
    println!("Derived output key material: {}", hex::encode(result)); // don't print this in production

    After running the code we get the following output:

    Input key material: 736563726574206b6579
    Derived output key material: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e
    Derived output key material: a0ed15560bbed56af00c380e985ef8f9162866332a6e107f823623861d1fc37f

    Generating Multiple Output Keys

    In the above example the generated output key was equal in length to the size of the generated pseudorandom key (32 bytes in the case of SHA256) but we can also generate longer output keys in the expand step, up to 255 times the length of the pseudorandom key. This is useful if we need to generate more than one key. In this example we generate three output keys:

    const NUM_OF_KEYS: usize = 3;
    const OUTPUT_KEY_SIZE: usize = NUM_OF_KEYS * SHA256_OUTPUT_LEN;
    
    struct MyKeyType(usize);
    
    impl KeyType for MyKeyType {
        fn len(&self) -> usize {
            self.0
        }
    }
    
    // Salt & HDKF-Extract steps omitted for brevity
    
    // The HKDF-Expand operation
    let context_data = &["context one".as_bytes()];
    let output_key_material: Okm<MyKeyType> = pseudo_rand_key.expand(context_data, MyKeyType(OUTPUT_KEY_SIZE)).unwrap();
    
    let mut result = [0u8; OUTPUT_KEY_SIZE];
    output_key_material.fill(&mut result).unwrap();
    println!("Derived output key 1: {}", hex::encode(&result[0..32])); // don't print this in production
    println!("Derived output key 2: {}", hex::encode(&result[32..64])); // don't print this in production
    println!("Derived output key 3: {}", hex::encode(&result[64..96])); // don't print this in production

    After running the code we get the following output:

    Derived output key 1: 811105f7ecc53c62b340bc116a94a57c740b33c4d306844d8a88a1431e8c7b6e
    Derived output key 2: f4ad09555fb7c36a21ee7632f35c89ecb82c99c7dac499ce3b495789528749a3
    Derived output key 3: e0897b8085fd74edfc868f850cabf8a4a1df7dbb038dbb0a385189bcefe0f234

    Full Sample Code

    Here is the full code sample for reference.

    use ring::digest::SHA256_OUTPUT_LEN;
    use ring::hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY;
    use ring::hkdf::HKDF_SHA256;
    use ring::hkdf::HKDF_SHA384;
    use ring::hkdf::HKDF_SHA512;
    use ring::hkdf::KeyType;
    use ring::hkdf::Algorithm;
    use ring::hkdf::Salt;
    use ring::hkdf::Prk;
    use ring::hkdf::Okm;
    
    fn main() {
        // scenario 1 - generate single output key
    
        let input_key_material = b"secret key";
        println!("Input key material: {}", hex::encode(input_key_material)); // don't print this in production
    
        // Constructs a new Salt with the given value based on the given digest algorithm
        let salt = Salt::new(HKDF_SHA256, b"salt bytes");
    
        // The HKDF-Extract operation
        let pseudo_rand_key: Prk = salt.extract(input_key_material);
    
        // The HKDF-Expand operation
        let context_data = &["context one".as_bytes()];
        let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
    
        let mut result = [0u8; SHA256_OUTPUT_LEN];
        output_key_material.fill(&mut result).unwrap();
        println!("Derived output key material: {}", hex::encode(result)); // don't print this in production
    
        // second call with different context data produces a different output
        let context_data = &["context two".as_bytes()];
        let output_key_material: Okm<Algorithm> = pseudo_rand_key.expand(context_data, HKDF_SHA256).unwrap();
    
        let mut result = [0u8; SHA256_OUTPUT_LEN];
        output_key_material.fill(&mut result).unwrap();
        println!("Derived output key material: {}", hex::encode(result)); // don't print this in production
    
    
        // scenario 2 - generate multiple output keys
    
        const NUM_OF_KEYS: usize = 3;
        const OUTPUT_KEY_SIZE: usize = NUM_OF_KEYS * SHA256_OUTPUT_LEN;
    
        struct MyKeyType(usize);
    
        impl KeyType for MyKeyType {
            fn len(&self) -> usize {
                self.0
            }
        }
    
        // The HKDF-Expand operation
        let context_data = &["context one".as_bytes()];
        let output_key_material: Okm<MyKeyType> = pseudo_rand_key.expand(context_data, MyKeyType(OUTPUT_KEY_SIZE)).unwrap();
    
        let mut result = [0u8; OUTPUT_KEY_SIZE];
        output_key_material.fill(&mut result).unwrap();
        println!("Derived output key 1: {}", hex::encode(&result[0..32])); // don't print this in production
        println!("Derived output key 2: {}", hex::encode(&result[32..64])); // don't print this in production
        println!("Derived output key 3: {}", hex::encode(&result[64..96])); // don't print this in production
    }
    

    Conclusion

    In this post we introduced the HKDF cryptographic primitive and algorithm, explained how it works and the cryptographic properties it can provide. Then we explained how to use the Ring hkdf module to extract strong cryptographic keys from a single input key using both the Salt::extract and Prk::expand functions. We demonstrated a few scenarios; one using different context data provided in the info parameter to provide domain separation between calls to Prk::expand in different contexts, and another showing how to generate multiple output keys.

    Thanks for reading. If you have any questions or comments, feel free to reach out. If you want to see more posts like this, you can subscribe via email or follow me on Twitter.

    Leave a Reply

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