diff --git a/.github/workflows/kmac.yml b/.github/workflows/kmac.yml new file mode 100644 index 0000000..62bab9b --- /dev/null +++ b/.github/workflows/kmac.yml @@ -0,0 +1,60 @@ +name: kmac + +on: + pull_request: + paths: + - ".github/workflows/kmac.yml" + - "kmac/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: kmac + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.85.0 # MSRV + - stable + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v6 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - run: cargo build --no-default-features --target ${{ matrix.target }} + + minimal-versions: + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + working-directory: ${{ github.workflow }} + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.85.0 # MSRV + - stable + steps: + - uses: actions/checkout@v6 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - uses: RustCrypto/actions/cargo-hack-install@master + - run: cargo hack test --feature-powerset + - run: cargo test --release --all-features diff --git a/Cargo.lock b/Cargo.lock index 8eb9cdb..449a0ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "cpubits" version = "0.1.0" @@ -130,6 +136,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "cshake" +version = "0.1.0" +source = "git+https://github.com/RustCrypto/hashes?branch=master#6c69ea988cac53319564ed90cd87671ed50d3579" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "ctutils" version = "0.4.0" @@ -165,11 +180,18 @@ checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "blobby", "block-buffer", + "const-oid", "crypto-common", "ctutils", "zeroize", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-literal" version = "1.1.0" @@ -207,6 +229,26 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "kmac" +version = "0.1.0" +dependencies = [ + "cshake", + "digest", + "hex", + "hex-literal", +] + [[package]] name = "kuznyechik" version = "0.9.0-rc.3" diff --git a/Cargo.toml b/Cargo.toml index e6b334a..2bfc135 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "cbc-mac", "cmac", "hmac", + "kmac", "pmac", "retail-mac", ] diff --git a/kmac/Cargo.toml b/kmac/Cargo.toml new file mode 100644 index 0000000..01af9bd --- /dev/null +++ b/kmac/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "kmac" +version = "0.1.0" +description = "Keccak Message Authentication Code (KMAC)" +authors = ["RustCrypto Developers"] +license = "MIT OR Apache-2.0" +edition = "2024" +readme = "README.md" +documentation = "https://docs.rs/kmac" +repository = "https://github.com/RustCrypto/MACs" +keywords = ["crypto", "mac", "kmac", "digest"] +categories = ["cryptography", "no-std"] +rust-version = "1.85" + +[dependencies] +cshake = { git = "https://github.com/RustCrypto/hashes", branch = "master" } +digest = { version = "0.11.0", features = ["mac"] } + +[dev-dependencies] +digest = { version = "0.11.0", features = ["dev"] } +hex-literal = "1.1.0" +hex = "0.4.3" diff --git a/kmac/README.md b/kmac/README.md new file mode 100644 index 0000000..b143caf --- /dev/null +++ b/kmac/README.md @@ -0,0 +1,122 @@ +# RustCrypto: KMAC + +A rust implementation of [KMAC](https://en.wikipedia.org/wiki/SHA-3#Additional_instances), following the [NIST SP 800-185] specification. + +This crate provides implementations for KMAC128, KMAC256, KMACXOF128, and KMACXOF256. KMAC is a PRF and keyed hash function based on the Keccak (SHA-3) sponge construction, designed for message authentication (MAC) and key derivation (KDF). + +## NIST security guidance + +Security guidance for KMAC is discussed in Section 8.4 of [NIST SP 800-185]. + +KMAC128 is built from cSHAKE128, giving it a security strength <= 128 bits. The `Kmac128` default MAC tag length is the NIST recommended 32 bytes (256 bits). The input key length must also be at least 16 bytes (128 bits) to achieve the full security strength. + +KMAC256 is built from cSHAKE256, giving it a security strength <= 256 bits. The `Kmac256` default MAC tag length is the NIST recommended 64 bytes (512 bits). The input key length must also be at least 32 bytes (256 bits) to achieve the full security strength. + +The below table summarises the equivalence with other MAC algorithms, where `K` is the input key, `text` is the input message, `L` is the output tag length, and `S` is an optional customization string. + +| Existing MAC Algorithm | KMAC Equivalent | +|------------------------|------------------------------| +| `AES-CMAC(K, text)` | `KMAC128(K, text, L=128, S)` | +| `HMAC-SHA256(K, text)` | `KMAC256(K, text, L=256, S)` | +| `HMAC-SHA512(K, text)` | `KMAC256(K, text, L=512, S)` | + +## Examples + +### Generating a MAC +```rust +use kmac::{Kmac128, Mac, KeyInit}; +use hex_literal::hex; + +// Use KMAC128 to produce a MAC +let mut kmac = Kmac128::new_from_slice(b"key material").unwrap(); +kmac.update(b"input message"); + +// `result` has type `CtOutput` which is a thin wrapper around array of +// bytes for providing constant time equality check +let result = kmac.finalize(); + +// To get underlying array use `into_bytes`, but be careful, since +// incorrect use of the code value may permit timing attacks which defeats +// the security provided by the `CtOutput` +let mac_bytes = result.into_bytes(); +let expected = hex!(" + c39a8f614f8821443599440df5402787 + 0f67e4c47919061584f14a616f3efcf5 +"); +assert_eq!(mac_bytes[..], expected[..]); +``` + +### Verifying a MAC +```rust +use kmac::{Kmac128, Mac, KeyInit}; +use hex_literal::hex; + +let mut kmac = Kmac128::new_from_slice(b"key material").unwrap(); +kmac.update(b"input message"); + +let mac_bytes = hex!(" + c39a8f614f8821443599440df5402787 + 0f67e4c47919061584f14a616f3efcf5 +"); + +// `verify_slice` will return `Ok(())` if code is correct, `Err(MacError)` otherwise +kmac.verify_slice(&mac_bytes).unwrap(); +``` + +### Producing a custom-length output + +KMAC can produce an output of any length via `finalize_into_buf`, which is particularly useful when KMAC is being used as a [key-derivation function (KDF)](https://en.wikipedia.org/wiki/Key_derivation_function). + +This method mixes the requested output length into the KMAC domain separation, so the resulting bytes depend on the exact length of `out`. This is distinct from both `finalize()` (which uses the type's default output length) and `finalize_xof()` (KMACXOF, which does not bind output length at all). + +A customisation string can also be provided via `new_customization` to further domain-separate different uses of KMAC with the same key. + +```rust +use kmac::{Kmac256, Mac}; +use hex_literal::hex; + +let mut mac = Kmac256::new_customization(b"key material", b"customization").unwrap(); +mac.update(b"input message"); +let mut output = [0u8; 32]; +mac.finalize_into_buf(&mut output); + +let expected = hex!(" + 85fb77da3a35e4c4b0057c3151e6cc54 + ee401ffe65ec2f0239f439be8896f7b6 +"); +assert_eq!(output[..], expected[..]); +``` + +### KMACXOF: variable-length output + +KMACXOF (defined in Section 4.3.1 of [NIST SP 800-185]) produces an arbitrary-length output via the `ExtendableOutput` trait. Unlike `finalize()` and `finalize_into_buf()`, KMACXOF does not bind the output length into the domain separation — the returned reader yields an effectively infinite stream of bytes, and reading the first `N` bytes produces the same `N`-byte prefix regardless of how many bytes are read in total. + +```rust +use kmac::{Kmac256, Mac, ExtendableOutput, XofReader}; +use hex_literal::hex; + +let mut kmac = Kmac256::new_customization(b"key material", b"customization").unwrap(); +kmac.update(b"input message"); +let mut reader = kmac.finalize_xof(); + +let mut output = [0u8; 32]; +reader.read(&mut output); + +let expected = hex!(" + b675b75668eab0706ab05650f34fa1b6 + 24051a9a42b5e42cfe9970e8f903d45b +"); +assert_eq!(output[..], expected[..]); +``` + +## License + +Licensed under either of: +- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) +- [MIT license](http://opensource.org/licenses/MIT) +at your option. + +### Contribution +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[NIST SP 800-185]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf diff --git a/kmac/benches/mod.rs b/kmac/benches/mod.rs new file mode 100644 index 0000000..20e081d --- /dev/null +++ b/kmac/benches/mod.rs @@ -0,0 +1,22 @@ +#![feature(test)] +extern crate test; + +use core::hint::black_box; +use kmac::{KeyInit, Kmac128, Kmac256}; +use test::Bencher; + +digest::bench_update!( + Kmac128::new(black_box(&Default::default())); + kmac128_update_10 10; + kmac128_update_100 100; + kmac128_update_1000 1000; + kmac128_update_10000 10000; +); + +digest::bench_update!( + Kmac256::new(black_box(&Default::default())); + kmac256_update_10 10; + kmac256_update_100 100; + kmac256_update_1000 1000; + kmac256_update_10000 10000; +); diff --git a/kmac/src/encoding.rs b/kmac/src/encoding.rs new file mode 100644 index 0000000..d3f93b7 --- /dev/null +++ b/kmac/src/encoding.rs @@ -0,0 +1,114 @@ +/// The number of bytes required to write a number in the KMAC encoded format, excluding the +/// leading byte that indicates the length of the encoding. +#[inline(always)] +pub(crate) fn num_encoding_size(num: u64) -> usize { + let bits = 64 - (num | 1).leading_zeros() as usize; + bits.div_ceil(8) +} + +#[inline(always)] +pub(crate) fn left_encode(num: u64, buffer: &mut [u8; 9]) -> &[u8] { + let encoding_size = num_encoding_size(num); + buffer[0] = encoding_size as u8; + buffer[1..=encoding_size].copy_from_slice(&num.to_be_bytes()[8 - encoding_size..]); + &buffer[..=encoding_size] +} + +#[inline(always)] +pub(crate) fn right_encode(num: u64, buffer: &mut [u8; 9]) -> &[u8] { + let encoding_size = num_encoding_size(num); + buffer[0..encoding_size].copy_from_slice(&num.to_be_bytes()[8 - encoding_size..]); + buffer[encoding_size] = encoding_size as u8; + &buffer[..=encoding_size] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_num_encoding_size() { + let test_cases = [ + (0, 1), + (1, 1), + (2, 1), + (3, 1), + (4, 1), + (5, 1), + (6, 1), + (7, 1), + (8, 1), + (9, 1), + (10, 1), + (255, 1), + (256, 2), + (257, 2), + (65535, 2), + (65536, 3), + (65537, 3), + (16777215, 3), + (16777216, 4), + (16777217, 4), + ]; + + for &(num, expected_size) in &test_cases { + assert_eq!( + num_encoding_size(num), + expected_size, + "num_encoding_size({}) should return {}", + num, + expected_size + ); + } + } + + #[test] + fn test_left_encoding() { + let mut buf = [0u8; 9]; + + assert_eq!(left_encode(0, &mut buf), &[1, 0]); + assert_eq!(left_encode(1, &mut buf), &[1, 1]); + assert_eq!(left_encode(8, &mut buf), &[1, 8]); + assert_eq!(left_encode(256, &mut buf), &[2, 1, 0]); + + for i in 0..usize::BITS { + let x: usize = 1 << i; + let be_bytes = x.to_be_bytes(); + let skip = be_bytes + .iter() + .position(|&v| v != 0) + .unwrap_or(be_bytes.len() - 1); + let len = be_bytes.len() - skip; + let mut want = [0u8; 9]; + want[0] = len as u8; + want[1..=len].copy_from_slice(&be_bytes[skip..]); + let total_len = len + 1; + assert_eq!(left_encode(x as u64, &mut buf), &want[..total_len], "#{x}"); + } + } + + #[test] + fn test_right_encoding() { + let mut buf = [0u8; 9]; + + assert_eq!(right_encode(0, &mut buf), &[0, 1]); + assert_eq!(right_encode(1, &mut buf), &[1, 1]); + assert_eq!(right_encode(8, &mut buf), &[8, 1]); + assert_eq!(right_encode(256, &mut buf), &[1, 0, 2]); + + for i in 0..usize::BITS { + let x: usize = 1 << i; + let be_bytes = x.to_be_bytes(); + let skip = be_bytes + .iter() + .position(|&v| v != 0) + .unwrap_or(be_bytes.len() - 1); + let len = be_bytes.len() - skip; + let mut want = [0u8; 9]; + want[..len].copy_from_slice(&be_bytes[skip..]); + want[len] = len as u8; + let total_len = len + 1; + assert_eq!(right_encode(x as u64, &mut buf), &want[..total_len], "#{x}"); + } + } +} diff --git a/kmac/src/lib.rs b/kmac/src/lib.rs new file mode 100644 index 0000000..fc049ba --- /dev/null +++ b/kmac/src/lib.rs @@ -0,0 +1,237 @@ +//! [NIST SP 800-185]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf + +#![no_std] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/26acc39f/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/26acc39f/logo.svg" +)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +mod encoding; + +use crate::encoding::{left_encode, right_encode}; +use cshake::{CShake, CShakeReader}; +use digest::block_buffer::BlockSizes; +use digest::consts::{U136, U168}; +pub use digest::{self, ExtendableOutput, FixedOutput, KeyInit, Mac, XofReader}; +use digest::{InvalidLength, MacMarker, Output, OutputSizeUser, Update}; + +mod sealed { + use digest::array::ArraySize; + use digest::consts::{U32, U64, U136, U168}; + + pub trait KmacParams { + type OutputSize: ArraySize; + } + + impl KmacParams for U168 { + type OutputSize = U32; + } + + impl KmacParams for U136 { + type OutputSize = U64; + } +} + +/// KMAC implementation as per Section 4 of [NIST SP 800-185]. +/// +/// [NIST SP 800-185]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +#[derive(Clone)] +pub struct Kmac { + cshake: CShake, +} + +/// KMAC128: KMAC with 128-bit security strength, as defined in Section 4 of +/// [NIST SP 800-185]. +/// +/// Produces a 32-byte (256-bit) fixed-length output by default via [`Mac::finalize`]. +/// For a custom output length where the length is mixed into the domain separation, +/// use [`Kmac::finalize_into_buf`]. For KMACXOF128 (arbitrary-length XOF output), use +/// [`ExtendableOutput::finalize_xof`]. +/// +/// # Example +/// ``` +/// use kmac::{Kmac128, Mac, KeyInit}; +/// +/// let mut mac = Kmac128::new_from_slice(b"my secret key").unwrap(); +/// mac.update(b"input message"); +/// let result = mac.finalize(); +/// let tag = result.into_bytes(); +/// ``` +/// +/// [NIST SP 800-185]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +pub type Kmac128 = Kmac; + +/// KMAC256: KMAC with 256-bit security strength, as defined in Section 4 of +/// [NIST SP 800-185]. +/// +/// Produces a 64-byte (512-bit) fixed-length output by default via [`Mac::finalize`]. +/// For a custom output length where the length is mixed into the domain separation, +/// use [`Kmac::finalize_into_buf`]. For KMACXOF256 (arbitrary-length XOF output), use +/// [`ExtendableOutput::finalize_xof`]. +/// +/// # Example +/// ``` +/// use kmac::{Kmac256, Mac, KeyInit}; +/// +/// let mut mac = Kmac256::new_from_slice(b"my secret key").unwrap(); +/// mac.update(b"input message"); +/// let result = mac.finalize(); +/// let tag = result.into_bytes(); +/// ``` +/// +/// [NIST SP 800-185]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +pub type Kmac256 = Kmac; + +/// KMACXOF128 reader, returned by calling [`ExtendableOutput::finalize_xof`] on [`Kmac128`]. +/// +/// Implements [`XofReader`] to produce an arbitrary-length output stream (KMACXOF128). +pub type Kmac128Reader = CShakeReader; + +/// KMACXOF256 reader, returned by calling [`ExtendableOutput::finalize_xof`] on [`Kmac256`]. +/// +/// Implements [`XofReader`] to produce an arbitrary-length output stream (KMACXOF256). +pub type Kmac256Reader = CShakeReader; + +impl MacMarker for Kmac {} + +impl OutputSizeUser for Kmac { + type OutputSize = ::OutputSize; +} + +impl digest::common::KeySizeUser for Kmac { + type KeySize = Rate; +} + +impl KeyInit for Kmac { + #[inline] + fn new(key: &digest::Key) -> Self { + Self::new_customization_inner(key.as_slice(), &[]) + } + + #[inline(always)] + fn new_from_slice(key: &[u8]) -> Result { + Ok(Self::new_customization_inner(key, &[])) + } +} + +impl Update for Kmac { + #[inline(always)] + fn update(&mut self, data: &[u8]) { + self.cshake.update(data); + } +} + +impl FixedOutput for Kmac { + #[inline(always)] + fn finalize_into(self, out: &mut Output) { + self.finalize_fixed_inner(out.as_mut_slice()); + } +} + +impl ExtendableOutput for Kmac { + type Reader = CShakeReader; + + // Finalize as KMACXOF, a variable-length (extendable) output stream, as defined in + // Section 4.3.1 (KMAC with Arbitrary-Length Output) of [NIST SP 800-185]. + #[inline(always)] + fn finalize_xof(self) -> Self::Reader { + self.finalize_xof_inner() + } +} + +impl Kmac { + /// Create a new KMAC with the given key and customisation. + /// + /// Section 4.2 of [NIST SP 800-185] specifies that KMAC takes both a key (K) and an + /// optional customisation string (S). + #[inline] + pub fn new_customization(key: &[u8], customisation: &[u8]) -> Result { + Ok(Self::new_customization_inner(key, customisation)) + } + + /// Finalize this KMAC into a fixed-length output buffer, as defined in Section 4.3 + /// (Definition) of [NIST SP 800-185]. + /// + /// This method finalizes the KMAC and *mixes the requested output length into the + /// KMAC domain separation*. That means the resulting bytes are dependent on the + /// exact length of `out`. Use this when the output length is part of the MAC/derivation + /// semantics (for example, when the length itself must influence the MAC result). + /// + /// This is *not* equivalent to calling `finalize_xof()` and then reading `out.len()` + /// bytes from the returned reader; the two approaches produce different outputs. + /// + /// # Example + /// ``` + /// use kmac::{Kmac256, Mac}; + /// + /// let mut mac = Kmac256::new_customization(b"my key", b"my customization").unwrap(); + /// mac.update(b"input message"); + /// let mut output = [0u8; 48]; + /// mac.finalize_into_buf(&mut output); + /// ``` + #[inline] + pub fn finalize_into_buf(self, out: &mut [u8]) { + self.finalize_fixed_inner(out); + } + + #[inline(always)] + fn new_customization_inner(key: &[u8], customisation: &[u8]) -> Self { + let mut cshake = CShake::::new_with_function_name(b"KMAC", customisation); + let block_size = Rate::USIZE; + let mut encode_buffer = [0u8; 9]; + + // bytepad: left_encode(w) + let le_w = left_encode(block_size as u64, &mut encode_buffer); + let mut total = le_w.len(); + cshake.update(le_w); + + // encode_string(K): left_encode(8*len(K)) || K + let le_k = left_encode(8 * key.len() as u64, &mut encode_buffer); + total += le_k.len(); + cshake.update(le_k); + + total += key.len(); + cshake.update(key); + + // pad to block boundary + let pad_len = (block_size - (total % block_size)) % block_size; + if pad_len > 0 { + let zeros = [0u8; 168]; // max block size + let mut remaining = pad_len; + while remaining > 0 { + let chunk = core::cmp::min(remaining, zeros.len()); + cshake.update(&zeros[..chunk]); + remaining -= chunk; + } + } + + Self { cshake } + } + + /// Finalizes the KMAC for any output array size (fixed-length output). + #[inline(always)] + fn finalize_fixed_inner(mut self, out: &mut [u8]) { + // right_encode(L), where L = output length in bits + let mut encode_buffer = [0u8; 9]; + let re = right_encode(8 * out.len() as u64, &mut encode_buffer); + self.cshake.update(re); + + let mut reader = self.cshake.finalize_xof(); + reader.read(out); + } + + /// Finalizes the KMAC for extendable output (XOF). + #[inline(always)] + fn finalize_xof_inner(mut self) -> CShakeReader { + // right_encode(0), as L = 0 for extendable output + let mut encode_buffer = [0u8; 9]; + let re = right_encode(0, &mut encode_buffer); + self.cshake.update(re); + + self.cshake.finalize_xof() + } +} diff --git a/kmac/tests/kmac.rs b/kmac/tests/kmac.rs new file mode 100644 index 0000000..73fcefe --- /dev/null +++ b/kmac/tests/kmac.rs @@ -0,0 +1,162 @@ +use hex_literal::hex; +use kmac::{ExtendableOutput, KeyInit, Kmac128, Kmac256, Mac, XofReader}; + +fn run_kmac128() -> Kmac128 { + let mut mac = Kmac128::new_customization(b"my secret key", b"S") + .expect("Failed to create a KMAC128 instance from key"); + mac.update(b"my message"); + mac +} + +fn run_kmac256() -> Kmac256 { + let mut mac = Kmac256::new_customization(b"my secret key", b"S") + .expect("Failed to create a KMAC256 instance from key"); + mac.update(b"my message"); + mac +} + +#[test] +fn test_kmac128() { + let out_default = run_kmac128().finalize(); + assert_eq!( + out_default.as_bytes().as_slice(), + hex!("f875fb68694ac0ab29775a918901f3a81c8b5e1771b024c20a0928d1c1a7b5fe"), + "Expected hex output is {}", + hex::encode(out_default.as_bytes().as_slice()) + ); + + // confirm finalize_into_buf works the same way + let mut out_into = [0u8; 32]; + run_kmac128().finalize_into_buf(&mut out_into); + assert_eq!(out_default.as_bytes().as_slice(), &out_into); + + // confirm finalize_into_buf does not compute subsets + let mut out_into_subset = [0u8; 16]; + run_kmac128().finalize_into_buf(&mut out_into_subset); + assert_ne!(&out_into_subset, &out_into[..16]); + + // confirm xof is different + let mut reader_xof = run_kmac128().finalize_xof(); + let mut out_xof = [0u8; 32]; + reader_xof.read(&mut out_xof); + assert_ne!(out_xof, out_default.as_bytes().as_slice()); + assert_eq!( + &out_xof, + &hex!("47381a6f7b0f78a624fa8f50743fce59716053a957c8e90bca915ac46c185267"), + "Expected hex output is {}", + hex::encode(out_xof) + ); + + // confirm xof is subset + let mut reader_xof_subset = run_kmac128().finalize_xof(); + let mut out_xof_subset = [0u8; 16]; + reader_xof_subset.read(&mut out_xof_subset); + assert_eq!(&out_xof[..16], &out_xof_subset); +} + +#[test] +fn test_kmac256() { + let out_default = run_kmac256().finalize(); + assert_eq!( + out_default.as_bytes().as_slice(), + hex!( + "9eaffe657c105dc6b036f94ea770ce9fe537e1a847e41cdec394fff1c4ac253c" + "87439b862b3dd7f38037e3a9af160e84ae3f453c322958940b29095a00578f83" + ), + "Expected hex output is {}", + hex::encode(out_default.as_bytes().as_slice()) + ); + + // confirm finalize_into_buf works the same way + let mut out_into = [0u8; 64]; + run_kmac256().finalize_into_buf(&mut out_into); + assert_eq!(out_default.as_bytes().as_slice(), &out_into); + + // confirm finalize_into_buf does not compute subsets + let mut out_into_subset = [0u8; 32]; + run_kmac256().finalize_into_buf(&mut out_into_subset); + assert_ne!(&out_into_subset, &out_into[..32]); + + // confirm xof is different + let mut reader_xof = run_kmac256().finalize_xof(); + let mut out_xof = [0u8; 64]; + reader_xof.read(&mut out_xof); + assert_ne!(out_xof, out_default.as_bytes().as_slice()); + assert_eq!( + &out_xof, + &hex!( + "25556b2b74cc9163a196ae6ecef0812c4087345314fa65a663bd813dccd2c596" + "112b63da9f57559bf0c57361d191e4ec56688fc2bf45e2ceade0e2190a0dc3fc" + ), + "Expected hex output is {}", + hex::encode(out_xof) + ); + + // confirm xof is subset + let mut reader_xof_subset = run_kmac256().finalize_xof(); + let mut out_xof_subset = [0u8; 32]; + reader_xof_subset.read(&mut out_xof_subset); + assert_eq!(&out_xof[..32], &out_xof_subset); +} + +#[test] +fn test_readme_example_verify() { + let mut mac = Kmac128::new_from_slice(b"key material").unwrap(); + mac.update(b"input message"); + let result = mac.finalize(); + let code_bytes = result.into_bytes(); + let expected = hex!( + "c39a8f614f8821443599440df5402787" + "0f67e4c47919061584f14a616f3efcf5" + ); + assert_eq!( + code_bytes[..], + expected[..], + "Expected hex output is {}", + hex::encode(code_bytes) + ); + + let mut mac = Kmac128::new_from_slice(b"key material").unwrap(); + mac.update(b"input message"); + mac.verify_slice(&expected).unwrap(); +} + +#[test] +fn test_readme_example_into() { + let mut mac = Kmac256::new_customization(b"key material", b"customization").unwrap(); + mac.update(b"input message"); + let mut output = [0u8; 32]; + mac.finalize_into_buf(&mut output); + + let expected = hex!( + "85fb77da3a35e4c4b0057c3151e6cc54" + "ee401ffe65ec2f0239f439be8896f7b6" + ); + assert_eq!( + output[..], + expected[..], + "Expected hex output is {}", + hex::encode(output) + ); +} + +#[test] +fn test_readme_example_xof() { + let mut mac = Kmac256::new_customization(b"key material", b"customization").unwrap(); + mac.update(b"input message"); + let mut reader = mac.finalize_xof(); + + let mut output = [0u8; 32]; + reader.read(&mut output); + + let expected = hex!( + "b675b75668eab0706ab05650f34fa1b6" + "24051a9a42b5e42cfe9970e8f903d45b" + ); + assert_eq!( + output[..], + expected[..], + "Expected hex output is {}", + hex::encode(output) + ); +} diff --git a/kmac/tests/nist.rs b/kmac/tests/nist.rs new file mode 100644 index 0000000..cdb20ed --- /dev/null +++ b/kmac/tests/nist.rs @@ -0,0 +1,223 @@ +//! KMAC and KMACXOF test vectors, sourced from NIST SP 800-185 in: +//! - https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/KMAC_samples.pdf +//! - https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/KMACXOF_samples.pdf + +use hex_literal::hex; +use kmac::{ExtendableOutput, Kmac128, Kmac256, Mac, XofReader}; + +struct NistVector { + key: &'static [u8], + data: &'static [u8], + customization: &'static [u8], + output: &'static [u8], +} + +// These same key and data fields are used throughout the test vectors. +const KEY: [u8; 32] = hex!( + "404142434445464748494A4B4C4D4E4F" + "505152535455565758595A5B5C5D5E5F" +); +const DATA_SHORT: [u8; 4] = hex!("00010203"); +const DATA_LONG: [u8; 200] = hex!( + "000102030405060708090A0B0C0D0E0F" + "101112131415161718191A1B1C1D1E1F" + "202122232425262728292A2B2C2D2E2F" + "303132333435363738393A3B3C3D3E3F" + "404142434445464748494A4B4C4D4E4F" + "505152535455565758595A5B5C5D5E5F" + "606162636465666768696A6B6C6D6E6F" + "707172737475767778797A7B7C7D7E7F" + "808182838485868788898A8B8C8D8E8F" + "909192939495969798999A9B9C9D9E9F" + "A0A1A2A3A4A5A6A7A8A9AAABACADAEAF" + "B0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF" + "C0C1C2C3C4C5C6C7" +); + +#[test] +fn test_kmac128() { + let vectors: &[NistVector] = &[ + // KMAC Sample #1 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: &[], + output: &hex!( + "E5780B0D3EA6F7D3A429C5706AA43A00" + "FADBD7D49628839E3187243F456EE14E" + ), + }, + // KMAC Sample #2 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: b"My Tagged Application", + output: &hex!( + "3B1FBA963CD8B0B59E8C1A6D71888B71" + "43651AF8BA0A7070C0979E2811324AA5" + ), + }, + // KMAC Sample #3 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: b"My Tagged Application", + output: &hex!( + "1F5B4E6CCA02209E0DCB5CA635B89A15" + "E271ECC760071DFD805FAA38F9729230" + ), + }, + ]; + for (i, v) in vectors.iter().enumerate() { + let mut hash = Kmac128::new_customization(v.key, v.customization).unwrap(); + hash.update(v.data); + let result = hash.finalize(); + assert_eq!(result.as_bytes().as_slice().len(), v.output.len(), "#{i}"); + assert_eq!(result.as_bytes().as_slice(), v.output, "#{i}"); + } +} + +#[test] +fn test_kmacxof128() { + let vectors: &[NistVector] = &[ + // KMACXOF Sample #1 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: &[], + output: &hex!( + "CD83740BBD92CCC8CF032B1481A0F446" + "0E7CA9DD12B08A0C4031178BACD6EC35" + ), + }, + // KMACXOF Sample #2 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: b"My Tagged Application", + output: &hex!( + "31A44527B4ED9F5C6101D11DE6D26F06" + "20AA5C341DEF41299657FE9DF1A3B16C"), + }, + // KMACXOF Sample #3 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: b"My Tagged Application", + output: &hex!( + "47026C7CD793084AA0283C253EF65849" + "0C0DB61438B8326FE9BDDF281B83AE0F"), + }, + ]; + + for (i, v) in vectors.iter().enumerate() { + let mut hash = Kmac128::new_customization(v.key, v.customization).unwrap(); + hash.update(v.data); + let mut reader = hash.finalize_xof(); + let mut result = [0u8; 32]; + reader.read(&mut result); + assert_eq!(result.as_slice().len(), v.output.len(), "#{i}"); + assert_eq!(result.as_slice(), v.output, "#{i}"); + } +} + +#[test] +fn test_kmac256() { + let vectors: &[NistVector] = &[ + // KMAC Sample #4 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: b"My Tagged Application", + output: &hex!( + "20C570C31346F703C9AC36C61C03CB64" + "C3970D0CFC787E9B79599D273A68D2F7" + "F69D4CC3DE9D104A351689F27CF6F595" + "1F0103F33F4F24871024D9C27773A8DD" + ), + }, + // KMAC Sample #5 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: &[], + output: &hex!( + "75358CF39E41494E949707927CEE0AF2" + "0A3FF553904C86B08F21CC414BCFD691" + "589D27CF5E15369CBBFF8B9A4C2EB178" + "00855D0235FF635DA82533EC6B759B69" + ), + }, + // KMAC Sample #6 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: b"My Tagged Application", + output: &hex!( + "B58618F71F92E1D56C1B8C55DDD7CD18" + "8B97B4CA4D99831EB2699A837DA2E4D9" + "70FBACFDE50033AEA585F1A2708510C3" + "2D07880801BD182898FE476876FC8965" + ), + }, + ]; + for (i, v) in vectors.iter().enumerate() { + let mut hash = Kmac256::new_customization(v.key, v.customization).unwrap(); + hash.update(v.data); + let result = hash.finalize(); + assert_eq!(result.as_bytes().as_slice().len(), v.output.len(), "#{i}"); + assert_eq!(result.as_bytes().as_slice(), v.output, "#{i}"); + } +} + +#[test] +fn test_kmacxof256() { + let vectors: &[NistVector] = &[ + // KMACXOF Sample #4 + NistVector { + key: &KEY, + data: &DATA_SHORT, + customization: b"My Tagged Application", + output: &hex!( + "1755133F1534752AAD0748F2C706FB5C" + "784512CAB835CD15676B16C0C6647FA9" + "6FAA7AF634A0BF8FF6DF39374FA00FAD" + "9A39E322A7C92065A64EB1FB0801EB2B" + ), + }, + // KMACXOF Sample #5 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: &[], + output: &hex!( + "FF7B171F1E8A2B24683EED37830EE797" + "538BA8DC563F6DA1E667391A75EDC02C" + "A633079F81CE12A25F45615EC8997203" + "1D18337331D24CEB8F8CA8E6A19FD98B" + ), + }, + // KMACXOF Sample #6 + NistVector { + key: &KEY, + data: &DATA_LONG, + customization: b"My Tagged Application", + output: &hex!( + "D5BE731C954ED7732846BB59DBE3A8E3" + "0F83E77A4BFF4459F2F1C2B4ECEBB8CE" + "67BA01C62E8AB8578D2D499BD1BB2767" + "68781190020A306A97DE281DCC30305D" + ), + }, + ]; + + for (i, v) in vectors.iter().enumerate() { + let mut hash = Kmac256::new_customization(v.key, v.customization).unwrap(); + hash.update(v.data); + let mut reader = hash.finalize_xof(); + let mut result = [0u8; 64]; + reader.read(&mut result); + assert_eq!(result.as_slice().len(), v.output.len(), "#{i}"); + assert_eq!(result.as_slice(), v.output, "#{i}"); + } +}