Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions one-step-kdf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
28 changes: 27 additions & 1 deletion one-step-kdf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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;
Expand All @@ -27,6 +29,29 @@ one_step_kdf::derive_key_into::<Sha256>(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::<Sha256>::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:
Expand Down Expand Up @@ -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
106 changes: 89 additions & 17 deletions one-step-kdf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Sha256>(b"secret", b"shared-info", &mut key).unwrap();
/// assert_eq!(key, hex!("960db2c549ab16d71a7b008e005c2bdc"));
///
/// one_step_kdf::derive_key_into::<Sha256>(
/// 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<D>(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<Sha256>`;
/// - 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<A>(
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);
Expand All @@ -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<u32, Error> {
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,
}
Expand All @@ -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));
}
}
137 changes: 137 additions & 0 deletions one-step-kdf/tests/hmac.rs
Original file line number Diff line number Diff line change
@@ -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<Sha224>;
type HmacSha256 = HmacReset<Sha256>;

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::<Sha256>(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);
}