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.