HMAC Key Derivation function (HKDF) is used to derive an encryption key from initial key material (IKM). With HKDF we use a given hashing method to the bases of the function, such as with SHA-512. With this, HKDF creates a pseudorandom key (PRK) using an IKM and a salt value in order to produce an HMAC hash function (such as HMAC-SHA256). This PRK output is used to produce a key of the required length. Next the PRK output is used to produce a key of the required length. If we generate a 16-byte output (32 hex characters), we have a 128-bit key, and a 32-byte output (64 hex characters) will generate a 256-bit key. HKDF is used in TLS 1.3 for generating encryption keys [RFC 5869][article]. In this case we use the IKM as the input keying material and a salt value. From this we compute the pseudo random key (PRK) with HMAC-Hash(salt, IKM) and the OKM (output keying material) with HKDF-Expand(PRK, info, L), and where L is the length, and info is additional information. The PRK is a fixed length hash, whereas the OKM can be used to create any length of a pseudo random value. In practice, though, we use HMAC-SHA256 not for password hashing, but for generating encryption keys based on a shared secret, such as within a Diffie-Hellman key exchange.
HKDF (HMAC-SHA512) in Rust |
Test vectors
A basic test for HMAC-SHA256 is:
// Basic test case with SHA-256 // Hash = SHA-256 // IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 octets) // salt = 0x000102030405060708090a0b0c (13 octets) // PRK = 0x077709362c2e32df0ddc3f0dc47bba63 // 90b6c73bb50f9c3122ec844ad7c2b3e5 (32 octets) // OKM = 0x3cb25f25faacd57a90434f64d0362f2a // 2d2d0a90cf1a5a4c5db02d56ecc4c5bf // 34007208d5b887185865 (42 octets)
An outline of the Rust code is:
extern crate crypto; extern crate sha1; use rustc_serialize::hex::FromHex; use std::iter::repeat; use crypto::sha2::Sha256; use std::env; fn hex_to_bytes(s: String) -> Vec{ s.as_str().from_hex().unwrap() } fn main() { let mut myikm= String::from("0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"); // let mut myikm=String::from("hello"); let mut mysalt=String::from("000102030405060708090a0b0c"); let mut myinfo =String::from("f0f1f2f3f4f5f6f7f8f9"); let mut mylen=42; let args: Vec = env::args().collect(); if args.len() >1 { myikm = args[1].to_string();} if args.len() >2 { mysalt = args[2].to_string();} if args.len() >3 { myinfo = args[3].to_string();} if args.len() >4 { mylen = args[4].to_string().parse:: ().unwrap();} println!("== HMAC-SHA256 =="); println!("Message: {}",myikm.as_str()); let dig=Sha256::new(); let salt=&hex_to_bytes( mysalt.clone())[..]; let info=&hex_to_bytes( myinfo.clone())[..]; let ikm = if myikm.starts_with("0x") { hex_to_bytes(myikm.replace("0x", "")) } else { myikm.into_bytes() }; let mut prk: Vec<u8> = repeat(0).take(32).collect(); crypto::hkdf::hkdf_extract(dig, &salt[..], &ikm[..], &mut prk); let mut okm: Vec<u8> = repeat(0).take(mylen as usize).collect(); crypto::hkdf::hkdf_expand(dig, &prk[..], &info[..], &mut okm); println!("Salt: {}",mysalt.as_str()); println!("Info: {}",myinfo.as_str()); println!("Len: {}",mylen); println!("\nPRK (pseudo-random key): {}",hex::encode(prk)); println!("OKM (output keying material): {}",hex::encode(okm)); // Basic test case with SHA-256 // Hash = SHA-256 // IKM = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b (22 octets) // salt = 0x000102030405060708090a0b0c (13 octets) // PRK = 0x077709362c2e32df0ddc3f0dc47bba63 // 90b6c73bb50f9c3122ec844ad7c2b3e5 (32 octets) // OKM = 0x3cb25f25faacd57a90434f64d0362f2a // 2d2d0a90cf1a5a4c5db02d56ecc4c5bf // 34007208d5b887185865 (42 octets) }
Finally we simply build with:
cargo build
A sample run is:
== HMAC-SHA256 == Message: hello Salt: 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b Info: 000102030405060708090a0b0c PRK (pseudo-random key): c196df69a5e50fc8a8304eb90033d8f234af75cea73a4e9b0777b8bf04540a44 OKM (output keying material): 22d46257730f68d51cd441b5c999d9bcb11e20a5eb82be0d93807cb5288eacc70dcdb7c141e329a455fc