diff --git a/Cargo.lock b/Cargo.lock index 1d9788d..bde85cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,7 @@ version = "0.1.0-rc.1" dependencies = [ "digest", "hex-literal", + "hmac", "sha2", ] diff --git a/one-step-kdf/Cargo.toml b/one-step-kdf/Cargo.toml index 61a2859..db2a7f0 100644 --- a/one-step-kdf/Cargo.toml +++ b/one-step-kdf/Cargo.toml @@ -17,6 +17,7 @@ digest = "0.11" [dev-dependencies] hex-literal = "1" +hmac = { version = "0.13", default-features = false } sha2 = { version = "0.11", default-features = false } [lints] diff --git a/one-step-kdf/README.md b/one-step-kdf/README.md index 5aa9cdf..7eef09c 100644 --- a/one-step-kdf/README.md +++ b/one-step-kdf/README.md @@ -8,7 +8,7 @@ [![Project Chat][chat-image]][chat-link] Pure Rust implementation of the One-Step Key Derivation Function (formerly known as Concat KDF) -implemented generically over the underlying hash function. +supporting hash- and HMAC-based auxiliary functions. This KDF is described in the section 4 of [NIST SP 800-56C: Recommendation for Key-Derivation Methods in Key-Establishment Schemes](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Cr2.pdf). @@ -18,6 +18,8 @@ This KDF is described in the section 4 of The most common way to use One-Step KDF is as follows: you generate a shared secret with other party (e.g. via Diffie-Hellman algorithm) and use key derivation function to derive a shared key. +## Hash-based + ```rust use hex_literal::hex; use sha2::Sha256; @@ -27,6 +29,29 @@ one_step_kdf::derive_key_into::(b"secret", b"shared-info", &mut key).unw assert_eq!(key, hex!("960db2c549ab16d71a7b008e005c2bdc")); ``` +## HMAC-based + +Use [`derive_key_into_with`] with an initialized HMAC instance to derive a key using the +HMAC-based auxiliary function: + +```rust +use hex_literal::hex; +use hmac::{HmacReset, KeyInit}; +use sha2::Sha256; + +let salt = [0u8; 64]; +let secret = [0u8; 32]; +let hmac = HmacReset::::new_from_slice(&salt).unwrap(); +let mut key = [0u8; 32]; + +one_step_kdf::derive_key_into_with(hmac, &secret, &[], &mut key).unwrap(); + +assert_eq!( + key, + hex!("ceb496ba22edd29dfc5fa4e2d58abcc30b49af2d76d754b54b5c02cf0a2c02dc") +); +``` + ## License Licensed under either of: @@ -58,3 +83,4 @@ dual licensed as above, without any additional terms or conditions. [//]: # (links) [RustCrypto]: https://github.com/RustCrypto +[`derive_key_into_with`]: https://docs.rs/one-step-kdf/latest/one_step_kdf/fn.derive_key_into_with.html diff --git a/one-step-kdf/src/lib.rs b/one-step-kdf/src/lib.rs index bc84946..00a463d 100644 --- a/one-step-kdf/src/lib.rs +++ b/one-step-kdf/src/lib.rs @@ -9,25 +9,75 @@ use core::fmt; use digest::{Digest, FixedOutputReset, Update, array::typenum::Unsigned}; -/// Derives `key` in-place from `secret` and `other_info`. +/// Derives `key` in-place using a hash function. +/// +/// This is the hash-based One-Step KDF variant. For a configurable +/// auxiliary function, use [`derive_key_into_with`]. /// /// # Example +/// /// ```rust /// use hex_literal::hex; /// use sha2::Sha256; /// /// let mut key = [0u8; 16]; -/// one_step_kdf::derive_key_into::(b"secret", b"shared-info", &mut key).unwrap(); -/// assert_eq!(key, hex!("960db2c549ab16d71a7b008e005c2bdc")); +/// +/// one_step_kdf::derive_key_into::( +/// b"secret", +/// b"shared-info", +/// &mut key, +/// ) +/// .unwrap(); +/// +/// assert_eq!( +/// key, +/// hex!("960db2c549ab16d71a7b008e005c2bdc") +/// ); /// ``` /// /// # Errors +/// /// - Returns [`Error::NoSecret`] if `secret` is empty. -/// - Returns [`Error::NoOutput`] if `output` is empty. +/// - Returns [`Error::NoOutput`] if `key` is empty. /// - Returns [`Error::CounterOverflow`] if `key` is too large. pub fn derive_key_into(secret: &[u8], other_info: &[u8], key: &mut [u8]) -> Result<(), Error> where D: Digest + FixedOutputReset, +{ + derive_key_into_with(D::new(), secret, other_info, key) +} + +/// Derives `key` in-place using an initialized auxiliary function. +/// +/// The auxiliary function processes each block as: +/// +/// ```text +/// counter || secret || other_info +/// ``` +/// +/// `aux` must be freshly initialized. After finalizing a block, its +/// [`FixedOutputReset`] implementation must restore the same initial +/// configuration. +/// +/// This allows the same One-Step KDF implementation to be used with: +/// +/// - a digest such as SHA-256; +/// - a keyed construction such as `HmacReset`; +/// - another compatible fixed-output auxiliary function. +/// +/// # Errors +/// +/// - Returns [`Error::NoSecret`] if `secret` is empty. +/// - Returns [`Error::NoOutput`] if `key` is empty. +/// - Returns [`Error::CounterOverflow`] if `key` is too large. +pub fn derive_key_into_with( + mut aux: A, + secret: &[u8], + other_info: &[u8], + key: &mut [u8], +) -> Result<(), Error> +where + A: Update + FixedOutputReset, { if secret.is_empty() { return Err(Error::NoSecret); @@ -37,32 +87,39 @@ where return Err(Error::NoOutput); } - // Key length shall be less than or equal to hash output length * (2^32 - 1). - if (key.len() as u64) >= D::OutputSize::U64 * u64::from(u32::MAX) { - return Err(Error::CounterOverflow); - } + let output_size = A::OutputSize::USIZE; + let block_count = block_count(key.len(), output_size)?; + + for (counter, chunk) in (1..=block_count).zip(key.chunks_mut(output_size)) { + Update::update(&mut aux, &counter.to_be_bytes()); + Update::update(&mut aux, secret); + Update::update(&mut aux, other_info); - let mut digest = D::new(); - let mut counter: u32 = 1; + let block = aux.finalize_fixed_reset(); - for chunk in key.chunks_mut(D::OutputSize::USIZE) { - Update::update(&mut digest, &counter.to_be_bytes()); - Update::update(&mut digest, secret); - Update::update(&mut digest, other_info); - chunk.copy_from_slice(&digest.finalize_reset()[..chunk.len()]); - counter += 1; + chunk.copy_from_slice(&block[..chunk.len()]); } Ok(()) } +fn block_count(key_len: usize, output_size: usize) -> Result { + if output_size == 0 { + return Err(Error::CounterOverflow); + } + + u32::try_from(key_len.div_ceil(output_size)).map_err(|_| Error::CounterOverflow) +} + /// One-Step KDF errors. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Error { /// The length of the secret is zero. NoSecret, + /// The length of the output is zero. NoOutput, + /// The length of the output is too big. CounterOverflow, } @@ -72,9 +129,24 @@ impl fmt::Display for Error { f.write_str(match self { Error::NoSecret => "Buffer for secret has zero length.", Error::NoOutput => "Buffer for key has zero length.", - Error::CounterOverflow => "Requested key length is to big.", + Error::CounterOverflow => "Requested key length is too big.", }) } } impl core::error::Error for Error {} + +#[cfg(test)] +mod tests { + use super::{Error, block_count}; + + #[test] + fn maximum_block_count_is_accepted() { + let maximum = usize::try_from(u32::MAX).expect("usize is at least 32 bits"); + + assert_eq!(block_count(maximum, 1), Ok(u32::MAX)); + + #[cfg(target_pointer_width = "64")] + assert_eq!(block_count(maximum + 1, 1), Err(Error::CounterOverflow)); + } +} diff --git a/one-step-kdf/tests/hmac.rs b/one-step-kdf/tests/hmac.rs new file mode 100644 index 0000000..c723e41 --- /dev/null +++ b/one-step-kdf/tests/hmac.rs @@ -0,0 +1,137 @@ +//! HMAC-based One-Step KDF tests. +#![allow(clippy::unwrap_used, reason = "tests")] + +use digest::Digest; +use hex_literal::hex; +use hmac::{HmacReset, KeyInit}; + +use sha2::{Sha224, Sha256}; + +type HmacSha224 = HmacReset; +type HmacSha256 = HmacReset; + +struct HmacFixture<'a> { + secret: &'a [u8], + salt: &'a [u8], + other_info: &'a [u8], + expected_key: &'a [u8], +} + +fn test_hmac_key_derivation(fixtures: &[HmacFixture<'_>]) { + for fixture in fixtures { + let mut buf = [0u8; 256]; + + for key_length in 1..=fixture.expected_key.len() { + let key = &mut buf[..key_length]; + + let aux = HmacSha256::new_from_slice(fixture.salt).unwrap(); + + one_step_kdf::derive_key_into_with(aux, fixture.secret, fixture.other_info, key) + .unwrap(); + + assert_eq!(&fixture.expected_key[..key_length], key); + } + } +} + +#[test] +fn test_input_output_hmac_sha256() { + let fixtures = [HmacFixture { + secret: &[0u8; 32], + salt: &[0u8; 64], + other_info: &[], + expected_key: &hex!( + "ceb496ba22edd29dfc5fa4e2d58abcc3" + "0b49af2d76d754b54b5c02cf0a2c02dc" + ), + }]; + + test_hmac_key_derivation(&fixtures); +} + +#[test] +fn test_hmac_sha256_multiple_blocks() { + let secret = [0u8; 32]; + let salt = [0u8; 64]; + let other_info = []; + + let aux = HmacSha256::new_from_slice(&salt).unwrap(); + let mut key = [0u8; 64]; + + one_step_kdf::derive_key_into_with(aux, &secret, &other_info, &mut key).unwrap(); + + assert_eq!( + key, + hex!( + // HMAC-SHA256(salt, 00000001 || secret) + "ceb496ba22edd29dfc5fa4e2d58abcc3" + "0b49af2d76d754b54b5c02cf0a2c02dc" + + // HMAC-SHA256(salt, 00000002 || secret) + "142cd755e28b5aae1958ac736f2c3190" + "75137e3fe4d94f64c6fc99cb31e2ad53" + ) + ); +} + +#[test] +fn nist_acvp_hmac_sha224() { + // NIST ACVP sample vector for KDA OneStep SP 800-56C Rev. 2: + // https://github.com/usnistgov/ACVP-Server/tree/master/ + // gen-val/json-files/KDA-OneStep-Sp800-56Cr2 + // Test group 16, test case 76. + let salt = hex!( + "00000000000000000000000000000000" + "00000000000000000000000000000000" + "00000000000000000000000000000000" + "00000000000000000000000000000000" + ); + let secret = hex!( + "b41cb9f2a6fbe1ca42bf99c138e85437" + "bdbd2e0ef64c1482d93fc8c6" + ); + let fixed_info = hex!( + // t + "7bec348a2276f0e6a3c8a9950296d962" + // uPartyInfo: partyId || ephemeralData + "67f39f8c4d47cd8140f787478abdece0" + "51f3b6a85db0763371211598f8c8e799" + "cde4b1aa74f0613e08916b0a" + // vPartyInfo: partyId + "d69c15cc8ba1ac8a4ee7ba93ed2ab572" + // L = 1024 bits, encoded as a big-endian 32-bit integer + "00000400" + ); + let expected = hex!( + "5f850b63457b03c0ae065f31bf5f8849" + "cdbb9376dd8e147bb8d2393d8db976dd" + "01eae51c1d2a59a68f5bd760aca4adde" + "19772028f3baeec07826f8cdb752f021" + "7b100446b1650840d750a814dcad6999" + "6c2f7caf34ff40115da23d386ad74866" + "6a47ecc0bb72bfbcbc22505a3dbb1437" + "d5518c7bc7aafe1026caa28524ebbfe2" + ); + let aux = HmacSha224::new_from_slice(&salt).unwrap(); + let mut key = [0u8; 128]; + + one_step_kdf::derive_key_into_with(aux, &secret, &fixed_info, &mut key).unwrap(); + + assert_eq!(key, expected); +} + +#[test] +fn generic_aux_matches_digest_api() { + let secret = b"secret"; + let other_info = b"shared-info"; + + let mut through_wrapper = [0u8; 64]; + let mut through_aux = [0u8; 64]; + + one_step_kdf::derive_key_into::(secret, other_info, &mut through_wrapper).unwrap(); + + one_step_kdf::derive_key_into_with(Sha256::new(), secret, other_info, &mut through_aux) + .unwrap(); + + assert_eq!(through_wrapper, through_aux); +}