diff --git a/.github/workflows/aes-ct-asm.yml b/.github/workflows/aes-ct-asm.yml new file mode 100644 index 000000000..88a6df8db --- /dev/null +++ b/.github/workflows/aes-ct-asm.yml @@ -0,0 +1,26 @@ +name: AES Constant-Time Assembly + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: aes-ct-asm-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + aes-ct-asm: + if: github.repository == 'bcgit/bc-rust' + name: aes-ct-asm + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install nightly with rust-src + run: rustup toolchain install nightly --profile minimal --component rust-src + + - name: Check the round-key widening assembly on Cortex-M0 and RISC-V 32I + run: ./dev_scripts/check_aes_ct_asm.sh diff --git a/.gitignore b/.gitignore index bdc86bd8b..41d845b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ custom_mutants_output/ !.claude/agents/ .claude/settings.local.json .claude 2/ +dev_scripts/aes_ct_harness/Cargo.lock diff --git a/CLAUDE.md b/CLAUDE.md index a5583b749..b19324e89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,8 @@ Revisit this section at the first non-alpha release. - Builds on Rust **stable**: there is no toolchain pin, and no crate enables a `#![feature(...)]` gate, so nightly-only tooling (`-Z` flags and the like) is not available. CI builds, tests and docs on stable; only the - `rustfmt` job installs nightly. + `rustfmt` job and the AES assembly check (`-Zbuild-std=core` for targets without a shipped `core`) install + nightly. - 2024 edition (set workspace-wide in the root `Cargo.toml`). - Minimum Rust is 1.88: `rust-version` in the root `Cargo.toml`, inherited by `bouncycastle-utils` (the crate that needs it, for `slice::as_chunks`) and so enforced for every crate that depends on it. @@ -326,12 +327,15 @@ external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING ## CI -Five workflows under `.github/workflows/`, all triggered by pull requests: +Six workflows under `.github/workflows/`, all triggered by pull requests: - `rust-build.yml`: `cargo build --workspace --all-targets --all-features` on stable. - `rust-test.yml`: `cargo test --all` on stable. - `rust-style.yml`: `cargo fmt --all --check` on **nightly** rustfmt, so format before every commit. - `rust-docs.yml`: `cargo doc --all` on stable. +- `aes-ct-asm.yml`: `dev_scripts/check_aes_ct_asm.sh` on **nightly** with `rust-src`: builds + `dev_scripts/aes_ct_harness` for thumbv6m and riscv32i and greps the assembly of the AES round-key + widening for a multiply, call or branch (see the comment on `PlaneWord::splat` in the aes crate). - `publish_doc_benches_to_ghpages.yaml`: builds rustdoc and runs `quality_stats.sh`; on `main` it also publishes docs and code stats to GitHub Pages (`https://bcgit.github.io/bc-rust/`). Its bench step is commented out because the benches are too slow on the hosted runner, so benchmark figures come from a diff --git a/README.md b/README.md index ddc49928a..1830e39ae 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,20 @@ You can build the main library and the `bc-rust` command-line utility with: cargo build -p cli ``` +The utility exposes every primitive as a streaming stdin-to-stdout subcommand; `bc-rust --help` +lists them and `bc-rust --help` the flags. For example: + +``` +echo -n abc | target/debug/bc-rust sha256 -x +target/debug/bc-rust rng --len 32 > key.bin +target/debug/bc-rust aes256-gcm -d encrypt --key-file key.bin < plain > cipher +target/debug/bc-rust aes256-gcm -d decrypt --key-file key.bin < cipher +``` + +Values given inline (`--key`, `--iv`, ...) are hex; files are raw bytes, or hex text with the global +`--hex-input` flag. The shell tests under `cli/tests/` (`cli/tests/test_all.sh`) show every +subcommand in use. + Or you can build a single sub-crate by name, for example: ``` diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs index d2592dc08..cc2af0bcc 100644 --- a/cli/src/aes_ccm_cmd.rs +++ b/cli/src/aes_ccm_cmd.rs @@ -45,7 +45,6 @@ //! //! The output layout is Sec 6.1 step 8's own: `ciphertext || tag`. -use std::io::{self, Read}; use std::process::exit; use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; @@ -55,7 +54,6 @@ use bouncycastle::core::key_material::KeyMaterial; use bouncycastle::core::symmetric_key::{ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, FromKeyMaterial, }; -use bouncycastle::hex; use crate::helpers; use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; @@ -89,6 +87,7 @@ impl CcmKeys<32> for Aes256 { } /// AES-128 CCM. See the module docs and the subcommand help. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes128_ccm_cmd( action: &CipherDirection, key: &Option, @@ -113,6 +112,7 @@ pub(crate) fn aes128_ccm_cmd( } /// AES-192 CCM. See [`aes128_ccm_cmd`]. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes192_ccm_cmd( action: &CipherDirection, key: &Option, @@ -137,6 +137,7 @@ pub(crate) fn aes192_ccm_cmd( } /// AES-256 CCM. See [`aes128_ccm_cmd`]. +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes256_ccm_cmd( action: &CipherDirection, key: &Option, @@ -165,10 +166,10 @@ pub(crate) fn aes256_ccm_cmd( /// Unlike the key there is no entropy question here: Sec 5.3 asks for uniqueness, not randomness, /// so an all-zero nonce is a perfectly valid *first* nonce and only a repeat is a problem. /// -/// `--nonce-file` reads raw bytes ([`helpers::read_from_file_raw`]), not the hex-or-raw guess -/// [`helpers::read_from_file`] uses for keys: a repeated nonce under one key is fatal for CCM (see -/// the module docs), so two distinct binary nonce files that happen to look like hex text of the -/// same value must not silently collapse to the same nonce. +/// `--nonce-file` reads raw bytes (hex only under the global `--hex-input`), never a guess from +/// the contents: a repeated nonce under one key is fatal for CCM (see the module docs), so two +/// distinct binary nonce files that happen to look like hex text of the same value must not +/// silently collapse to the same nonce. /// /// For the same reason a trailing newline is **not** stripped: a 13-byte file ending in `0x0a` and /// the 12-byte file without it are two different nonces, and silently treating them as one would @@ -178,8 +179,8 @@ pub(crate) fn aes256_ccm_cmd( /// is not a silent one; the bytes are still used exactly as they are. fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { let bytes = if let Some(file) = nonce_file { - let bytes = helpers::read_from_file_raw(file); - if bytes.last() == Some(&b'\n') { + let bytes = helpers::read_input_file(file); + if !helpers::hex_input() && bytes.last() == Some(&b'\n') { eprintln!( "Warning: nonce file '{file}' ends with a newline byte (0x0a), which is used as \ part of the nonce." @@ -191,10 +192,7 @@ fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { } bytes } else if let Some(v) = nonce { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: nonce is not valid hex."); - exit(-1) - }) + helpers::decode_hex_arg(v, "nonce") } else { eprintln!("Error: --nonce or --nonce-file must be supplied. CCM has no generated nonce;"); eprintln!(" see the subcommand help for why, and for the uniqueness requirement."); @@ -215,31 +213,20 @@ fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { /// Loads the AAD from `--aad-file` (raw bytes, never hex-decoded) or `--aad` (hex); the file wins /// if both are given, as for `aes*-gcm`. Empty if neither is given. fn load_aad(aad: &Option, aad_file: &Option) -> Vec { - if let Some(path) = aad_file { - helpers::read_from_file_raw(path) - } else if let Some(v) = aad { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: associated data is not valid hex. Use --aad-file for raw bytes."); - exit(-1) - }) - } else { - Vec::new() + match (aad_file, aad) { + (Some(path), _) => helpers::read_input_file(path), + (None, Some(v)) => helpers::decode_hex_arg(v, "aad"), + (None, None) => Vec::new(), } } -/// Reads all of stdin. See the module docs on why this is not a streaming command. -fn read_all_stdin() -> Vec { - let mut input = Vec::new(); - io::stdin().read_to_end(&mut input).expect("Failed to read from stdin"); - input -} - /// Turns the runtime nonce and tag lengths into the mode's const generic parameters. /// /// `NONCE_LEN` and `TAG_LEN` are const parameters of the key type -- that is what makes A.1's /// length conditions compile-time checks rather than runtime ones -- so a command-line value has to /// be matched into one of the permitted instantiations. The two nested matches are the price of /// that, and they are exhaustive over A.1's sets: 7 nonce lengths x 7 tag lengths. +#[allow(clippy::too_many_arguments)] // the three key lengths' inputs, already parsed fn run, const KEY_LEN: usize>( action: &CipherDirection, key: &KeyMaterial, @@ -262,7 +249,8 @@ fn run, const KEY_LEN: usize>( let nonce_bytes = load_nonce(nonce, nonce_file); let aad = load_aad(aad, aad_file); - let input = read_all_stdin(); + // See the module docs on why this is not a streaming command. + let input = helpers::read_stdin_all(); let encrypt = matches!(action, CipherDirection::Encrypt); macro_rules! with_tag_len { diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs index 5575c8d6d..0f6c7845c 100644 --- a/cli/src/aes_gcm_cmd.rs +++ b/cli/src/aes_gcm_cmd.rs @@ -19,6 +19,7 @@ use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; use bouncycastle::cipher::modes::GCM; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes128_gcm_cmd( action: &CipherDirection, key: &Option, @@ -38,6 +39,7 @@ pub(crate) fn aes128_gcm_cmd( ); } +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes192_gcm_cmd( action: &CipherDirection, key: &Option, @@ -57,6 +59,7 @@ pub(crate) fn aes192_gcm_cmd( ); } +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aes256_gcm_cmd( action: &CipherDirection, key: &Option, diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs index a51ba01b5..e035d3511 100644 --- a/cli/src/ascon_cmd.rs +++ b/cli/src/ascon_cmd.rs @@ -1,3 +1,6 @@ +//! The Ascon subcommands (NIST SP 800-232): `ascon-hash256`, `ascon-xof128`, `ascon-cxof128` +//! and `ascon-aead128`. + use std::io::{self, Read}; use std::process::exit; @@ -13,54 +16,25 @@ use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey, Fr use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle::hex; use crate::helpers; - -/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied. -fn load_bytes(value: &Option, value_file: &Option, label: &str) -> Vec { - if let Some(file) = value_file { - helpers::read_from_file(file) - } else if let Some(v) = value { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: {label} is not valid hex."); - exit(-1) - }) - } else { - eprintln!("Error: {label} must be supplied."); - exit(-1) - } -} - -fn load_optional_bytes( - value: &Option, - value_file: &Option, - label: &str, -) -> Option> { - if let Some(file) = value_file { - Some(helpers::read_from_file(file)) - } else { - value.as_ref().map(|v| { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: {label} is not valid hex."); - exit(-1) - }) - }) +use crate::helpers::{CHUNK_LEN, decode_hex_arg, load_secret, read_input_file, read_stdin_chunk}; + +/// Build the AEAD key from `--key-file` or `--key`, warning (and forcing usable metadata on the +/// material) only if the key turns out to be low-entropy (e.g. all-zero), the same way +/// `helpers::parse_seed` does. +fn load_aead_key(key: &Option, key_file: &Option) -> AsconAEAD128Key { + let secret = load_secret(key, key_file, "key"); + if secret.key_len() != 16 { + eprintln!("Error: the key must be exactly 16 bytes, got {}.", secret.key_len()); + exit(-1); } -} - -fn require_16(bytes: Vec, label: &str) -> [u8; 16] { - bytes.try_into().unwrap_or_else(|_: Vec| { - eprintln!("Error: {label} must be exactly 16 bytes."); - exit(-1) - }) -} - -/// Build the AEAD key, warning (and forcing usable metadata on the material) only if the key -/// turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. -fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { let mut key = - KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap(); + KeyMaterial::<16>::from_bytes_as_type(secret.ref_to_bytes(), KeyType::SymmetricCipherKey) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit { eprintln!( "Warning: low entropy key provided. We'll still process it, but it may be insecure." @@ -69,7 +43,10 @@ fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { k.set_key_type(KeyType::SymmetricCipherKey)?; k.set_security_strength(SecurityStrength::_128bit) }) - .unwrap(); + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the key: {e:?}"); + exit(-1); + }); } AsconAEAD128Key::from_key_material(&key).unwrap_or_else(|e| { eprintln!("Error: couldn't load the key: {e:?}"); @@ -77,6 +54,19 @@ fn load_aead_key(key_bytes: &[u8; 16]) -> AsconAEAD128Key { }) } +/// The 16-byte nonce from `--nonce-file` or `--nonce` (hex), or none. +fn load_nonce(nonce: &Option, nonce_file: &Option) -> Option<[u8; 16]> { + let bytes = match (nonce_file, nonce) { + (Some(file), _) => read_input_file(file), + (None, Some(v)) => decode_hex_arg(v, "nonce"), + (None, None) => return None, + }; + Some(bytes.try_into().unwrap_or_else(|bytes: Vec| { + eprintln!("Error: the nonce must be exactly 16 bytes, got {}.", bytes.len()); + exit(-1) + })) +} + /// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. pub(crate) fn hash256_cmd(output_hex: bool) { helpers::stream_hash(AsconHash256::new(), output_hex); @@ -87,17 +77,10 @@ pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) { helpers::stream_xof(AsconXof128::new(), output_len, output_hex); } -/// Ascon-CXOF128 of stdin with a hex customization string, producing `output_len` bytes. -pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, output_hex: bool) { - let z = match customization { - Some(v) => hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: customization is not valid hex."); - exit(-1) - }), - None => Vec::new(), - }; - let x = AsconCXof128::with_customization(&z).unwrap_or_else(|_| { - eprintln!("Error: customization string exceeds 256 bytes."); +/// Ascon-CXOF128 of stdin with a customization string, producing `output_len` bytes. +pub(crate) fn cxof128_cmd(customization: &[u8], output_len: usize, output_hex: bool) { + let x = AsconCXof128::with_customization(customization).unwrap_or_else(|_| { + eprintln!("Error: the customization string exceeds 256 bytes."); exit(-1) }); helpers::stream_xof(x, output_len, output_hex); @@ -117,31 +100,30 @@ pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, out /// ciphertext eagerly, before the tag is known; note that in the decryption direction, plaintext /// is likewise emitted before the tag has been checked, so it should not be treated as /// authentic until this command exits with status 0 (see the crate's "Security Considerations"). +#[allow(clippy::too_many_arguments)] // one flag per input, each inline or from a file pub(crate) fn aead128_cmd( key: &Option, key_file: &Option, nonce: &Option, nonce_file: &Option, - ad: &Option, + aad: &Option, + aad_file: &Option, decrypt: bool, output_hex: bool, ) { - let key = load_aead_key(&require_16(load_bytes(key, key_file, "key"), "key")); - let nonce = - load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce")); - let ad_bytes = match ad { - Some(v) => hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: associated data is not valid hex."); - exit(-1) - }), - None => Vec::new(), + let key = load_aead_key(key, key_file); + let nonce = load_nonce(nonce, nonce_file); + let aad_bytes = match (aad_file, aad) { + (Some(file), _) => read_input_file(file), + (None, Some(v)) => decode_hex_arg(v, "aad"), + (None, None) => Vec::new(), }; - let ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) }; + let aad_opt = if aad_bytes.is_empty() { None } else { Some(aad_bytes.as_slice()) }; if decrypt { - aead128_decrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + aead128_decrypt_stream(&key, nonce.as_ref(), aad_opt, output_hex); } else { - aead128_encrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + aead128_encrypt_stream(&key, nonce.as_ref(), aad_opt, output_hex); } } @@ -152,7 +134,7 @@ pub(crate) fn aead128_cmd( fn aead128_encrypt_stream( key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, - ad_opt: Option<&[u8]>, + aad_opt: Option<&[u8]>, output_hex: bool, ) { let (mut cipher, generated_nonce) = match nonce { @@ -165,23 +147,23 @@ fn aead128_encrypt_stream( exit(-1); }), }; - if let Some(ad) = ad_opt { + if let Some(aad) = aad_opt { // infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none // has been yet. - cipher.do_update_aad(ad).unwrap(); + cipher.do_update_aad(aad).unwrap(); } if let Some(nonce) = generated_nonce { helpers::write_bytes_or_hex(&nonce, output_hex); } - let mut buf = [0u8; 1024]; + let mut buf = [0u8; CHUNK_LEN]; loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let n = read_stdin_chunk(&mut buf); if n == 0 { break; } - let mut out = [0u8; 1024]; + let mut out = [0u8; CHUNK_LEN]; // infallible: `out` is as long as `buf`, so it cannot be shorter than the `n` bytes read // into it, which is the only length `OutputBufferTooSmall` could complain about. let written = cipher.do_encrypt_out(&buf[..n], &mut out).unwrap(); @@ -203,10 +185,9 @@ fn aead128_encrypt_stream( fn aead128_decrypt_stream( key: &AsconAEAD128Key, nonce: Option<&[u8; 16]>, - ad_opt: Option<&[u8]>, + aad_opt: Option<&[u8]>, output_hex: bool, ) { - const CHUNK: usize = 1024; let nonce = match nonce { Some(nonce) => *nonce, None => { @@ -214,29 +195,30 @@ fn aead128_decrypt_stream( if let Err(e) = io::stdin().read_exact(&mut nonce) { if e.kind() == io::ErrorKind::UnexpectedEof { eprintln!("Error: ciphertext is shorter than the 16-byte nonce."); - exit(-1); + } else { + eprintln!("Error: failed to read from stdin: {e}"); } - panic!("Failed to read from stdin: {e}"); + exit(-1); } nonce } }; let mut cipher = key.create_decryptor(&nonce); - if let Some(ad) = ad_opt { + if let Some(aad) = aad_opt { // infallible: as on the encrypt side, no ciphertext has been fed in yet. - cipher.do_update_aad(ad).unwrap(); + cipher.do_update_aad(aad).unwrap(); } - let mut buf = [0u8; CHUNK]; - let mut out = [0u8; CHUNK]; + let mut buf = [0u8; CHUNK_LEN]; + let mut out = [0u8; CHUNK_LEN]; loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let n = read_stdin_chunk(&mut buf); if n == 0 { break; } // infallible: the decryptor releases at most what it has held back (16 bytes) plus what - // it is given, less the 16 it keeps, so never more than the `n <= CHUNK` bytes read. + // it is given, less the 16 it keeps, so never more than the `n <= CHUNK_LEN` bytes read. let written = cipher.do_decrypt_out(&buf[..n], &mut out).unwrap(); helpers::write_bytes_or_hex(&out[..written], output_hex); } diff --git a/cli/src/encoders_cmd.rs b/cli/src/encoders_cmd.rs index 6a9aaf8b7..f6ab129cf 100644 --- a/cli/src/encoders_cmd.rs +++ b/cli/src/encoders_cmd.rs @@ -94,7 +94,7 @@ pub(crate) fn base64_encode_cmd() { exit(-1); }); if n == 0 { - crate::helpers::write_stdout(encoder.do_final(&[]).as_bytes()); + crate::helpers::write_stdout(encoder.do_final([]).as_bytes()); return; } crate::helpers::write_stdout(encoder.do_update(&buf[..n]).as_bytes()); @@ -125,7 +125,7 @@ pub(crate) fn base64_decode_cmd() { let n = read_chunk(&mut buf); if n == 0 { // End of input with no padding seen: finish whatever partial block is held. - crate::helpers::write_stdout(&decoder.do_final(&[]).unwrap_or_else(|e| fail(e))); + crate::helpers::write_stdout(&decoder.do_final([]).unwrap_or_else(|e| fail(e))); return; } match decoder.do_update(&buf[..n]) { @@ -135,7 +135,10 @@ pub(crate) fn base64_decode_cmd() { crate::helpers::write_stdout( &decoder.do_final(&buf[..n]).unwrap_or_else(|e| fail(e)), ); - // Padding ends the message. Anything but whitespace after it is not base64. + // Padding ends the message. Anything but whitespace after it is not base64. The + // library refuses data after the padding within the chunk it is given; what later + // chunks hold it never sees, since `do_final` consumed the decoder, so the check + // is repeated here for them. loop { let n = read_chunk(&mut buf); if n == 0 { diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs index 3574f6aa4..518822149 100644 --- a/cli/src/helpers/aead_cipher_helpers.rs +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -32,18 +32,19 @@ //! one call before any ciphertext, matching SP 800-38D Algorithm 4's requirement that AAD precede //! data. //! -//! `--aad-file` is read as raw bytes ([`read_from_file_raw`]), never hex-decoded. The -//! hex-or-raw guess `--key-file` uses would change what is authenticated without any error: a +//! `--aad-file` is read as raw bytes, or as hex text under the global `--hex-input`; nothing +//! is guessed from its contents. A guess would change what is authenticated without any error: a //! binary header that happens to parse as hex text (`cafe`, or sixteen zero bytes, which the hex -//! decoder skips) would be authenticated as its decoding, and the tag would not verify against any -//! other GCM implementation given the same file. +//! decoder skips) would be authenticated as its decoding, and the tag would not verify against +//! any other GCM implementation given the same file. -use crate::helpers::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout}; +use crate::helpers::{ + decode_hex_arg, flush_stdout, read_input_file, write_bytes_or_hex, write_stdout, +}; use bouncycastle::core::symmetric_key::{AeadDecryptionKey, AeadEncryptionKey}; use bouncycastle::core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; -use bouncycastle::hex; use std::io; use std::io::{Read, Write}; use std::process::exit; @@ -57,18 +58,13 @@ const CHUNK_LEN: usize = 1024; /// calls the value the IV; the trait and this file's identifiers call it the nonce. const GCM_NONCE_LEN: usize = 12; -/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; see the -/// module docs for why not hex). Empty if neither is given: AAD is optional, unlike the key. +/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; hex +/// with `--hex-input`). Empty if neither is given: AAD is optional, unlike the key. pub fn load_aad(aad: &Option, aad_file: &Option) -> Vec { - if let Some(path) = aad_file { - read_from_file_raw(path) - } else if let Some(hex_str) = aad { - hex::decode(hex_str).unwrap_or_else(|_| { - eprintln!("Error: `--aad` must be hex. Use `--aad-file` for raw bytes."); - exit(-1); - }) - } else { - Vec::new() + match (aad_file, aad) { + (Some(path), _) => read_input_file(path), + (None, Some(hex_str)) => decode_hex_arg(hex_str, "aad"), + (None, None) => Vec::new(), } } diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs index cc3b9ec71..0c40ba5e9 100644 --- a/cli/src/helpers/block_mode_helpers.rs +++ b/cli/src/helpers/block_mode_helpers.rs @@ -55,8 +55,8 @@ //! ``` use crate::helpers::{ - flush_stdout, read_from_file, read_from_file_raw, strip_trailing_newline, write_bytes_or_hex, - write_stdout, + decode_hex_arg, flush_stdout, hex_input, load_secret, read_input_file, strip_trailing_newline, + write_bytes_or_hex, write_stdout, }; use bouncycastle::core::errors::{RNGError, SymmetricCipherError}; use bouncycastle::core::hazmat::do_hazardous_operations; @@ -64,7 +64,6 @@ use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; use bouncycastle::core::symmetric_key::FromKeyMaterial; use bouncycastle::core::traits::{RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; -use bouncycastle::hex; use bouncycastle::rng::HashDRBG_SHA512; use clap::ValueEnum; use std::io; @@ -88,7 +87,8 @@ pub(crate) enum CipherDirection { Decrypt, } -/// Loads the key from `--key` (hex) or `--key-file` (binary or hex), and checks its length. +/// Loads the key from `--key-file` (raw bytes; hex with `--hex-input`) or `--key` (hex), and +/// checks its length. /// /// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the /// wrong length is a mistake rather than something to truncate or pad. @@ -97,24 +97,22 @@ pub(crate) fn load_key( key_file: &Option, alg: &str, ) -> KeyMaterial { - let key_bytes: Vec = if let Some(key_file) = key_file { - // A file may hold raw bytes or hex; `read_from_file` tries hex first, as the other - // commands do. - let bytes = read_from_file(key_file); - // `read_from_file` already ignores a trailing newline on a hex file. A *raw* key file may - // end in one too, which lengthens the key by a byte; strip it only when that leaves - // exactly the key, so a binary key whose last byte really is `0x0a` is not shortened. - let trimmed = strip_trailing_newline(&bytes); - if trimmed.len() == KEY_LEN { trimmed.to_vec() } else { bytes } - } else if let Some(key) = key { - hex::decode(key).unwrap_or_else(|_| { - eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); - exit(-1); - }) - } else { - eprintln!("Error: either `--key` or `--key-file` must be supplied."); - exit(-1); - }; + load_key_typed(key, key_file, alg, KeyType::SymmetricCipherKey) +} + +/// As [`load_key`], tagged `key_type`: `MACKey` for the CMAC and GMAC keys. +pub(crate) fn load_key_typed( + key: &Option, + key_file: &Option, + alg: &str, + key_type: KeyType, +) -> KeyMaterial { + let secret = load_secret(key, key_file, "key"); + // A raw key file may end in a newline, which lengthens the key by a byte; strip it only + // when that leaves exactly the key, so a binary key whose last byte really is `0x0a` is not + // shortened. + let trimmed = strip_trailing_newline(secret.ref_to_bytes()); + let key_bytes = if trimmed.len() == KEY_LEN { trimmed } else { secret.ref_to_bytes() }; if key_bytes.len() != KEY_LEN { eprintln!("Error: {alg} needs a {KEY_LEN}-byte key, got {} bytes.", key_bytes.len()); @@ -124,20 +122,19 @@ pub(crate) fn load_key( // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what // the engine requires -- except for an all-zero key, which it marks Zeroized instead. let mut key = - KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) - .unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }); + KeyMaterial::::from_bytes_as_type(key_bytes, key_type).unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); - if key.key_type() != KeyType::SymmetricCipherKey { + if key.key_type() != key_type { // Same stance as `helpers::parse_seed`: warn, then do what was asked. A CLI is used for // test vectors and scripting, where an all-zero key is a legitimate thing to want. eprintln!( "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." ); do_hazardous_operations(&mut key, |key| { - key.set_key_type(KeyType::SymmetricCipherKey)?; + key.set_key_type(key_type)?; key.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) }) .unwrap_or_else(|e| { @@ -156,10 +153,22 @@ pub(crate) fn load_key_object, const KEY_LEN: usize> key_file: &Option, alg: &str, ) -> K { - K::from_key_material(&load_key::(key, key_file, alg)).unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }) + load_key_object_typed(key, key_file, alg, KeyType::SymmetricCipherKey) +} + +/// As [`load_key_object`], with the material tagged `key_type`. +pub(crate) fn load_key_object_typed, const KEY_LEN: usize>( + key: &Option, + key_file: &Option, + alg: &str, + key_type: KeyType, +) -> K { + K::from_key_material(&load_key_typed::(key, key_file, alg, key_type)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }, + ) } /// An [`RNG`] that yields the caller's IV once, so `--iv` can reach a key whose only @@ -209,19 +218,20 @@ impl RNG for FixedIv { } } -/// Loads `--iv-file` (raw bytes, never hex-decoded) or `--iv` (hex); the file wins if both are -/// given, as for `--key-file`. `None` when neither is given, which selects the generated, -/// in-band IV. The IV must be exactly `INIT_DATA_LEN` bytes. +/// Loads `--iv-file` (raw bytes; hex with `--hex-input`) or `--iv` (hex); the file wins if +/// both are given, as for `--key-file`. `None` when neither is given, which selects the +/// generated, in-band IV. The IV must be exactly `INIT_DATA_LEN` bytes. /// -/// As for CCM's `--nonce-file`, a trailing newline in the file is part of the IV, and is warned -/// about, because a file written with `echo` rather than `echo -n` is the usual way to get one. +/// As for CCM's `--nonce-file`, a trailing newline in a raw file is part of the IV, and is +/// warned about, because a file written with `echo` rather than `echo -n` is the usual way to +/// get one. pub(crate) fn load_iv( iv: &Option, iv_file: &Option, ) -> Option<[u8; INIT_DATA_LEN]> { let bytes = if let Some(file) = iv_file { - let bytes = read_from_file_raw(file); - if bytes.last() == Some(&b'\n') { + let bytes = read_input_file(file); + if !hex_input() && bytes.last() == Some(&b'\n') { eprintln!( "Warning: IV file '{file}' ends with a newline byte (0x0a), which is used as part \ of the IV. If that is not intended, write it with `printf` or `echo -n`." @@ -229,10 +239,7 @@ pub(crate) fn load_iv( } bytes } else if let Some(v) = iv { - hex::decode(v).unwrap_or_else(|_| { - eprintln!("Error: IV is not valid hex."); - exit(-1) - }) + decode_hex_arg(v, "iv") } else { return None; }; diff --git a/cli/src/helpers/mod.rs b/cli/src/helpers/mod.rs index eb56485d9..e82ac8431 100644 --- a/cli/src/helpers/mod.rs +++ b/cli/src/helpers/mod.rs @@ -1,3 +1,22 @@ +//! Shared plumbing: how files and stdin are read, how secrets are held, how output is written, +//! and the streaming loops the hash-like commands share. +//! +//! # Input conventions, the same for every subcommand +//! +//! - A value given inline (`--key`, `--iv`, `--nonce`, `--aad`, `--salt`, `--ikm`, `--verify`, +//! `--element`, `--customization-hex`, ...) is hex. +//! - A value given as a file (`--key-file`, `--iv-file`, `--skfile`, `--sigfile`, ...), or on +//! stdin where a command reads a key, seed, signature or ciphertext there, is the file's raw +//! bytes. With the global `--hex-input` flag it is hex text instead, with whitespace, `\x` +//! prefixes and a trailing newline ignored. There is no guessing: a raw file whose bytes happen +//! to be hex digits is still raw. +//! - A trailing newline is part of a raw value except where a command says it strips one. +//! - Keys, seeds, private keys, salts and IKM are read into [`SecretBytes`], a `KeyMaterial` +//! buffer that is zeroized when dropped, and never through a `Vec`; a secret longer than +//! [`MAX_SECRET_LEN`] is refused. +//! - A read or write failure, a malformed value and a wrong length are reported on stderr with +//! an `Error:` line and exit status 255; a panic is never the answer to input. + use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::core::security_strength::SecurityStrength; @@ -7,87 +26,251 @@ use std::fs::File; use std::io; use std::io::{Read, Write}; use std::process::exit; +use std::sync::atomic::{AtomicBool, Ordering}; pub mod aead_cipher_helpers; pub mod block_mode_helpers; pub mod stream_mode_helpers; -/// Reads a file's bytes exactly as they are, with no hex-or-raw guessing. -/// -/// Use this where a misread would silently change the *value* the caller asked for rather than -/// merely fail to match it -- a nonce is the reason this exists: two distinct binary nonce files -/// that happen to decode as hex to the same bytes must not collapse to one nonce (see -/// `aes_ccm_cmd::load_nonce`). [`read_from_file`]'s "try hex, fall back to raw" heuristic is fine -/// for a key, where a wrong guess only ever produces a mismatch, never a same-looking-different -/// value. -pub(crate) fn read_from_file_raw(filename: &str) -> Vec { +/// Bytes read from stdin per call, and squeezed per call for an XOF. +pub(crate) const CHUNK_LEN: usize = 1024; + +/// The most a secret read from a file, stdin or the command line may be: enough for an ML-DSA-87 +/// private key (4896 bytes) in hex with room to spare. +pub(crate) const MAX_SECRET_LEN: usize = 16 * 1024; + +/// A secret as read: a zeroized buffer holding up to [`MAX_SECRET_LEN`] bytes, tagged +/// [`KeyType::Unknown`] (or `Zeroized` when all zero); the command re-tags what it makes of it. +pub(crate) type SecretBytes = KeyMaterial; + +/// The parsed `--hex-input` flag, process-wide: a CLI has one invocation, and `main` stores it +/// before any reader below runs, so no reader can see the default by mistake. +static HEX_INPUT: AtomicBool = AtomicBool::new(false); + +/// Records the global `--hex-input` flag for the readers below. +pub(crate) fn set_hex_input(on: bool) { + HEX_INPUT.store(on, Ordering::Relaxed); +} + +/// Whether files and stdin values are hex text rather than raw bytes. +pub(crate) fn hex_input() -> bool { + HEX_INPUT.load(Ordering::Relaxed) +} + +// ---- public (non-secret) input --------------------------------------------------------------- + +/// A file's bytes exactly as they are, regardless of `--hex-input`: for the data a command +/// authenticates or hashes rather than a value it decodes. +pub(crate) fn read_file(filename: &str) -> Vec { std::fs::read(filename).unwrap_or_else(|e| { eprintln!("Error: couldn't read file '{filename}': {e}"); exit(-1); }) } -/// `bytes` without one trailing line ending (`\n` or `\r\n`), if it has one. -/// -/// Files written by shell tools usually end in a newline that is not part of the value. Whether -/// stripping it is right depends on what the caller expects -- a binary key whose last byte -/// happens to be `0x0a` must not lose it -- so this only removes the bytes; the caller decides, -/// by the length it knows, whether to use the result. -pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] { - bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes) +/// A file's value: its raw bytes, or its hex decoding under `--hex-input`. +pub(crate) fn read_input_file(filename: &str) -> Vec { + let bytes = read_file(filename); + if !hex_input() { + return bytes; + } + hex::decode(&bytes).unwrap_or_else(|e| { + eprintln!("Error: file '{filename}' is not valid hex (given --hex-input): {e:?}"); + exit(-1); + }) +} + +/// Stdin's value, as [`read_input_file`] for a file. +pub(crate) fn read_input_stdin() -> Vec { + let bytes = read_stdin_all(); + if !hex_input() { + return bytes; + } + hex::decode(&bytes).unwrap_or_else(|e| { + eprintln!("Error: stdin is not valid hex (given --hex-input): {e:?}"); + exit(-1); + }) +} + +/// The value from the file when one is named, else from stdin. +pub(crate) fn read_input_file_or_stdin(filename: &Option) -> Vec { + match filename { + Some(filename) => read_input_file(filename), + None => read_input_stdin(), + } +} + +/// Decodes an inline hex argument, naming the flag in the error. +pub(crate) fn decode_hex_arg(value: &str, flag: &str) -> Vec { + hex::decode(value).unwrap_or_else(|_| { + eprintln!("Error: `--{flag}` must be hex."); + exit(-1); + }) } -/// The bytes a hex-or-raw input stands for: its hex decoding if it is hex -- with one trailing -/// line ending ignored, since files and pasted input usually end in one -- and the bytes -/// themselves, untouched, otherwise. A raw input keeps its trailing newline; only the caller -/// knows whether that byte is part of the value (see `block_mode_helpers::load_key`). -pub(crate) fn hex_or_raw(buf: Vec) -> Vec { - // Decide by the bytes themselves rather than by whether the decoder accepts them: it skips - // NUL bytes as well as whitespace, so an all-zero binary key would otherwise "decode" to an - // empty hex string. Hex text is hex digits, whitespace and `\x` escapes, and nothing else. - let looks_like_hex = buf.iter().any(u8::is_ascii_hexdigit) - && buf - .iter() - .all(|b| b.is_ascii_hexdigit() || b.is_ascii_whitespace() || *b == b'\\' || *b == b'x'); - if !looks_like_hex { - return buf; - } - match hex::decode(strip_trailing_newline(&buf)) { - Ok(decoded) => decoded, - Err(_) => buf, - } -} - -/// Reads either bin or hex -pub(crate) fn read_from_file(filename: &str) -> Vec { - let file = File::open(&filename); - if file.is_ok() { - let mut buf = Vec::::new(); - match file.unwrap().read_to_end(&mut buf) { - Ok(_bytes_read) => hex_or_raw(buf), - Err(_) => { - eprintln!("Error: couldn't open file '{}'", &filename); +// ---- secret input ---------------------------------------------------------------------------- + +/// Reads `source` to its end into a fresh [`SecretBytes`], refusing more than +/// [`MAX_SECRET_LEN`] bytes. `what` names the source for the messages. +fn read_secret_from(source: &mut impl Read, what: &str) -> SecretBytes { + let mut secret = SecretBytes::new(); + let mut len = 0usize; + let result = do_hazardous_operations(&mut secret, |secret| { + let buf = secret.ref_to_bytes_mut()?; + loop { + let n = source.read(&mut buf[len..]).unwrap_or_else(|e| { + eprintln!("Error: couldn't read {what}: {e}"); exit(-1); + }); + if n == 0 { + break; + } + len += n; + if len == buf.len() { + // Full: anything more is over the limit. (A read of 0 into an empty slice is + // indistinguishable from end of input, so probe explicitly.) + let mut probe = [0u8; 1]; + if source.read(&mut probe).unwrap_or(0) != 0 { + eprintln!( + "Error: {what} is longer than the {MAX_SECRET_LEN} bytes a secret may be." + ); + exit(-1); + } + break; } } - } else { - eprintln!("Error: couldn't open file '{}'", &filename); + secret.set_key_len(len)?; + secret.set_key_type(KeyType::Unknown) + }); + secret_tagging_error(result, what); + secret +} + +/// The hex decoding of `text`, into a fresh [`SecretBytes`]: the decoder writes straight into +/// the zeroized buffer, so the plaintext of the secret is never in a `Vec`. +fn decode_secret_hex(text: &[u8], what: &str) -> SecretBytes { + let mut secret = SecretBytes::new(); + let result = do_hazardous_operations(&mut secret, |secret| { + let out = secret.ref_to_bytes_mut()?; + let len = hex::decode_out(text, out).unwrap_or_else(|e| { + eprintln!("Error: {what} is not valid hex: {e:?}"); + exit(-1); + }); + secret.set_key_len(len)?; + secret.set_key_type(KeyType::Unknown) + }); + secret_tagging_error(result, what); + secret +} + +/// `set_key_len` and `set_key_type` refuse nothing a reader can cause except an all-zero value, +/// which they tag `Zeroized` and report; that is a value, not an error, so it passes. +fn secret_tagging_error( + result: Result<(), bouncycastle::core::errors::KeyMaterialError>, + what: &str, +) { + use bouncycastle::core::errors::KeyMaterialError; + match result { + Ok(()) | Err(KeyMaterialError::ActingOnZeroizedKey) => {} + Err(e) => { + eprintln!("Error: couldn't hold {what} as key material: {e:?}"); + exit(-1); + } + } +} + +/// A secret from a file: its raw bytes, or its hex decoding under `--hex-input`. +pub(crate) fn read_secret_file(filename: &str) -> SecretBytes { + let mut file = File::open(filename).unwrap_or_else(|e| { + eprintln!("Error: couldn't open file '{filename}': {e}"); exit(-1); + }); + let what = format!("file '{filename}'"); + let raw = read_secret_from(&mut file, &what); + if !hex_input() { + return raw; } + decode_secret_hex(raw.ref_to_bytes(), &what) } -/// Reads either bin or hex -pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { - if filename.is_some() { - // This already reads either bin or hex - return read_from_file(filename.as_ref().unwrap()); +/// A secret from stdin, as [`read_secret_file`] for a file. +pub(crate) fn read_secret_stdin() -> SecretBytes { + let raw = read_secret_from(&mut io::stdin(), "stdin"); + if !hex_input() { + return raw; } + decode_secret_hex(raw.ref_to_bytes(), "stdin") +} - let mut buf = Vec::::new(); - io::stdin().read_to_end(&mut buf).expect("Failed to read from stdin"); - hex_or_raw(buf) +/// The secret from the file when one is named, else from stdin. +pub(crate) fn read_secret_file_or_stdin(filename: &Option) -> SecretBytes { + match filename { + Some(filename) => read_secret_file(filename), + None => read_secret_stdin(), + } } +/// A secret given inline, in hex. The command line is visible to other users and kept in shell +/// history, which the help texts say; this only decodes what was given. +pub(crate) fn decode_hex_secret(value: &str, flag: &str) -> SecretBytes { + decode_secret_hex(value.as_bytes(), &format!("`--{flag}`")) +} + +/// A secret from `---file` or `--` (hex); the file wins when both are given, as the +/// help texts say. Neither is an error. +pub(crate) fn load_secret( + inline: &Option, + file: &Option, + flag: &str, +) -> SecretBytes { + match (file, inline) { + (Some(file), _) => read_secret_file(file), + (None, Some(value)) => decode_hex_secret(value, flag), + (None, None) => { + eprintln!("Error: either `--{flag}` or `--{flag}-file` must be supplied."); + exit(-1); + } + } +} + +/// `bytes` without one trailing line ending (`\n` or `\r\n`), if it has one. Only the caller +/// knows, by the length it expects, whether that byte is part of the value. +pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] { + bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes) +} + +// ---- stdin as data --------------------------------------------------------------------------- + +/// One read from stdin into `buf`; a read error is reported, not a panic. Returns 0 at the end. +pub(crate) fn read_stdin_chunk(buf: &mut [u8]) -> usize { + io::stdin().read(buf).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }) +} + +/// Feeds stdin to `sink` in pieces of at most [`CHUNK_LEN`] bytes, so a long input is never +/// held in memory. +pub(crate) fn stream_stdin(mut sink: impl FnMut(&[u8])) { + let mut buf = [0u8; CHUNK_LEN]; + loop { + let n = read_stdin_chunk(&mut buf); + if n == 0 { + return; + } + sink(&buf[..n]); + } +} + +/// All of stdin, where the whole input must be held anyway. +pub(crate) fn read_stdin_all() -> Vec { + let mut out = Vec::new(); + stream_stdin(|chunk| out.extend_from_slice(chunk)); + out +} + +// ---- output ---------------------------------------------------------------------------------- + /// Writes `bytes` to stdout, exiting quietly if the reader has gone away. /// /// A closed stdout -- `bc-rust ... | head -c 16` -- is the reader's choice, not a failure of ours, @@ -131,93 +314,107 @@ pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { } } +/// Writes `bytes`, raw or as hex, to a new file `filename`; a failure names the path. pub(crate) fn write_bytes_or_hex_to_file(bytes: &[u8], filename: &str, output_hex: bool) { - let mut file = File::create(filename).expect("Failed to create file"); - - if output_hex { - for b in bytes.iter() { - file.write_all(format!("{b:02x}").as_bytes()).unwrap(); + let result = File::create(filename).and_then(|mut file| { + if output_hex { + file.write_all(hex::encode(bytes).as_bytes()) + } else { + file.write_all(bytes) } - } else { - file.write_all(bytes).unwrap(); + }); + if let Err(e) = result { + eprintln!("Error: couldn't write file '{filename}': {e}"); + exit(-1); } } -/// Loads it as either hex or bytes -pub(crate) fn parse_seed(bytes: &[u8]) -> Result, ()> { - let bytes = if bytes.len() == 65 { &bytes[..64] } else { bytes }; - - // try decoding it as hex first - let seed_bytes: [u8; SEED_LEN] = match &hex::decode(&bytes) { - Ok(decoded_bytes) => { - if decoded_bytes.len() < SEED_LEN || decoded_bytes.len() > SEED_LEN + 1 { - // it was valid hex, but the wrong length - return Err(()); - } +// ---- seeds ----------------------------------------------------------------------------------- - decoded_bytes[..SEED_LEN].try_into().unwrap() - } - Err(_) => { - // it's not hex, so take the first SEED_LEN bytes of the raw binary - if bytes.len() < SEED_LEN || bytes.len() > SEED_LEN + 1 { - return Err(()); - } - - bytes[..SEED_LEN].try_into().unwrap() - } - }; - - // TODO: Verify that all error conditions have been checked - let mut seed = KeyMaterial::::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); +/// Why a seed was refused: its length, which is the only thing checked. +pub(crate) struct SeedError { + pub(crate) got: usize, +} +/// A `SEED_LEN`-byte seed from its bytes, which the caller has already read (raw, or decoded +/// from hex under `--hex-input`). One trailing newline is tolerated, since a seed file written +/// with `echo` has one. +/// +/// A seed is required to be unpredictable at the parameter set's strength; the CLI cannot know +/// whether it is, so an all-zero seed is accepted with a warning, which is what test vectors +/// need. +pub(crate) fn parse_seed( + bytes: &[u8], +) -> Result, SeedError> { + let bytes = if bytes.len() == SEED_LEN + 1 { strip_trailing_newline(bytes) } else { bytes }; + if bytes.len() != SEED_LEN { + return Err(SeedError { got: bytes.len() }); + } + let mut seed = KeyMaterial::::from_bytes_as_type(bytes, KeyType::Seed) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't hold the seed as key material: {e:?}"); + exit(-1); + }); if seed.key_type() == KeyType::Zeroized || seed.security_strength() < SecurityStrength::_256bit { eprintln!( "Warning: low entropy seed provided. We'll still process it, but it may be insecure." ); - do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed)?; seed.set_security_strength(SecurityStrength::_256bit) }) - .unwrap(); + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the seed: {e:?}"); + exit(-1); + }); } - Ok(seed) } -/// Stream stdin through a [`Hash`] and write the digest to stdout (hex or binary), followed by a -/// newline. Used by both the SHA-3 and Ascon-Hash256 subcommands. -pub(crate) fn stream_hash(mut hasher: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - - while bytes_read != 0 { - hasher.do_update(&buf[..bytes_read]); +/// Reports a refused seed and exits. +pub(crate) fn seed_error(e: SeedError) -> ! { + eprintln!( + "Error: the seed must be {SEED_LEN} bytes (raw, or hex with --hex-input), got {} bytes.", + e.got + ); + exit(-1); +} - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } +// ---- streaming loops ------------------------------------------------------------------------- +/// Streams stdin through a [`Hash`] and writes the digest to stdout (hex or binary), followed by +/// a newline. Used by the SHA-2, SHA-3, SM3 and Ascon-Hash256 subcommands. +pub(crate) fn stream_hash(mut hasher: impl Hash, output_hex: bool) { + stream_stdin(|chunk| hasher.do_update(chunk)); let out = hasher.do_final(); write_bytes_or_hex(&out, output_hex); write_stdout(b"\n"); } -/// Stream stdin through an [`XOF`] and squeeze `output_len` bytes to stdout (hex or binary), -/// followed by a newline. Used by both the SHAKE and Ascon-XOF128/CXOF128 subcommands. +/// Streams stdin through an [`XOF`] and squeezes `output_len` bytes to stdout (hex or binary), +/// followed by a newline. Used by the SHAKE, cSHAKE, the SP 800-185 XOFs and Ascon-XOF128/CXOF128 +/// subcommands. pub(crate) fn stream_xof(mut xof: impl XOF, output_len: usize, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - - while bytes_read != 0 { - xof.do_update(&buf[..bytes_read]); + stream_stdin(|chunk| xof.do_update(chunk)); + squeeze_to_stdout(xof.into_squeezer(), output_len, output_hex); +} - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); +/// Writes `output_len` bytes of `squeezer` to stdout in [`CHUNK_LEN`] pieces, followed by a +/// newline, so the output is never held whole and a very long one is a long run rather than an +/// allocation. +pub(crate) fn squeeze_to_stdout( + mut squeezer: impl XOFSqueezer, + output_len: usize, + output_hex: bool, +) { + let mut buf = [0u8; CHUNK_LEN]; + let mut left = output_len; + while left > 0 { + let n = left.min(CHUNK_LEN); + squeezer.do_output_out(&mut buf[..n]); + write_bytes_or_hex(&buf[..n], output_hex); + left -= n; } - - let out = xof.into_squeezer().do_output_final(output_len); - write_bytes_or_hex(&out, output_hex); write_stdout(b"\n"); } diff --git a/cli/src/hkdf_cmd.rs b/cli/src/hkdf_cmd.rs index 3fc0e1723..3ba6a1818 100644 --- a/cli/src/hkdf_cmd.rs +++ b/cli/src/hkdf_cmd.rs @@ -1,17 +1,29 @@ -use std::fs; +//! The `hkdf-*` subcommands: RFC 5869 extract-then-expand. + use std::process::exit; +use crate::helpers::{ + decode_hex_arg, load_secret, read_input_file, write_bytes_or_hex, write_stdout, +}; use bouncycastle::core::hazmat::do_hazardous_operations; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle::hex; use bouncycastle::hkdf; +use bouncycastle::hkdf::HKDF; +use bouncycastle::hmac::HMACParams; use bouncycastle::sha2::hkdf::{HKDF_SHA256, HKDF_SHA384, HKDF_SHA512}; +use bouncycastle::sm3::hkdf::HKDF_SM3; -/// The salt is held in a fixed-size key buffer; the library itself puts no bound on it. -const MAX_SALT_LEN: usize = 1024; +#[allow(non_camel_case_types)] +pub(crate) enum HKDFVariant { + SHA256, + SHA384, + SHA512, + SM3, +} +#[allow(clippy::too_many_arguments)] // one flag per RFC 5869 input, each inline or from a file pub(crate) fn hkdf_cmd( - hkdfname: &str, + variant: HKDFVariant, salt: &Option, salt_file: &Option, ikm: &Option, @@ -21,78 +33,60 @@ pub(crate) fn hkdf_cmd( len: usize, output_hex: bool, ) { - // Each value may come from hex on the command line or from a binary file; the file wins if - // both are given, as the subcommands' help says. - let salt_bytes = if let Some(file) = salt_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = salt { - hex::decode(hex_str).unwrap() - } else { - eprintln!("Error: either `salt` or `salt-file` must be supplied."); - exit(-1) - }; - if salt_bytes.len() > MAX_SALT_LEN { - eprintln!("Error: The CLI only supports HKDF salts up to {MAX_SALT_LEN} bytes."); - exit(-1); - } - let mut salt_key = KeyMaterial::::from_bytes(&salt_bytes).unwrap(); - // force it just so the CLI behaves properly even with all-zero or zero-length keys - do_hazardous_operations(&mut salt_key, |salt_key| salt_key.set_key_type(KeyType::MACKey)) - .unwrap(); - - let ikm_bytes = if let Some(file) = ikm_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = ikm { - hex::decode(hex_str).unwrap() - } else { - eprintln!("Error: either `ikm` or `ikm_file` must be supplied."); - exit(-1) - }; + // Each value may come from hex on the command line or from a file; the file wins if both + // are given, as the subcommands' help says. + let mut salt = load_secret(salt, salt_file, "salt"); + // Tagged as the secret salt of SP 800-56C: that is what credits its entropy, and an all-zero + // or empty salt is a legitimate thing to give on the command line. + do_hazardous_operations(&mut salt, |salt| salt.set_key_type(KeyType::MACKey)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't load the salt: {e:?}"); + exit(-1); + }, + ); + let ikm = load_secret(ikm, ikm_file, "ikm"); // RFC 5869 Section 2.3: info is "optional context and application specific information (can // be a zero-length string)", so neither flag is required. - let info = if let Some(file) = additional_input_file { - fs::read(file).unwrap() - } else if let Some(hex_str) = additional_input { - hex::decode(hex_str).unwrap() - } else { - Vec::new() + let info = match (additional_input_file, additional_input) { + (Some(file), _) => read_input_file(file), + (None, Some(hex_str)) => decode_hex_arg(hex_str, "additional-input"), + (None, None) => Vec::new(), }; // RFC 5869: PRK = HKDF-Extract(salt, IKM), then OKM = HKDF-Expand(PRK, info, L). The IKM is - // streamed into the extract phase, so its length is not bounded by a KeyMaterial buffer, and the - // additional input is the expand phase's `info`. The library refuses an L above 255 * HashLen. + // streamed into the extract phase and the additional input is the expand phase's `info`. The + // library refuses an L above 255 * HashLen. let mut out_key = KeyMaterial::<{ 255 * hkdf::MAX_HMAC_OUTPUT_LEN }>::new(); - let result = match hkdfname { - "HKDF-SHA256" => { - let mut h = HKDF_SHA256::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA256::expand_out(&prk, &info, len, &mut out_key) - } - "HKDF-SHA384" => { - let mut h = HKDF_SHA384::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA384::expand_out(&prk, &info, len, &mut out_key) - } - "HKDF-SHA512" => { - let mut h = HKDF_SHA512::new(); - h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(&ikm_bytes).unwrap(); - let prk = h.do_extract_final().unwrap(); - HKDF_SHA512::expand_out(&prk, &info, len, &mut out_key) - } - _ => { - panic!("{} is not a supported HKDF variant.", hkdfname); - } + let result = match variant { + HKDFVariant::SHA256 => run(HKDF_SHA256::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SHA384 => run(HKDF_SHA384::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SHA512 => run(HKDF_SHA512::new(), &salt, &ikm, &info, len, &mut out_key), + HKDFVariant::SM3 => run(HKDF_SM3::new(), &salt, &ikm, &info, len, &mut out_key), }; if let Err(e) = result { - eprintln!("Error: {e:?}"); + eprintln!("Error: HKDF failed: {e}"); exit(-1); } - crate::helpers::write_bytes_or_hex(out_key.ref_to_bytes(), output_hex); + write_bytes_or_hex(out_key.ref_to_bytes(), output_hex); + if output_hex { + write_stdout(b"\n"); + } +} + +/// Extract then expand through `h`, into `out_key`. +fn run( + mut h: HKDF, + salt: &impl KeyMaterialTrait, + ikm: &impl KeyMaterialTrait, + info: &[u8], + len: usize, + out_key: &mut impl KeyMaterialTrait, +) -> Result { + h.do_extract_init(salt).map_err(|e| format!("{e:?}"))?; + h.do_extract_update_bytes(ikm.ref_to_bytes()).map_err(|e| format!("{e:?}"))?; + let prk = h.do_extract_final().map_err(|e| format!("{e:?}"))?; + HKDF::::expand_out(&prk, info, len, out_key) + .map_err(|e| format!("{e:?}")) } diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 6cd942a39..a5b48dedc 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -1,51 +1,55 @@ -use std::io; -use std::io::Read; -use std::process::exit; +//! The MAC subcommands: `hmac-*`, `kmac*` and `kmacxof*`, `aes*-cmac` and `aes*-gmac`. A MAC +//! over stdin, computed or verified. -use crate::helpers::read_from_file_raw; +use crate::helpers::block_mode_helpers::{load_iv, load_key_object_typed}; +use crate::helpers::{ + SecretBytes, decode_hex_arg, load_secret, stream_stdin, stream_xof, write_bytes_or_hex, + write_stdout, +}; +use crate::sha3_cmd::{MAX_FIXED_OUTPUT_LEN, Strength}; +use bouncycastle::aes::hazmat::AESInternal; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key, AESKey, AESParams}; +use bouncycastle::cipher::modes::{CMAC, GMAC, ModeNames}; use bouncycastle::core::errors::MACError; -use bouncycastle::core::hazmat::do_hazardous_operations; -use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; -use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle::core::hazmat::{BlockPermutation, do_hazardous_operations}; +use bouncycastle::core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, FromKeyMaterial, MacGenerationKey}; use bouncycastle::core::traits::MAC; -use bouncycastle::hex; use bouncycastle::hmac::HmacKey; -use bouncycastle::sha2::{SHA256, SHA512, SHA512_224, SHA512_256}; -use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key}; +use bouncycastle::rng::HashDRBG_SHA512; +use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; +use bouncycastle::sha3::kmac::{KMAC128Key, KMAC256Key, KMACXOF128, KMACXOF256}; +use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; use bouncycastle::sm3::SM3; +use std::process::exit; #[allow(non_camel_case_types)] pub(crate) enum HMACVariant { + SHA224, SHA256, + SHA384, SHA512, SHA512_224, SHA512_256, + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, SM3, } -/// Loads a MAC key from `--key-file` (raw) or `--key` (hex), tagged as a MAC key. The file wins -/// when both are given, as the help text says and the other commands' loaders do. -fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 { - let key_bytes: Vec = if let Some(key_file) = key_file { - read_from_file_raw(key_file) - } else if let Some(key) = key { - hex::decode(key).unwrap_or_else(|_| { - eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); - exit(-1); - }) - } else { - eprintln!("Error: either `key` or `key-file` must be supplied."); - exit(-1) - }; - - if key_bytes.len() > 64 { - eprintln!("Error: CLI only supports MAC keys 64 bytes."); - exit(-1); +/// The MAC key from `--key-file` or `--key`, tagged as a MAC key. Any length the secret buffer +/// holds is accepted: HMAC hashes a key longer than its block (RFC 2104 Section 2), and KMAC +/// encodes the key's length (SP 800-185 Section 4.2). +fn load_mac_key(key: &Option, key_file: &Option) -> SecretBytes { + let mut key = load_secret(key, key_file, "key"); + if key.key_type() == KeyType::Zeroized { + // Same stance as the cipher loaders: warn, then do what was asked. A CLI is used for + // test vectors and scripting, where an all-zero key is a legitimate thing to want. + eprintln!( + "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." + ); } - let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap_or_else(|e| { - eprintln!("Error: couldn't load the key: {e:?}"); - exit(-1); - }); do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap_or_else( |e| { eprintln!("Error: couldn't load the key: {e:?}"); @@ -55,6 +59,23 @@ fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial5 key } +fn mac_key_error(e: MACError) -> ! { + eprintln!("Error: couldn't build the MAC key: {e:?}"); + exit(-1) +} + +/// HMAC over `H` with the allow-weak key form, as every `hmac-*` command uses: a CLI is used for +/// test vectors and scripting, where a short or all-zero key is a legitimate thing to want. +fn hmac( + key: &SecretBytes, + verify_val: &Option, + output_hex: bool, +) { + let hmac_key = + HmacKey::::from_key_material_allow_weak(key).unwrap_or_else(|e| mac_key_error(e)); + do_mac(hmac_key.create_generator(), verify_val, output_hex); +} + pub(crate) fn mac_cmd( hmac_variant: HMACVariant, key: &Option, @@ -63,90 +84,240 @@ pub(crate) fn mac_cmd( output_hex: bool, ) { let key = load_mac_key(key, key_file); - - // instantiate the MAC object and call do_mac() match hmac_variant { - HMACVariant::SHA256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512_224 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SHA512_256 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } - HMACVariant::SM3 => { - let hmac_key = HmacKey::::from_key_material_allow_weak(&key).unwrap(); - do_mac(hmac_key.create_generator(), verify_val, output_hex); - } + HMACVariant::SHA224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA384 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512_224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA512_256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_224 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_256 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_384 => hmac::(&key, verify_val, output_hex), + HMACVariant::SHA3_512 => hmac::(&key, verify_val, output_hex), + HMACVariant::SM3 => hmac::(&key, verify_val, output_hex), } } /// KMAC (NIST SP 800-185 Sec 4), which unlike HMAC takes a customization string and a caller- /// chosen tag length -- both are bound into the computation, so the verifier must use the same. pub(crate) fn kmac_cmd( - bit_len: usize, + strength: Strength, length: usize, - customization: &Option, + customization: &[u8], key: &Option, key_file: &Option, verify_val: &Option, output_hex: bool, ) { + if length > MAX_FIXED_OUTPUT_LEN { + eprintln!( + "Error: a KMAC tag is computed whole; this command takes at most {MAX_FIXED_OUTPUT_LEN} bytes. Use kmacxof* for a long, streamed output." + ); + exit(-1); + } let key = load_mac_key(key, key_file); - let s = customization.as_deref().unwrap_or("").as_bytes(); - // The allow-weak form, as the HMAC commands use: a CLI is used for test vectors and - // scripting, where a short or all-zero key is a legitimate thing to want. - match bit_len { - 128 => { - let kmac_key = KMAC128Key::with_params_allow_weak(&key, s, length) - .unwrap_or_else(|e| kmac_key_error(e)); + match strength { + Strength::Bits128 => { + let kmac_key = KMAC128Key::with_params_allow_weak(&key, customization, length) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } - 256 => { - let kmac_key = KMAC256Key::with_params_allow_weak(&key, s, length) - .unwrap_or_else(|e| kmac_key_error(e)); + Strength::Bits256 => { + let kmac_key = KMAC256Key::with_params_allow_weak(&key, customization, length) + .unwrap_or_else(|e| mac_key_error(e)); do_mac(kmac_key.create_generator(), verify_val, output_hex) } - _ => panic!("Unsupported algorithm: KMAC-{bit_len}"), } } -/// The key is already a MAC key, so what fails here is the tag length, which came from the -/// command line: report it the way the key errors are reported rather than through a panic. -fn kmac_key_error(e: MACError) -> ! { - eprintln!("Error: couldn't build the KMAC key: {e:?}"); - exit(-1) +/// KMACXOF (SP 800-185 Sec 4.3.1): KMAC with the length not bound in, so the output is a +/// stream of any length. There is nothing to verify against: a reader squeezes the same stream. +pub(crate) fn kmacxof_cmd( + strength: Strength, + output_len: usize, + customization: &[u8], + key: &Option, + key_file: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); + match strength { + Strength::Bits128 => { + let x = KMACXOF128::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e)); + stream_xof(x, output_len, output_hex); + } + Strength::Bits256 => { + let x = KMACXOF256::new(&key, customization, true).unwrap_or_else(|e| mac_key_error(e)); + stream_xof(x, output_len, output_hex); + } + } +} + +/// AES-CMAC (NIST SP 800-38B) under the key the command's length selects. +pub(crate) fn cmac_cmd + MacGenerationKey, const KEY_LEN: usize>( + alg: &str, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: K = load_key_object_typed(key, key_file, alg, KeyType::MACKey); + do_mac(key.create_generator(), verify_val, output_hex); +} + +pub(crate) fn aes128_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 16>("AES-128", key, key_file, verify_val, output_hex); } -fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { - // read the content to be MAC'd from stdin - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - mac.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); +pub(crate) fn aes192_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 24>("AES-192", key, key_file, verify_val, output_hex); +} + +pub(crate) fn aes256_cmac_cmd( + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + cmac_cmd::, 32>("AES-256", key, key_file, verify_val, output_hex); +} + +/// The GMAC nonce length: GCM's 12 bytes (SP 800-38D Sec 5.2.1.1's recommended IV length). +const GMAC_NONCE_LEN: usize = 12; +/// The GMAC tag length the `aes*-gmac` commands fix, as `aes*-gcm` do: the full 128 bits. +const GMAC_TAG_LEN: usize = 16; + +/// AES-GMAC (NIST SP 800-38D Sec 5.2): a GCM tag over the data as AAD. A tag is bound to a +/// nonce, so the shape is the MAC commands' with the IV flags of `aes*-gcm` added. Without +/// `--iv` the nonce is drawn from the OS-backed DRBG and written ahead of the tag, so the +/// output is `nonce || tag` and a reader has what it needs; with `--iv` only the tag is +/// written. `--verify` takes the tag, with `--iv`, or the 28-byte `nonce || tag` without it. +/// +/// The GMAC workers have no trait: their generator and verifier are inherent on the key, so +/// this is generic over the AES parameter set the way the key's own impl is. +fn gmac_run( + key: &AESKey>, + iv: Option<[u8; GMAC_NONCE_LEN]>, + verify_val: &Option, + output_hex: bool, +) where + AESInternal

: BlockPermutation<16> + ModeNames + Clone, +{ + match verify_val { + None => { + let (mut mac, generated) = match iv { + Some(iv) => (key.create_generator_nonce(&iv), None), + None => key + .create_generator(&mut HashDRBG_SHA512::new_from_os()) + .map(|(mac, nonce)| (mac, Some(nonce))) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't draw the nonce: {e:?}"); + exit(-1); + }), + }; + stream_stdin(|chunk| mac.do_update(chunk)); + let tag = mac.do_final(); + if let Some(nonce) = generated { + write_bytes_or_hex(&nonce, output_hex); + } + write_bytes_or_hex(&tag, output_hex); + write_stdout(b"\n"); + } + Some(verify_val) => { + let given = decode_hex_arg(verify_val, "verify"); + let (nonce, tag): ([u8; GMAC_NONCE_LEN], &[u8]) = match iv { + Some(iv) => (iv, &given), + None => { + if given.len() != GMAC_NONCE_LEN + GMAC_TAG_LEN { + eprintln!( + "Error: without --iv, --verify takes the {GMAC_NONCE_LEN}-byte nonce followed by the {GMAC_TAG_LEN}-byte tag." + ); + exit(-1); + } + let (nonce, tag) = given.split_at(GMAC_NONCE_LEN); + (nonce.try_into().expect("split at the nonce length"), tag) + } + }; + let tag: &[u8; GMAC_TAG_LEN] = tag.try_into().unwrap_or_else(|_| { + eprintln!("Error: the tag must be {GMAC_TAG_LEN} bytes, got {}.", tag.len()); + exit(-1); + }); + let mut mac = key.create_verifier(&nonce); + stream_stdin(|chunk| mac.do_update(chunk)); + if mac.do_verify_final(tag) { + exit(0) + } + eprintln!("Error: MAC verification failed."); + exit(-1) + } } +} + +pub(crate) fn aes128_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES128Key = load_key_object_typed(key, key_file, "AES-128", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} + +pub(crate) fn aes192_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES192Key = load_key_object_typed(key, key_file, "AES-192", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} - if verify_val.is_none() { - // compute a MAC value - let out = mac.do_final(); +pub(crate) fn aes256_gmac_cmd( + key: &Option, + key_file: &Option, + iv: &Option, + iv_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key: AES256Key = load_key_object_typed(key, key_file, "AES-256", KeyType::MACKey); + gmac_run(&key, load_iv::(iv, iv_file), verify_val, output_hex); +} - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); - } else { - // verify a MAC - let expected = hex::decode(verify_val.as_ref().unwrap()).unwrap_or_else(|_| { - eprintln!("Error: the MAC to verify must be hex."); - exit(-1); - }); - if mac.do_verify_final(&expected) { exit(0) } else { exit(-1) } +/// Streams stdin into `mac`, then writes the tag, or verifies `--verify` against it and reports +/// the outcome through the exit status (0 verified, 255 not) and an `Error:` line. +pub(crate) fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { + stream_stdin(|chunk| mac.do_update(chunk)); + + match verify_val { + None => { + let out = mac.do_final(); + write_bytes_or_hex(&out, output_hex); + write_stdout(b"\n"); + } + Some(verify_val) => { + let expected = decode_hex_arg(verify_val, "verify"); + if mac.do_verify_final(&expected) { + exit(0) + } + eprintln!("Error: MAC verification failed."); + exit(-1) + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index c443f6fd3..289bd5b7a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + mod aes_cbc_cmd; mod aes_ccm_cmd; mod aes_cfb8_cmd; @@ -17,20 +19,30 @@ mod sha2_cmd; mod sha3_cmd; mod sm3_cmd; +use crate::hkdf_cmd::HKDFVariant; use crate::mac_cmd::HMACVariant; -use crate::mldsa_cmd::MLDSAAction; +use crate::mldsa_cmd::{MLDSAAction, PreHashAlg}; +use crate::rng_cmd::DrbgHash; use crate::sha2_cmd::SHA2Variant; +use crate::sha3_cmd::{SHA3Variant, Strength}; use clap::{Parser, Subcommand}; use helpers::block_mode_helpers::CipherDirection; #[derive(Parser)] #[command(version, about, long_about=None, arg_required_else_help=true)] struct Cli { + /// Read the files a command decodes (keys, IVs and nonces, seeds, private and public keys, + /// signatures, ciphertexts, contexts, salts, IKM, AAD files), and stdin where it carries + /// one of those, as hex text rather than raw bytes. Values given inline (--key, --iv, ...) + /// are always hex. Without this flag a file is its raw bytes, however hex-like they look. + #[arg(long, global = true)] + hex_input: bool, + #[command(subcommand)] subcommands: Option, } -#[allow(non_camel_case_types)] +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] #[derive(Subcommand)] enum Subcommands { /// AES-128 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. @@ -39,7 +51,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -64,7 +76,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -74,8 +86,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -132,7 +143,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -141,7 +152,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -172,7 +183,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -197,7 +208,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -207,7 +218,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -228,7 +239,7 @@ enum Subcommands { /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two /// compose directly in a pipeline. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from stdin. This is for /// a format that carries the IV itself, and for reproducing published vectors. The IV must /// then be unpredictable to an attacker (SP 800-38A Appendix C) and never reused under one @@ -253,7 +264,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -263,7 +274,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -273,6 +284,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-128 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES128_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-128 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one @@ -283,7 +319,7 @@ enum Subcommands { /// on `decrypt` it is read back from the first 12 bytes of the input, so the two compose /// directly in a pipeline. Note that this is 12 bytes, not the 16 the other modes write. /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the 12-byte nonce is supplied instead: + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the 12-byte nonce is supplied instead: /// `encrypt` uses it and writes ONLY the ciphertext, and `decrypt` reads ONLY ciphertext from /// stdin. The counter block is then `nonce || 00000000`, which is how an OpenSSL `-iv` whose /// low four bytes are zero lines up with it. The nonce MUST be unique per message under one @@ -309,7 +345,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -319,8 +355,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -354,7 +389,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -374,13 +409,13 @@ enum Subcommands { /// back from the first 12 bytes of input and streams the rest, checking the tag once input is /// exhausted. (SP 800-38D calls this value the IV, and defines it as a nonce.) /// - /// With `--iv` (hex) or `--iv-file` (raw bytes), the IV is supplied instead: `encrypt` uses it + /// With `--iv` (hex) or `--iv-file` (raw bytes; hex with --hex-input), the IV is supplied instead: `encrypt` uses it /// and writes ONLY `ciphertext || tag`, and `decrypt` reads ONLY that from stdin. The IV MUST /// be unique per message under one key (Sec 8), and a repeat is worse here than merely /// unwise: it lets an attacker recover the hash subkey (Appendix A). The default takes care of /// that; with `--iv` it is on the caller. /// - /// `--aad` (hex) or `--aad-file` (raw bytes) supply the additional authenticated data, + /// `--aad` (hex) or `--aad-file` (raw bytes; hex with --hex-input) supply the additional authenticated data, /// which is covered by the tag but not encrypted; if neither is given, AAD is empty. /// /// Input may be ANY length: GCM needs no padding. @@ -401,7 +436,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key, in binary or hex. + /// A file containing the 16-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -411,7 +446,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -430,6 +465,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-128 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES128_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// AES-192 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. /// /// See `aes128-cbc` for the IV convention, padding and warnings; only the @@ -443,7 +516,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -453,8 +526,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -476,7 +548,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -485,7 +557,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -520,7 +592,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -530,7 +602,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -553,7 +625,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -563,7 +635,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -573,6 +645,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-192 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES192_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-192 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key @@ -586,7 +683,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -596,8 +693,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -619,7 +715,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -642,7 +738,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key, in binary or hex. + /// A file containing the 24-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -652,7 +748,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -671,6 +767,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-192 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES192_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// AES-256 in CBC mode (NIST SP 800-38A Sec 6.2) with PKCS7 padding, streaming stdin to stdout. /// /// See `aes128-cbc` for the IV convention, padding and warnings; only the @@ -684,7 +818,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -694,8 +828,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -717,7 +850,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -726,7 +859,7 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + /// A file containing the nonce, (raw bytes; hex with --hex-input). #[arg(long)] nonce_file: Option, @@ -761,7 +894,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -771,7 +904,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -794,7 +927,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -804,7 +937,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 16-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 16-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -814,6 +947,31 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-256 CMAC (NIST SP 800-38B) over the content provided on stdin: + /// a 16-byte tag. Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES256_CMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + /// AES-256 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. /// /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key @@ -827,7 +985,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -837,8 +995,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte nonce, as raw bytes exactly as they are (no hex - /// decoding). If both iv and iv_file options are provided, the file will be used. + /// A file containing the 12-byte nonce, (raw bytes; hex with --hex-input). If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -860,7 +1017,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -883,7 +1040,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key, in binary or hex. + /// A file containing the 32-byte AES key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -893,7 +1050,7 @@ enum Subcommands { #[arg(long)] iv: Option, - /// A file containing the 12-byte IV, as raw bytes exactly as they are (no hex decoding). + /// A file containing the 12-byte IV, (raw bytes; hex with --hex-input). /// If both iv and iv_file options are provided, the file will be used. #[arg(long)] iv_file: Option, @@ -912,6 +1069,44 @@ enum Subcommands { x: bool, }, + /// Compute or verify an AES-256 GMAC (NIST SP 800-38D Sec 5.2) over the content provided on + /// stdin: a 16-byte tag bound to a 12-byte nonce. Without `--iv` a fresh nonce is drawn and + /// written FIRST, so the output is `nonce || tag`; with `--iv` only the tag is written. To + /// verify, give `--verify` the tag along with `--iv`, or the 28-byte `nonce || tag` alone. + /// A nonce MUST NOT repeat under one key (SP 800-38D Sec 8). + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES256_GMAC { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The 12-byte nonce in hex. Hazardous: supplying it makes uniqueness the caller's + /// responsibility. + #[arg(long)] + iv: Option, + + /// A file containing the 12-byte nonce (raw bytes; hex with --hex-input); the same + /// hazardous override. + #[arg(long)] + iv_file: Option, + + /// A MAC value to be verified: the tag (with --iv) or nonce || tag. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin /// (NIST SP 800-232). /// @@ -939,7 +1134,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 128-bit key in hex or binary. + /// A file containing the 128-bit key (raw bytes; hex with --hex-input). #[arg(long)] key_file: Option, @@ -948,13 +1143,17 @@ enum Subcommands { #[arg(long)] nonce: Option, - /// A file containing a 128-bit nonce in hex or binary; the same hazardous override. + /// A file containing a 128-bit nonce (raw bytes; hex with --hex-input); the same hazardous override. #[arg(long)] nonce_file: Option, /// Associated data in hex (authenticated but not encrypted). #[arg(long)] - ad: Option, + aad: Option, + + /// A file holding the associated data (raw bytes; hex with --hex-input). + #[arg(long)] + aad_file: Option, #[arg(short, long)] direction: CipherDirection, @@ -970,10 +1169,14 @@ enum Subcommands { /// Length of the output in bytes. length: usize, - /// Customization string in hex (optional). + /// Customization string Z, as the characters given (optional). #[arg(long)] customization: Option, + /// Customization string Z in hex, for bytes that are not text. + #[arg(long)] + customization_hex: Option, + #[arg(short)] /// Output in hex format. x: bool, @@ -1006,76 +1209,168 @@ enum Subcommands { /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. Base64Encode, - /// The HashML-DSA-44 signature algorithm. + /// Perform cSHAKE128 (NIST SP 800-185 Sec 3) of the content provided on stdin, under a + /// function name N and a customization string S; with both empty it is SHAKE128. Requires + /// the output length in bytes. Supports streaming update for low memory footprint. + CSHAKE128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'n', long)] + /// Function name N, as the characters given (SP 800-185 reserves it for NIST-defined + /// functions; leave it empty otherwise). + function_name: Option, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform cSHAKE256 (NIST SP 800-185 Sec 3). See cshake128. + CSHAKE256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'n', long)] + /// Function name N, as the characters given. + function_name: Option, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// The HashML-DSA-44 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA44 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, }, - /// The HashML-DSA-65 signature algorithm. + /// The HashML-DSA-65 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA65 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, }, - /// The HashML-DSA87 signature algorithm. + /// The HashML-DSA-87 signature algorithm, with the hash given by --hash (SHA-512 by default). HashMLDSA87 { action: MLDSAAction, + #[arg(long, value_enum, default_value_t = PreHashAlg::Sha512)] + /// The pre-hash (FIPS 204 Section 5.4); it must be at least as strong as the parameter + /// set, so sha256 and shake128 fit hash-mldsa44 only. + hash: PreHashAlg, + #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1218,48 +1513,91 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SHA256 of the content provided on stdin. + /// Perform HKDF-SM3 (RFC 5869 extract-then-expand) of the provided input keying material: + /// OKM = HKDF-Expand(HKDF-Extract(salt, ikm), info, len) + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HKDF_SM3 { + /// The salt value in hex. + /// The `salt_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + salt: Option, + + /// A file containing the salt value in binary. + /// If both salt and salt_file options are provided, the file will be used. + #[arg(short, long)] + salt_file: Option, + + /// An Input Keying Material in hex. + /// The `ikm_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + ikm: Option, + + /// A file containing the input keying material in binary. + /// If both ikm and ikm_file options are provided, the file will be used. + #[arg(short, long)] + ikm_file: Option, + + /// The HKDF `info` input (RFC 5869 Section 2.3) in hex. Optional; defaults to empty. + #[arg(long)] + additional_input: Option, + + /// A file containing the HKDF `info` input in binary. Optional; defaults to empty. + /// If both additional_input and additional_input_file options are provided, the file will be used. + #[arg(short, long)] + additional_input_file: Option, + + /// Length of output to produce, in bytes: at most 255 times the hash's output length. + #[arg(short, long)] + len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform HMAC-SHA224 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA256 { + HMAC_SHA224 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, #[arg(short)] - /// Output the hashes in hex format. + /// Output the tag in hex format. x: bool, }, - /// Perform HMAC-SHA512 of the content provided on stdin. + /// Perform HMAC-SHA256 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512 { + HMAC_SHA256 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1268,48 +1606,48 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SHA512/224 of the content provided on stdin. + /// Perform HMAC-SHA384 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512_224 { + HMAC_SHA384 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, #[arg(short)] - /// Output the hashes in hex format. + /// Output the tag in hex format. x: bool, }, - /// Perform HMAC-SHA512/256 of the content provided on stdin. + /// Perform HMAC-SHA512 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SHA512_256 { + HMAC_SHA512 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1318,23 +1656,23 @@ enum Subcommands { x: bool, }, - /// Perform HMAC-SM3 of the content provided on stdin. + /// Perform HMAC-SHA512/224 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. - HMAC_SM3 { + HMAC_SHA512_224 { /// The MAC key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] key: Option, - /// A file containing the MAC key in binary. + /// A file containing the MAC key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] verify: Option, @@ -1343,23 +1681,182 @@ enum Subcommands { x: bool, }, - /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. - /// The tag length and customization string are bound into the computation, so the verifier - /// must use the same values. - KMAC128 { - /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). - length: usize, - - #[arg(short = 's', long)] - /// Customization string, domain-separating this use of KMAC from another. - customization: Option, + /// Perform HMAC-SHA512/256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA512_256 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. #[arg(short, long)] - /// The key, in hex. + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-224 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_224 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] key: Option, + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_256 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - /// File containing the key, as raw bytes. + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-384 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_384 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SHA3-512 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SHA3_512 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform HMAC-SM3 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + HMAC_SM3 { + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// Exits 0 if the value matches and 255 with an error message if not; nothing is written. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. + /// The tag length and customization string are bound into the computation, so the verifier + /// must use the same values. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMAC128 { + /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] key_file: Option, #[arg(short, long)] @@ -1373,20 +1870,29 @@ enum Subcommands { /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. /// See kmac128. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. KMAC256 { /// Length of the tag in bytes, at least 4 (SP 800-185 Sec 8.4.2). length: usize, #[arg(short = 's', long)] - /// Customization string, domain-separating this use of KMAC from another. + /// Customization string S, as the characters given, domain-separating this use of KMAC + /// from another. customization: Option, - #[arg(short, long)] - /// The key, in hex. - key: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + /// The MAC key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - /// File containing the key, as raw bytes. + key: Option, + + /// A file containing the MAC key (raw bytes; hex with --hex-input). + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] key_file: Option, #[arg(short, long)] @@ -1398,26 +1904,98 @@ enum Subcommands { x: bool, }, + /// Compute a KMACXOF128 (NIST SP 800-185 Sec 4.3.1) over the content provided on stdin: KMAC + /// as a XOF, with the output length not bound in, so any length streams. Requires the + /// output length in bytes. Nothing is verified; a reader squeezes the same stream. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMACXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the key (raw bytes; hex with --hex-input). + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Compute a KMACXOF256 (NIST SP 800-185 Sec 4.3.1) over the content provided on stdin: KMAC + /// as a XOF, with the output length not bound in, so any length streams. Requires the + /// output length in bytes. Nothing is verified; a reader squeezes the same stream. + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + KMACXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + /// The key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the key (raw bytes; hex with --hex-input). + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-DSA-44 signature algorithm. MLDSA44 { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1428,21 +2006,31 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1453,21 +2041,31 @@ enum Subcommands { action: MLDSAAction, #[arg(long)] - /// The file containing context string (in hex) for signing or verifying + /// The file containing the context string (raw bytes; hex with --hex-input) for signing or verifying ctxfile: Option, #[arg(long)] - /// The private key file (in hex or binary) for signing + /// The private key file (raw bytes; hex with --hex-input) for signing skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for verifying + /// The public key file (raw bytes; hex with --hex-input) for verifying pkfile: Option, #[arg(long)] - /// The signature value file (in hex or binary) for verifying + /// The signature value file (raw bytes; hex with --hex-input) for verifying sigfile: Option, + #[arg(long)] + /// The message representative file (64 bytes; raw bytes; hex with --hex-input) for + /// sign-mu and verify-mu; stdin without it. + mufile: Option, + + #[arg(long)] + /// Sign with the deterministic variant (FIPS 204 Section 3.4: rnd all zero) rather than + /// the hedged default. + deterministic: bool, + #[arg(short)] /// Output in hex format. x: bool, @@ -1478,15 +2076,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1499,15 +2097,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1520,15 +2118,15 @@ enum Subcommands { action: mlkem_cmd::MLKEMAction, #[arg(long)] - /// The private key file (in hex or binary) for decaps + /// The private key file (raw bytes; hex with --hex-input) for decaps skfile: Option, #[arg(long)] - /// The public key file (in hex or binary) for encaps + /// The public key file (raw bytes; hex with --hex-input) for encaps pkfile: Option, #[arg(long)] - /// The ciphertext value file (in hex or binary) either for encaps to output to, or for decaps to read from. + /// The ciphertext value file (raw bytes; hex with --hex-input) either for encaps to output to, or for decaps to read from. ctfile: Option, #[arg(short)] @@ -1549,9 +2147,13 @@ enum Subcommands { block_size: usize, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1567,21 +2169,81 @@ enum Subcommands { block_size: usize, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, }, - /// Generate cryptographically-secure random bytes, seeded from the operating system's entropy source (/dev/random or equivalent). - /// Uses the library's default 256-bit secure RNG algorithm. + /// Perform ParallelHashXOF128 (NIST SP 800-185 Sec 6.3.1): ParallelHash as a XOF, with + /// the output length not bound in. See parallelhash128 for the block size. + PARALLELHASHXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform ParallelHashXOF256 (NIST SP 800-185 Sec 6.3.1): ParallelHash as a XOF, with + /// the output length not bound in. See parallelhash128 for the block size. + PARALLELHASHXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Generate cryptographically-secure random bytes from a Hash_DRBG (SP 800-90A Rev. 1), seeded + /// from the operating system's entropy source (/dev/random or equivalent) unless --seed-file. RNG { /// Number of bytes to generate. If omitted, it will stream continuously until the process is terminated. #[arg(short, long)] len: Option, + /// The hash under the DRBG: sha512 is a 256-bit instance, sha256 a 128-bit one. + #[arg(long, value_enum, default_value_t = DrbgHash::Sha512)] + hash: DrbgHash, + + /// HAZARDOUS: instantiate the DRBG from this file's bytes (raw; hex with --hex-input) + /// as its entropy input instead of the operating system, so the same file gives the + /// same stream. For reproducible test data only; the output is as secret as the file. + #[arg(long)] + seed_file: Option, + #[arg(short)] /// Output in hex format. x: bool, @@ -1603,7 +2265,7 @@ enum Subcommands { x: bool, }, - /// Perform SHA3-256 of the content provided on stdin. + /// Perform SHA3-384 of the content provided on stdin. /// Supports streaming update for low memory footprint. SHA3_384 { #[arg(short)] @@ -1611,7 +2273,7 @@ enum Subcommands { x: bool, }, - /// Perform SHA3-256 of the content provided on stdin. + /// Perform SHA3-512 of the content provided on stdin. /// Supports streaming update for low memory footprint. SHA3_512 { #[arg(short)] @@ -1709,9 +2371,13 @@ enum Subcommands { elements: Vec, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, @@ -1727,13 +2393,63 @@ enum Subcommands { elements: Vec, #[arg(short = 's', long)] - /// Customization string. + /// Customization string S, as the characters given. customization: Option, + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + #[arg(short)] /// Output the hashes in hex format. x: bool, }, + + /// Perform TupleHashXOF128 (NIST SP 800-185 Sec 5.3.1): TupleHash as a XOF, with the + /// output length not bound in. See tuplehash128 for the tuple. + TUPLEHASHXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform TupleHashXOF256 (NIST SP 800-185 Sec 5.3.1): TupleHash as a XOF, with the + /// output length not bound in. See tuplehash128 for the tuple. + TUPLEHASHXOF256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string S, as the characters given. + customization: Option, + + #[arg(long)] + /// Customization string S in hex, for bytes that are not text. + customization_hex: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, } fn main() { @@ -1748,6 +2464,7 @@ fn main() { fn run() { let cli = Cli::parse(); + helpers::set_hex_input(cli.hex_input); match &cli.subcommands { Some(Subcommands::AES128_CBC { direction, key, key_file, iv, iv_file, x }) => { @@ -1876,12 +2593,22 @@ fn run() { }) => { aes_gcm_cmd::aes256_gcm_cmd(direction, key, key_file, iv, iv_file, aad, aad_file, *x); } - Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, direction, x }) => { + Some(Subcommands::AsconAEAD128 { + key, + key_file, + nonce, + nonce_file, + aad, + aad_file, + direction, + x, + }) => { let decrypt = matches!(direction, CipherDirection::Decrypt); - ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, decrypt, *x); + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, aad, aad_file, decrypt, *x); } - Some(Subcommands::AsconCXOF128 { length, customization, x }) => { - ascon_cmd::cxof128_cmd(customization, *length, *x); + Some(Subcommands::AsconCXOF128 { length, customization, customization_hex, x }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + ascon_cmd::cxof128_cmd(&s, *length, *x); } Some(Subcommands::AsconHash256 { x }) => { ascon_cmd::hash256_cmd(*x); @@ -1895,14 +2622,71 @@ fn run() { Some(Subcommands::Base64Encode) => { encoders_cmd::base64_encode_cmd(); } - Some(Subcommands::HashMLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa44_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::HashMLDSA44 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA44_SET, + action, + &files, + Some(*hash), + *deterministic, + *x, + ); } - Some(Subcommands::HashMLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa65_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::HashMLDSA65 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA65_SET, + action, + &files, + Some(*hash), + *deterministic, + *x, + ); } - Some(Subcommands::HashMLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::hash_mldsa87_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::HashMLDSA87 { + action, + hash, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd( + &mldsa_cmd::MLDSA87_SET, + action, + &files, + Some(*hash), + *deterministic, + *x, + ); } Some(Subcommands::HexDecode) => { encoders_cmd::hex_decode_cmd(); @@ -1920,8 +2704,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA256", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA256, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HKDF_SHA384 { salt, @@ -1933,8 +2724,15 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA384", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA384, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HKDF_SHA512 { salt, @@ -1946,8 +2744,35 @@ fn run() { len, x, }) => hkdf_cmd::hkdf_cmd( - "HKDF-SHA512", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, - *len, *x, + HKDFVariant::SHA512, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, + ), + Some(Subcommands::HKDF_SM3 { + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + len, + x, + }) => hkdf_cmd::hkdf_cmd( + HKDFVariant::SM3, + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + *len, + *x, ), Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) @@ -1964,48 +2789,115 @@ fn run() { Some(Subcommands::HMAC_SM3 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SM3, key, key_file, verify, *x) } - Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(128, *length, customization, key, key_file, verify, *x) + Some(Subcommands::KMAC128 { + length, + customization, + customization_hex, + key, + key_file, + verify, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmac_cmd(Strength::Bits128, *length, &s, key, key_file, verify, *x) } - Some(Subcommands::KMAC256 { length, customization, key, key_file, verify, x }) => { - mac_cmd::kmac_cmd(256, *length, customization, key, key_file, verify, *x) + Some(Subcommands::KMAC256 { + length, + customization, + customization_hex, + key, + key_file, + verify, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmac_cmd(Strength::Bits256, *length, &s, key, key_file, verify, *x) } - Some(Subcommands::MLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa44_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::MLDSA44 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA44_SET, action, &files, None, *deterministic, *x); } - Some(Subcommands::MLDSA65 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa65_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::MLDSA65 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA65_SET, action, &files, None, *deterministic, *x); } - Some(Subcommands::MLDSA87 { action, ctxfile, skfile, pkfile, sigfile, x }) => { - mldsa_cmd::mldsa87_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); + Some(Subcommands::MLDSA87 { + action, + ctxfile, + skfile, + pkfile, + sigfile, + mufile, + deterministic, + x, + }) => { + let files = + mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; + mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA87_SET, action, &files, None, *deterministic, *x); } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem512_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); } Some(Subcommands::MLKEM768 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem768_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM768_SET, action, skfile, pkfile, ctfile, *x); } Some(Subcommands::MLKEM1024 { action, skfile, pkfile, ctfile, x }) => { - mlkem_cmd::mlkem1024_cmd(action, skfile, pkfile, ctfile, *x); + mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM1024_SET, action, skfile, pkfile, ctfile, *x); } - Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(128, *length, *block_size, customization, *x); + Some(Subcommands::PARALLELHASH128 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhash_cmd(Strength::Bits128, *length, *block_size, &s, *x); + } + Some(Subcommands::PARALLELHASH256 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhash_cmd(Strength::Bits256, *length, *block_size, &s, *x); } - Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { - sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *x); + Some(Subcommands::RNG { len, hash, seed_file, x }) => { + rng_cmd::rng_cmd(*len, *hash, seed_file, *x) } - Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), Some(Subcommands::SHA3_224 { x }) => { - sha3_cmd::sha3_cmd(224, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_224, *x); } Some(Subcommands::SHA3_256 { x }) => { - sha3_cmd::sha3_cmd(256, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_256, *x); } Some(Subcommands::SHA3_384 { x }) => { - sha3_cmd::sha3_cmd(384, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_384, *x); } Some(Subcommands::SHA3_512 { x }) => { - sha3_cmd::sha3_cmd(512, *x); + sha3_cmd::sha3_cmd(SHA3Variant::SHA3_512, *x); } Some(Subcommands::SHA224 { x }) => { sha2_cmd::sha2_cmd(SHA2Variant::SHA224, *x); @@ -2026,19 +2918,153 @@ fn run() { sha2_cmd::sha2_cmd(SHA2Variant::SHA512_256, *x); } Some(Subcommands::SHAKE128 { length, x }) => { - sha3_cmd::shake_cmd(128, *length, *x); + sha3_cmd::shake_cmd(Strength::Bits128, *length, *x); } Some(Subcommands::SHAKE256 { length, x }) => { - sha3_cmd::shake_cmd(256, *length, *x); + sha3_cmd::shake_cmd(Strength::Bits256, *length, *x); } Some(Subcommands::SM3 { x }) => { sm3_cmd::sm3_cmd(*x); } - Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); + Some(Subcommands::TUPLEHASH128 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehash_cmd(Strength::Bits128, *length, elements, &s, *x); + } + Some(Subcommands::TUPLEHASH256 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehash_cmd(Strength::Bits256, *length, elements, &s, *x); + } + Some(Subcommands::AES128_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes128_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES128_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes128_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::AES192_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes192_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES192_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes192_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::AES256_CMAC { key, key_file, verify, x }) => { + mac_cmd::aes256_cmac_cmd(key, key_file, verify, *x) + } + Some(Subcommands::AES256_GMAC { key, key_file, iv, iv_file, verify, x }) => { + mac_cmd::aes256_gmac_cmd(key, key_file, iv, iv_file, verify, *x) + } + Some(Subcommands::CSHAKE128 { + length, + function_name, + customization, + customization_hex, + x, + }) => { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::cshake_cmd(Strength::Bits128, *length, n, &s, *x); + } + Some(Subcommands::KMACXOF128 { + length, + customization, + customization_hex, + key, + key_file, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmacxof_cmd(Strength::Bits128, *length, &s, key, key_file, *x) + } + Some(Subcommands::TUPLEHASHXOF128 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehashxof_cmd(Strength::Bits128, *length, elements, &s, *x); + } + Some(Subcommands::PARALLELHASHXOF128 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhashxof_cmd(Strength::Bits128, *length, *block_size, &s, *x); + } + Some(Subcommands::CSHAKE256 { + length, + function_name, + customization, + customization_hex, + x, + }) => { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::cshake_cmd(Strength::Bits256, *length, n, &s, *x); + } + Some(Subcommands::KMACXOF256 { + length, + customization, + customization_hex, + key, + key_file, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + mac_cmd::kmacxof_cmd(Strength::Bits256, *length, &s, key, key_file, *x) + } + Some(Subcommands::TUPLEHASHXOF256 { + length, + elements, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::tuplehashxof_cmd(Strength::Bits256, *length, elements, &s, *x); + } + Some(Subcommands::PARALLELHASHXOF256 { + length, + block_size, + customization, + customization_hex, + x, + }) => { + let s = sha3_cmd::load_customization(customization, customization_hex); + sha3_cmd::parallelhashxof_cmd(Strength::Bits256, *length, *block_size, &s, *x); + } + Some(Subcommands::HMAC_SHA224 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA224, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA384 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA384, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_224 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_224, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_256 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_256, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA3_384 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_384, key, key_file, verify, *x) } - Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { - sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x); + Some(Subcommands::HMAC_SHA3_512 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA3_512, key, key_file, verify, *x) } None => { eprintln!("No command provided. See -h") diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index b6697b357..e8ee1cf88 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -1,1199 +1,471 @@ -//! Work in progress. -//! TODO: Use generic macros to eliminate duplicated code. - -use crate::helpers::{parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex}; +//! The `mldsa44`, `mldsa65`, `mldsa87` and `hash-mldsa*` subcommands: one body, [`mldsa_cmd`], +//! over the three parameter sets, which differ only in their key types, their lengths and the +//! functions that make and check a key pair. Those come in through a [`ParameterSet`], so a +//! length or a function can no longer be right for two sets and wrong for the third. +//! +//! Private keys and seeds are read through the zeroized secret path; public keys, signatures, +//! contexts and message representatives through the plain one. All of them are raw bytes, or +//! hex with `--hex-input`. + +use crate::helpers::{ + parse_seed, read_input_file, read_input_file_or_stdin, read_secret_file, + read_secret_file_or_stdin, seed_error, stream_stdin, write_bytes_or_hex, +}; +use bouncycastle::core::errors::SignatureError; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; use bouncycastle::core::signature_key::{ - PreHashSigningKey, PreHashVerifyingKey, SigningKey, VerifyingKey, + DeterministicPreHashSigningKey, DeterministicSigningKey, PreHashSigningKey, + PreHashVerifyingKey, SignatureAlgorithm, SigningKey, VerifyingKey, }; use bouncycastle::core::traits::{ - SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, + RNG, SignaturePrivateKey, SignaturePublicKey, SignatureSigner, SignatureVerifier, }; -use bouncycastle::hex; use bouncycastle::mldsa::{ - MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, - MLDSA65, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, - MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, - MLDSATrait, + MLDSA_MU_LEN, MLDSA_SEED_LEN, MLDSA44, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, + MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, + MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, + MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, MLDSATrait, + MuBuilder, }; use bouncycastle::rng::HashDRBG_SHA512; -use bouncycastle::sha2::SHA512; +use bouncycastle::sha2::{SHA256, SHA512}; +use bouncycastle::sha3::{SHAKE128, SHAKE256}; use clap::ValueEnum; -use std::io; -use std::io::Read; +use core::marker::PhantomData; use std::process::exit; #[derive(ValueEnum, Clone, Debug)] pub(crate) enum MLDSAAction { /// Generate and output a new private key Keygen, - /// Generate and output a private key from a seed read from stdin. - /// Accepts either binary or hex. + /// Generate and output a private key from a 32-byte seed read from stdin (or --skfile). KeygenFromSeed, - /// Generate and output a new public key from a private key read from stdin. - /// Accepts either binary or hex. + /// Generate and output a new public key from a private key read from stdin (or --skfile). PkFromSk, - /// Accepts a sk and pk, and checks that they match. + /// Accepts a sk (stdin or --skfile) and a pk (--pkfile), and checks that they match. CheckConsistency, - /// Sign a message read from stdin with a private key file and output the signature. - /// Accepts private key as full or seed, binary or hex. + /// Sign a message read from stdin with the private key in --skfile and output the signature. + /// Accepts the private key as full or seed. Hedged (FIPS 204 Section 3.4) unless + /// --deterministic. Sign, - /// Verify a message read from stdin with a public key file and a signature file - /// Accepts the public key and signature as binary or hex. + /// Verify a message read from stdin against the public key in --pkfile and the signature in + /// --sigfile. Verify, + /// Output the 64-byte message representative mu (FIPS 204 Algorithm 7 line 6) of the + /// message on stdin under the public key in --pkfile and the context, for signing elsewhere + /// with sign-mu. Not for hash-mldsa*, whose representative frames the digest differently. + ComputeMu, + /// Sign a message representative (64 bytes, from --mufile or stdin) with the private key in + /// --skfile: the external-mu interface. The signature verifies as an ordinary one. + SignMu, + /// Verify the signature in --sigfile against a message representative (64 bytes, from + /// --mufile or stdin) under the public key in --pkfile. + VerifyMu, } -pub(crate) fn mldsa44_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA44::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = match parse_seed::(&buf) { - Ok(seed) => seed, - Err(()) => { - eprintln!( - "Error: input could not be parsed as a {} byte seed in either hex or bin", - MLDSA_SEED_LEN - ); - exit(-1); - } - }; - - let (_pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA44::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa44_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } +/// The pre-hash of HashML-DSA (FIPS 204 Section 5.4): the hash must carry at least the +/// parameter set's strength, so SHA-256 and SHAKE128 (RFC 8702's 32-byte digest) fit ML-DSA-44 +/// only. +#[derive(ValueEnum, Clone, Copy, Debug)] +pub(crate) enum PreHashAlg { + Sha256, + Sha512, + Shake128, + Shake256, } -pub(crate) fn mldsa65_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA65::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA65::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa65_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } +/// The files a command names. +pub(crate) struct Files<'a> { + pub(crate) ctx: &'a Option, + pub(crate) sk: &'a Option, + pub(crate) pk: &'a Option, + pub(crate) sig: &'a Option, + pub(crate) mu: &'a Option, } -pub(crate) fn mldsa87_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA87::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA87::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa87_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = - sk.create_signer(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_verifier(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } +/// A parameter set as the command sees it: its name for messages, the algorithm marker the +/// signing traits are keyed by, and the operations the core traits do not carry: the key pair's, +/// and the external-mu interface of the ML-DSA key traits. The three sets below are the only +/// instances. +#[allow(clippy::type_complexity)] // the function-pointer fields are the table's point +pub(crate) struct ParameterSet< + A, + PK, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> { + name: &'static str, + alg: PhantomData, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), SignatureError>, + keygen_from_seed: fn(&KeyMaterial) -> Result<(PK, SK), SignatureError>, + keypair_consistency_check: fn(&PK, &SK) -> Result<(), SignatureError>, + pk_of: fn(&SK) -> PK, + mu_builder: fn(&PK, Option<&[u8]>) -> Result, + sign_mu: fn(&SK, &[u8; MLDSA_MU_LEN], &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError>, + sign_mu_deterministic: fn(&SK, &[u8; MLDSA_MU_LEN]) -> Result<[u8; SIG_LEN], SignatureError>, + verify_mu: fn(&PK, &[u8; MLDSA_MU_LEN], &[u8; SIG_LEN]) -> Result<(), SignatureError>, } -pub(crate) fn hash_mldsa44_sha512_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA44::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa44_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA44::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa44_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); - } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa44_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } +pub(crate) const MLDSA44_SET: ParameterSet< + MLDSA44, + MLDSA44PublicKey, + MLDSA44PrivateKey, + MLDSA44_PK_LEN, + MLDSA44_SK_LEN, + MLDSA44_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-44", + alg: PhantomData, + keygen: MLDSA44::keygen, + keygen_from_seed: MLDSA44::keygen_from_seed, + keypair_consistency_check: MLDSA44::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), +}; - let sig = verifier.do_verify_final(&sig); +pub(crate) const MLDSA65_SET: ParameterSet< + MLDSA65, + MLDSA65PublicKey, + MLDSA65PrivateKey, + MLDSA65_PK_LEN, + MLDSA65_SK_LEN, + MLDSA65_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-65", + alg: PhantomData, + keygen: MLDSA65::keygen, + keygen_from_seed: MLDSA65::keygen_from_seed, + keypair_consistency_check: MLDSA65::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), +}; - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } -} +pub(crate) const MLDSA87_SET: ParameterSet< + MLDSA87, + MLDSA87PublicKey, + MLDSA87PrivateKey, + MLDSA87_PK_LEN, + MLDSA87_SK_LEN, + MLDSA87_SIG_LEN, +> = ParameterSet { + name: "ML-DSA-87", + alg: PhantomData, + keygen: MLDSA87::keygen, + keygen_from_seed: MLDSA87::keygen_from_seed, + keypair_consistency_check: MLDSA87::keypair_consistency_check, + pk_of: |sk| sk.derive_pk(), + mu_builder: |pk, ctx| pk.create_mu_builder(ctx), + sign_mu: |sk, mu, rng| sk.sign_mu(mu, rng), + sign_mu_deterministic: |sk, mu| sk.sign_mu_deterministic(mu), + verify_mu: |pk, mu, sig| pk.verify_mu(mu, sig), +}; -pub(crate) fn hash_mldsa65_sha512_cmd( +/// `prehash` selects HashML-DSA with that hash (FIPS 204 Section 5.4) over pure ML-DSA; +/// `deterministic` the variant with `rnd` all zero (Section 3.4) over the hedged one. +pub(crate) fn mldsa_cmd< + A: SignatureAlgorithm, + PK: SignaturePublicKey + VerifyingKey + PreHashVerifyingKey, + SK: SignaturePrivateKey + + SigningKey + + DeterministicSigningKey + + PreHashSigningKey + + DeterministicPreHashSigningKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, + files: &Files, + prehash: Option, + deterministic: bool, output_hex: bool, ) { match action { MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA65::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()) + .unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let input = read_secret_file_or_stdin(files.sk); + let seed = parse_seed::(input.ref_to_bytes()) + .unwrap_or_else(|e| seed_error::(e)); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); + let sk = parse_sk(set, read_secret_file_or_stdin(files.sk).ref_to_bytes()); + write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa65_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA65::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } + let sk = parse_sk(set, read_secret_file_or_stdin(files.sk).ref_to_bytes()); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + match (set.keypair_consistency_check)(&pk, &sk) { + Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); + eprintln!("Error: the public and private keys do not match."); exit(-1); } } } MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); - exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa65_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); + let sk = parse_sk(set, read_required_secret(files.sk, "skfile").ref_to_bytes()); + let ctx = read_ctx(files.ctx); + let ctx = Some(ctx.as_slice()); + let mut rng = HashDRBG_SHA512::new_from_os(); + match (prehash, deterministic) { + (None, false) => { + let s = sk.create_signer(&mut rng, ctx).unwrap_or_else(|e| signer_error(e)); + sign_stream(s, output_hex) + } + (None, true) => { + let s = sk.create_signer_deterministic(ctx).unwrap_or_else(|e| signer_error(e)); + sign_stream(s, output_hex) + } + (Some(PreHashAlg::Sha256), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha512), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake128), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake256), false) => { + let s = sk.create_ph_signer::(&mut rng, ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha256), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Sha512), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake128), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) + } + (Some(PreHashAlg::Shake256), true) => { + let s = sk.create_ph_signer_deterministic::(ctx); + sign_stream(s.unwrap_or_else(|e| signer_error(e)), output_hex) } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); } - - let sig = signer.do_final().unwrap(); - - write_bytes_or_hex(&sig, output_hex); } MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa65_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { - crate::helpers::println_stdout("Signature is valid."); - } else { - eprintln!("Signature is invalid."); - exit(-1); - } - } - } -} -pub(crate) fn hash_mldsa87_sha512_cmd( - action: &MLDSAAction, - ctxfile: &Option, - skfile: &Option, - pkfile: &Option, - sigfile: &Option, - output_hex: bool, -) { - match action { - MLDSAAction::Keygen => { - let (_pk, sk) = MLDSA87::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLDSAAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.derive_pk().encode(), output_hex); - } - MLDSAAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mldsa87_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLDSA87::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let ctx = read_ctx(files.ctx); + let ctx = Some(ctx.as_slice()); + let sig = read_required_file(files.sig, "sigfile"); + match prehash { + None => { + let v = pk.create_verifier(ctx).unwrap_or_else(|e| signer_error(e)); + verify_stream(v, &sig) + } + Some(PreHashAlg::Sha256) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Sha512) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Shake128) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) + } + Some(PreHashAlg::Shake256) => { + let v = pk.create_ph_verifier::(ctx); + verify_stream(v.unwrap_or_else(|e| signer_error(e)), &sig) } } } - MLDSAAction::Sign => { - // first, read the sk - let sk_bytes = if skfile.is_some() { - read_from_file(skfile.as_ref().unwrap()) - } else { - eprintln!("Error: no skfile provided."); + MLDSAAction::ComputeMu => { + refuse_mu_with_prehash(prehash); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let ctx = read_ctx(files.ctx); + let mut mb = (set.mu_builder)(&pk, Some(&ctx)).unwrap_or_else(|e| signer_error(e)); + stream_stdin(|chunk| mb.do_update(chunk)); + write_bytes_or_hex(&mb.do_final(), output_hex); + } + MLDSAAction::SignMu => { + refuse_mu_with_prehash(prehash); + let sk = parse_sk(set, read_required_secret(files.sk, "skfile").ref_to_bytes()); + let mu = read_mu(files.mu); + let sig = if deterministic { + (set.sign_mu_deterministic)(&sk, &mu) + } else { + (set.sign_mu)(&sk, &mu, &mut HashDRBG_SHA512::new_from_os()) + }; + let sig = sig.unwrap_or_else(|e| { + eprintln!("Error: signing failed: {e:?}"); exit(-1); - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // and now sign, streaming the message from stdin - let sk = match parse_mldsa87_sk(&sk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - let mut signer = sk - .create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), Some(&ctx)) - .unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.do_update(&buf[..bytes_read]); - } - - let sig = signer.do_final().unwrap(); - + }); write_bytes_or_hex(&sig, output_hex); } - MLDSAAction::Verify => { - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); + MLDSAAction::VerifyMu => { + refuse_mu_with_prehash(prehash); + let pk = parse_pk(set, &read_required_file(files.pk, "pkfile")); + let sig = read_required_file(files.sig, "sigfile"); + let sig: &[u8; SIG_LEN] = sig.as_slice().try_into().unwrap_or_else(|_| { + eprintln!( + "Error: the signature is {} bytes; an {} signature is {SIG_LEN}.", + sig.len(), + set.name + ); exit(-1); - }; - let pk = match parse_mldsa87_pk(&pk_bytes) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // then read ctx - let ctx = if ctxfile.is_some() { - read_from_file(ctxfile.as_ref().unwrap()) - } else { - vec![0u8; 0] - }; - - // then read the sig - let sig = if sigfile.is_some() { - read_from_file(sigfile.as_ref().unwrap()) - } else { - eprintln!("Error: no sigfile provided."); - exit(-1); - }; - - // and now verify, streaming the message from stdin - let mut verifier = pk.create_ph_verifier::(Some(&ctx)).unwrap(); - - let mut buf = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - while bytes_read != 0 { - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.do_update(&buf[..bytes_read]); - } - - let sig = verifier.do_verify_final(&sig); - - if sig.is_ok() { + }); + let mu = read_mu(files.mu); + if (set.verify_mu)(&pk, &mu, sig).is_ok() { crate::helpers::println_stdout("Signature is valid."); } else { - eprintln!("Signature is invalid."); + eprintln!("Error: the signature is invalid."); exit(-1); } } } } -fn parse_mldsa44_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA44_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA44_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA44PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// The mu actions are pure ML-DSA's: HashML-DSA's representative frames the digest and the +/// hash's OID (FIPS 204 Algorithm 4 line 23), which `compute-mu` does not produce. +fn refuse_mu_with_prehash(prehash: Option) { + if prehash.is_some() { + eprintln!("Error: the mu actions are for the mldsa* commands, not hash-mldsa*."); + exit(-1); } +} - // try it as a binary full key - if bytes.len() == MLDSA44_SK_LEN { - let sk = MLDSA44PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things +/// The 64-byte message representative from `--mufile` or stdin. +fn read_mu(mufile: &Option) -> [u8; MLDSA_MU_LEN] { + let mu = read_input_file_or_stdin(mufile); + mu.as_slice().try_into().unwrap_or_else(|_| { + eprintln!("Error: mu must be {MLDSA_MU_LEN} bytes, got {}.", mu.len()); + exit(-1); + }) +} - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA44::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } +fn keygen_error(e: SignatureError) -> ! { + eprintln!("Error: key generation failed: {e:?}"); + exit(-1); +} - Err("Error: couldn't parse the input as a valid ML-DSA-44 private key or seed.") +/// A signer or verifier refused to start: the context is over 255 bytes (FIPS 204 Algorithm 2 +/// line 1), the pre-hash is too weak for the parameter set, or the RNG failed. +fn signer_error(e: SignatureError) -> ! { + eprintln!("Error: couldn't start the signature: {e:?}"); + exit(-1); } -fn parse_mldsa44_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order +/// Streams stdin into `signer` and writes the signature. +fn sign_stream(mut signer: impl SignatureSigner, output_hex: bool) { + stream_stdin(|chunk| signer.do_update(chunk)); + let sig = signer.do_final().unwrap_or_else(|e| { + eprintln!("Error: signing failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&sig, output_hex); +} - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA44_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA44_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA44PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } +/// Streams stdin into `verifier` and reports whether `sig` verifies, through the exit status. +fn verify_stream(mut verifier: impl SignatureVerifier, sig: &[u8]) { + stream_stdin(|chunk| verifier.do_update(chunk)); + if verifier.do_verify_final(sig).is_ok() { + crate::helpers::println_stdout("Signature is valid."); + } else { + eprintln!("Error: the signature is invalid."); + exit(-1); } - - // try it as a binary full key - if bytes.len() == MLDSA44_PK_LEN { - let pk = MLDSA44PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-44 public key.") } -fn parse_mldsa65_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order +/// `--ctxfile`'s bytes, or the empty context (FIPS 204's default) without one. +fn read_ctx(ctxfile: &Option) -> Vec { + ctxfile.as_ref().map(|f| read_input_file(f)).unwrap_or_default() +} - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA65_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA65_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA65PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things +/// `--pkfile` and the like where the command cannot fall back to stdin. +fn read_required_file(file: &Option, flag: &str) -> Vec { + match file { + Some(file) => read_input_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); } } - - // try it as a binary full key - if bytes.len() == MLDSA65_SK_LEN { - let sk = MLDSA65PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA65::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-DSA-65 private key or seed.") } -fn parse_mldsa65_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA65_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA65_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA65PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things +/// `--skfile` where signing cannot take the key from stdin, which carries the message. +fn read_required_secret(file: &Option, flag: &str) -> crate::helpers::SecretBytes { + match file { + Some(file) => read_secret_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); } } - - // try it as a binary full key - if bytes.len() == MLDSA65_PK_LEN { - let pk = MLDSA65PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-65 public key or seed.") } -fn parse_mldsa87_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA87_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLDSA87_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLDSA87PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// A private key from its bytes: a full key, or a seed. +fn parse_sk< + A, + PK, + SK: SignaturePrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> SK { + if bytes.len() == SK_LEN + && let Ok(sk) = SK::from_bytes(bytes) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLDSA87_SK_LEN { - let sk = MLDSA87PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLDSA87::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try + if let Ok(seed) = parse_seed::(bytes) + && let Ok((_pk, sk)) = (set.keygen_from_seed)(&seed) + { + return sk; } - - Err("Error: couldn't parse the input as a valid ML-DSA-87 private key.") + eprintln!( + "Error: couldn't parse the input as a valid {} private key or seed (raw bytes; hex needs --hex-input).", + set.name + ); + exit(-1); } -fn parse_mldsa87_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLDSA87_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLDSA87_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLDSA87PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } +/// A public key from its bytes. +fn parse_pk< + A, + PK: SignaturePublicKey, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> PK { + if bytes.len() == PK_LEN + && let Ok(pk) = PK::from_bytes(bytes) + { + return pk; } - - // try it as a binary full key - if bytes.len() == MLDSA87_PK_LEN { - let pk = MLDSA87PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-DSA-87 public key.") + eprintln!( + "Error: couldn't parse the input as a valid {} public key (raw bytes; hex needs --hex-input).", + set.name + ); + exit(-1); } diff --git a/cli/src/mlkem_cmd.rs b/cli/src/mlkem_cmd.rs index 54c09e2ad..eb5ca5faf 100644 --- a/cli/src/mlkem_cmd.rs +++ b/cli/src/mlkem_cmd.rs @@ -1,13 +1,20 @@ -//! Yup, this file is as absolutely atrocious mess of duplicate code that could be much improved -//! by using generics or macros. I just, haven't ... yet. +//! The `mlkem512`, `mlkem768` and `mlkem1024` subcommands: one body, [`mlkem_cmd`], over the +//! three parameter sets, which differ only in their key types, their lengths and the functions +//! that make and check a key pair. Those come in through a [`ParameterSet`], so a length or a +//! function can no longer be right for two sets and wrong for the third. +//! +//! Private keys and seeds are read through the zeroized secret path; public keys and +//! ciphertexts through the plain one. All of them are raw bytes, or hex with `--hex-input`. use crate::helpers::{ - parse_seed, read_from_file, read_from_file_or_stdin, write_bytes_or_hex, - write_bytes_or_hex_to_file, + parse_seed, read_input_file, read_input_file_or_stdin, read_secret_file_or_stdin, seed_error, + write_bytes_or_hex, write_bytes_or_hex_to_file, +}; +use bouncycastle::core::errors::KEMError; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait}; +use bouncycastle::core::traits::{ + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, }; -use bouncycastle::core::key_material::KeyMaterialTrait; -use bouncycastle::core::traits::{KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey}; -use bouncycastle::hex; use bouncycastle::mlkem::{ MLKEM512, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512PrivateKey, MLKEM512PublicKey, MLKEM768, MLKEM768_CT_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN, @@ -18,11 +25,14 @@ use bouncycastle::rng::HashDRBG_SHA512; use clap::ValueEnum; use std::process::exit; +/// The ML-KEM seed is `d || z`, 64 bytes (FIPS 203 Algorithm 19). +const SEED_LEN: usize = 64; + #[derive(ValueEnum, Clone, Debug)] pub(crate) enum MLKEMAction { /// Generate and output a new private key Keygen, - /// Generate and output a private key from a seed read from stdin. + /// Generate and output a private key from a 64-byte seed (d || z) read from stdin. /// Accepts either binary or hex. KeygenFromSeed, /// Generate and output a new public key from a private key read from stdin. @@ -37,262 +47,96 @@ pub(crate) enum MLKEMAction { /// If no ctfile is provided then ct followed by ss are output to stdout in hex, separated by a newline. /// The shared secret is always output to stdout to prevent it /// staying in a file longer than it should and causing a vulnerability. - /// Accepts private key as full or seed, binary or hex. + /// Accepts the public key as binary or hex. Encaps, /// Perform a decapsulation with a private key and ciphertext and output the shared secret key to stdout either in /// binary or hex. /// The ciphertext can be read either from ctfile or stdin, in binary or hex. - /// Accepts the public key and signature as binary or hex. + /// Accepts the private key as full or seed, binary or hex. Decaps, } -pub(crate) fn mlkem512_cmd( - action: &MLKEMAction, - skfile: &Option, - pkfile: &Option, - ctfile: &Option, - output_hex: bool, -) { - match action { - MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM512::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem512_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); - } - MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem512_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem512_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM512::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem512_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM512::encaps(&pk).unwrap(); - - if ctfile.is_some() { - // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } else { - // write both to stdout in hex, separated by a newline. - write_bytes_or_hex(&ct, true); - crate::helpers::write_stdout(b"\n"); - write_bytes_or_hex(ss.ref_to_bytes(), true); - } - } - MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem512_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM512_CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); - exit(-1); - } - - let ct: [u8; MLKEM512_CT_LEN] = ct_vec.try_into().unwrap(); - - let ss = match MLKEM512::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } - } +/// A parameter set as the command sees it: its name for messages, and the operations the key +/// types and the `KEM*` traits do not carry on their own. The three sets below are the only +/// instances. +#[allow(clippy::type_complexity)] // the function-pointer fields are the table's point +pub(crate) struct ParameterSet< + PK, + SK, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +> { + name: &'static str, + keygen: fn(&mut dyn RNG) -> Result<(PK, SK), KEMError>, + keygen_from_seed: fn(&KeyMaterial) -> Result<(PK, SK), KEMError>, + keypair_consistency_check: fn(&PK, &SK) -> Result<(), KEMError>, + pk_of: fn(&SK) -> PK, + encaps: fn(&PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>, + decaps: fn(&SK, &[u8]) -> Result, KEMError>, } -pub(crate) fn mlkem768_cmd( - action: &MLKEMAction, - skfile: &Option, - pkfile: &Option, - ctfile: &Option, - output_hex: bool, -) { - match action { - MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM768::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - write_bytes_or_hex(&sk.encode(), output_hex); - } - MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem768_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); - } - MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem768_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem768_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM768::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } - Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); - exit(-1); - } - } - } - MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem768_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM768::encaps(&pk).unwrap(); - - if ctfile.is_some() { - // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } else { - // write both to stdout in hex, separated by a newline. - write_bytes_or_hex(&ct, true); - crate::helpers::write_stdout(b"\n"); - write_bytes_or_hex(ss.ref_to_bytes(), true); - } - } - MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem768_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM768_CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); - exit(-1); - } - - let ct: [u8; MLKEM768_CT_LEN] = ct_vec.try_into().unwrap(); +pub(crate) const MLKEM512_SET: ParameterSet< + MLKEM512PublicKey, + MLKEM512PrivateKey, + MLKEM512_PK_LEN, + MLKEM512_SK_LEN, + MLKEM512_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-512", + keygen: MLKEM512::keygen, + keygen_from_seed: MLKEM512::keygen_from_seed, + keypair_consistency_check: MLKEM512::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM512::encaps, + decaps: MLKEM512::decaps, +}; - let ss = match MLKEM768::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; +pub(crate) const MLKEM768_SET: ParameterSet< + MLKEM768PublicKey, + MLKEM768PrivateKey, + MLKEM768_PK_LEN, + MLKEM768_SK_LEN, + MLKEM768_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-768", + keygen: MLKEM768::keygen, + keygen_from_seed: MLKEM768::keygen_from_seed, + keypair_consistency_check: MLKEM768::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM768::encaps, + decaps: MLKEM768::decaps, +}; - write_bytes_or_hex(ss.ref_to_bytes(), output_hex); - } - } -} +pub(crate) const MLKEM1024_SET: ParameterSet< + MLKEM1024PublicKey, + MLKEM1024PrivateKey, + MLKEM1024_PK_LEN, + MLKEM1024_SK_LEN, + MLKEM1024_CT_LEN, + 32, +> = ParameterSet { + name: "ML-KEM-1024", + keygen: MLKEM1024::keygen, + keygen_from_seed: MLKEM1024::keygen_from_seed, + keypair_consistency_check: MLKEM1024::keypair_consistency_check, + pk_of: |sk| sk.pk().clone(), + encaps: MLKEM1024::encaps, + decaps: MLKEM1024::decaps, +}; -pub(crate) fn mlkem1024_cmd( +pub(crate) fn mlkem_cmd< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, action: &MLKEMAction, skfile: &Option, pkfile: &Option, @@ -301,79 +145,41 @@ pub(crate) fn mlkem1024_cmd( ) { match action { MLKEMAction::Keygen => { - let (_pk, sk) = MLKEM512::keygen(&mut HashDRBG_SHA512::new_from_os()).unwrap(); + let (_pk, sk) = (set.keygen)(&mut HashDRBG_SHA512::new_from_os()) + .unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::KeygenFromSeed => { - let buf = read_from_file_or_stdin(skfile); - let seed = parse_seed(&buf).unwrap(); - - let (_pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let input = read_secret_file_or_stdin(skfile); + let seed = parse_seed::(input.ref_to_bytes()) + .unwrap_or_else(|e| seed_error::(e)); + let (_pk, sk) = (set.keygen_from_seed)(&seed).unwrap_or_else(|e| keygen_error(e)); write_bytes_or_hex(&sk.encode(), output_hex); } MLKEMAction::PkFromSk => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem1024_sk(&buf) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - write_bytes_or_hex(&sk.pk().encode(), output_hex); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); + write_bytes_or_hex(&(set.pk_of)(&sk).encode(), output_hex); } MLKEMAction::CheckConsistency => { - let buf = read_from_file_or_stdin(skfile); - let sk = match parse_mlkem1024_sk(buf.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - // first, read the pk - let pk_bytes = if pkfile.is_some() { - read_from_file(pkfile.as_ref().unwrap()) - } else { - eprintln!("Error: no pkfile provided."); - exit(-1); - }; - let pk = match parse_mlkem1024_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - match MLKEM1024::keypair_consistency_check(&pk, &sk) { - Ok(_) => { - crate::helpers::println_stdout("SUCCESS: pk and sk match."); - } + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); + let pk = parse_pk(set, &read_required_file(pkfile, "pkfile")); + match (set.keypair_consistency_check)(&pk, &sk) { + Ok(_) => crate::helpers::println_stdout("SUCCESS: pk and sk match."), Err(_) => { - eprintln!("FAILURE: pk and sk do not match."); + eprintln!("Error: the public and private keys do not match."); exit(-1); } } } MLKEMAction::Encaps => { - // first, read the pk - let pk_bytes = read_from_file_or_stdin(pkfile); - let pk = match parse_mlkem1024_pk(pk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let (ss, ct) = MLKEM1024::encaps(&pk).unwrap(); - - if ctfile.is_some() { + let pk = parse_pk(set, &read_input_file_or_stdin(pkfile)); + let (ss, ct) = (set.encaps)(&pk).unwrap_or_else(|e| { + eprintln!("Error: encapsulation failed: {e:?}"); + exit(-1); + }); + if let Some(ctfile) = ctfile { // write ct to file and ss to stdout - write_bytes_or_hex_to_file(&ct, ctfile.as_ref().unwrap(), output_hex); + write_bytes_or_hex_to_file(&ct, ctfile, output_hex); write_bytes_or_hex(ss.ref_to_bytes(), output_hex); } else { // write both to stdout in hex, separated by a newline. @@ -383,220 +189,90 @@ pub(crate) fn mlkem1024_cmd( } } MLKEMAction::Decaps => { - // first, read the sk - let sk_bytes = read_from_file_or_stdin(skfile); - - let sk = match parse_mlkem1024_sk(sk_bytes.as_slice()) { - Ok(sk) => sk, - Err(estr) => { - eprintln!("{}", estr); - exit(-1); - } - }; - - let ct_vec = read_from_file_or_stdin(ctfile); - if ct_vec.len() != MLKEM1024_CT_LEN { - eprintln!("Error: ciphertexts is not the correct length."); + let sk = parse_sk(set, read_secret_file_or_stdin(skfile).ref_to_bytes()); + let ct = read_input_file_or_stdin(ctfile); + if ct.len() != CT_LEN { + eprintln!( + "Error: the ciphertext is {} bytes; an {} ciphertext is {CT_LEN}.", + ct.len(), + set.name + ); exit(-1); } - - let ct: [u8; MLKEM1024_CT_LEN] = ct_vec.try_into().unwrap(); - - let ss = match MLKEM1024::decaps(&sk, &ct) { - Ok(ss) => ss, - Err(estr) => { - eprintln!("{:?}", estr); - exit(-1); - } - }; - + let ss = (set.decaps)(&sk, &ct).unwrap_or_else(|e| { + eprintln!("Error: decapsulation failed: {e:?}"); + exit(-1); + }); write_bytes_or_hex(ss.ref_to_bytes(), output_hex); } } } -fn parse_mlkem512_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM512_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM512_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM512PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } - } - - // try it as a binary full key - if bytes.len() == MLKEM512_SK_LEN { - let sk = MLKEM512PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM512::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-KEM-512 private key or seed.") +fn keygen_error(e: KEMError) -> ! { + eprintln!("Error: key generation failed: {e:?}"); + exit(-1); } -fn parse_mlkem512_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM512_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM512_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM512PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things +/// `--pkfile` and the like where the command cannot fall back to stdin. +fn read_required_file(file: &Option, flag: &str) -> Vec { + match file { + Some(file) => read_input_file(file), + None => { + eprintln!("Error: no {flag} provided."); + exit(-1); } } - - // try it as a binary full key - if bytes.len() == MLKEM512_PK_LEN { - let pk = MLKEM512PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-768 public key.") } -fn parse_mlkem768_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM768_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM768_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM768PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } +/// A private key from its bytes: a full key, or a seed. +fn parse_sk< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> SK { + if bytes.len() == SK_LEN + && let Ok(sk) = SK::from_bytes(bytes) + { + return sk; } - - // try it as a binary full key - if bytes.len() == MLKEM768_SK_LEN { - let sk = MLKEM768PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM768::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try + if let Ok(seed) = parse_seed::(bytes) + && let Ok((_pk, sk)) = (set.keygen_from_seed)(&seed) + { + return sk; } - - Err("Error: couldn't parse the input as a valid ML-KEM-768 private key or seed.") -} - -fn parse_mlkem768_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM768_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM512_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM768PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } - } - - // try it as a binary full key - if bytes.len() == MLKEM768_PK_LEN { - let pk = MLKEM768PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-768 public key.") -} - -fn parse_mlkem1024_sk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM1024_SK_LEN { - let maybe_sk = hex::decode(&bytes[..2 * MLKEM1024_SK_LEN]); - if maybe_sk.is_ok() { - // it was hex - let sk = MLKEM1024PrivateKey::from_bytes(&maybe_sk.unwrap()); - if sk.is_ok() { - return Ok(sk.unwrap()); - } // else: keep trying things - } - } - - // try it as a binary full key - if bytes.len() == MLKEM1024_SK_LEN { - let sk = MLKEM1024PrivateKey::from_bytes(&bytes); - if sk.is_ok() { - return Ok(sk.unwrap()); - } - } // else: keep trying things - - // try it as a seed - let seed = parse_seed(bytes); - if seed.is_ok() { - let maybe_sk = MLKEM1024::keygen_from_seed(&seed.unwrap()); - if maybe_sk.is_ok() { - let (_pk, sk) = maybe_sk.unwrap(); - return Ok(sk); - } // else: we're out of things to try - } - - Err("Error: couldn't parse the input as a valid ML-KEM-1024 private key or seed.") + eprintln!( + "Error: couldn't parse the input as a valid {} private key or seed (raw bytes; hex needs --hex-input).", + set.name + ); + exit(-1); } -fn parse_mlkem1024_pk(bytes: &[u8]) -> Result { - // try it in Biggest -> Smallest order - - // try it as a hex'd full key - if bytes.len() >= 2 * MLKEM1024_PK_LEN { - let maybe_pk = hex::decode(&bytes[..2 * MLKEM1024_PK_LEN]); - if maybe_pk.is_ok() { - // it was hex - let pk = MLKEM1024PublicKey::from_bytes(&maybe_pk.unwrap()); - if pk.is_ok() { - return Ok(pk.unwrap()); - } // else: keep trying things - } +/// A public key from its bytes. +fn parse_pk< + PK: KEMPublicKey, + SK: KEMPrivateKey, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +>( + set: &ParameterSet, + bytes: &[u8], +) -> PK { + if bytes.len() == PK_LEN + && let Ok(pk) = PK::from_bytes(bytes) + { + return pk; } - - // try it as a binary full key - if bytes.len() == MLKEM1024_PK_LEN { - let pk = MLKEM1024PublicKey::from_bytes(&bytes); - if pk.is_ok() { - return Ok(pk.unwrap()); - } - } // else: we're out of things to try - - Err("Error: couldn't parse the input as a valid ML-KEM-1024 public key.") + eprintln!( + "Error: couldn't parse the input as a valid {} public key (raw bytes; hex needs --hex-input).", + set.name + ); + exit(-1); } diff --git a/cli/src/rng_cmd.rs b/cli/src/rng_cmd.rs index a05d9d751..83bd6e50d 100644 --- a/cli/src/rng_cmd.rs +++ b/cli/src/rng_cmd.rs @@ -1,17 +1,74 @@ +//! The `rng` subcommand: bytes from a Hash_DRBG (SP 800-90A Rev. 1), seeded from the operating +//! system, or from a file for a reproducible stream. + +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle::core::security_strength::SecurityStrength; use bouncycastle::core::traits::RNG; -use bouncycastle::factory::AlgorithmFactory; -use bouncycastle::factory::rng_factory::RNGFactory; +use bouncycastle::rng::hazmat::NewUninitialized; +use bouncycastle::rng::{HashDRBG_SHA256, HashDRBG_SHA512, Sp80090ADrbg}; +use clap::ValueEnum; +use std::process::exit; + +use crate::helpers::{CHUNK_LEN, read_secret_file, write_bytes_or_hex, write_stdout}; + +/// The hash under the DRBG: SHA-256 gives a 128-bit instance, SHA-512 a 256-bit one. +#[derive(ValueEnum, Clone, Copy, Debug)] +pub(crate) enum DrbgHash { + Sha256, + Sha512, +} + +pub(crate) fn rng_cmd( + len: Option, + hash: DrbgHash, + seed_file: &Option, + output_hex: bool, +) { + match (hash, seed_file) { + (DrbgHash::Sha256, None) => generate(HashDRBG_SHA256::new_from_os(), len, output_hex), + (DrbgHash::Sha512, None) => generate(HashDRBG_SHA512::new_from_os(), len, output_hex), + (DrbgHash::Sha256, Some(file)) => { + generate(seeded::(file, SecurityStrength::_128bit), len, output_hex) + } + (DrbgHash::Sha512, Some(file)) => { + generate(seeded::(file, SecurityStrength::_256bit), len, output_hex) + } + } +} -use crate::helpers::write_bytes_or_hex; +/// A DRBG instantiated from the bytes of `file` as its entropy input, with an empty nonce and +/// personalization string (SP 800-90A Rev. 1 Section 10.1.1.2): the same file gives the same +/// stream, which is the point, and the reason the help calls it hazardous. The seed must carry +/// at least the instance's strength, which the library checks by its length. +fn seeded(file: &str, strength: SecurityStrength) -> D { + let mut seed = read_secret_file(file); + do_hazardous_operations(&mut seed, |seed| seed.set_key_type(KeyType::Seed)).unwrap_or_else( + |e| { + eprintln!("Error: couldn't use the seed file: {e:?}"); + exit(-1); + }, + ); + let mut drbg = D::new_uninitialized(); + drbg.instantiate(false, seed, &KeyMaterial::<0>::new(), &[], strength).unwrap_or_else(|e| { + eprintln!( + "Error: couldn't instantiate the DRBG from the seed file (it must carry {strength:?} of entropy, by its length): {e:?}" + ); + exit(-1); + }); + drbg +} -pub(crate) fn rng_cmd(len: Option, output_hex: bool) { - let mut rng = RNGFactory::default_256_bit(); - let mut buf = vec![0u8; 1024]; +fn generate(mut rng: impl RNG, len: Option, output_hex: bool) { + let mut buf = vec![0u8; CHUNK_LEN]; let loop_forever = len.is_none(); let mut bytes_left_to_write = len.unwrap_or(u32::MAX) as usize; while loop_forever || bytes_left_to_write > 0 { - rng.next_bytes_out(&mut buf).unwrap(); + rng.next_bytes_out(&mut buf).unwrap_or_else(|e| { + eprintln!("Error: the DRBG refused to generate: {e:?}"); + exit(-1); + }); if bytes_left_to_write < buf.len() { buf.truncate(bytes_left_to_write); @@ -22,6 +79,6 @@ pub(crate) fn rng_cmd(len: Option, output_hex: bool) { // Only a hex line gets a terminator: raw output is exactly `len` bytes, so that // `rng --len 16 > key.bin` is a 16-byte key and not a 17-byte one. if output_hex { - crate::helpers::write_stdout(b"\n"); + write_stdout(b"\n"); } } diff --git a/cli/src/sha2_cmd.rs b/cli/src/sha2_cmd.rs index f2fb889ad..73794ec60 100644 --- a/cli/src/sha2_cmd.rs +++ b/cli/src/sha2_cmd.rs @@ -1,7 +1,4 @@ -use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; - +use crate::helpers::stream_hash; use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; #[allow(non_camel_case_types)] @@ -16,27 +13,11 @@ pub(crate) enum SHA2Variant { pub(crate) fn sha2_cmd(variant: SHA2Variant, output_hex: bool) { match variant { - SHA2Variant::SHA224 => do_sha2(SHA224::new(), output_hex), - SHA2Variant::SHA256 => do_sha2(SHA256::new(), output_hex), - SHA2Variant::SHA384 => do_sha2(SHA384::new(), output_hex), - SHA2Variant::SHA512 => do_sha2(SHA512::new(), output_hex), - SHA2Variant::SHA512_224 => do_sha2(SHA512_224::new(), output_hex), - SHA2Variant::SHA512_256 => do_sha2(SHA512_256::new(), output_hex), + SHA2Variant::SHA224 => stream_hash(SHA224::new(), output_hex), + SHA2Variant::SHA256 => stream_hash(SHA256::new(), output_hex), + SHA2Variant::SHA384 => stream_hash(SHA384::new(), output_hex), + SHA2Variant::SHA512 => stream_hash(SHA512::new(), output_hex), + SHA2Variant::SHA512_224 => stream_hash(SHA512_224::new(), output_hex), + SHA2Variant::SHA512_256 => stream_hash(SHA512_256::new(), output_hex), } } - -fn do_sha2(mut sha2: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sha2.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = sha2.do_final(); - - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); -} diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index 69858e9cc..6babe84f4 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,31 +1,144 @@ +//! The SHA-3 family subcommands: `sha3-*`, `shake*`, `cshake*`, `tuplehash*`, `tuplehashxof*`, +//! `parallelhash*` and `parallelhashxof*`. + use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; -use bouncycastle::hex; -use bouncycastle::sha3::parallelhash::{ParallelHash128, ParallelHash256}; -use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256}; -use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +use bouncycastle::core::traits::XOF; +use bouncycastle::sha3::parallelhash::{ + ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, +}; +use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; +use bouncycastle::sha3::{ + CSHAKE128, CSHAKE256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, +}; use core::num::NonZeroUsize; use std::process::exit; -use crate::helpers::{stream_hash, stream_xof}; +use crate::helpers::{ + decode_hex_arg, read_stdin_all, squeeze_to_stdout, stream_hash, stream_stdin, stream_xof, +}; + +/// The most a fixed-length SP 800-185 function (KMAC, TupleHash, ParallelHash) will produce +/// here: their output length is bound into the computation before any output, so the library +/// computes it whole, and a request of gigabytes would be an allocation rather than a run. The +/// `*xof` subcommands stream any length. +pub(crate) const MAX_FIXED_OUTPUT_LEN: usize = 1 << 28; + +#[allow(non_camel_case_types)] +pub(crate) enum SHA3Variant { + SHA3_224, + SHA3_256, + SHA3_384, + SHA3_512, +} + +/// The security strength that selects a SHAKE-based function's instance: SHAKE128 or SHAKE256, +/// and with them the 128- and 256-bit KMAC, TupleHash and ParallelHash. +#[derive(Clone, Copy)] +pub(crate) enum Strength { + Bits128, + Bits256, +} + +/// The customization string S of SP 800-185, from `--customization` (the characters) or +/// `--customization-hex` (bytes); empty when neither is given. Both given is an error, since +/// the one would silently win over the other. +pub(crate) fn load_customization(text: &Option, hex: &Option) -> Vec { + match (text, hex) { + (Some(_), Some(_)) => { + eprintln!("Error: give `--customization` or `--customization-hex`, not both."); + exit(-1); + } + (Some(text), None) => text.as_bytes().to_vec(), + (None, Some(hex)) => decode_hex_arg(hex, "customization-hex"), + (None, None) => Vec::new(), + } +} + +fn refuse_long_fixed_output(output_len: usize, xof_command: &str) { + if output_len > MAX_FIXED_OUTPUT_LEN { + eprintln!( + "Error: this output is computed whole; the command takes at most {MAX_FIXED_OUTPUT_LEN} bytes. Use {xof_command} for a long, streamed output." + ); + exit(-1); + } +} + +pub(crate) fn sha3_cmd(variant: SHA3Variant, output_hex: bool) { + match variant { + SHA3Variant::SHA3_224 => stream_hash(SHA3_224::new(), output_hex), + SHA3Variant::SHA3_256 => stream_hash(SHA3_256::new(), output_hex), + SHA3Variant::SHA3_384 => stream_hash(SHA3_384::new(), output_hex), + SHA3Variant::SHA3_512 => stream_hash(SHA3_512::new(), output_hex), + } +} + +pub(crate) fn shake_cmd(strength: Strength, output_len: usize, output_hex: bool) { + match strength { + Strength::Bits128 => stream_xof(SHAKE128::new(), output_len, output_hex), + Strength::Bits256 => stream_xof(SHAKE256::new(), output_len, output_hex), + } +} -pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { - match bit_len { - 224 => stream_hash(SHA3_224::new(), output_hex), - 256 => stream_hash(SHA3_256::new(), output_hex), - 384 => stream_hash(SHA3_384::new(), output_hex), - 512 => stream_hash(SHA3_512::new(), output_hex), - _ => panic!("Unsupported algorithm: SHA3-{}", bit_len), +/// cSHAKE (NIST SP 800-185 Sec 3): SHAKE with a function name N and a customization string S +/// bound in; with both empty it is SHAKE itself. +pub(crate) fn cshake_cmd( + strength: Strength, + output_len: usize, + function_name: &[u8], + customization: &[u8], + output_hex: bool, +) { + match strength { + Strength::Bits128 => { + stream_xof(CSHAKE128::new(function_name, customization), output_len, output_hex) + } + Strength::Bits256 => { + stream_xof(CSHAKE256::new(function_name, customization), output_len, output_hex) + } + } +} + +/// TupleHashXOF (SP 800-185 Sec 5.3.1): TupleHash with the length not bound in, so the output +/// streams at any length. +pub(crate) fn tuplehashxof_cmd( + strength: Strength, + output_len: usize, + elements: &[String], + customization: &[u8], + output_hex: bool, +) { + let tuple = load_tuple(elements); + match strength { + Strength::Bits128 => { + let mut x = TupleHashXOF128::new(customization); + tuple.iter().for_each(|e| x.do_update(e)); + squeeze_to_stdout(x.into_squeezer(), output_len, output_hex); + } + Strength::Bits256 => { + let mut x = TupleHashXOF256::new(customization); + tuple.iter().for_each(|e| x.do_update(e)); + squeeze_to_stdout(x.into_squeezer(), output_len, output_hex); + } } } -pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { - match bit_len { - 128 => stream_xof(SHAKE128::new(), output_len, output_hex), - 256 => stream_xof(SHAKE256::new(), output_len, output_hex), - _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), +/// ParallelHashXOF (SP 800-185 Sec 6.3.1): ParallelHash with the length not bound in. +pub(crate) fn parallelhashxof_cmd( + strength: Strength, + output_len: usize, + block_size: usize, + customization: &[u8], + output_hex: bool, +) { + let block_size = require_block_size(block_size); + match strength { + Strength::Bits128 => { + stream_xof(ParallelHashXOF128::new(block_size, customization), output_len, output_hex) + } + Strength::Bits256 => { + stream_xof(ParallelHashXOF256::new(block_size, customization), output_len, output_hex) + } } } @@ -35,92 +148,76 @@ pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { /// hashed as a single-element tuple -- which is not the same as hashing those bytes with SHAKE, /// because the element is length-prefixed. pub(crate) fn tuplehash_cmd( - bit_len: usize, + strength: Strength, output_len: usize, elements: &[String], - customization: &Option, + customization: &[u8], output_hex: bool, ) { - let s = customization.as_deref().unwrap_or("").as_bytes(); - - // Either the tuple came from flags, or stdin is the single element. - let tuple: Vec> = if elements.is_empty() { - vec![read_stdin()] - } else { - elements - .iter() - .map(|e| { - hex::decode(e).unwrap_or_else(|_| { - eprintln!("Error: --element must be hex."); - exit(-1); - }) - }) - .collect() - }; + refuse_long_fixed_output(output_len, "tuplehashxof*"); + let tuple = load_tuple(elements); let refs: Vec<&[u8]> = tuple.iter().map(|v| v.as_slice()).collect(); - let out = match bit_len { - 128 => TupleHash128::new(s, output_len).hash_tuple(&refs), - 256 => TupleHash256::new(s, output_len).hash_tuple(&refs), - _ => panic!("Unsupported algorithm: TupleHash-{bit_len}"), + let out = match strength { + Strength::Bits128 => TupleHash128::new(customization, output_len) + .unwrap_or_else(|_| output_len_error()) + .hash_tuple(&refs), + Strength::Bits256 => TupleHash256::new(customization, output_len) + .unwrap_or_else(|_| output_len_error()) + .hash_tuple(&refs), }; write_out(&out, output_hex); } +/// The tuple: the `--element` flags decoded, or stdin as the single element. +pub(crate) fn load_tuple(elements: &[String]) -> Vec> { + if elements.is_empty() { + vec![read_stdin_all()] + } else { + elements.iter().map(|e| decode_hex_arg(e, "element")).collect() + } +} + +fn output_len_error() -> ! { + eprintln!("Error: the output length is larger than SP 800-185 allows here (Sec 7.2)."); + exit(-1); +} + /// ParallelHash (NIST SP 800-185 Sec 6): hashes stdin in `block_size`-byte blocks. /// /// The block size is part of the function, not a tuning knob -- the same input under a different /// block size gives an unrelated hash, so it must match on both sides. pub(crate) fn parallelhash_cmd( - bit_len: usize, + strength: Strength, output_len: usize, block_size: usize, - customization: &Option, + customization: &[u8], output_hex: bool, ) { - let Some(block_size) = NonZeroUsize::new(block_size) else { - eprintln!("Error: --block-size must be greater than zero (SP 800-185 Sec 6.2)."); - exit(-1); - }; - let s = customization.as_deref().unwrap_or("").as_bytes(); - match bit_len { - 128 => { - let mut p = ParallelHash128::new(block_size, s, output_len); + refuse_long_fixed_output(output_len, "parallelhashxof*"); + let block_size = require_block_size(block_size); + match strength { + Strength::Bits128 => { + let mut p = ParallelHash128::new(block_size, customization, output_len) + .unwrap_or_else(|_| output_len_error()); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } - 256 => { - let mut p = ParallelHash256::new(block_size, s, output_len); + Strength::Bits256 => { + let mut p = ParallelHash256::new(block_size, customization, output_len) + .unwrap_or_else(|_| output_len_error()); stream_stdin(|chunk| p.do_update(chunk)); write_out(&p.do_final(), output_hex); } - _ => panic!("Unsupported algorithm: ParallelHash-{bit_len}"), } } -/// Reads all of stdin. Used where the whole input must be held anyway (a tuple element). -fn read_stdin() -> Vec { - let mut out = Vec::new(); - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - return out; - } - out.extend_from_slice(&buf[..n]); - } -} - -/// Feeds stdin to `sink` in 1 KiB pieces, so a long input is never held in memory. -fn stream_stdin(mut sink: impl FnMut(&[u8])) { - let mut buf = [0u8; 1024]; - loop { - let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - if n == 0 { - return; - } - sink(&buf[..n]); - } +/// SP 800-185 Sec 6.2: B must be greater than zero. +pub(crate) fn require_block_size(block_size: usize) -> NonZeroUsize { + NonZeroUsize::new(block_size).unwrap_or_else(|| { + eprintln!("Error: --block-size must be greater than zero (SP 800-185 Sec 6.2)."); + exit(-1); + }) } /// Writes the digest as raw bytes or hex, with the trailing newline the other commands emit. diff --git a/cli/src/sm3_cmd.rs b/cli/src/sm3_cmd.rs index c7bc5acdc..f713d2040 100644 --- a/cli/src/sm3_cmd.rs +++ b/cli/src/sm3_cmd.rs @@ -1,22 +1,6 @@ -use bouncycastle::core::traits::Hash; -use std::io; -use std::io::Read; - +use crate::helpers::stream_hash; use bouncycastle::sm3::SM3; pub(crate) fn sm3_cmd(output_hex: bool) { - let mut sm3 = SM3::new(); - let mut buf: [u8; 1024] = [0u8; 1024]; - - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sm3.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = sm3.do_final(); - - crate::helpers::write_bytes_or_hex(&out, output_hex); - crate::helpers::write_stdout(b"\n"); + stream_hash(SM3::new(), output_hex); } diff --git a/cli/tests/lib.sh b/cli/tests/lib.sh index 5e2876526..913987917 100644 --- a/cli/tests/lib.sh +++ b/cli/tests/lib.sh @@ -23,6 +23,13 @@ if [ ! -x "$BC_RUST" ]; then exit 2 fi +# `timeout` is GNU coreutils; macOS has none, but has perl, whose alarm does the same job. +if ! command -v timeout >/dev/null 2>&1; then + timeout() { + perl -e 'alarm shift; exec @ARGV' "$@" + } +fi + SCRATCH_ROOT=/tmp/bc-rust-cli-tests mkdir -p "$SCRATCH_ROOT" SCRATCH="$(mktemp -d "$SCRATCH_ROOT/$(basename "$0" .sh).XXXXXXXX")" diff --git a/cli/tests/test_aes_cbc.sh b/cli/tests/test_aes_cbc.sh index 3a32d1c28..5ad094392 100755 --- a/cli/tests/test_aes_cbc.sh +++ b/cli/tests/test_aes_cbc.sh @@ -94,7 +94,12 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cbc -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done + for form in key.bin.nl; do "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done diff --git a/cli/tests/test_aes_cfb.sh b/cli/tests/test_aes_cfb.sh index a8fa0f4b4..b404ed056 100755 --- a/cli/tests/test_aes_cfb.sh +++ b/cli/tests/test_aes_cfb.sh @@ -120,10 +120,13 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do - "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cfb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key.bin.nl" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "key.bin.nl must load as the same key as key.bin" } test_key_on_the_command_line_matches_the_key_file() { diff --git a/cli/tests/test_aes_cfb8.sh b/cli/tests/test_aes_cfb8.sh index 5592ae809..cf7be8c8d 100755 --- a/cli/tests/test_aes_cfb8.sh +++ b/cli/tests/test_aes_cfb8.sh @@ -104,10 +104,13 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do - "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-cfb8 -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key.bin.nl" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "key.bin.nl must load as the same key as key.bin" } test_key_on_the_command_line_matches_the_key_file() { diff --git a/cli/tests/test_aes_ctr.sh b/cli/tests/test_aes_ctr.sh index 1806b0d18..e93f66673 100755 --- a/cli/tests/test_aes_ctr.sh +++ b/cli/tests/test_aes_ctr.sh @@ -148,7 +148,7 @@ test_each_invocation_uses_a_fresh_nonce() { "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" done - assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" + assert_eq "$(sort -u "$TMP/nonces" | wc -l | tr -d ' ')" 8 "eight encryptions must draw eight distinct nonces" } # ---- keys ------------------------------------------------------------------------------------ @@ -162,7 +162,12 @@ test_key_file_accepts_binary_hex_and_a_trailing_newline() { "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" local form - for form in key.hex key.hex.nl key.bin.nl; do + # The hex forms need --hex-input: a key file is raw bytes unless the flag says otherwise. + for form in key.hex key.hex.nl; do + "$BC_RUST" --hex-input aes128-ctr -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done + for form in key.bin.nl; do "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" done diff --git a/cli/tests/test_aes_ecb.sh b/cli/tests/test_aes_ecb.sh index d80eb76a5..0a56c8f20 100755 --- a/cli/tests/test_aes_ecb.sh +++ b/cli/tests/test_aes_ecb.sh @@ -135,10 +135,14 @@ test_key_file_accepts_hex_and_binary() { unhex "$F1_KEY_128" >"$TMP/key.bin" unhex "$F1_CT_128" >"$TMP/ct" unhex "$F1_PLAINTEXT" >"$TMP/pt" - for form in key.hex key.bin; do - "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" - assert_same "$TMP/pt" "$TMP/rec" "--key-file $form must decrypt the F.1.1 ciphertext" - done + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key.bin" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key-file key.bin must decrypt the F.1.1 ciphertext" + # A hex key file needs --hex-input; without it the 32 ASCII bytes are a wrong-length key. + "$BC_RUST" --hex-input aes128-ecb -d decrypt --key-file "$TMP/key.hex" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--hex-input --key-file key.hex must decrypt the F.1.1 ciphertext" + expect_fail "a hex key file without --hex-input is 32 raw bytes" \ + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key.hex" <"$TMP/ct" + assert_stderr_has "got 32 bytes" } test_a_key_of_the_wrong_length_is_rejected() { diff --git a/cli/tests/test_aes_gcm.sh b/cli/tests/test_aes_gcm.sh index 98c32662a..53adbaa42 100755 --- a/cli/tests/test_aes_gcm.sh +++ b/cli/tests/test_aes_gcm.sh @@ -77,7 +77,7 @@ test_each_invocation_uses_a_fresh_nonce() { "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" done - assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" + assert_eq "$(sort -u "$TMP/nonces" | wc -l | tr -d ' ')" 8 "eight encryptions must draw eight distinct nonces" } test_hex_output_composes_through_hex_decode() { diff --git a/cli/tests/test_aes_mac.sh b/cli/tests/test_aes_mac.sh new file mode 100755 index 000000000..0a76908ab --- /dev/null +++ b/cli/tests/test_aes_mac.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# The aes*-cmac and aes*-gmac subcommands against the ACVP known answers, and their round trips. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ACVP CMAC-AES (bc-test-data/crypto/aes_tdes_vectors/CMAC/CMAC-AES.4014544.{req,rsp}.json), +# tgId 6 tcId 41: AES-128, a 416-byte message, a 16-byte tag. +test_aes128_cmac_matches_acvp() { + local got + unhex 93D255B8DD1D4FEF87188DE84B0D011887DC20BB688B5C26C263D449CD2E34D0A155715EC8F22090C5EB3D1E9DE35A201BA964F571D28605301EABFEAE15DBC4372ACCA52E1CC2EBD1918EEE918D446D61825EA8F36CFA6EFA9F7B8ADDF22253DACEABE151BBA37A5257616C4181A5ABA888F0D068B593E30C1E60D197144233FECF6F3639EDF65152C75D864F068D5B71129E1F2520224A04E5CF480CF542AFDEB173CEDC16BE47A815F08BFF98C8DB676DC6D9C420E4719D4C4CF7349E6642535D1E18E2F4C837EB63400991C09651DC03E77338950DFCC955C2A44DAAAAE81E814C96E9A5DF4EE0FE6ED11DAEC75FF19AA1DF8785C49FF308BDC242A8365A345DB7581F87EB7EE36FF8FC3500048617044D8404DF46D6DA6B1DD523C2BDD9FBA4D12235DF643F1EA6BECE5B9E0B11DDE085008231FF0680EC3784C60A24A13E549A4111E1679D05CFB603E5C7C311F0E4DF4864FC2D96E5E3049C7C690ED5A9A3961EB0FBF5BC28E9FCAFC7E4E98489B3EED5DFCE45D4734B3D079E9086027F7EE8CF4B4CC5DEBABF16AAD0A1E123EBA22D036C8E539273E16B75DAB07C3F >"$TMP/msg" + assert_size "$TMP/msg" 416 "message length" + got=$(hex_out "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -x <"$TMP/msg") + assert_eq "$got" "2c930aaccc775c99e421a0989621947c" "CMAC-AES128 tcId 41" + expect_ok "the tag verifies" "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -v 2c930aaccc775c99e421a0989621947c <"$TMP/msg" + expect_fail "a flipped tag does not" "$BC_RUST" aes128-cmac --key 94cd50a907d427b6b75a643361b8b5de -v 3c930aaccc775c99e421a0989621947c <"$TMP/msg" + assert_stderr_has "MAC verification failed" +} + +# The same file, tgId 24 tcId 185: AES-256, a 416-byte message, a 16-byte tag. +test_aes256_cmac_matches_acvp() { + local got + unhex 9DB5CEAEC709883E16E4474BE66842D184026CDC0BCE6CCDB41187E9F59FD665F473B29B3B47E33695D21DF9E34E09517BE2E14760897C505109F5AB3F1E1034DD2A99EFBABEB4664ACC66881EBED6AABDC70806D3B4908E626D59EFAD2D02FC5A3BB7BAF8C61984E8B5FBF118DFBDABF9A3D9421B6B6F95E592B6D4429C1B45BD7E68DC69D04D960A24724349762019419C8CC873C07BC318218B465D23A85BF8446738F9FF47866B4C02972E87B670D814F7EAF8148A24B761DBB157B9E30CBBA1CB00E6F92D398685DCFEE43324F3D2E2FF413063FCC4454D322638CE0B484BE7CA537B8953D755284EF4D8CF72E988137F63012EF485F4468939D18361D479B5CD561CAC065245016F24E8F00BCE8703AA9AA385FE183EE6FF82DB99D9C54D36E5E39EFAEC32AD22E80047326CF4C865D2AA48AF0B5E996C5109DEB4752E01B75754E5D4345AADF249AEE497858FF0DE3651C7DA7CA48BCAB8D2549A24F7D1CC97CDC15CD2FFB69A84C5539629513FA6A403AC46FA988418B1B7D4EBF432B79D450DE8C53594E2E398C17301BD3C828CBD49CCDC9417DA8092F7F1B4E34E >"$TMP/msg" + got=$(hex_out "$BC_RUST" aes256-cmac --key 5cf7c611d3abddfdf070e0405ed5191610a26cb73951297eae18b9c6c22505c2 -x <"$TMP/msg") + assert_eq "$got" "c23b79870186e3294e7d9a2e2ef0993e" "CMAC-AES256 tcId 185" +} + +# CMAC round trips for the three key lengths: the tag is 16 bytes, verifies, and rejects another +# message, another key and a truncated tag; the key file form agrees with the inline form. +test_cmac_round_trips() { + local bits tag + rng 100 >"$TMP/msg" + rng 100 >"$TMP/other" + for bits in 128 192 256; do + rng $((bits / 8)) >"$TMP/key" + rng $((bits / 8)) >"$TMP/other_key" + tag=$(hex_out "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" 32 "aes$bits-cmac tag length" + assert_eq "$(hex_out "$BC_RUST" "aes$bits-cmac" --key "$(hex "$TMP/key")" -x <"$TMP/msg")" "$tag" "inline key agrees" + expect_ok "verifies" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "not another message" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "$tag" <"$TMP/other" + expect_fail "not another key" "$BC_RUST" "aes$bits-cmac" -k "$TMP/other_key" -v "$tag" <"$TMP/msg" + expect_fail "not a truncated tag" "$BC_RUST" "aes$bits-cmac" -k "$TMP/key" -v "${tag:0:16}" <"$TMP/msg" + done + expect_fail "a wrong-length key" "$BC_RUST" aes128-cmac --key 0011 "$TMP/aad" + got=$(hex_out "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4e -x <"$TMP/aad") + assert_eq "$got" "349c7f0b588598c575bf10b29330a9fb" "GMAC-AES128 tcId 16" + expect_ok "the tag verifies with --iv" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4e -v 349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + expect_ok "and as nonce || tag" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a -v 2920dcb53254a95eaba3dd4e349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + expect_fail "not under another nonce" "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv 2920dcb53254a95eaba3dd4f -v 349c7f0b588598c575bf10b29330a9fb <"$TMP/aad" + unhex 2920dcb53254a95eaba3dd4e >"$TMP/iv" + got=$(hex_out "$BC_RUST" aes128-gmac --key 209730e4737a56343a8bd50013e7cf9a --iv-file "$TMP/iv" -x <"$TMP/aad") + assert_eq "$got" "349c7f0b588598c575bf10b29330a9fb" "the nonce from a raw file" +} + +# The same file, tgId 9 tcId 121: AES-256, 32 bytes of AAD. +test_aes256_gmac_matches_acvp() { + local got + unhex 8AF7DADB6A185623BDDAB8637D6C9D3BAD39FAE4B6833AB41EC5DFB1BEE5A0DD >"$TMP/aad" + got=$(hex_out "$BC_RUST" aes256-gmac --key dc4185d2af1d50b6272e5e2b498200c8c351a16bde94a4033f4d58fdf0211215 --iv fdbba151a8245eaafa7ead03 -x <"$TMP/aad") + assert_eq "$got" "2f762326f3b2bf7e4971eac077958eb8" "GMAC-AES256 tcId 121" +} + +# Without --iv a fresh nonce is drawn and written ahead of the tag, so the output is 28 bytes, +# verifies as nonce || tag, and two runs draw two nonces; the tag is bound to the data. +test_gmac_generates_a_nonce_per_tag() { + local bits out + rng 100 >"$TMP/msg" + for bits in 128 192 256; do + rng $((bits / 8)) >"$TMP/key" + "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" <"$TMP/msg" >"$TMP/out1" + assert_size "$TMP/out1" 29 "nonce || tag and a newline" + out=$(hex_out "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#out}" 56 "hex nonce || tag" + expect_ok "verifies as nonce || tag" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -v "$out" <"$TMP/msg" + expect_ok "and as --iv plus the tag" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" --iv "${out:0:24}" -v "${out:24}" <"$TMP/msg" + expect_fail "not over other data" "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -v "$out" < <(rng 100) + [ "$out" != "$(hex_out "$BC_RUST" "aes$bits-gmac" -k "$TMP/key" -x <"$TMP/msg")" ] || fail "two runs must draw two nonces" + done + rng 16 >"$TMP/key" + expect_fail "a verify value of the wrong shape" "$BC_RUST" aes128-gmac -k "$TMP/key" -v 00 <"$TMP/msg" + assert_stderr_has "nonce" + expect_fail "an IV of the wrong length" "$BC_RUST" aes128-gmac -k "$TMP/key" --iv 0011 <"$TMP/msg" + assert_stderr_has "12" +} + +run_all diff --git a/cli/tests/test_ascon.sh b/cli/tests/test_ascon.sh index c3a6f575d..791041166 100755 --- a/cli/tests/test_ascon.sh +++ b/cli/tests/test_ascon.sh @@ -61,7 +61,7 @@ test_xof128_output_length_is_a_prefix_of_a_longer_squeeze() { test_cxof128_matches_the_kat() { local got unhex 00 >"$TMP/msg" - got=$(hex_out "$BC_RUST" ascon-cxof128 64 --customization 10 -x <"$TMP/msg") + got=$(hex_out "$BC_RUST" ascon-cxof128 64 --customization-hex 10 -x <"$TMP/msg") assert_eq "$got" \ "63fa8ba86382f2d544580f51322d080424b42c556eb74503cd73cf052bb993bd6f5210984c71c9c445f43ccc5b158226e509bd339cd634414377f79411aa8d5c" \ "customized squeeze" @@ -72,7 +72,7 @@ test_cxof128_matches_the_kat() { test_cxof128_with_no_customization_matches_an_empty_one() { local without with_empty without=$(hex_out "$BC_RUST" ascon-cxof128 64 -x "$TMP/key" rng 256 >"$TMP/pt" - "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" - "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/ct" >"$TMP/rec" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/rec" assert_same "$TMP/pt" "$TMP/rec" "the same AD on both sides must round-trip" } @@ -135,9 +135,9 @@ test_aead128_each_invocation_uses_a_fresh_nonce() { test_aead128_wrong_associated_data_is_rejected() { rng 16 >"$TMP/key" rng 64 >"$TMP/pt" - "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" expect_fail "different AD must fail the tag check" \ - "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad cafebabe <"$TMP/ct" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --aad cafebabe <"$TMP/ct" assert_stderr_has "authentication failed" } diff --git a/cli/tests/test_encoders.sh b/cli/tests/test_encoders.sh new file mode 100755 index 000000000..db736e41e --- /dev/null +++ b/cli/tests/test_encoders.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# The hex-encode / hex-decode / base64-encode / base64-decode subcommands against RFC 4648 Sec 10 and the streaming boundaries. +# +# Generated for the 2026-11-03 CLI review (plan 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 4648 Sec 10 (standards/RFC-4648-base64-hex.txt l.652-664): BASE64 of "" .. "foobar", both ways. +test_base64_matches_rfc4648_section_10() { + local got + got=$(printf '%s' '' | "$BC_RUST" base64-encode) + assert_eq "$got" '' "BASE64('')" + got=$(printf '%s' '' | "$BC_RUST" base64-decode) + assert_eq "$got" '' "BASE64 decode of ''" + got=$(printf '%s' 'f' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zg==' "BASE64('f')" + got=$(printf '%s' 'Zg==' | "$BC_RUST" base64-decode) + assert_eq "$got" 'f' "BASE64 decode of 'Zg=='" + got=$(printf '%s' 'fo' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm8=' "BASE64('fo')" + got=$(printf '%s' 'Zm8=' | "$BC_RUST" base64-decode) + assert_eq "$got" 'fo' "BASE64 decode of 'Zm8='" + got=$(printf '%s' 'foo' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9v' "BASE64('foo')" + got=$(printf '%s' 'Zm9v' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foo' "BASE64 decode of 'Zm9v'" + got=$(printf '%s' 'foob' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYg==' "BASE64('foob')" + got=$(printf '%s' 'Zm9vYg==' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foob' "BASE64 decode of 'Zm9vYg=='" + got=$(printf '%s' 'fooba' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYmE=' "BASE64('fooba')" + got=$(printf '%s' 'Zm9vYmE=' | "$BC_RUST" base64-decode) + assert_eq "$got" 'fooba' "BASE64 decode of 'Zm9vYmE='" + got=$(printf '%s' 'foobar' | "$BC_RUST" base64-encode) + assert_eq "$got" 'Zm9vYmFy' "BASE64('foobar')" + got=$(printf '%s' 'Zm9vYmFy' | "$BC_RUST" base64-decode) + assert_eq "$got" 'foobar' "BASE64 decode of 'Zm9vYmFy'" +} + +# RFC 4648 Sec 10 (l.701-713): BASE16 of "" .. "foobar". The RFC prints upper-case digits; the CLI +# emits lower-case (RFC 4648 Sec 8: "The encoded output ... case-insensitive" on decode), so the +# comparison lower-cases the RFC value and the decoder is given the upper-case form as printed. +test_base16_matches_rfc4648_section_10() { + local got + got=$(printf '%s' '' | "$BC_RUST" hex-encode) + assert_eq "$got" '' "BASE16('')" + got=$(printf '%s' '' | "$BC_RUST" hex-decode) + assert_eq "$got" '' "BASE16 decode of ''" + got=$(printf '%s' 'f' | "$BC_RUST" hex-encode) + assert_eq "$got" '66' "BASE16('f')" + got=$(printf '%s' '66' | "$BC_RUST" hex-decode) + assert_eq "$got" 'f' "BASE16 decode of '66'" + got=$(printf '%s' 'fo' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f' "BASE16('fo')" + got=$(printf '%s' '666F' | "$BC_RUST" hex-decode) + assert_eq "$got" 'fo' "BASE16 decode of '666F'" + got=$(printf '%s' 'foo' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f' "BASE16('foo')" + got=$(printf '%s' '666F6F' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foo' "BASE16 decode of '666F6F'" + got=$(printf '%s' 'foob' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f62' "BASE16('foob')" + got=$(printf '%s' '666F6F62' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foob' "BASE16 decode of '666F6F62'" + got=$(printf '%s' 'fooba' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f6261' "BASE16('fooba')" + got=$(printf '%s' '666F6F6261' | "$BC_RUST" hex-decode) + assert_eq "$got" 'fooba' "BASE16 decode of '666F6F6261'" + got=$(printf '%s' 'foobar' | "$BC_RUST" hex-encode) + assert_eq "$got" '666f6f626172' "BASE16('foobar')" + got=$(printf '%s' '666F6F626172' | "$BC_RUST" hex-decode) + assert_eq "$got" 'foobar' "BASE16 decode of '666F6F626172'" +} + +# A chunk boundary (1 KiB reads) may fall between the two digits of a byte, inside a `\x` escape +# or after a backslash; the decoder must carry the partial token into the next read. +test_hex_decode_carries_partial_tokens_across_the_chunk_boundary() { + local got + { printf '00%.0s' $(seq 511); printf '41'; } >"$TMP/pair-split" # '4' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/pair-split" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "digit pair split across reads" + { printf '00%.0s' $(seq 511); printf ' \\x41'; } >"$TMP/bs-last" # '\' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/bs-last" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "backslash last in a read" + { printf '00%.0s' $(seq 511); printf '\\x41'; } >"$TMP/x-last" # '\x' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/x-last" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "escape split after the x" + { printf '00%.0s' $(seq 510); printf ' \\x4'; printf '1'; } >"$TMP/digit-split" # '\x4' ends the first read + got=$("$BC_RUST" hex-decode <"$TMP/digit-split" | tail -c 1 | "$BC_RUST" hex-encode) + assert_eq "$got" "41" "escape split inside its digits" +} + +# Round trips of random data through both encoders at sizes around the 1 KiB read and the +# 3-byte / 2-digit group boundaries. +test_round_trips_across_chunk_boundaries() { + local size + for size in 0 1 2 3 4 1023 1024 1025 1026 3071 3072 3073 65536; do + rng "$size" >"$TMP/in" + "$BC_RUST" base64-encode <"$TMP/in" | "$BC_RUST" base64-decode >"$TMP/out" + assert_same "$TMP/in" "$TMP/out" "base64 round trip of $size bytes" + "$BC_RUST" hex-encode <"$TMP/in" | "$BC_RUST" hex-decode >"$TMP/out" + assert_same "$TMP/in" "$TMP/out" "hex round trip of $size bytes" + done +} + +test_malformed_hex_is_rejected() { + expect_fail "odd digit count" "$BC_RUST" hex-decode < <(printf '0') + assert_stderr_has "OddLengthInput" + expect_fail "non-hex character" "$BC_RUST" hex-decode < <(printf 'zz') + assert_stderr_has "InvalidHexCharacter" + expect_fail "lone trailing backslash" "$BC_RUST" hex-decode < <(printf '\\') + assert_stderr_has "backslash" +} + +# RFC 4648 Sec 3.3: "non-alphabet characters ... should be rejected"; and after the padding the +# message is over. Both must be rejected whether the offending bytes arrive in a later read or in +# the same read as the padding. +test_base64_rejects_data_after_the_padding() { + expect_fail "data after padding, next read" "$BC_RUST" base64-decode < <(printf 'QUJD%.0s' $(seq 256); printf 'QUI='; printf 'ZZZZ') + assert_stderr_has "not valid base64" + expect_fail "data after padding, same read" "$BC_RUST" base64-decode < <(printf 'QUI=XXXX') + assert_stderr_has "not valid base64" + expect_fail "two padded blocks in one read" "$BC_RUST" base64-decode < <(printf 'QUI=\nQUI=') +} + +# RFC 4648 Sec 3.3: a final quantum of a single symbol cannot be valid; it must be rejected rather +# than decoded to bytes made from stale bits. +test_base64_rejects_a_single_trailing_symbol() { + expect_fail "one symbol" "$BC_RUST" base64-decode < <(printf 'Q') + expect_fail "one symbol then padding" "$BC_RUST" base64-decode < <(printf 'Q===') +} + +run_all diff --git a/cli/tests/test_hkdf.sh b/cli/tests/test_hkdf.sh index 740879144..4434c34d0 100755 --- a/cli/tests/test_hkdf.sh +++ b/cli/tests/test_hkdf.sh @@ -4,7 +4,8 @@ # the same output as the same values passed as hex. # # The known answers: HKDF-SHA256 is RFC 5869 Appendix A.1; HKDF-SHA384 and HKDF-SHA512 are -# tcId 11 of Wycheproof's testvectors_v1/hkdf_sha384_test.json and hkdf_sha512_test.json. +# tcId 11 of Wycheproof's testvectors_v1/hkdf_sha384_test.json and hkdf_sha512_test.json; +# HKDF-SM3, which has no published vectors, is RFC 5869 A.1's inputs through OpenSSL 3.0.13. source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" @@ -58,6 +59,18 @@ test_hkdf_sha512_matches_wycheproof_tc11() { "Wycheproof hkdf_sha512 tcId 11 OKM" } +test_hkdf_sm3_matches_openssl_on_rfc5869_a1_inputs() { + local got + got=$(hex_out "$BC_RUST" hkdf-sm3 \ + --salt 000102030405060708090a0b0c \ + --ikm 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b \ + --additional-input f0f1f2f3f4f5f6f7f8f9 \ + --len 42 -x) + assert_eq "$got" \ + "c69fe91b7aaee2dd5718d72dcaee0cce93f1b8e41f792da51261b6a517e68b36ed2c595572b01dfa359b" \ + "HKDF-SM3 OKM" +} + # ---- file inputs -------------------------------------------------------------------------- # `hkdf_files_match_hex SUBCOMMAND` derives from random inputs passed once as hex and once as @@ -86,6 +99,10 @@ test_hkdf_sha512_files_match_hex() { hkdf_files_match_hex hkdf-sha512 } +test_hkdf_sm3_files_match_hex() { + hkdf_files_match_hex hkdf-sm3 +} + # When a value is given both as hex and as a file, the file is used, as the help text says. test_hkdf_sha256_file_wins_over_hex() { local from_files with_both @@ -122,4 +139,8 @@ test_hkdf_sha512_one_octet_past_a_block() { hkdf_short_output_is_a_prefix hkdf-sha512 65 } +test_hkdf_sm3_one_octet_past_a_block() { + hkdf_short_output_is_a_prefix hkdf-sm3 33 +} + run_all diff --git a/cli/tests/test_hmac_kat.sh b/cli/tests/test_hmac_kat.sh new file mode 100755 index 000000000..c2339aa80 --- /dev/null +++ b/cli/tests/test_hmac_kat.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# The hmac-* subcommands against RFC 4231 and Wycheproof known answers (test_hmac.sh covers the random round trips). +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 4231 Sec 4.2 Test Case 1 (standards/RFC-4231-HMAC-vectors.txt): 20-byte key, 8-byte data. +test_rfc4231_case_1() { + local got + unhex 4869205468657265 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" "HMAC-SHA-256 case 1" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854" "HMAC-SHA-512 case 1" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify 87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854 <"$TMP/data" + unhex 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" "HMAC-SHA-256 case 1 with the key from a raw file" +} + +# RFC 4231 Sec 4.3 Test Case 2 (standards/RFC-4231-HMAC-vectors.txt): 4-byte key, 28-byte data. +test_rfc4231_case_2() { + local got + unhex 7768617420646f2079612077616e7420666f72206e6f7468696e673f >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 4a656665 -x <"$TMP/data") + assert_eq "$got" "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" "HMAC-SHA-256 case 2" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 4a656665 -x <"$TMP/data") + assert_eq "$got" "164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737" "HMAC-SHA-512 case 2" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 4a656665 --verify 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 4a656665 --verify 164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737 <"$TMP/data" + unhex 4a656665 >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843" "HMAC-SHA-256 case 2 with the key from a raw file" +} + +# RFC 4231 Sec 4.4 Test Case 3 (standards/RFC-4231-HMAC-vectors.txt): 20-byte key, 50-byte data. +test_rfc4231_case_3() { + local got + unhex dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -x <"$TMP/data") + assert_eq "$got" "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" "HMAC-SHA-256 case 3" + got=$(hex_out "$BC_RUST" hmac-sha512 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -x <"$TMP/data") + assert_eq "$got" "fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb" "HMAC-SHA-512 case 3" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --verify 773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --verify fa73b0089d56a284efb0f0756c890be9b1b5dbdd8ee81a3655f83e33b2279d39bf3e848279a722c806b485a47e67c807b946a337bee8942674278859e13292fb <"$TMP/data" + unhex aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe" "HMAC-SHA-256 case 3 with the key from a raw file" +} + +# RFC 4231 Sec 4.5 Test Case 4 (standards/RFC-4231-HMAC-vectors.txt): 25-byte key, 50-byte data. +test_rfc4231_case_4() { + local got + unhex cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 -x <"$TMP/data") + assert_eq "$got" "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b" "HMAC-SHA-256 case 4" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 -x <"$TMP/data") + assert_eq "$got" "b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd" "HMAC-SHA-512 case 4" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha256 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 --verify 82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b <"$TMP/data" + expect_ok "the RFC tag must verify" "$BC_RUST" hmac-sha512 --key 0102030405060708090a0b0c0d0e0f10111213141516171819 --verify b0ba465637458c6990e5a8c5f61d4af7e576d97ff94b872de76f8050361ee3dba91ca5c11aa25eb4d679275cc5788063a5f19741120c4f2de2adebeb10a298dd <"$TMP/data" + unhex 0102030405060708090a0b0c0d0e0f10111213141516171819 >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b" "HMAC-SHA-256 case 4 with the key from a raw file" +} + +# RFC 4231 Sec 4.6 Test Case 5: the RFC truncates the output to 128 bits. The CLI has no truncation +# option, so the full tag's first 16 bytes must equal the RFC value; `--verify` of the truncated +# tag is expected to fail (the CLI compares full tags only). +test_rfc4231_case_5_truncated_output_is_a_prefix() { + local got + unhex 546573742057697468205472756e636174696f6e >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c -x <"$TMP/data") + assert_eq "${got:0:32}" "a3b6167473100ee06e0c796c2955552b" "HMAC-SHA-256 case 5 (first 128 bits)" + got=$(hex_out "$BC_RUST" hmac-sha512 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c -x <"$TMP/data") + assert_eq "${got:0:32}" "415fad6271580a531d4179bc891d87a6" "HMAC-SHA-512 case 5 (first 128 bits)" + expect_fail "a truncated tag is not a full tag" "$BC_RUST" hmac-sha256 --key 0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c --verify a3b6167473100ee06e0c796c2955552b <"$TMP/data" +} + +# RFC 4231 Sec 4.7 Test Case 6: a 131-byte key, longer than the SHA-256 block, which HMAC hashes +# first (RFC 2104 Sec 2). The CLI takes a key of any length up to 16 KiB. +test_rfc4231_case_6_long_key() { + local got key + key=$(printf 'aa%.0s' $(seq 131)) + unhex 54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key "$key" -x <"$TMP/data") + assert_eq "$got" "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" "HMAC-SHA-256 case 6" + got=$(hex_out "$BC_RUST" hmac-sha512 --key "$key" -x <"$TMP/data") + assert_eq "$got" "80b24263c7c1a3ebb71493c1dd7be8b49b46d1f41b4aeec1121b013783f8f3526b56d037e05f2598bd0fd2215d6a1e5295e64f73f63f0aec8b915a985d786598" "HMAC-SHA-512 case 6" + unhex "$key" >"$TMP/key" + got=$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/data") + assert_eq "$got" "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54" "HMAC-SHA-256 case 6 with the key from a raw file" +} + +# RFC 4231 Sec 4.8 Test Case 7: a 131-byte key and a 152-byte message, both over the SHA-512 block. +test_rfc4231_case_7_long_key_and_data() { + local got key + key=$(printf 'aa%.0s' $(seq 131)) + unhex 5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha256 --key "$key" -x <"$TMP/data") + assert_eq "$got" "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2" "HMAC-SHA-256 case 7" + got=$(hex_out "$BC_RUST" hmac-sha512 --key "$key" -x <"$TMP/data") + assert_eq "$got" "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58" "HMAC-SHA-512 case 7" +} + +# RFC 4231 Sec 4.2 Test Case 1 for the hmac-sha224 and hmac-sha384 subcommands. +test_rfc4231_case_1_sha224_and_sha384() { + local got + unhex 4869205468657265 >"$TMP/data" + got=$(hex_out "$BC_RUST" hmac-sha224 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22" "HMAC-SHA-224 case 1" + got=$(hex_out "$BC_RUST" hmac-sha384 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b -x <"$TMP/data") + assert_eq "$got" "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6" "HMAC-SHA-384 case 1" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha224 --key 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b --verify 896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22 <"$TMP/data" +} + +# No published HMAC-SHA3 vector is in bc-test-data, so the four hmac-sha3-* subcommands are pinned +# by shape: the tag length, a verify round trip, and that they differ from each other and from +# HMAC-SHA2 of the same size. +test_hmac_sha3_variants_round_trip() { + local h len tag + rng 32 >"$TMP/key" + rng 300 >"$TMP/msg" + for h in 224 256 384 512; do + tag=$(hex_out "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" $((h / 4)) "hmac-sha3-$h tag length" + expect_ok "hmac-sha3-$h verifies its own tag" "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "and not a flipped one" "$BC_RUST" hmac-sha3-$h -k "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg" + done + assert_eq "$(hex_out "$BC_RUST" hmac-sha3-256 -k "$TMP/key" -x <"$TMP/msg" | cut -c1-16 | wc -c | tr -d ' ')" 17 "hex tag" + [ "$(hex_out "$BC_RUST" hmac-sha3-256 -k "$TMP/key" -x <"$TMP/msg")" != "$(hex_out "$BC_RUST" hmac-sha256 -k "$TMP/key" -x <"$TMP/msg")" ] || fail "HMAC-SHA3-256 must differ from HMAC-SHA-256" +} + +# Wycheproof testvectors_v1/hmac_sha512_224_test.json tcId 3 (keySize 224, tagSize 224): a +# full-length tag, since RFC 4231 has no hmac-sha512-224 vector. +test_hmac_sha512_224_matches_wycheproof_tc3() { + local got + unhex 2e >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sha512-224 --key 8648ee936c6ebc5ae4bb48c1139a54e3ac5d897beec492dc4d740752 -x <"$TMP/msg") + assert_eq "$got" "5979cc5dcdaf49daaadfc3388de3f5a04d183ebc896fd4f9dbdcc513" "hmac-sha512-224 tcId 3" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha512-224 --key 8648ee936c6ebc5ae4bb48c1139a54e3ac5d897beec492dc4d740752 --verify 5979cc5dcdaf49daaadfc3388de3f5a04d183ebc896fd4f9dbdcc513 <"$TMP/msg" +} + +# Wycheproof testvectors_v1/hmac_sha512_256_test.json tcId 3 (keySize 256, tagSize 256): a +# full-length tag, since RFC 4231 has no hmac-sha512-256 vector. +test_hmac_sha512_256_matches_wycheproof_tc3() { + local got + unhex 77 >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sha512-256 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a -x <"$TMP/msg") + assert_eq "$got" "aee7a277e0f36a042368ae52b1337ba13f1c00fdb2dfb1d5f0c304e485c3a7dc" "hmac-sha512-256 tcId 3" + expect_ok "the tag must verify" "$BC_RUST" hmac-sha512-256 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a --verify aee7a277e0f36a042368ae52b1337ba13f1c00fdb2dfb1d5f0c304e485c3a7dc <"$TMP/msg" +} + +# Wycheproof testvectors_v1/hmac_sm3_test.json tcId 2 (keySize 256, tagSize 256): a +# full-length tag, since RFC 4231 has no hmac-sm3 vector. +test_hmac_sm3_matches_wycheproof_tc2() { + local got + unhex 77 >"$TMP/msg" + got=$(hex_out "$BC_RUST" hmac-sm3 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a -x <"$TMP/msg") + assert_eq "$got" "51aaf64843b6d2fc7a244ab9cda9b7b57164ce6a865f9808c01931b46195411d" "hmac-sm3 tcId 2" + expect_ok "the tag must verify" "$BC_RUST" hmac-sm3 --key 8159fd15133cd964c9a6964c94f0ea269a806fd9f43f0da58b6cd1b33d189b2a --verify 51aaf64843b6d2fc7a244ab9cda9b7b57164ce6a865f9808c01931b46195411d <"$TMP/msg" +} + +# A key that is not hex, or a key file that does not exist, must be a reported error, not a panic. +test_bad_key_inputs_are_errors_not_panics() { + expect_fail "non-hex --key" "$BC_RUST" hmac-sha256 --key zz -x "$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa44 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d6412a9e06cd5963dfd8be96baf98b2bb61909b26006eafebd7d20dc9cb611ce7aa590a725e2bacb71eeafc4e057dc20539733b28a789108711f48aa4d947aa225dc4848a9de09ebd59365c287a4646650832225d2712eb0251cadc1f99daf981f101334803937108316843c464c9c44811a59194122d10c30c538401992642e2186ed0160053b6259908401c030ae2382482808c22a16c139524e102900819869128125b082e230772d4c2210c003141920da108212104809cb085e1380518356489c28080c23114314490846911390512a40c41164800823021962d81143061c830a0229022326ed240518824520241320a3782631649c91611c3149020394a024162d842661a132ecc34664a409004b62403074592906d01b64108b12c53a44c81c6619904010934511c1700014592e1b46c4ac610240184e4222520854023218a5c426ee0c44c94806998026d0c092101218c02982d62482420a16d099509400242a2b64942268c212609140044182042111342c0464693444a54a40c63128d00168a494665c2108808990c1b37110c3444d3a011a21460c3a205142844822409ca940d938404999441591870a3b84c8a4265181052e4922c020460c808129a904553944c1481619a04729c045298087022340959400ad0802914038a82182c1ab40554806960b841242670821490004162041020d4965098124122b445c0a08051c44c9ca21149c689c49088001450e4a4850a950919162a1b060212892492466081b24064980d843232da440104001093188511930122116d8a1210c3462ccb084c112604549468113685140484242144a4104150164a5cc00848308990420c113942e434508c90499c9869139721c1169121966580a83012100d63a88c0b054c8a2025d0a808a0043093944904223141b670c418708c924d99480c42188560100604446812100a42c86d08314dd94260d8c448daa82994040911458054b4310c49054920509c286100b070123011c8160e99126692126a02a585811644c334129a400e93944010b064190410889469d002721049000bb00d18074062402240122910162891b680d11231c2220e12008451966921176e923241440091d39081c2122c44460d90b051103805a19084188708e3802c24308049264449a89011c38c5a86901c82691215688a466e0bb88d83404148a65022920884c401a1108abb3902c1b31799a75924465953e9ff278a0bc840d829e053913e6385aa2df05cfde16dcf2c09f5faeacb062749174e6f61c4d264dbecca8a9dafeeaebe208e11a50fe8930a483f33e4ee21f0ade85ffc263f3995e4f71fe6d5fbea8f26c50f4782066dcd31116c46e6b0eb2aeb7ec06e487fec74f3a90f0ba8acd0769372f1ccadda23c5e0a5507e38ced0f3daf4033cef9e7c7c563950902be8b442b76400a3837121c753c7535dc6dd956cd75dd51943021a4432bab428c0df5ab33320c18187191e79f2a007410992e53bb9effbba1c54e03763846362d279cf46be4f8697a88db336127ddd7b20e54706e12cd2861ca6ae5f6ff14deaf77d29bad5b987ee6b3dc622b7dc757a44a43be1f9a69ab33547edfad87f643b18964faee9ce922fc21d8272d18152316e052f60f0441594fd6a4f0abb86ca298f9eddff9439470e95bf981f560f256d6519b920c4bdf9573507426e283b37dd3fe4002b31e85b4f25adf916277b6eab26f5ae8118c451953646e6b9f488bb977c17ecd57c5033e11dc168355a869411c6a78343bd6e071d69a4d7d2e7b5ade33508e22614b635817f035e7c1e16f07ca46c7eb98c448e320fd9b10208f82c394166208954850da4e4cd6d6e92737e3d6a99dbd46dbd30c3dc52e84db7f1ad95d7e579d3904d6bcbe6b3a05557962c8f74b09fe17c0e9ba4c48796b399de4ddb1ad15a1cf7294eb636fdac12a2c8d81f47a0b636b4e488d326dc51c94134e54e7d5960194e6f70cc156b312460092aa60f58dd91a607a495c19f053d7089ffe9962da0d9cfc1f528c743aa5f5f48d84ec660f4e225056d2e851c133cae930b8d49219b6aaeadc89ce6b30c3a1d84038db04f9cf969e93952a495818e36ae318f734353b696f4faaa2b59af0e98092b9f1ad5d21af96878af812bf79c697979c30a096deb1a54f3beeb998d679be2dd0009829ccb907a4e13204cf4943cc51562279d4904b959c2dfcec8e306ed613e1ab874dc805712345d1e552c49583526e3833a121ad301a5a677d8175d80a6cb4fdce3bc5c8d7f3747d75eca0f8ef45b2ce1876e34829409768d7438d79bc56fc51ccdbe0d62597faddd80151b541e32828bc287d9fa40eab868e07349c6b33a2f8b516b85e06d39474177a84db8165d4f27da09a91489a7f637e4ce30a7a14e2b7adc5de9c95372827f8eb4fa2f5436fc93113a001b1a99c269c0f372c7f19ef92d641915fc823d6bf96932cfd4b4c75341dcd1eed741a13ff69175c122cd9c7c80c805f996341a2e3c7366173ebe7aaee321e231390c07366ce36ce1da3c6391a908464f2adec37aa6b39146fe8b63fefea2bdd4915ab999f87f4343a9259518316c2cc1e0e3e3e4d3146da41f35ccf9a6faff2a85b1ceb00f6df76097ce8559c87a0ca47584084b02138eaed1a59909ad10948384a3d896b090ee6b4217344d96bb43e8ac58719ff19da362a11fcb5ee1b818e0c841eccf1976f50959d464209d87cb9d5864240ed023b6574f019b33469f56ac3cd507030b9af1b88dcd72a0deb0ad581d243d8e4678ed487d6fdbdba75738cdc4a5b899dad48f322b63d8d869b839e375f96fe2afc387cbf843cf4d232bb578cd86b5b06e911108660f06353948f10d79a325dc31c8fa9d0a373725dd1c92048b7b5cf0c802ceda0486b5dc6e6b6bc6a5256f4eb9f8432c570fbbd2722ac276881c42d8e190d8f6acb1b5b5901490fe42ede6198af204a0a84dd83b6572082c32371478d820bfebbe2507e25b770481192e06305dbf3d0f219d922fce665b058cc35c692fc05e3938485171a72482532e30445e9ade3970c5f35e291758dab31d8bba2e6c053164b62facb136703e211917b7c447b0a35e01e7879072686987c9da5b09a76cb2b2597bfb33486eff3cda9f34942bedcd91fb4243618b742ba738e466c9fc077fb70db57a60cc54b4a84849688bd8f4782562b257b81161e98d06a93e04ababf3b51bf2871bc49c651ce24b576e849d6d690f3de5dd22f453fbcce219461367613b0111d7b3188c41ca01fe90f74dcdf56308869217f080f561d787a065a69f7617d21225cc3819b2d93ceff1f6ad4bb7d1ae282323fb556fb7fbc92ad378e64a4e4d424c59c1449fb3bde3d2b2d784440da85456b8adc17d185a59ac69b3b38386245fcd8dae38d04817d38a5f3b1f71fb7d211eb83efb876345bcc99042b7e55bbed7b2af6a975f88d61f707501029634d5187ef4a118270026ef4e6aa417d6f586c8b158e2abdf7b84906b1f6198aea946a1c30e5a1d0e60bbcbc0a1b88d03ffebf88fa52255317ed94d7e79a5bc851d85e5ea5a1cdd8e0c78581320" "sk from a raw seed" + got=$(printf '%s' 61C5442C5C769EAC3021DCB9FFAB0D46223D456F0A72305E2F203DDD57BFFFCE | hex_out "$BC_RUST" --hex-input mldsa44 keygen-from-seed -x) + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d6412a9e06cd5963dfd8be96baf98b2bb61909b26006eafebd7d20dc9cb611ce7aa590a725e2bacb71eeafc4e057dc20539733b28a789108711f48aa4d947aa225dc4848a9de09ebd59365c287a4646650832225d2712eb0251cadc1f99daf981f101334803937108316843c464c9c44811a59194122d10c30c538401992642e2186ed0160053b6259908401c030ae2382482808c22a16c139524e102900819869128125b082e230772d4c2210c003141920da108212104809cb085e1380518356489c28080c23114314490846911390512a40c41164800823021962d81143061c830a0229022326ed240518824520241320a3782631649c91611c3149020394a024162d842661a132ecc34664a409004b62403074592906d01b64108b12c53a44c81c6619904010934511c1700014592e1b46c4ac610240184e4222520854023218a5c426ee0c44c94806998026d0c092101218c02982d62482420a16d099509400242a2b64942268c212609140044182042111342c0464693444a54a40c63128d00168a494665c2108808990c1b37110c3444d3a011a21460c3a205142844822409ca940d938404999441591870a3b84c8a4265181052e4922c020460c808129a904553944c1481619a04729c045298087022340959400ad0802914038a82182c1ab40554806960b841242670821490004162041020d4965098124122b445c0a08051c44c9ca21149c689c49088001450e4a4850a950919162a1b060212892492466081b24064980d843232da440104001093188511930122116d8a1210c3462ccb084c112604549468113685140484242144a4104150164a5cc00848308990420c113942e434508c90499c9869139721c1169121966580a83012100d63a88c0b054c8a2025d0a808a0043093944904223141b670c418708c924d99480c42188560100604446812100a42c86d08314dd94260d8c448daa82994040911458054b4310c49054920509c286100b070123011c8160e99126692126a02a585811644c334129a400e93944010b064190410889469d002721049000bb00d18074062402240122910162891b680d11231c2220e12008451966921176e923241440091d39081c2122c44460d90b051103805a19084188708e3802c24308049264449a89011c38c5a86901c82691215688a466e0bb88d83404148a65022920884c401a1108abb3902c1b31799a75924465953e9ff278a0bc840d829e053913e6385aa2df05cfde16dcf2c09f5faeacb062749174e6f61c4d264dbecca8a9dafeeaebe208e11a50fe8930a483f33e4ee21f0ade85ffc263f3995e4f71fe6d5fbea8f26c50f4782066dcd31116c46e6b0eb2aeb7ec06e487fec74f3a90f0ba8acd0769372f1ccadda23c5e0a5507e38ced0f3daf4033cef9e7c7c563950902be8b442b76400a3837121c753c7535dc6dd956cd75dd51943021a4432bab428c0df5ab33320c18187191e79f2a007410992e53bb9effbba1c54e03763846362d279cf46be4f8697a88db336127ddd7b20e54706e12cd2861ca6ae5f6ff14deaf77d29bad5b987ee6b3dc622b7dc757a44a43be1f9a69ab33547edfad87f643b18964faee9ce922fc21d8272d18152316e052f60f0441594fd6a4f0abb86ca298f9eddff9439470e95bf981f560f256d6519b920c4bdf9573507426e283b37dd3fe4002b31e85b4f25adf916277b6eab26f5ae8118c451953646e6b9f488bb977c17ecd57c5033e11dc168355a869411c6a78343bd6e071d69a4d7d2e7b5ade33508e22614b635817f035e7c1e16f07ca46c7eb98c448e320fd9b10208f82c394166208954850da4e4cd6d6e92737e3d6a99dbd46dbd30c3dc52e84db7f1ad95d7e579d3904d6bcbe6b3a05557962c8f74b09fe17c0e9ba4c48796b399de4ddb1ad15a1cf7294eb636fdac12a2c8d81f47a0b636b4e488d326dc51c94134e54e7d5960194e6f70cc156b312460092aa60f58dd91a607a495c19f053d7089ffe9962da0d9cfc1f528c743aa5f5f48d84ec660f4e225056d2e851c133cae930b8d49219b6aaeadc89ce6b30c3a1d84038db04f9cf969e93952a495818e36ae318f734353b696f4faaa2b59af0e98092b9f1ad5d21af96878af812bf79c697979c30a096deb1a54f3beeb998d679be2dd0009829ccb907a4e13204cf4943cc51562279d4904b959c2dfcec8e306ed613e1ab874dc805712345d1e552c49583526e3833a121ad301a5a677d8175d80a6cb4fdce3bc5c8d7f3747d75eca0f8ef45b2ce1876e34829409768d7438d79bc56fc51ccdbe0d62597faddd80151b541e32828bc287d9fa40eab868e07349c6b33a2f8b516b85e06d39474177a84db8165d4f27da09a91489a7f637e4ce30a7a14e2b7adc5de9c95372827f8eb4fa2f5436fc93113a001b1a99c269c0f372c7f19ef92d641915fc823d6bf96932cfd4b4c75341dcd1eed741a13ff69175c122cd9c7c80c805f996341a2e3c7366173ebe7aaee321e231390c07366ce36ce1da3c6391a908464f2adec37aa6b39146fe8b63fefea2bdd4915ab999f87f4343a9259518316c2cc1e0e3e3e4d3146da41f35ccf9a6faff2a85b1ceb00f6df76097ce8559c87a0ca47584084b02138eaed1a59909ad10948384a3d896b090ee6b4217344d96bb43e8ac58719ff19da362a11fcb5ee1b818e0c841eccf1976f50959d464209d87cb9d5864240ed023b6574f019b33469f56ac3cd507030b9af1b88dcd72a0deb0ad581d243d8e4678ed487d6fdbdba75738cdc4a5b899dad48f322b63d8d869b839e375f96fe2afc387cbf843cf4d232bb578cd86b5b06e911108660f06353948f10d79a325dc31c8fa9d0a373725dd1c92048b7b5cf0c802ceda0486b5dc6e6b6bc6a5256f4eb9f8432c570fbbd2722ac276881c42d8e190d8f6acb1b5b5901490fe42ede6198af204a0a84dd83b6572082c32371478d820bfebbe2507e25b770481192e06305dbf3d0f219d922fce665b058cc35c692fc05e3938485171a72482532e30445e9ade3970c5f35e291758dab31d8bba2e6c053164b62facb136703e211917b7c447b0a35e01e7879072686987c9da5b09a76cb2b2597bfb33486eff3cda9f34942bedcd91fb4243618b742ba738e466c9fc077fb70db57a60cc54b4a84849688bd8f4782562b257b81161e98d06a93e04ababf3b51bf2871bc49c651ce24b576e849d6d690f3de5dd22f453fbcce219461367613b0111d7b3188c41ca01fe90f74dcdf56308869217f080f561d787a065a69f7617d21225cc3819b2d93ceff1f6ad4bb7d1ae282323fb556fb7fbc92ad378e64a4e4d424c59c1449fb3bde3d2b2d784440da85456b8adc17d185a59ac69b3b38386245fcd8dae38d04817d38a5f3b1f71fb7d211eb83efb876345bcc99042b7e55bbed7b2af6a975f88d61f707501029634d5187ef4a118270026ef4e6aa417d6f586c8b158e2abdf7b84906b1f6198aea946a1c30e5a1d0e60bbcbc0a1b88d03ffebf88fa52255317ed94d7e79a5bc851d85e5ea5a1cdd8e0c78581320" "sk from a hex seed" + "$BC_RUST" mldsa44 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa44 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d64e8a2f9aea78b11ef18a2b2ea9d852ba6c5c0278b36cd899a5397189780d63eb3601dfddd6d127a900d68abcec8e3379b57d5bbc99591daa6da230935bb9a7ded40e76bda363f1a30fe9539adffcf3e882997d90b9426bfb25a28bbf3630f539f6819ae26b6705a33aef4ec09b6e5de2f1d83ad9ebb880f7a8cee31148829b9999d0f0e907ea28d8b3a14cd2071a9b2fcc14194bf82b4474976eb851eb9cb2209cbd1dd75d986231d01f0697bb65586cb0ef0d0fd8416b90e5c8cf9129924709fe998b5bfeb69dcc1d2fdf43422cfa1ad926506f8ca7674b6f4820cb76690524b9c1b9b3841cc375dd2861f1dd88b64d0e733ad8a5fd87078bfda207c489b813e12f8f24fb291dbd427de0ff01eab3b66f59fc81ccd1f81af9fa5a44880e57789c4bf6457adc20248aa6388bc8ba04cefad9c28431395cb8f6ea6084a38dee1613d988974f9c5dd3ca7578ecd4c688b73c600ac9c7d96dab748ff9faff9240ead4c854d868da3b3ea1a022866a36acfa432c9847a4dc631d67dfd9ed83869793c3064f0693c9fd63a3aca815540737ae24796841f7ecd94a2715f669dfeb294c4a0739e7f8e6092b41155f7701f95fc5761279d98aef6fc31a977aba678b8dd339b82b934dd3b74c9484791b00701ba1cf8ab369f2ed4f2995c6aeb0b1ad7181b3d5da4cb209d629bf621eaf9353263666e368402931b4ffd82f45e38c19ed7068046108cedf4dec5139bdd66b13ebcb7dacbaea770cefd994149f77e575dcda6b406cabc970c08c693844c3abec151972fe913514802fb974e396c5ba5f5493538185d2523569878032f8d128506804a12b36b5023122d01b86038cc9445317d8e8332d21c15ae298d73820362b097781f8d34803e7aff1ae929f0bba507ccc0b722750b8b1c7e5dd6b0194d677bb75d889a0061e48427f8c0709c227506ebf44d4e9cbec17f92a9ada7dd82d4a49a21b3664120cc6b6c023eabac72be1425ad6d0fd3f15c1f767a4af63e774447d52bda71f62accb9dc9e084b2baccb2e84be167b47f61b737d4862437aa4d7cbf078a90c35564ddf1535d365a4e9d8a789f311cc002ac1c7e377def9f231026ecbbcdaa4363f1942abf265350257b540079b559164d27c6eb21f2b7d184d33599ddf6ad082867c8751bf83ff2cec7322898de686bfc243f4a51b84b83a6e1bbc1f483bdbeb6991e8334eb299360434b1bb35245f46fc514a79fc9f54147636966a56a295e249bc2ca55e2f62f5513da77acf35f9d2006fd0104a8d5f70948ee0ca9b4e144aa498d597f6747cbe79ffb28aaa189588a79b1286cc9dcae708a3188d23ae0e10c012211fb48a40ab3c65afe2a4eaf99e79ff39e2352e2e4cbe98f6c6340d7697f82da0fbbb703c2c57f28651d1f83a83be015d0599c42bf55f1465f8a3007ef7179b5890524bcc0cf6b44adff3cb32593d8a05445f162a6226217389712401f85b6115366ada0122566302798ff92c0994d8e9e1ddfb24e6e46cc0cf64fc527a57f334443682a6343ce6ca6d2ee9accbe3ed1ba6cd9b9935b1d56564e35077073f5ced9a2ac9a4b3f0ca0684e69ac1d9dd83c3a661fbb5ac2853a382cb4842687ac16664bb8b82df9152df9210a0608de0483f7e1782ca932fd8f41d8f50ed10193ac0ad6263579d7261e443cddb279bc02b20e3f055169ee2a27566e6f47382e91d9241cfa7137732514fa460c8183b489efd8218c4f104351b9ff94da2952709ae7ed29ce88ff0debb45863265df7f1b536367816b57d42b4c8fccabd2bae3633d4a0af2faea7515f36babd3" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa44 check-consistency --pkfile <(unhex 3A909812EF8D581A3118C2E9C33748943063DAA58DBD96010428A7AE5D5D8D64E8A2F9AEA78B11EF18A2B2EA9D852BA6C5C0278B36CD899A5397189780D63EB3601DFDDD6D127A900D68ABCEC8E3379B57D5BBC99591DAA6DA230935BB9A7DED40E76BDA363F1A30FE9539ADFFCF3E882997D90B9426BFB25A28BBF3630F539F6819AE26B6705A33AEF4EC09B6E5DE2F1D83AD9EBB880F7A8CEE31148829B9999D0F0E907EA28D8B3A14CD2071A9B2FCC14194BF82B4474976EB851EB9CB2209CBD1DD75D986231D01F0697BB65586CB0EF0D0FD8416B90E5C8CF9129924709FE998B5BFEB69DCC1D2FDF43422CFA1AD926506F8CA7674B6F4820CB76690524B9C1B9B3841CC375DD2861F1DD88B64D0E733AD8A5FD87078BFDA207C489B813E12F8F24FB291DBD427DE0FF01EAB3B66F59FC81CCD1F81AF9FA5A44880E57789C4BF6457ADC20248AA6388BC8BA04CEFAD9C28431395CB8F6EA6084A38DEE1613D988974F9C5DD3CA7578ECD4C688B73C600AC9C7D96DAB748FF9FAFF9240EAD4C854D868DA3B3EA1A022866A36ACFA432C9847A4DC631D67DFD9ED83869793C3064F0693C9FD63A3ACA815540737AE24796841F7ECD94A2715F669DFEB294C4A0739E7F8E6092B41155F7701F95FC5761279D98AEF6FC31A977ABA678B8DD339B82B934DD3B74C9484791B00701BA1CF8AB369F2ED4F2995C6AEB0B1AD7181B3D5DA4CB209D629BF621EAF9353263666E368402931B4FFD82F45E38C19ED7068046108CEDF4DEC5139BDD66B13EBCB7DACBAEA770CEFD994149F77E575DCDA6B406CABC970C08C693844C3ABEC151972FE913514802FB974E396C5BA5F5493538185D2523569878032F8D128506804A12B36B5023122D01B86038CC9445317D8E8332D21C15AE298D73820362B097781F8D34803E7AFF1AE929F0BBA507CCC0B722750B8B1C7E5DD6B0194D677BB75D889A0061E48427F8C0709C227506EBF44D4E9CBEC17F92A9ADA7DD82D4A49A21B3664120CC6B6C023EABAC72BE1425AD6D0FD3F15C1F767A4AF63E774447D52BDA71F62ACCB9DC9E084B2BACCB2E84BE167B47F61B737D4862437AA4D7CBF078A90C35564DDF1535D365A4E9D8A789F311CC002AC1C7E377DEF9F231026ECBBCDAA4363F1942ABF265350257B540079B559164D27C6EB21F2B7D184D33599DDF6AD082867C8751BF83FF2CEC7322898DE686BFC243F4A51B84B83A6E1BBC1F483BDBEB6991E8334EB299360434B1BB35245F46FC514A79FC9F54147636966A56A295E249BC2CA55E2F62F5513DA77ACF35F9D2006FD0104A8D5F70948EE0CA9B4E144AA498D597F6747CBE79FFB28AAA189588A79B1286CC9DCAE708A3188D23AE0E10C012211FB48A40AB3C65AFE2A4EAF99E79FF39E2352E2E4CBE98F6C6340D7697F82DA0FBBB703C2C57F28651D1F83A83BE015D0599C42BF55F1465F8A3007EF7179B5890524BCC0CF6B44ADFF3CB32593D8A05445F162A6226217389712401F85B6115366ADA0122566302798FF92C0994D8E9E1DDFB24E6E46CC0CF64FC527A57F334443682A6343CE6CA6D2EE9ACCBE3ED1BA6CD9B9935B1D56564E35077073F5CED9A2AC9A4B3F0CA0684E69AC1D9DD83C3A661FBB5AC2853A382CB4842687AC16664BB8B82DF9152DF9210A0608DE0483F7E1782CA932FD8F41D8F50ED10193AC0AD6263579D7261E443CDDB279BC02B20E3F055169EE2A27566E6F47382E91D9241CFA7137732514FA460C8183B489EFD8218C4F104351B9FF94DA2952709AE7ED29CE88FF0DEBB45863265DF7F1B536367816B57D42B4C8FCCABD2BAE3633D4A0AF2FAEA7515F36BABD3) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa44 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "3a909812ef8d581a3118c2e9c33748943063daa58dbd96010428a7ae5d5d8d64e8a2f9aea78b11ef18a2b2ea9d852ba6c5c0278b36cd899a5397189780d63eb3601dfddd6d127a900d68abcec8e3379b57d5bbc99591daa6da230935bb9a7ded40e76bda363f1a30fe9539adffcf3e882997d90b9426bfb25a28bbf3630f539f6819ae26b6705a33aef4ec09b6e5de2f1d83ad9ebb880f7a8cee31148829b9999d0f0e907ea28d8b3a14cd2071a9b2fcc14194bf82b4474976eb851eb9cb2209cbd1dd75d986231d01f0697bb65586cb0ef0d0fd8416b90e5c8cf9129924709fe998b5bfeb69dcc1d2fdf43422cfa1ad926506f8ca7674b6f4820cb76690524b9c1b9b3841cc375dd2861f1dd88b64d0e733ad8a5fd87078bfda207c489b813e12f8f24fb291dbd427de0ff01eab3b66f59fc81ccd1f81af9fa5a44880e57789c4bf6457adc20248aa6388bc8ba04cefad9c28431395cb8f6ea6084a38dee1613d988974f9c5dd3ca7578ecd4c688b73c600ac9c7d96dab748ff9faff9240ead4c854d868da3b3ea1a022866a36acfa432c9847a4dc631d67dfd9ed83869793c3064f0693c9fd63a3aca815540737ae24796841f7ecd94a2715f669dfeb294c4a0739e7f8e6092b41155f7701f95fc5761279d98aef6fc31a977aba678b8dd339b82b934dd3b74c9484791b00701ba1cf8ab369f2ed4f2995c6aeb0b1ad7181b3d5da4cb209d629bf621eaf9353263666e368402931b4ffd82f45e38c19ed7068046108cedf4dec5139bdd66b13ebcb7dacbaea770cefd994149f77e575dcda6b406cabc970c08c693844c3abec151972fe913514802fb974e396c5ba5f5493538185d2523569878032f8d128506804a12b36b5023122d01b86038cc9445317d8e8332d21c15ae298d73820362b097781f8d34803e7aff1ae929f0bba507ccc0b722750b8b1c7e5dd6b0194d677bb75d889a0061e48427f8c0709c227506ebf44d4e9cbec17f92a9ada7dd82d4a49a21b3664120cc6b6c023eabac72be1425ad6d0fd3f15c1f767a4af63e774447d52bda71f62accb9dc9e084b2baccb2e84be167b47f61b737d4862437aa4d7cbf078a90c35564ddf1535d365a4e9d8a789f311cc002ac1c7e377def9f231026ecbbcdaa4363f1942abf265350257b540079b559164d27c6eb21f2b7d184d33599ddf6ad082867c8751bf83ff2cec7322898de686bfc243f4a51b84b83a6e1bbc1f483bdbeb6991e8334eb299360434b1bb35245f46fc514a79fc9f54147636966a56a295e249bc2ca55e2f62f5513da77acf35f9d2006fd0104a8d5f70948ee0ca9b4e144aa498d597f6747cbe79ffb28aaa189588a79b1286cc9dcae708a3188d23ae0e10c012211fb48a40ab3c65afe2a4eaf99e79ff39e2352e2e4cbe98f6c6340d7697f82da0fbbb703c2c57f28651d1f83a83be015d0599c42bf55f1465f8a3007ef7179b5890524bcc0cf6b44adff3cb32593d8a05445f162a6226217389712401f85b6115366ada0122566302798ff92c0994d8e9e1ddfb24e6e46cc0cf64fc527a57f334443682a6343ce6ca6d2ee9accbe3ed1ba6cd9b9935b1d56564e35077073f5ced9a2ac9a4b3f0ca0684e69ac1d9dd83c3a661fbb5ac2853a382cb4842687ac16664bb8b82df9152df9210a0608de0483f7e1782ca932fd8f41d8f50ed10193ac0ad6263579d7261e443cddb279bc02b20e3f055169ee2a27566e6f47382e91d9241cfa7137732514fa460c8183b489efd8218c4f104351b9ff94da2952709ae7ed29ce88ff0debb45863265df7f1b536367816b57d42b4c8fccabd2bae3633d4a0af2faea7515f36babd3" "hash-mldsa44 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_44_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa44_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' db9ac67708f2ba0fac1f92bd802f9be89ecab966feef59872a1a9ac90b1111170a561290ae86b13968f2506023c014ba09fa449a26e4e9d35595e73986506cc8790e4d07a94d6c736f7ae78cc5e3e3cf025ce06a09252bef97fe92e94cbd107b1844d1a7c690d88bff9e9336f8f58e0bd5ee384de9c7ffbb149a6fcd87c77288601d8843e28e0c7a60149d02ebc57b183c39888d98b61cd8ad48135ddb8a1666743bb689f44c1a92d52017b6a8fa493eeb839dffb086a9a6c399b194a52f0e4164c96ff8a2a54337de24350a866b5fe4195257778e72511221778f1eae5fa93ed3532f696b9b0767aded85f62ea311027c7f5fc4182dcd2864b1c26bd6dcf72ebdedf70471327be0ea1c2ae53e46489c6dbefa512a78fdd7be0ad3ada16a7f7b1ece49817b44868a2cc234bfdba556c32cc92ec2c5e8a5d206f2e4ee372d41681e67d1b7e7b0061870c57f600fafca85f98aed8ce4ba76bba961f9ed56e563220d3ced853b6b28e7527da0e0912bc932a23c8bab811429bbb4d49b2770bcda44abb932b11c0a5866409fce39fed2b459c86c8f6e1ab0aefc5879503f4b21a49b4b2de6760c9b6aaf041144a656a26af39f4578e1d482ddc1360ef751d9784b860ec373d415360fe99f32e126a2ac1243430e8bed1bc90b19b3d219c2712edcf81c44b4331f6421088e662b695e1fd8fa5091f616ab60af70f159b63368f1ac60d77b279ed47ef7f24ec2044bb6c2bc76d933ecd568f7e663392afc1d335abac6c03670adf87747dde90052f5cd45f7d30f43a4dc3c500ceb658fce235c171240baca1b5a14733d774b9416c540f53eb83481afc98344b12a4309e6222b08d978430467497010314c6f6b8caf65361c216106395275a67d7500dbc120f7918c6f8db7aa63fa965b4a22c70dc88f727d768ce2bfc7597fd470184e1c59a6b2e1204cc8c3d052c594d5771e0ccc8cfb191f47038b1c0672f07caf4747562d3d76a9816fb1def1391cf0f05fcdbf2a0eb6c21ac24b26e74ee403133e80a79313ddb02c1fa386c6dd1d420195343e3a104aff6d60887f7304fa9e3bb59bb55f820dd85b1445c54e9a38dc1c7f3b88eb36a9f48d13455e51c934825ff3cd8bedb2b5422344120399eef83a360b83440ebdd8ea6e01c95159e3735bb4408500caa785ca4049891c7331c4ea31ad9060ece768fd339e6904f88e27bad3b28845687be2cc9314f300fda56fe3ff2508e54c59123b068f86fe00213d5af8da1b1735423ed688f097c306dbc121b81f532fcaf872d9f80596642295d6e4bead478644081618ab903b39e9b5e7cc0b5f2742d8337b18d4ad4788db7443e946cafc1762a5da84070e8c2fd86d6c633f0b44ee234ba11b9e1440c94a08d0437015279690405353059020fd2f58f15dab18754177244adfb81ceab79c7840bf3884a3d364afc8c453a425fd8c5378eaa7445f8c6256bfbd03a66c53e8cf27e2c52f14ef3294afe79cda408f5dff933ca0211a78a4e3be3d9a932558ed71ed19bbb57f87937fa3d4a78128491ff096a261045bdd186325c42caa8c7564195a4d2499a1c17d21a52d1aacd221d9c8a1866963a20390f2fd43dcf56b308a1c01c38091fd3e04c12b695de497d48bcc268d50cb0bed793b8e6937e8d533afd568521f1c9377a3804d38e785674d7ce868d289938e33dda6edc76d25b15fcb38852b7803cfe62f08d9fbd070957c4e6f134973964c9dc009985c8501e7d8f72e7ec285d5289fdd07f64d62acaa9737b039efa7a9d1d175577c6bcf9dddcf692877af38e75263bebe2453155be61f0723c274388a532abe29dd7023e327085f4c9dda41839b7b3357ab9d >"$TMP/pk.hex" + printf '%s' 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f323e >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex db9ac67708f2ba0fac1f92bd802f9be89ecab966feef59872a1a9ac90b1111170a561290ae86b13968f2506023c014ba09fa449a26e4e9d35595e73986506cc8790e4d07a94d6c736f7ae78cc5e3e3cf025ce06a09252bef97fe92e94cbd107b1844d1a7c690d88bff9e9336f8f58e0bd5ee384de9c7ffbb149a6fcd87c77288601d8843e28e0c7a60149d02ebc57b183c39888d98b61cd8ad48135ddb8a1666743bb689f44c1a92d52017b6a8fa493eeb839dffb086a9a6c399b194a52f0e4164c96ff8a2a54337de24350a866b5fe4195257778e72511221778f1eae5fa93ed3532f696b9b0767aded85f62ea311027c7f5fc4182dcd2864b1c26bd6dcf72ebdedf70471327be0ea1c2ae53e46489c6dbefa512a78fdd7be0ad3ada16a7f7b1ece49817b44868a2cc234bfdba556c32cc92ec2c5e8a5d206f2e4ee372d41681e67d1b7e7b0061870c57f600fafca85f98aed8ce4ba76bba961f9ed56e563220d3ced853b6b28e7527da0e0912bc932a23c8bab811429bbb4d49b2770bcda44abb932b11c0a5866409fce39fed2b459c86c8f6e1ab0aefc5879503f4b21a49b4b2de6760c9b6aaf041144a656a26af39f4578e1d482ddc1360ef751d9784b860ec373d415360fe99f32e126a2ac1243430e8bed1bc90b19b3d219c2712edcf81c44b4331f6421088e662b695e1fd8fa5091f616ab60af70f159b63368f1ac60d77b279ed47ef7f24ec2044bb6c2bc76d933ecd568f7e663392afc1d335abac6c03670adf87747dde90052f5cd45f7d30f43a4dc3c500ceb658fce235c171240baca1b5a14733d774b9416c540f53eb83481afc98344b12a4309e6222b08d978430467497010314c6f6b8caf65361c216106395275a67d7500dbc120f7918c6f8db7aa63fa965b4a22c70dc88f727d768ce2bfc7597fd470184e1c59a6b2e1204cc8c3d052c594d5771e0ccc8cfb191f47038b1c0672f07caf4747562d3d76a9816fb1def1391cf0f05fcdbf2a0eb6c21ac24b26e74ee403133e80a79313ddb02c1fa386c6dd1d420195343e3a104aff6d60887f7304fa9e3bb59bb55f820dd85b1445c54e9a38dc1c7f3b88eb36a9f48d13455e51c934825ff3cd8bedb2b5422344120399eef83a360b83440ebdd8ea6e01c95159e3735bb4408500caa785ca4049891c7331c4ea31ad9060ece768fd339e6904f88e27bad3b28845687be2cc9314f300fda56fe3ff2508e54c59123b068f86fe00213d5af8da1b1735423ed688f097c306dbc121b81f532fcaf872d9f80596642295d6e4bead478644081618ab903b39e9b5e7cc0b5f2742d8337b18d4ad4788db7443e946cafc1762a5da84070e8c2fd86d6c633f0b44ee234ba11b9e1440c94a08d0437015279690405353059020fd2f58f15dab18754177244adfb81ceab79c7840bf3884a3d364afc8c453a425fd8c5378eaa7445f8c6256bfbd03a66c53e8cf27e2c52f14ef3294afe79cda408f5dff933ca0211a78a4e3be3d9a932558ed71ed19bbb57f87937fa3d4a78128491ff096a261045bdd186325c42caa8c7564195a4d2499a1c17d21a52d1aacd221d9c8a1866963a20390f2fd43dcf56b308a1c01c38091fd3e04c12b695de497d48bcc268d50cb0bed793b8e6937e8d533afd568521f1c9377a3804d38e785674d7ce868d289938e33dda6edc76d25b15fcb38852b7803cfe62f08d9fbd070957c4e6f134973964c9dc009985c8501e7d8f72e7ec285d5289fdd07f64d62acaa9737b039efa7a9d1d175577c6bcf9dddcf692877af38e75263bebe2453155be61f0723c274388a532abe29dd7023e327085f4c9dda41839b7b3357ab9d >"$TMP/pk.bin"; unhex 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f323e >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' 1aa69cb5ed35204534f25f40a17eb0d767f8981f5e7cec46d3bf3252bfc78e09d02ef0c82da6dde973611c849472890106158cb15ffa6ca891615e888efa0d2d8a121b75ca440228ad32991be34249620f158ffd6f74d7b03bf919218ce259b500808ec157ead67b56b79e9e6607eafb9227b8a30adbec087d35bc1aec2f1a0c4dd126dfcfb9fbf0bd74fb1e092495fa994ab5a7cd1333281aafe834694a6dc11e889c762b5645638e172dfab3060031ebcdc1fd455d5de6050bf71b074a4dd34af5ebf15487651f0f13e5d3cee231b9b347810bfc418196df9d7231780c09171b9aae732bea27a1649d8c03220f417e30a016b08ccb1d55c9337b4812e20e04523f5d29a760a01b3a80d76285521206481ee1e44df09a76913ba54ae50c8eb973a3ced73950fbf39c4c0c1262a216821a442072c10cc82839ac57b898411be9e810f893272a2546ff7d1d920f146210efc2b4528bc98a099a398302d301fc1dea31b3d8ff78246a66f690ef536b68e02bd7ea23a5378930dde7f1beb51749896a7944e5a40e6fbbb1f76c3fda09e32e0a58062c24cac7ddb8c1d2cbb352a81e336425c5f551246db45ecd0ab29dac88cdebf51c60bebc2e27c974f56da12c1fec4f5745850429b607f5ed7cd821f2c91fc2dda8c2c8e8ec278d1b2a5bf50ec70c5623fc681b4d1dccff96b324cb53ff97470f9de177c2006a89af8f18603a6d4ea2625794695fe79cd30318a1e76307a4c2a353db1e076ad9b609a2489b94cb6dd821c3af31046bb7a5d43d190e09fce4969fe4e93c8393975e64cb2d9c2294fc427ef5191c40937929b3b0e1b037e6b84cc0299d5af2b5410118bfd88ef6491af6f21233390ca7a19f1576e6c5a10a673796905562075047896e3a2379f64dfbbc12b9bfe64939c2d05efbc5f6e4b5ca69ce1ed4b7d25e8c835b0612b33e13ed7a8a7233b4b3d58eead0bc4c841acb65a5ec0ed45e2584c23c2a162392b5789c62358e4038864e20c10e10c67d940ce78993178dbeb3de1fea1e50e7c29f4d7d938c3bfe50229ea040102f30d5b3a64cb8e13420065d54a1ac50a77383bdff3cae2340ebf15a1557fde897007c1b67d04f19431ca00cb0f08db87e90e166e0f4ce6fd69c6ecef1b3f70d9eb601b57a7bf931057c2afe2d3567b6bbec7891c664713385122fdd789c1d5a8a9cfd491f407c16d0b0c5dfc53a6862208e264b981bf2ddbdd1d7db9729b5265c4c3868a947c982880bc55b786153b89ef3324067b35a928c51236bcbe9f860ad9eee5644478f894a9fe78d26a5a17d482612f1cb9983b864e6fba84591c0f73b7b27918819d2121d4af640f533e2939d3da0f0aa6b9df80837a80165ae8b7579715192eb0f6cca78a43d8ad8d7abb56d816e3af2de59b88bfdcf6767abfb043d3ae24223d05001953faa292671c57ade1fe28988075ab8d14ac98363412bd694c40ae85b1f104afcd0f25aa590f57ba4f5dfdf613bf8594e3f54baadfdf50c0881af2475590758a23b7eee725513e4d1ea9f4630159c424a289f18a9879e5e173390f8e630f6ee2a6043d82a1983dc97c7acfea3b0c03e27e865d810d012daeec28dc454f59334edf24627d435701d329ff5e68d19bbdca5ef7d5e00204fa947d08f81cb6484cabe60989d2f61fbe70940f7e4f449b3fcb103a89143d74b15d72e7913dce9193a0b9c5a7b2a97bde6d7f396ae80b4b566f9f2e7345bc42ce3b002818e19f0f16416b850832cd02279ee8d58a381deaac09b1b4d4613f4d066805d2faea6716e015fe361c0526c6e4617a389ffdf930213c1dc0c4c905c3106a7517dce7abea7a9341132f8ad98de3e42f6b75809fff38f6eeafb97398e79d50a5622338763c4e45a88ddda7fb87ab7f5cec61109fdc1c5d4a16310275241fc34178028a49fd79581a05c3b6984eecd6cb9bd60a44da72600f8f2604a4ff4578126194fb2269c8e6e71447445e8e80bf8c6063dbbf29c7ded58abbc0d2eed347bf495b6a9cbe68585a594aa0e65834bfbcccc3f6bf42fe4ef42d86232c3fb1412dd0b5f8a0489958f5c3b883bf7851337a35b13dd2b6517626ff2d1064cd189beb402497dd6b8893d414dde7d1d51018c7a83766a2d8a29b80e8f428237732f7ee5ba878163f0aa8af5b60533b4d4621a38fdc54383acb3325b5e876e21483eddcc64c419596e656d1557be31b530ea66054f79d4f4a755f9ee33c84fed5d552ec5e2eccd061cf4c4b5c3c16a70a7baecccfc208d2430e78621c9ae5b0d080973b4e1df0a1f5c0415db6d3c85f9ea9041e9a9abdde71d6776a522aa957f708b14a33eda10ebbab93bb8ec2b7a04679f38eb44fc558ee698d3c6937e0e647dce898d7599fef6de32ae4d52adc722443610b2126559756aa36f3c79696b99be3d908f780fcbaef33b215693634d63ce2a0777dfbbc899d2be72efdbafbc10aefb26a1a63cdcfda00235b34db723c91624ee5f939024dfedba0863ad08f648767d41fc7fd6b317c51f4b1b87e21e07d488c9423581f9bbdae43e4d32b37e8283960524f9a601ab69f5cd7fb01c9a8a5c64fe863519a1e9f3426398f691a96e1748491b4e209fca2ab29481a674621c797614ba16fbfdfd1a4184e7f84667e720e6bcd9debef32c2b9e891a6e3c0423158f539838d8413e9fc707d5c65b23368b6edc95d9c3f8e20bbb844499311614945606c1487ef4015d1e260fa8239abaa071be572163132bdb06ef21e31be0f9d4b6747134e4842bedcd3bf53b0d6f054693bc428e9a715d5a32a79e6a3cb8b81faf2c04087d8816752637a2fe11eacb38341e024848562a29d4585e4ee56552ce9b1fad43b965a37bff8558921790ef0f4ac55bcac4327d1783f5e1e79bf01e96934bb4a8f5dd06c83bb70b2377189d622a106100f0cbdd38e34c0565900c616561161b3261859133f83893feb22b0bafc82cf4f0dbafde0648e2f86260e6e747034e5cb3ece98087fbf74179c6306f0c460b5d609b9b3a66761472ee0b9dabb5dfa872d5c6bc9b33461a27b5427bd8833f874f479ecc5f0a20304b9a75aacc82420a87af6469daaef53391ae8a25468e717dc47f464fce45a31147c0c4e12ac2f834567e4005b0827d13b3ec80cd8b7a907436e6624c6c8ac6a80add35cfa1a28872fb65cb3fa46894d116a052f19b5fe20c7ead10bd24d27ad2b5683f299d1193ec6d9ff3379f3b3e39cb9991831f194af2041085508da4dcb7b8785cbdc4e04cf4d826d1ef4a11036e4c5803c3aaa7669b4dd3bc12ff888984e9bbdace0e772ab59332b47300334757677578808e90929697a9cecfd6ed1b303d4250738a97a0aabac6d91e283a4b696f7181828493aec8cdcfe1eaecfc0227484957595badbcbfc8fa000000000000000000000000000000000000121f32 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa44 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa44_sign_verify_round_trip() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + assert_size "$TMP/sk" 2560 "sk length of ML-DSA-44" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 2420 "signature length of ML-DSA-44" + expect_ok "pure signature verifies" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# ACVP ML-DSA keyGen (bc-test-data/pqc/crypto/mldsa/ML-DSA-keyGen.txt, ML-DSA-65 tcId 1): the 32-byte +# seed xi gives the vector's sk (`keygen-from-seed`) and pk (`pk-from-sk`), from raw and hex seeds +# (hex needs --hex-input). +test_mldsa65_keygen_from_seed_matches_acvp() { + local got + unhex A30C1FB71307C921442B8095B1FA6FB56D418073FB96014EDDB94BE47F1C590B >"$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa65 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf45ee2d280dd9186625095236726efbc0e9d09f38d13c7ff92c894b026a23fb8a62e2abafcadf97cfbee688b6deed98da562aa0e0465ab9c82da0bdf73391ab32d1d6caa298a6f251ae72920054a50bccb44371e78cb21eecc11b42e2d6ebd89aba425008141512044350457338104086260085760805818887677628826675386877655463302458724413104357504433463681638635081416202227420265045656403887228586256253142331657414512376031243840167256003323287066454533601386741221351413735622287328338666424630384220216135137727336075685327387824102080607742316242411866331118585073381322353815061214040308853831303815008786606106575756475234833872076085878543638174411022578635758021510067461608816435348857546724876755672536240452261540581871481768768745373710504610843621851433432270100233584027345228651424568513480728154438717048561688473172725827316702708113142862458114060326606673738880672380747522788854843662100181051353141285548157158518447126460460001051141784268107477265386582313484558814471783417151416510147401168023471333176118543848715741771155571246745362007352023250154035187058044678578377838631585522218658213244382534223688427353657745643601325371676777312772111770070473132776710057242647641032380360043250886170431726153612420260006718070443034131123751544700757062682177822766573005050088825201660246666172284611010544821220007651176606638880576172512824653442411381334382242401866704176856347866574151178433227566682161677134325628005814002682338168807384483636176476721347120467833470225825221183326083748386468000558238346851243077403150015313428071538563800602363051342088734081018385243051000457514518205827470767041715835721317805836847100648507717826138225870478084646656704805533372436648757223161210438212355414474482075133156224860003735117882816467533247388185708661874510563177336864424337361584603038176641520756687043858552258611225280255028021318183374268742875132215454858401270306463734013763200026563065427682276437371877507182185632305183265004167364357640508117831177077516306770308327761514556248846033003703832573421814886162764015373467701508322570778512225126842866716506664128343833718476034878816154626566523667516423245516704702272472707222836551083041121223374447737884131482462377833376537753184831038545867626862707753527461568060746372021270872146673342460272608517761713806243316841242618240388042464285805355466122672754650122722386252243082760783276356613738722838240311313203124856474354236870266834678463478235082444861155222645412618508374810775462186750270510550813567113717642671654352108831288576557236052424804845831257781247840678373401316413588301661844353541748601438153585204687560287627607318683757145102401727210356874155015088750121757581406501447038323667347748771453703634816756433071274651277161375023431310880328545106606480808583010304442584861458681182281687302053251545032123848182608515460200681118841480427015311246876148253708432752612153233638800581554243678114425420815153073435685312308742523856074138277667161783438318268888211782548144507012886106672548051300557122413175773704457043734341367258bbfb3d95a547db281d08a8fbd8fe6c5496b7d05ce1342a6e89488a43697b0c19d554db4199ca5e7f6b3c0488e8227c700226d0763731193ce9ec54b30d9e6cf35dab538fa848d9707cfdfb6b0d6f892710bf86360f874e66bd674f726ba6710b8baee0f63525955a551d1d5ed111c2981a0bc8de651fb7079efee4a3569f09625f105d043ade4ac633108e0597c747ced1c1fb2a90a2d98fbe495704a8c582bcbcaea14b499651f831341ebb3671f2d15f66dfa6ee6ef1e0fc85f1a09a551c807dcebcefaf1f879e5dac5ff7cd2d8fa08cb96784c9e1e96f1dfff94d199d4739d0a73943bbf56fdfb81c2555a1c2be90a1ce0fd3fed2aa7f09d1b564bb99cdab0e800e6435d36ee8b57d46e0b37a1766de78b1e71c2b4c3b110d6d402e76610767c85e945fd3b8707db00bef4ccb156cd2959d1c7442212f74b35990cfaa4c38221c4c837f9090b901b2da53429276ef92911c88c1ba207c3b4a9a1a185864548b05f0fdfb65398206a7fa22fc568c0b2512e407566f64e550c75165e843e67783e34071f3c4004ef124600261098ff561ff3dc9569b20a2c7c7a37c7deb922443e2d61a18be0d3a5857adb5ef9f011e5bb8d36de7e3510990a70398aa1ae3e0f935fc78754cdf90e91e88c7ad3abbca4cd809421cb5e9eb014d4269884c6eb8d85c42f2676e39e9da30abbfc0807e2ead8ca177d9f27e6598bf9da29b999e3ba66bbeb6b27421013f67c389322e15353ddd3ad4f57c44e37be27be6e2311c1da0f4d3a6fdce318004306efe0424476dabdce43eee8acc0f5465f5c8d7bddbc372670660e36b97ac7ae17979b3ea76a8ab4e3035a2fb95dbf48d5ea69acd658550ec310e76860a4082fa7164b335dadd5756e794005b5e8d686243c32433988632c89745c037debd51204112154bc6d0b37842e609b5392619f44b5fd29f7ff36f80286c1bae439799660380fc6a207abebbd7c42473d8d5d5db12e18466d82e4720fe8a7af6b80aa38580cf71f72746b2e76f2630ba7438ab9944334f78d9450579fec825bee08c11e0e31bc762cbd69ab11705a5dc5bc1c559639d5fd85e55a078b2a017f49717b0b287d7cbb75547943992e2e5ea684b76d7753a2ca3159697b28be0ed12145c96f0294b2e4710e7b479a7aa4d27d7baf81a287fb73b259868599455b751522dc9813a1b1f908883b3607cf427bfc301b3b6d118a9d401fc9121fe0f1bd5965bc5c9ba3f7e29d877480a3cf047fc393313ce4468b491686ab7d91945ec276e0ab83da04cf3195d809c774d2aea049a98ba34b19e9e23c158b6694cdf596162788e27b1b075917415b1d4e1b55dbb5374f0c8b90c20cc05c9c35f9b6a3cdf91907e95829134eddacf6e78058629016344ed294c7158d00d639acd4057eb175ef4e409bba9d443cb20a8108f622361ee78a101189a033f7a1e82b75f9f66923eba5cabceffe3312fd1ce02454e64ee0feb9b6357c78bdb53a3e318f73b39253d4d122c35c71054e1516e157668c290bdfdb660329822579e0c7cca6c482da4d62a8f2e4177972d3f67f10c18f9eb5a61e815e09c3909b1f415141560be2ebbec7735b89ed2b12cc7b2028c027c95e6d293a92bea09bf354a69b6b3c0671c324d05a877f9f2088f9777d85d9ee83150446526fb13b9ae89b93c5ab20d07fac48fd93f1b10dfd5023deca84eacab6a8eb0d817ca27cef14a88c8dc3b432d470e88dc50c2c5c75ace5cf1710706b7ae3e9ebdbc247bdd89f08f7496f14111de054469450217b099901badb8e4c4c683240eee05cc55d23a68a0f2401b1913b95d9c8add5d54c1db013d8da5e3abec2993ff98160d3fc9b3786184cb9cd74e3f20ae33039ab7b1df2d430136b39ad434c8a901e8ad2c6017ce3a17383251ba51eaa11a48725265a3647bdc248a390eaf0114b5c85968f4cd2e7f37d652048a922d2535cd1f4408f999c64b00a31a097a7a66103d39e0613853b4dafa690350963ae9686cb3b9c0e7c7b1c3eaa857439cbf188e906b6ef2582f86d61fc1cbb166900983b17a0d130977a0785b38407738bcbdb0cd6d738b15c9eb4da0292dffbb0cf737983582ffcce249848c61369db2c3cbef61f5483f71d5736e1ca730a9b6e284bec23591a734e1b592759d91fb3f25ecb94bd9bafc8639cbb7a216766285ef0d00034094019ceb30739b5545717ee050ce900a78499cbf8c7b08d8e926f72133c427e0db77caaa1b47565d35583be4232677e87da2bf1eab31502a65c3c835a6ef89e0b85be54e9b97a636bd23cf982a3441996dd639320d8f65a28bc28c5288ac27abfb328d5718061e2197eec6e08e5c43aefc9cd736b75ac9b54135fd251d25faf242a3c407fdd7a19314dd992504bbfc7f8c2990441b21c7ccfd161005d2234aa1726e3b92e4efbcd78cb365a2e8ab64500ef190026f5eef017cd8f1257b70368a5bbf36e72cc86969fe968de45a48bab4f235263eb4e07730883e97a0e2012a6207d727e04affe6ea1ed00dda3cd4a91f515681282b7f2ab2458e6dd163c76f66ca6678de0f6b0c1afbc0abadbdeef00a19785131a60feb0eb0ba094c9fc648d4b1a22f2810bbca94547bed7710c97623c0c97a0d2bfa18c783a6638119a8f3b85c69c81012b49ebd6ec3a2520d719f787d39f7d25a24aa259e106a6c9557f7c9ac3e912f4e42840bf0b34704b962430f2e6ec9326651820a31fe2d1c397aa4fe7e6359acabf22acf701fb9762dc08fa83d4f524e4de41b6a56be8ecbe3456982f42eeb234564485bad145b44336dabc0d77737709021c29cc657d5d43d0c40fe219cdcfcf5d2c4384cb0e73bc76196943194f90aa2dc265a7ffe3da285a204690f2980e4c5f90ef5dfb14712d107cfde877a4723d8d30ceac2487f0bdb170410db6f39e6a68362c88727111e7a9f0c3f036b4a8b86f49f0db1f7d86264f9f6033166687da91a10829ae39170d635e7d3830bc065ec67c9557766c926db448ea13e5dbf18cd313e0bfc563cd11aef181cec2648e64cf11be76a935df752631e3779c41546ec9bfdadd2414b115ff456825766953f6420234c53235d228941df314a9fabaabac877208398bcaf27cc02796d2d0e5369766ea11a436017fa2d15db19d08eb7888c56abd17e1ba8d6aca2b76163313c8df4e607f7af4f8d4a0eb92cac8014c3249dfc17dec03971f811c943550c4dac1ce664a7eb0f4c109664bbb1c2087a5282284a3572aa9f6cd5e36ff156e5d7e7bf6e4aabbf70ab91defc5ca0cc1d7957fb3725b5980d3bbdf246a18ade7e81f318b1c9aa11a8fe5e6949be1002cf9a62e53d71df98cc39f5b2bd8729d3d5ae0d604dba3cbe357aa6aea5dfb513505c525068152ec52eced0c6e2efb4063279097882f3fc715560d309273d352d310900ef50191a4bcb7bfcbaf03a48a841389fe8132fb77a690f17049512c0795a25b7dcb6b702fd31c0789c5a42c0ed4a5df21eccc79e3c4da597d55b170768f5d168005e5f93c06d7f281697099080938b34c8e" "sk from a raw seed" + got=$(printf '%s' A30C1FB71307C921442B8095B1FA6FB56D418073FB96014EDDB94BE47F1C590B | hex_out "$BC_RUST" --hex-input mldsa65 keygen-from-seed -x) + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf45ee2d280dd9186625095236726efbc0e9d09f38d13c7ff92c894b026a23fb8a62e2abafcadf97cfbee688b6deed98da562aa0e0465ab9c82da0bdf73391ab32d1d6caa298a6f251ae72920054a50bccb44371e78cb21eecc11b42e2d6ebd89aba425008141512044350457338104086260085760805818887677628826675386877655463302458724413104357504433463681638635081416202227420265045656403887228586256253142331657414512376031243840167256003323287066454533601386741221351413735622287328338666424630384220216135137727336075685327387824102080607742316242411866331118585073381322353815061214040308853831303815008786606106575756475234833872076085878543638174411022578635758021510067461608816435348857546724876755672536240452261540581871481768768745373710504610843621851433432270100233584027345228651424568513480728154438717048561688473172725827316702708113142862458114060326606673738880672380747522788854843662100181051353141285548157158518447126460460001051141784268107477265386582313484558814471783417151416510147401168023471333176118543848715741771155571246745362007352023250154035187058044678578377838631585522218658213244382534223688427353657745643601325371676777312772111770070473132776710057242647641032380360043250886170431726153612420260006718070443034131123751544700757062682177822766573005050088825201660246666172284611010544821220007651176606638880576172512824653442411381334382242401866704176856347866574151178433227566682161677134325628005814002682338168807384483636176476721347120467833470225825221183326083748386468000558238346851243077403150015313428071538563800602363051342088734081018385243051000457514518205827470767041715835721317805836847100648507717826138225870478084646656704805533372436648757223161210438212355414474482075133156224860003735117882816467533247388185708661874510563177336864424337361584603038176641520756687043858552258611225280255028021318183374268742875132215454858401270306463734013763200026563065427682276437371877507182185632305183265004167364357640508117831177077516306770308327761514556248846033003703832573421814886162764015373467701508322570778512225126842866716506664128343833718476034878816154626566523667516423245516704702272472707222836551083041121223374447737884131482462377833376537753184831038545867626862707753527461568060746372021270872146673342460272608517761713806243316841242618240388042464285805355466122672754650122722386252243082760783276356613738722838240311313203124856474354236870266834678463478235082444861155222645412618508374810775462186750270510550813567113717642671654352108831288576557236052424804845831257781247840678373401316413588301661844353541748601438153585204687560287627607318683757145102401727210356874155015088750121757581406501447038323667347748771453703634816756433071274651277161375023431310880328545106606480808583010304442584861458681182281687302053251545032123848182608515460200681118841480427015311246876148253708432752612153233638800581554243678114425420815153073435685312308742523856074138277667161783438318268888211782548144507012886106672548051300557122413175773704457043734341367258bbfb3d95a547db281d08a8fbd8fe6c5496b7d05ce1342a6e89488a43697b0c19d554db4199ca5e7f6b3c0488e8227c700226d0763731193ce9ec54b30d9e6cf35dab538fa848d9707cfdfb6b0d6f892710bf86360f874e66bd674f726ba6710b8baee0f63525955a551d1d5ed111c2981a0bc8de651fb7079efee4a3569f09625f105d043ade4ac633108e0597c747ced1c1fb2a90a2d98fbe495704a8c582bcbcaea14b499651f831341ebb3671f2d15f66dfa6ee6ef1e0fc85f1a09a551c807dcebcefaf1f879e5dac5ff7cd2d8fa08cb96784c9e1e96f1dfff94d199d4739d0a73943bbf56fdfb81c2555a1c2be90a1ce0fd3fed2aa7f09d1b564bb99cdab0e800e6435d36ee8b57d46e0b37a1766de78b1e71c2b4c3b110d6d402e76610767c85e945fd3b8707db00bef4ccb156cd2959d1c7442212f74b35990cfaa4c38221c4c837f9090b901b2da53429276ef92911c88c1ba207c3b4a9a1a185864548b05f0fdfb65398206a7fa22fc568c0b2512e407566f64e550c75165e843e67783e34071f3c4004ef124600261098ff561ff3dc9569b20a2c7c7a37c7deb922443e2d61a18be0d3a5857adb5ef9f011e5bb8d36de7e3510990a70398aa1ae3e0f935fc78754cdf90e91e88c7ad3abbca4cd809421cb5e9eb014d4269884c6eb8d85c42f2676e39e9da30abbfc0807e2ead8ca177d9f27e6598bf9da29b999e3ba66bbeb6b27421013f67c389322e15353ddd3ad4f57c44e37be27be6e2311c1da0f4d3a6fdce318004306efe0424476dabdce43eee8acc0f5465f5c8d7bddbc372670660e36b97ac7ae17979b3ea76a8ab4e3035a2fb95dbf48d5ea69acd658550ec310e76860a4082fa7164b335dadd5756e794005b5e8d686243c32433988632c89745c037debd51204112154bc6d0b37842e609b5392619f44b5fd29f7ff36f80286c1bae439799660380fc6a207abebbd7c42473d8d5d5db12e18466d82e4720fe8a7af6b80aa38580cf71f72746b2e76f2630ba7438ab9944334f78d9450579fec825bee08c11e0e31bc762cbd69ab11705a5dc5bc1c559639d5fd85e55a078b2a017f49717b0b287d7cbb75547943992e2e5ea684b76d7753a2ca3159697b28be0ed12145c96f0294b2e4710e7b479a7aa4d27d7baf81a287fb73b259868599455b751522dc9813a1b1f908883b3607cf427bfc301b3b6d118a9d401fc9121fe0f1bd5965bc5c9ba3f7e29d877480a3cf047fc393313ce4468b491686ab7d91945ec276e0ab83da04cf3195d809c774d2aea049a98ba34b19e9e23c158b6694cdf596162788e27b1b075917415b1d4e1b55dbb5374f0c8b90c20cc05c9c35f9b6a3cdf91907e95829134eddacf6e78058629016344ed294c7158d00d639acd4057eb175ef4e409bba9d443cb20a8108f622361ee78a101189a033f7a1e82b75f9f66923eba5cabceffe3312fd1ce02454e64ee0feb9b6357c78bdb53a3e318f73b39253d4d122c35c71054e1516e157668c290bdfdb660329822579e0c7cca6c482da4d62a8f2e4177972d3f67f10c18f9eb5a61e815e09c3909b1f415141560be2ebbec7735b89ed2b12cc7b2028c027c95e6d293a92bea09bf354a69b6b3c0671c324d05a877f9f2088f9777d85d9ee83150446526fb13b9ae89b93c5ab20d07fac48fd93f1b10dfd5023deca84eacab6a8eb0d817ca27cef14a88c8dc3b432d470e88dc50c2c5c75ace5cf1710706b7ae3e9ebdbc247bdd89f08f7496f14111de054469450217b099901badb8e4c4c683240eee05cc55d23a68a0f2401b1913b95d9c8add5d54c1db013d8da5e3abec2993ff98160d3fc9b3786184cb9cd74e3f20ae33039ab7b1df2d430136b39ad434c8a901e8ad2c6017ce3a17383251ba51eaa11a48725265a3647bdc248a390eaf0114b5c85968f4cd2e7f37d652048a922d2535cd1f4408f999c64b00a31a097a7a66103d39e0613853b4dafa690350963ae9686cb3b9c0e7c7b1c3eaa857439cbf188e906b6ef2582f86d61fc1cbb166900983b17a0d130977a0785b38407738bcbdb0cd6d738b15c9eb4da0292dffbb0cf737983582ffcce249848c61369db2c3cbef61f5483f71d5736e1ca730a9b6e284bec23591a734e1b592759d91fb3f25ecb94bd9bafc8639cbb7a216766285ef0d00034094019ceb30739b5545717ee050ce900a78499cbf8c7b08d8e926f72133c427e0db77caaa1b47565d35583be4232677e87da2bf1eab31502a65c3c835a6ef89e0b85be54e9b97a636bd23cf982a3441996dd639320d8f65a28bc28c5288ac27abfb328d5718061e2197eec6e08e5c43aefc9cd736b75ac9b54135fd251d25faf242a3c407fdd7a19314dd992504bbfc7f8c2990441b21c7ccfd161005d2234aa1726e3b92e4efbcd78cb365a2e8ab64500ef190026f5eef017cd8f1257b70368a5bbf36e72cc86969fe968de45a48bab4f235263eb4e07730883e97a0e2012a6207d727e04affe6ea1ed00dda3cd4a91f515681282b7f2ab2458e6dd163c76f66ca6678de0f6b0c1afbc0abadbdeef00a19785131a60feb0eb0ba094c9fc648d4b1a22f2810bbca94547bed7710c97623c0c97a0d2bfa18c783a6638119a8f3b85c69c81012b49ebd6ec3a2520d719f787d39f7d25a24aa259e106a6c9557f7c9ac3e912f4e42840bf0b34704b962430f2e6ec9326651820a31fe2d1c397aa4fe7e6359acabf22acf701fb9762dc08fa83d4f524e4de41b6a56be8ecbe3456982f42eeb234564485bad145b44336dabc0d77737709021c29cc657d5d43d0c40fe219cdcfcf5d2c4384cb0e73bc76196943194f90aa2dc265a7ffe3da285a204690f2980e4c5f90ef5dfb14712d107cfde877a4723d8d30ceac2487f0bdb170410db6f39e6a68362c88727111e7a9f0c3f036b4a8b86f49f0db1f7d86264f9f6033166687da91a10829ae39170d635e7d3830bc065ec67c9557766c926db448ea13e5dbf18cd313e0bfc563cd11aef181cec2648e64cf11be76a935df752631e3779c41546ec9bfdadd2414b115ff456825766953f6420234c53235d228941df314a9fabaabac877208398bcaf27cc02796d2d0e5369766ea11a436017fa2d15db19d08eb7888c56abd17e1ba8d6aca2b76163313c8df4e607f7af4f8d4a0eb92cac8014c3249dfc17dec03971f811c943550c4dac1ce664a7eb0f4c109664bbb1c2087a5282284a3572aa9f6cd5e36ff156e5d7e7bf6e4aabbf70ab91defc5ca0cc1d7957fb3725b5980d3bbdf246a18ade7e81f318b1c9aa11a8fe5e6949be1002cf9a62e53d71df98cc39f5b2bd8729d3d5ae0d604dba3cbe357aa6aea5dfb513505c525068152ec52eced0c6e2efb4063279097882f3fc715560d309273d352d310900ef50191a4bcb7bfcbaf03a48a841389fe8132fb77a690f17049512c0795a25b7dcb6b702fd31c0789c5a42c0ed4a5df21eccc79e3c4da597d55b170768f5d168005e5f93c06d7f281697099080938b34c8e" "sk from a hex seed" + "$BC_RUST" mldsa65 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa65 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf4549a9de8b0311aec5f8a20c10754b587b9f554954016598b3b63fe771a426bae15850435158544e6e3824d03944680c568b95534d205308d3d357a3b3cb4c1e096913d0c1c5b1e24b3a68a95a6ef1055d3ebd172d8b5f5e3000314e54c98948f731cf1a21beb376c55eeef45101dcd99e95acd6a29239e7c6b48dea3a032fc6a4faa20cdae02a4a393c7fe10db764d81bff74d6971b391cd01cd9c79ce2c0b58172bb37cdce6941a722f89cc3fd18a1f839b2021671f082f5e97db049bb5424d5326ab21aa4249548297d1f9bfc9317098b85bc57a0eb3510eb718a70b334675773eb658439209d0179642db0c534d4bc67c549f1ebeb2214d726fb4c29798243e1e99a9464407912df26352019d6d224a7f130eb041bcebbdfd4811401d95426bc701965a5d51cadaf8d9567b4d5c62521527474f92f72098375a32246363ef8be4e6f36b1f42d26c127b75c2334264008f56731c711a525dff640b9379c2c818c09992ae87a8b36e9b5291cd61391aef2570e9c62355822e4447bbf56b6e0a8ee95cc2bd9ce6a6eb8e93dc480e8602084d839347df6c5889d164b5f0a82e27d2fc3a6ec6296eb3c1aa958cfa7999306ccc2a5af62f3b4e9107159abbf12960271b67d5571303abf5dc08b80d11bd6c6ea5e0abe6ddc9ba8495ff1c4e6609a73ad1092d39b94b826fc2b087766e6e57e42e468d56c4383970d764ddd9a61540a5b259db5b8d3c6647033fe52682d20a1cee15e5973f4a56953442705ded63ce29aa9e9650de636bc9d7bbd242db82bfc3ab2ab5be5d52e8ec4903ed71053c4dcef63fb77a53ff7f5f31d2e5cc88eacfc3a5cb6318d405a952164a986da2fc7a575cbbaa537802aeb795546637abdd664c49b5f031ced0b85752087af6ac7441ab3487ce713fd14dff7b1ee6fc06b5790bbc320b73dc076d5d0a061a96548f9f0a3b2721f843b63273519202910a6cf6a6620827b80cb5667ca7b5c3f224e8200bbf7c19b68e799155aeea414ec0322e305a31804c72da5983f77892e3d3878eab8e2f02f948a9291cd857a37b5fa938a5dc3d0d9a37ef6383d5aa3a43c8463a9367f023a67595f05468069ce284e50200d0ab5f9cea52e0ead067b98f84ad09c03ae049c0382b9f04c5fad161b8d2aabb4fbd00bd3277d01de5ead95dc68208fa1ce83d8a40fa8d4e5a177c7230259a25cc8d7120288fcf9214a3d86c5c77c64d6319e9c558d0d03b9b15ce2ddf9fce04b4646f18a86343f4f70f1df18780ea5b75e3bdf8cf292a6127f6dcb7ae9734c794daa57bd827bab26d9f3249b06a57081d153cdeb182ea503f4d7aaec22ec979634ed217f3e98813c0582baca597740404f560f08f203d26a44e8649f6e9459e269b5989ed145c4618a2a2ded9c1a7c052db8d03f11af032587aa15ae9900d229d08d1cc7d5f72b31e0bf6ca7dcf0efc3f7bd45fdfa6b2a8eb90718ad7eda033847fea9bd413b42242b9105bc63976f6f4d6f66a30e2e684e7255df93509ed18085d9124e72b7db2b1c8f2c967d9667b8756219fb8fc403730ed53247abec7e77659a079ba757db4656c5b0f38670a21242ea185912ae58d18b3ee5f5eec88a9e161f8d4a2341fdfc25bd0379c1ca8b8e4d94e3c0abc3287ccef79adf650f571c485e32e18794b0f8d137e35062c40465beadc58e91bc8b9a14e4ee88a027241737fa9bd903c44df7cda2983059f928fff7cd01de9477f5a2b5bf6f5d031837a355dc605bcf8c2803912a6ee273f9b8ec0c03e64ec2e12e4b66e7a3aacc2d1643e678304589bd9e46369741825f603fe9bf41e8811a1378ffb90f08a85f4efb2f2206354a6db32354caec96d7189f009c9f4b85eb230df5ef923db4667edccc082022b090c2df7e83987d15df7a2cb9bd393f0d87c8945d380e856acfca00ab7a2e698453e0441e916dd28b5859d88b5decd2a4905ab62bd29d244fe5770faa15fe79d1ee93c4f6fadfb035f81e6be9f56628ea0436e8160909335b462807e64181c67f18105cbd2931a3825d498b19098afc2bac5464f1bccf68fc30f3f192aa59497a49bea292d6188bd7ae5efc1abf25cf8129ebfe03e3b7cc996f306bdee24ae661bb3156d0a907b6346c9836b1a715e7073a2b0d38da54ea2e1a392208b365f136719146f67105ee152c64fe74eb3062b343f4031fb62510a4e12fed1d5e453159121c619ab0aaf51413ffcd28d4256ab80fc8989f45ead6590cd9ba6e9d8e1024d8fb4b7dc3cd20654813e4bb716d12494d7708271084a6d2e9ff544c477161ad2e3b956c18a6e2c2a619f773f4028f7bc0eb767c0445fd4c432ab2392e063cb84aa266c2fcfb5703bf7fc15de4af108a8b486e9901a60237f1a056bf6f10b99646cdaa55dbff355ad356d3a9850ba9a871b5a4642715837885da33daf8db247eb923c97ba268d375588a7fe0c7364f1b0e1756b21e637a872be537e205a5430b2f2ac2e706899ad9ce1f02ec768fb1abe7b144935bf27dbd39f883770b94c27785dac7deb4a6b53597e851362b2d4934fcb9aed5a145b328ce2c72fc90a52211894ea7b8133bb66ad8c563ac38356ba1255356e04c4c0f93e3acfa23627a72fa70fc51e5f1847332e0cc0ca7733230109de416db86b3f6791267aff619a582de4a6e4e8d8fb7c7025ad12880a143b8936d347eed61a17fbd1723b6327f2966510d3570d5ead382a24319a516" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa65 check-consistency --pkfile <(unhex 892DA8B7B2C3B5FA066184E9491843FC3BD244F34C8D9F00AD2EFD801E32CF4549A9DE8B0311AEC5F8A20C10754B587B9F554954016598B3B63FE771A426BAE15850435158544E6E3824D03944680C568B95534D205308D3D357A3B3CB4C1E096913D0C1C5B1E24B3A68A95A6EF1055D3EBD172D8B5F5E3000314E54C98948F731CF1A21BEB376C55EEEF45101DCD99E95ACD6A29239E7C6B48DEA3A032FC6A4FAA20CDAE02A4A393C7FE10DB764D81BFF74D6971B391CD01CD9C79CE2C0B58172BB37CDCE6941A722F89CC3FD18A1F839B2021671F082F5E97DB049BB5424D5326AB21AA4249548297D1F9BFC9317098B85BC57A0EB3510EB718A70B334675773EB658439209D0179642DB0C534D4BC67C549F1EBEB2214D726FB4C29798243E1E99A9464407912DF26352019D6D224A7F130EB041BCEBBDFD4811401D95426BC701965A5D51CADAF8D9567B4D5C62521527474F92F72098375A32246363EF8BE4E6F36B1F42D26C127B75C2334264008F56731C711A525DFF640B9379C2C818C09992AE87A8B36E9B5291CD61391AEF2570E9C62355822E4447BBF56B6E0A8EE95CC2BD9CE6A6EB8E93DC480E8602084D839347DF6C5889D164B5F0A82E27D2FC3A6EC6296EB3C1AA958CFA7999306CCC2A5AF62F3B4E9107159ABBF12960271B67D5571303ABF5DC08B80D11BD6C6EA5E0ABE6DDC9BA8495FF1C4E6609A73AD1092D39B94B826FC2B087766E6E57E42E468D56C4383970D764DDD9A61540A5B259DB5B8D3C6647033FE52682D20A1CEE15E5973F4A56953442705DED63CE29AA9E9650DE636BC9D7BBD242DB82BFC3AB2AB5BE5D52E8EC4903ED71053C4DCEF63FB77A53FF7F5F31D2E5CC88EACFC3A5CB6318D405A952164A986DA2FC7A575CBBAA537802AEB795546637ABDD664C49B5F031CED0B85752087AF6AC7441AB3487CE713FD14DFF7B1EE6FC06B5790BBC320B73DC076D5D0A061A96548F9F0A3B2721F843B63273519202910A6CF6A6620827B80CB5667CA7B5C3F224E8200BBF7C19B68E799155AEEA414EC0322E305A31804C72DA5983F77892E3D3878EAB8E2F02F948A9291CD857A37B5FA938A5DC3D0D9A37EF6383D5AA3A43C8463A9367F023A67595F05468069CE284E50200D0AB5F9CEA52E0EAD067B98F84AD09C03AE049C0382B9F04C5FAD161B8D2AABB4FBD00BD3277D01DE5EAD95DC68208FA1CE83D8A40FA8D4E5A177C7230259A25CC8D7120288FCF9214A3D86C5C77C64D6319E9C558D0D03B9B15CE2DDF9FCE04B4646F18A86343F4F70F1DF18780EA5B75E3BDF8CF292A6127F6DCB7AE9734C794DAA57BD827BAB26D9F3249B06A57081D153CDEB182EA503F4D7AAEC22EC979634ED217F3E98813C0582BACA597740404F560F08F203D26A44E8649F6E9459E269B5989ED145C4618A2A2DED9C1A7C052DB8D03F11AF032587AA15AE9900D229D08D1CC7D5F72B31E0BF6CA7DCF0EFC3F7BD45FDFA6B2A8EB90718AD7EDA033847FEA9BD413B42242B9105BC63976F6F4D6F66A30E2E684E7255DF93509ED18085D9124E72B7DB2B1C8F2C967D9667B8756219FB8FC403730ED53247ABEC7E77659A079BA757DB4656C5B0F38670A21242EA185912AE58D18B3EE5F5EEC88A9E161F8D4A2341FDFC25BD0379C1CA8B8E4D94E3C0ABC3287CCEF79ADF650F571C485E32E18794B0F8D137E35062C40465BEADC58E91BC8B9A14E4EE88A027241737FA9BD903C44DF7CDA2983059F928FFF7CD01DE9477F5A2B5BF6F5D031837A355DC605BCF8C2803912A6EE273F9B8EC0C03E64EC2E12E4B66E7A3AACC2D1643E678304589BD9E46369741825F603FE9BF41E8811A1378FFB90F08A85F4EFB2F2206354A6DB32354CAEC96D7189F009C9F4B85EB230DF5EF923DB4667EDCCC082022B090C2DF7E83987D15DF7A2CB9BD393F0D87C8945D380E856ACFCA00AB7A2E698453E0441E916DD28B5859D88B5DECD2A4905AB62BD29D244FE5770FAA15FE79D1EE93C4F6FADFB035F81E6BE9F56628EA0436E8160909335B462807E64181C67F18105CBD2931A3825D498B19098AFC2BAC5464F1BCCF68FC30F3F192AA59497A49BEA292D6188BD7AE5EFC1ABF25CF8129EBFE03E3B7CC996F306BDEE24AE661BB3156D0A907B6346C9836B1A715E7073A2B0D38DA54EA2E1A392208B365F136719146F67105EE152C64FE74EB3062B343F4031FB62510A4E12FED1D5E453159121C619AB0AAF51413FFCD28D4256AB80FC8989F45EAD6590CD9BA6E9D8E1024D8FB4B7DC3CD20654813E4BB716D12494D7708271084A6D2E9FF544C477161AD2E3B956C18A6E2C2A619F773F4028F7BC0EB767C0445FD4C432AB2392E063CB84AA266C2FCFB5703BF7FC15DE4AF108A8B486E9901A60237F1A056BF6F10B99646CDAA55DBFF355AD356D3A9850BA9A871B5A4642715837885DA33DAF8DB247EB923C97BA268D375588A7FE0C7364F1B0E1756B21E637A872BE537E205A5430B2F2AC2E706899AD9CE1F02EC768FB1ABE7B144935BF27DBD39F883770B94C27785DAC7DEB4A6B53597E851362B2D4934FCB9AED5A145B328CE2C72FC90A52211894EA7B8133BB66AD8C563AC38356BA1255356E04C4C0F93E3ACFA23627A72FA70FC51E5F1847332E0CC0CA7733230109DE416DB86B3F6791267AFF619A582DE4A6E4E8D8FB7C7025AD12880A143B8936D347EED61A17FBD1723B6327F2966510D3570D5EAD382A24319A516) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa65 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "892da8b7b2c3b5fa066184e9491843fc3bd244f34c8d9f00ad2efd801e32cf4549a9de8b0311aec5f8a20c10754b587b9f554954016598b3b63fe771a426bae15850435158544e6e3824d03944680c568b95534d205308d3d357a3b3cb4c1e096913d0c1c5b1e24b3a68a95a6ef1055d3ebd172d8b5f5e3000314e54c98948f731cf1a21beb376c55eeef45101dcd99e95acd6a29239e7c6b48dea3a032fc6a4faa20cdae02a4a393c7fe10db764d81bff74d6971b391cd01cd9c79ce2c0b58172bb37cdce6941a722f89cc3fd18a1f839b2021671f082f5e97db049bb5424d5326ab21aa4249548297d1f9bfc9317098b85bc57a0eb3510eb718a70b334675773eb658439209d0179642db0c534d4bc67c549f1ebeb2214d726fb4c29798243e1e99a9464407912df26352019d6d224a7f130eb041bcebbdfd4811401d95426bc701965a5d51cadaf8d9567b4d5c62521527474f92f72098375a32246363ef8be4e6f36b1f42d26c127b75c2334264008f56731c711a525dff640b9379c2c818c09992ae87a8b36e9b5291cd61391aef2570e9c62355822e4447bbf56b6e0a8ee95cc2bd9ce6a6eb8e93dc480e8602084d839347df6c5889d164b5f0a82e27d2fc3a6ec6296eb3c1aa958cfa7999306ccc2a5af62f3b4e9107159abbf12960271b67d5571303abf5dc08b80d11bd6c6ea5e0abe6ddc9ba8495ff1c4e6609a73ad1092d39b94b826fc2b087766e6e57e42e468d56c4383970d764ddd9a61540a5b259db5b8d3c6647033fe52682d20a1cee15e5973f4a56953442705ded63ce29aa9e9650de636bc9d7bbd242db82bfc3ab2ab5be5d52e8ec4903ed71053c4dcef63fb77a53ff7f5f31d2e5cc88eacfc3a5cb6318d405a952164a986da2fc7a575cbbaa537802aeb795546637abdd664c49b5f031ced0b85752087af6ac7441ab3487ce713fd14dff7b1ee6fc06b5790bbc320b73dc076d5d0a061a96548f9f0a3b2721f843b63273519202910a6cf6a6620827b80cb5667ca7b5c3f224e8200bbf7c19b68e799155aeea414ec0322e305a31804c72da5983f77892e3d3878eab8e2f02f948a9291cd857a37b5fa938a5dc3d0d9a37ef6383d5aa3a43c8463a9367f023a67595f05468069ce284e50200d0ab5f9cea52e0ead067b98f84ad09c03ae049c0382b9f04c5fad161b8d2aabb4fbd00bd3277d01de5ead95dc68208fa1ce83d8a40fa8d4e5a177c7230259a25cc8d7120288fcf9214a3d86c5c77c64d6319e9c558d0d03b9b15ce2ddf9fce04b4646f18a86343f4f70f1df18780ea5b75e3bdf8cf292a6127f6dcb7ae9734c794daa57bd827bab26d9f3249b06a57081d153cdeb182ea503f4d7aaec22ec979634ed217f3e98813c0582baca597740404f560f08f203d26a44e8649f6e9459e269b5989ed145c4618a2a2ded9c1a7c052db8d03f11af032587aa15ae9900d229d08d1cc7d5f72b31e0bf6ca7dcf0efc3f7bd45fdfa6b2a8eb90718ad7eda033847fea9bd413b42242b9105bc63976f6f4d6f66a30e2e684e7255df93509ed18085d9124e72b7db2b1c8f2c967d9667b8756219fb8fc403730ed53247abec7e77659a079ba757db4656c5b0f38670a21242ea185912ae58d18b3ee5f5eec88a9e161f8d4a2341fdfc25bd0379c1ca8b8e4d94e3c0abc3287ccef79adf650f571c485e32e18794b0f8d137e35062c40465beadc58e91bc8b9a14e4ee88a027241737fa9bd903c44df7cda2983059f928fff7cd01de9477f5a2b5bf6f5d031837a355dc605bcf8c2803912a6ee273f9b8ec0c03e64ec2e12e4b66e7a3aacc2d1643e678304589bd9e46369741825f603fe9bf41e8811a1378ffb90f08a85f4efb2f2206354a6db32354caec96d7189f009c9f4b85eb230df5ef923db4667edccc082022b090c2df7e83987d15df7a2cb9bd393f0d87c8945d380e856acfca00ab7a2e698453e0441e916dd28b5859d88b5decd2a4905ab62bd29d244fe5770faa15fe79d1ee93c4f6fadfb035f81e6be9f56628ea0436e8160909335b462807e64181c67f18105cbd2931a3825d498b19098afc2bac5464f1bccf68fc30f3f192aa59497a49bea292d6188bd7ae5efc1abf25cf8129ebfe03e3b7cc996f306bdee24ae661bb3156d0a907b6346c9836b1a715e7073a2b0d38da54ea2e1a392208b365f136719146f67105ee152c64fe74eb3062b343f4031fb62510a4e12fed1d5e453159121c619ab0aaf51413ffcd28d4256ab80fc8989f45ead6590cd9ba6e9d8e1024d8fb4b7dc3cd20654813e4bb716d12494d7708271084a6d2e9ff544c477161ad2e3b956c18a6e2c2a619f773f4028f7bc0eb767c0445fd4c432ab2392e063cb84aa266c2fcfb5703bf7fc15de4af108a8b486e9901a60237f1a056bf6f10b99646cdaa55dbff355ad356d3a9850ba9a871b5a4642715837885da33daf8db247eb923c97ba268d375588a7fe0c7364f1b0e1756b21e637a872be537e205a5430b2f2ac2e706899ad9ce1f02ec768fb1abe7b144935bf27dbd39f883770b94c27785dac7deb4a6b53597e851362b2d4934fcb9aed5a145b328ce2c72fc90a52211894ea7b8133bb66ad8c563ac38356ba1255356e04c4c0f93e3acfa23627a72fa70fc51e5f1847332e0cc0ca7733230109de416db86b3f6791267aff619a582de4a6e4e8d8fb7c7025ad12880a143b8936d347eed61a17fbd1723b6327f2966510d3570d5ead382a24319a516" "hash-mldsa65 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_65_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa65_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' f5408337d0fee65c28851226a5fa81b58464632c78e2a9bef70d330f2e3a5f74d9cf676aedd1067c91a5dd5d4edc46f868a93ffec9f44e254e44f682a153aeadf228e8db7c5fcfed30cc3408e261ab896876bee56660d2a7c1d7eac20c5754255206a178f7156295065ce7876f90c48f44bc37f3a00e32eefd3a4bb1e298fe283d106eaef92a33a594253a2a0790976a1d04636f8672d28c06c852ea8bb43b84bff512996e7616963d5b9a2906466a152c7ea9be178be35405683b44367af85d2daad87630c1e21ba5490154f0141780f5ed0407cb0b975dd56d5930f9b26413b843b83f3693304b0038bd3e4bb398868060ea18c9c67099376470a50deb052e4056743fbcdf0341b192663bd1c21ba3b3d5666e0d0e29c4e1ed0759ab0bd9d1d355011b94e0ff0c049b03ddb7138640667144fcacd7265f55a07e5387f1abd30c037cf14d436aa855f827049215440d8007f61460500d943f57ffb6bfee6fedd2fcec52882d7d8da1aab29e892c8beac3df3234b4a7d2eca3a45c6623c52bbdd07c1c94314b706988a52029f8f8b06e874b741d72926652c78c6ace2cfd8864eadb2e4b39cafe6e03e4edbafa2747db9bc42f92af8b031e3e380846b1bfd15ade88c285d6a6fffe91eafc8b17de6cbc68575f323cc09fc20e49e8efd76f9568bec486b78df4245428d8d0d5f53873e11de65fda4c770b521a8c67f5c51d48cc26358954514447881fd9a42e5891dac7e1db5249d7861b322111e5fb929bee9ff5e9d5a2667ba93e63fc03040d2e82648f89e89dec1d1d2dfb9efeceb7940f7dcbebeb5a239cc1c54d8f7d52cba220d0634e15df46a58280bc5a48840bd39274cfde150f9ad9a40f6398d715350925f0e0501944409f32331a362bdaaafb3d8ce71c964332d6afb7e684f99951246d88081c86744ae68133f22c53a4b5ae258f230a98491d2d43a79a6d0f4d54a3b62013965ac7c82d0507125a38a0277f81cbc1d46cef2a131c6f51b88ec0baae0c82a6a0e72831cb06f9116cff5111d597e01057d32805a008f52c9aec3311139bfb35982789ff83bdd0c31e9f1080e8ed8eb99fde66bafb29e3357389fe3785b60c78e229ef073e1b65e34d848bd4d8a4f251551e2d38d2546afbc205d3c6dab34d2b962b1afb44f1d22fc10c6744fcd6b636afd3cb414b16c2e0d708fe9f51ff19120bde693b028b6d1e6dbe37b4b8b3bc7c6f7a842701603869d3ded572500f085502efc8d3cc62b30e5cdbcb5e86d9c0d42973bf755df539cc0aea58f9148386db67bd2bf70cd12ccd96d5c66fb271416b772465228dc44b079178f9b766370b66a79b871faca246ca6f8f63be9f0668297ac446cad5cf4a83318b1b00ecbd283f0eecee60a9a37a27abdbdbe382e307970002837dfc0bd3934ebd008918fd4bd383c02c9d37f694996e989a49075767ebc4a2981ef5275455e026cb0bd70946cdd1fadaf251381d324f9efbb860d1b280c29685bab97d010676273b45cca12ac3966aae342c84e2357eccf252577743b8787967b40b07ef2d3d9e6c1a3bcb059cba0fdb7f0d4f815c242b8e14acd3375e608e9230ba3cf8718f43882a3e1e661a2bbe81830d34741f33473e263b3790abe67acf29f5df44865b2ffbc96975fd62738a64112deda5a2534fb0a23b3b3024df986391badf9041c593c313a7ca1e1fcffcb65b07b9a99337b4a4acf616cbe1553eb9541f38aa6247342905995233a28172ca13396b2a9662970120f82b92a213f43de7a232ccca3268265c9ce042d50915430a6c455f32277da42f9962fb9163b623231ebc080fa7b8e9f9021fcf85b98f9c483e4d2226b9326a5bcb2e7449ef029ae142d3a0f0c28bd4f7e9c51a12e1336f24dfacbc3f808a8f7dd683027bc948763b808fb0037394b8b41bc9b2ec7887e67584e03d11b15ca203b2bcb43f8881638c4e4eee7f846d09c7f89b7739df22b2c3acc235032ba8f7ae27b5b9d25733143e80a4cdde6770719c1e66ec2ce683612233e88fafff84c0745a98aa1254c8219c6c556348c2b5d1beeb61532d6bf7bde153271dc647460beb65fe0055b33fd6480dcbb9d7d471952cfa5be260c39721a8c5c89b9e966ae2dc9036451ec9f2c49433b2225e13f23e20c2bfba81a7b3a555883449238f7d48213e9f10ce19e76f1bdcfc73ee5524bd7d8be0a4b46784e238233c04fb99383ec7726f9717e1179dd14fba9ad6c2ebd1699f0ab0e57e6cad23875b029e89cfda06f51266ecd2eed4edafb51e82f2a506d57ba74da611774ca5fa2fff4a976519de425885e7d09219cf815b1767d4fc5a72c18918991a285086a6a766614a4d245387da50f28dd778fb33ab88c0918feba3768c55bb1f07aec33cfeed33d6faa4d34fd7227b365533c1e67dbc89f0b20195cf1cbd480d333ade1c9bb28308085b72ced430268c1492a27050c43668adc9cf8b8509447cfcd3c8f8d8eb554f704101786aa9ebca86991d250776a37a1f56fbf7d08e591f978da49c3870625879f70e2418aec5cba32fa8c346fa9038baebc35ad0068a4d03537aee14c2e71570a87490377fa8dd66f995aa044a522f0c7025a7ab2dd5ad30a64268dc112b7f9fa156df64d631f55f1d6edc55cec570a9c7372e29e02c8d4867bae249431dcf6ed2794a0183f0f7501201feca4a81d334c642fc8d38e9a90fa77429665e09e214797dfa455ff47c4f219d3a2cb0176bc2236455123c1c5da714ad29d580fb194f87173a18dc >"$TMP/pk.hex" + printf '%s' 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f9300000000000000000000000005101318212b >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex f5408337d0fee65c28851226a5fa81b58464632c78e2a9bef70d330f2e3a5f74d9cf676aedd1067c91a5dd5d4edc46f868a93ffec9f44e254e44f682a153aeadf228e8db7c5fcfed30cc3408e261ab896876bee56660d2a7c1d7eac20c5754255206a178f7156295065ce7876f90c48f44bc37f3a00e32eefd3a4bb1e298fe283d106eaef92a33a594253a2a0790976a1d04636f8672d28c06c852ea8bb43b84bff512996e7616963d5b9a2906466a152c7ea9be178be35405683b44367af85d2daad87630c1e21ba5490154f0141780f5ed0407cb0b975dd56d5930f9b26413b843b83f3693304b0038bd3e4bb398868060ea18c9c67099376470a50deb052e4056743fbcdf0341b192663bd1c21ba3b3d5666e0d0e29c4e1ed0759ab0bd9d1d355011b94e0ff0c049b03ddb7138640667144fcacd7265f55a07e5387f1abd30c037cf14d436aa855f827049215440d8007f61460500d943f57ffb6bfee6fedd2fcec52882d7d8da1aab29e892c8beac3df3234b4a7d2eca3a45c6623c52bbdd07c1c94314b706988a52029f8f8b06e874b741d72926652c78c6ace2cfd8864eadb2e4b39cafe6e03e4edbafa2747db9bc42f92af8b031e3e380846b1bfd15ade88c285d6a6fffe91eafc8b17de6cbc68575f323cc09fc20e49e8efd76f9568bec486b78df4245428d8d0d5f53873e11de65fda4c770b521a8c67f5c51d48cc26358954514447881fd9a42e5891dac7e1db5249d7861b322111e5fb929bee9ff5e9d5a2667ba93e63fc03040d2e82648f89e89dec1d1d2dfb9efeceb7940f7dcbebeb5a239cc1c54d8f7d52cba220d0634e15df46a58280bc5a48840bd39274cfde150f9ad9a40f6398d715350925f0e0501944409f32331a362bdaaafb3d8ce71c964332d6afb7e684f99951246d88081c86744ae68133f22c53a4b5ae258f230a98491d2d43a79a6d0f4d54a3b62013965ac7c82d0507125a38a0277f81cbc1d46cef2a131c6f51b88ec0baae0c82a6a0e72831cb06f9116cff5111d597e01057d32805a008f52c9aec3311139bfb35982789ff83bdd0c31e9f1080e8ed8eb99fde66bafb29e3357389fe3785b60c78e229ef073e1b65e34d848bd4d8a4f251551e2d38d2546afbc205d3c6dab34d2b962b1afb44f1d22fc10c6744fcd6b636afd3cb414b16c2e0d708fe9f51ff19120bde693b028b6d1e6dbe37b4b8b3bc7c6f7a842701603869d3ded572500f085502efc8d3cc62b30e5cdbcb5e86d9c0d42973bf755df539cc0aea58f9148386db67bd2bf70cd12ccd96d5c66fb271416b772465228dc44b079178f9b766370b66a79b871faca246ca6f8f63be9f0668297ac446cad5cf4a83318b1b00ecbd283f0eecee60a9a37a27abdbdbe382e307970002837dfc0bd3934ebd008918fd4bd383c02c9d37f694996e989a49075767ebc4a2981ef5275455e026cb0bd70946cdd1fadaf251381d324f9efbb860d1b280c29685bab97d010676273b45cca12ac3966aae342c84e2357eccf252577743b8787967b40b07ef2d3d9e6c1a3bcb059cba0fdb7f0d4f815c242b8e14acd3375e608e9230ba3cf8718f43882a3e1e661a2bbe81830d34741f33473e263b3790abe67acf29f5df44865b2ffbc96975fd62738a64112deda5a2534fb0a23b3b3024df986391badf9041c593c313a7ca1e1fcffcb65b07b9a99337b4a4acf616cbe1553eb9541f38aa6247342905995233a28172ca13396b2a9662970120f82b92a213f43de7a232ccca3268265c9ce042d50915430a6c455f32277da42f9962fb9163b623231ebc080fa7b8e9f9021fcf85b98f9c483e4d2226b9326a5bcb2e7449ef029ae142d3a0f0c28bd4f7e9c51a12e1336f24dfacbc3f808a8f7dd683027bc948763b808fb0037394b8b41bc9b2ec7887e67584e03d11b15ca203b2bcb43f8881638c4e4eee7f846d09c7f89b7739df22b2c3acc235032ba8f7ae27b5b9d25733143e80a4cdde6770719c1e66ec2ce683612233e88fafff84c0745a98aa1254c8219c6c556348c2b5d1beeb61532d6bf7bde153271dc647460beb65fe0055b33fd6480dcbb9d7d471952cfa5be260c39721a8c5c89b9e966ae2dc9036451ec9f2c49433b2225e13f23e20c2bfba81a7b3a555883449238f7d48213e9f10ce19e76f1bdcfc73ee5524bd7d8be0a4b46784e238233c04fb99383ec7726f9717e1179dd14fba9ad6c2ebd1699f0ab0e57e6cad23875b029e89cfda06f51266ecd2eed4edafb51e82f2a506d57ba74da611774ca5fa2fff4a976519de425885e7d09219cf815b1767d4fc5a72c18918991a285086a6a766614a4d245387da50f28dd778fb33ab88c0918feba3768c55bb1f07aec33cfeed33d6faa4d34fd7227b365533c1e67dbc89f0b20195cf1cbd480d333ade1c9bb28308085b72ced430268c1492a27050c43668adc9cf8b8509447cfcd3c8f8d8eb554f704101786aa9ebca86991d250776a37a1f56fbf7d08e591f978da49c3870625879f70e2418aec5cba32fa8c346fa9038baebc35ad0068a4d03537aee14c2e71570a87490377fa8dd66f995aa044a522f0c7025a7ab2dd5ad30a64268dc112b7f9fa156df64d631f55f1d6edc55cec570a9c7372e29e02c8d4867bae249431dcf6ed2794a0183f0f7501201feca4a81d334c642fc8d38e9a90fa77429665e09e214797dfa455ff47c4f219d3a2cb0176bc2236455123c1c5da714ad29d580fb194f87173a18dc >"$TMP/pk.bin"; unhex 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f9300000000000000000000000005101318212b >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' 69da5aec6d5f58fbf29439c520bd68b966e3dd2ca633b68351c2862344713a1e9c086a44f9a870a3ccc14de62d6c12b278c354d7197c4d6d7f83d1422b29b250f5ee3fec118311d905e5db2b4b8b23b8d542202d6652f6dc3f9d7ed51f2463082d3f145cfd0fa7ac548a47e91c1ccb1a55b215e90ab355bfc6d67154287b1dfae0fb530264dbb841a7684b396e5ca0459d795216416a9d232bc89b32e0f9461f53107c78e66c8e876554e8ddd501867b55dcfc1fb33f102e03373cdd192640f1027a08ce277b468f6ed0fe80a9d6cd2d6b2f7a3738c8325d95b0ccc6e7b9fb000c923b92298e0867d4a9f6dd5513e8001033c633bb1641ee66349487224dd43386c7fcc29916332066a868100d46e2c5b8354c28f087a024cba27694afc4c1665e0d72b37686919ad55052cc63a144febe4e2a0c9ae416e064e289f9f69cbb883665d1130826b7b74e30c94a2b98b67b471663e3d66326db3b43bebf958e8665b68eda90e8c5d9494b0c7c9ec48800910dd6d906b1fcd47a0aac462ac87b126d21b5ba150df61f752257ddf5a063b4a5b150371d625535e3b2874b9fe548960ff67931cd6c12496e8213e2ace6fff48e6bdc60310e49389f62579db26b92ad73e9d3f23942cab51784f48b3660b6450caecbb0df2aa4c8e56577f5ea450d2f7f51aacc0b304a62250bf2cae7b99dcd955b6596625d06da1c67f730b706fdba630f00fd891830d251484640b7258ab364d6fd9986878fffa69b7c44b92e43143affae8b098e1d27716850f37553bf266cdfb561abbcdbfeb80752b364434e64b80429b54cc88693ce03dc0fa147f0741b215f0728499bdc25140aafc976ac99e910ba8a8a50d21b7bddaa28626b3b90a93fd44077068357c81d36e735eda4362930adead4951a0baa104f384fc70e842a9f329e1868b07b455e9cc3fecd54805c9052e70f88c3b92fe0fc6a4d7dda18cf5694e5398860e439a1e19d5a66f2fbc0aacdd1a498711bb16054796c015a715395ef6174e37b04eda589b673c4d5dda737817fb52f392caf7a72d7a3e84b2180cb5b75bc8af065bdc05c3e4040435a1b160081352ac43e09cbf2ead6e09c2b0be0e37894888fe2812f68806f957c13fce6ff167bcee21d4f412ec95a4847f3db7bf441223a4d4ca9ed69adb4de8a4b5b01c775f2721226e6c59ff26fc38e1bb78a384b30e7b55f082e264d8f25e31518619ddd6b6a9faf8aa6cdb5eab75ed59a33825d5ef8b93bde5d120ada773fcc0852b918f4f03e2d2a543b15363adb823eb1f6c533b98d940411e1f5c1cf521f9f63d5454697608326625fffe01bf87f44187dad631df2898effd2c291d98222e564abe3b042b75e90c9c54667842fa8ebb68a1244bf8e0c3ae3ee5f97d5ddeefd986c4bd3f99d877c2cc2381a89abdc61713d38cee58bf69805a485c288d21b15843147066b4a74c69dc25de878e21d35fdfe6746feb4c166606bf3219e42cf63581e7e6bd6570f40f8fae590cedf5106fe57037ccb2324b74fca6500f6ed3d0736cdcc67d04f8fa9e80054a5bd7c8459fc1abb1c4c78677d7f6b325af94a0e5c9c7db0a748e12c5265e8724947d9b5c4bab1a8b6faec827cc41ec115ef3c2d7348cddabddfbc8436f3b41765e13f3762b3b45ed23156f085831e726a55d4b83848b3d1d3352aab9edcc0ac2388f2383f6301ad813b917ee3f23734e057832ae4cf65e668c9ddd0bdd0f9d8b6693254649668aa91a1fa5eb7c59859bb6ddd36c25f4a2223f5d688b480d0388fa307ea69298f9bf7737f6b3dbfda87b331affd75cd8d88f0460e98ebc2890b217bd6d11000a3a088cd837f4f8859a43f76afaaab05a0c3007a149d4d6b9155cadc2c9b55003efdec5012b6272b87183694c505f0446ede55f35b8ab201f9eda974ff840eccb0f004fa3acf753acd0613f66e2a6ac82e322199d37b4af83cbb3d98371c31be79bb42331e819644cbad2ce27a04e4c517998692cd8331552892e199a01a6922bda4d38ac4c01f708809e529c3216eaab399ef25b350ea213ba47126f278140e17391ca7139bd13c56f415e6b74aed8dbfbf38c95dc6db366fd72aa863a27fa1ebf198716400b978a3709e35039731930406588ebdffd35fa230a9b75fce41d7acd214ca4f0029896c137495eade0cf4d10fe621c73f01061acb077de72177ff5dbc6f0c5bec681aa34668ca4fcdd727525068b0b0e9072971b84ef6ce11d5c3c6024da40966703dcc2b33ae04f677677635a55db508f34f1403cdbe37960c8577dac3d848b29f3b5c5c6c56fb74f34c8f4634c04b8cce9b218f1760ca00e6de87efd14087c633469c892bf3e319443336733bb60cfb44941bfa25229aa24384d812db90fe74e0f93fda005eea87400736cabc036f71421b6657b1674d4a8f76cbbf3a8b1c0af82f72973927752257c532db439d96762ad64f102551a9d03f9ce3d8cc850c393c128bf8054bb55bb92ea31ec0706f083a9cf90424c617f8ad2a21225d1913c30e8f47a6b7131304d536a85596ebfd987b64b6bf3c51638d6c839214b53c3c10aa52bd9c6eb77fcf80b5e3b724dec1381d0e02207a6adc73ff53d9d1ffcee1c4a28fa5445ce518eee937074ff7a402f5bbcb362ff090415f9dbd93b62ee56dc8c50e4d2e34c6c621650c0dffe311484e95d68de77170c909c815828946aeeec7ede56bcf433e22fc63a33f764ced1f9242f3d26dc7558686e471f30fbe9304d3d56af8b23e72a4088970b24b2f7e968c1d0392eeeb0b0f0ac8c176547a5383d948ed15484b79e21314a1f28ed624f61e5aaecf2269e5b027e1910ffddede52fad4e8da224e8a10b079548fa7cd44172f4991adfd7623d13e5a19c812824bcf990c07c9721ded9093be6ce7bc7da3ac8c932133a64396b822be92b088844991596df893625a4ef24543bf75a10d7d17ff70350ef62ce3a7758aebbf9b3977b08becb9ea28376082f607965f2cded28bbdb39dab7e00833b0488370d221742b66e27d9ee2d9dd07f401bc22a62c8a9d8d3a290c63804991496aafa47a32578f583cfb53d0c2199055973440d7535e0da6cb2957f4e04002ecea68f9c3ff76cade27ed15fd7835989d0abb197fe32f68636139a42710644bb25860ff33f539200e3ccb8a7738422ca0fa0c744b4c19d15c5d4a3cb082e20a78e20b5a4965b043595cbcacad500b5adbb6cd597e6a4b9c5ea6a1f2e653b5474da277f1818048094ac9e0e1e0b20068d1c1ce5a114a4db7195057a6ce4d221c336fdc29190fee8ff855cae8b7f7c02eec21f972c827066d9c6dcc4a4179bc44ea9b88abe5124bf78b071e09e9af43f739a6e1030091fc091e73edc447f25c68bf84b8df7aa8f091ab42662b93e02c27003afc7b0ca69efcfa60bd53d4d78ceb7c4d2c8fd5ed7e8b35024de849e06400ad145fdb28348d22b317ccec704c401f88db1af2a5348223f5cefd914e404c9d73805d0de77211881486f1bf4aadacadd3ae2588f0db7b5e6957fed50a374f541cfe5e4e923c82ec47e5b3d2c70ad6760c79cd5080b490bdc75f9ef5e1d17f0978b1e8770775f902b9463e6980e1683b2454751ba2dad4a2e6460924bd60ff49b03230cb11fcd04a0388e60874c35d3f6cfc4dd487665e1b16578751eaea89e126bf58044596e3188c7a9631017be1f2dcd7d612331832ff8755460dc496aa99a61ea053c78e72607a18213ff9ef4bb880903b91e9a43e0b1f0ed1511b2eca2f4253fcfbd7d0faebf3680fbf0a45df231544882c9c46505c726d56905d02fd046c1652d8fd06d15286a1a8f8b69fbd825ca421fd80f5e9ba1a23f924937ad049adeec60c78fea1adf9b1ef7e8ac4d1ded18f1a801b0bda8fe9a88098825ff3eef5c1fc68cbea143310b39543293f3f5fbcf4773b02054c0bc79f00554947c7604b36389c0c45f597a88f3713456b4cfd83b30cb6520b624aa09c812066a8cd542dc67e19e4c92b562b4e0f6799fe57d9d4f4f3e0b6fabff4b1fc190bf1e78775ebcbe3655d370ca6c08f48decf6153a4989eeab6921f8475f85197f51d651e563994257df57977e5f219b4879751de57ab0374b407a21adb4ba520bb35e7b7508675bf49f4e432190451423cbd529fc79b22baae9cb1d8660c3a49c456ac03bc06c0ef3b02f7d8acd40919315206fb38e715139c9bd6f89a58634fe683df03f5bda719764f6c38131bc5ba1c53244472ef73834ade04b86ca08dd753141ac0a9a230e246735060a044018bc9b75d50134b20e6219c13f8325b5a0201e9453f6f012fe72e829ee1c637fe30037a9212a31c6e713726a6cd4cf2dd66ffdba77f1e2800e717940f231d04aa2e4e88dea084754947d848c0271856bfe659922408449858a81fa6583f062d96898d18ec53664f0067eb9b9c40ad2579ba9802abd8d1bf287e49d94ae397e784db14b5f7010ee4fc42e6e3c8ba80370afc188fcecaf466ea830d7b16362e5c9329980b981decc7174f3ff70a35d8a180ee12ed0cbffd4e8d14eb503387e4959f702d4293109e922eb561371f9ab21475821f8555d92f0aa1c3d841a6f1eabd4e663993636c754ce2b3c3f6a6b6d0b161e777b8296d7dce7fd162970496494d4f60716244a5a7fb7cee40e1d565e6566697e8f930000000000000000000000000510131821 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa65 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa65_sign_verify_round_trip() { + "$BC_RUST" mldsa65 keygen >"$TMP/sk" + assert_size "$TMP/sk" 4032 "sk length of ML-DSA-65" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa65 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 3309 "signature length of ML-DSA-65" + expect_ok "pure signature verifies" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa65 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# ACVP ML-DSA keyGen (bc-test-data/pqc/crypto/mldsa/ML-DSA-keyGen.txt, ML-DSA-87 tcId 1): the 32-byte +# seed xi gives the vector's sk (`keygen-from-seed`) and pk (`pk-from-sk`), from raw and hex seeds +# (hex needs --hex-input). +test_mldsa87_keygen_from_seed_matches_acvp() { + local got + unhex D67F9DB963E93302D0B7B996FDA537D31B1E7885F00C2675FE01E6F15595B4A6 >"$TMP/seed" + got=$(hex_out "$BC_RUST" mldsa87 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cb66a75a14a2a7734b671939b9fc5da5e8f3badf60b2ef9aea78c9c262959344f780455f7f719fbcdde674b0d50446651a9766c0f2e5cd3db90758cf7c58807ec47e0d3b95b9e8fcfadfc1b8d56543b5f7184d9148f32ad8c12391827a54df4c300ca9905a4469c0c4904092259b00068c162801120221364883340858b46c00a04de4227054a0641c090d0229690c116554466491140e5b184c11c84083c280e3a4711a262121312d18924c2334680bb78198342020a59101047041308c5aa06c10402d18854d01224a1c114848a465e0c42c031949a08204891632520488913201034610a03649929831e0824154424603318c5c0292489244c3886d2313204106609c404a1c4628e4168e62969094889104102d24c6440ab14de2246e5ca66810042d5a4840c1c0311a826d4cc84dc43645010868a208861323456220460a26051431081405852407425ca86801a2680a04501202246316120c3680d3b624d2885160b688d2c4691841650015710cc50d63062d1c162913c630d8224941a2410892259cb471cb428859824120a50154b001a3400ac912660b972ddba0910c438e21426024061254244692b26d9b084d43b809d33288e3a085a0b00c18b2089ab804e0b46003306a8b24688286710948899b147103300d91947020b40511b12d9a0048102004d2343184045151104ad9b0809ac80090c8240b078501050d8a4442822669c2a02904a28454880008396854b84122432419360a22028a04c980e2a42c12314a92326ce186210b344e04498523b26802c74014206661266920982083322c9c4882c0c88d13144152c84c0b32305a146a0191050a2782a2a66121448010a86501976918342610330e53000e214629192948241422da120554340109428018a40849c444e2466183a450d2266013436880402543b4856006081381114ca48ddb02842211406108644c48119c34921b480e03142999249100939153b20d41a2298104851a346422170a20b3015236505a106012a481e104702413505c82294b1465cbc4495410880ab70598922404c588203044cb0006c2002d233951023040811446c3c02900112a54980824066144468c1913020316121ba24c22b28411c730e30005491850501680cb366249926dd4284a51366cd120881346481b82254b98250ab909c3205063184988268251b00840160613478819c4041b156282c66191227192b2654a364a0a372593a245184066cc8481cc0609d8260a1920501b05900b050a9b3650222612cbb6649022814086418ca63120421218c68c13090c89144e4a4421e03072110645989604d0848521a0081b26115c827142848c48c6252443842431310c9910d144814b369162b805e0480a9224451aa38482b8109b845062c65193266c8ab685228284e2900141844cdc002c943092249889922802204101e2c0605bc48021100c5ab09101186463086523299263c86190b620098964d4b00c03097119869110276623310960c44c18b0215c3208c3a270110411a2b049492066424661d4008dc432681b9300a3b800419225da448d111169caa84d4a864088462d034872a2386a00b929931842198380524865cc460418c96849086ad33226c9c28c0111319912329886905a8604db00280a202d08998999106a58b60453a869cb9208c2c00549962509c86c93b42112202211b97189369208100254c049912611a40002c414621cb671189428c3066063124d0a1729134212d810811245440c160a62363001c6014ab8440b02720ba22c09b4219a244e820026984862193105db98899c406421484e8246660032642419492193618834611b0650c4246c19b208d828848ac2285b1682c2002604b68c2183449c169110088988105221388e0b41068cc25081a04004a38591902d11420d02a00883c86d044742134485cc346a44244e21c19061222193469162c00d8c265109878993100101988460c06921118101044d2248004cc0809310609908691a079022c665a0404423152c8c166924382191b850d2946500396d8280001b4981203020c336062029308c06918412225b206e5ab64422919164b845d8060e09228c9b806ddb30905b142522370d094904c9080662a891d9100060a885ca022e0cb471088628039491e3ee3d00690918fbe73f35a5eb0f08f223c6ec9e9b4d3cf388c99910caa16d496f16913bb024f0b437ddd80c16fd412fcff3c240a4148e5092b6ff9f8694d5d0eeabc8be4158f161bfb4d6ebee8b1a5290d4e811c1eb77766c08dcbac587f8724d65ad856a12daed257365e54dccacae742ebe4e963d493dde59aa5e674392ca6e899bd40f983fdb8edb53c02dfa84d81563f1756cb495932c516b189748089d021933989bbee17d964474c5235bb25940d0fc48e15f0facf4cf0257b1bb3764f0079ec0052e45ac10bb43b6cd413c30710a8d4c252387394d6e43bb37802658c23d8cd8e6f5ff5050e14b7865f9f1b3a236bb8ee5554bd47ded627bf0e747239cf27cc2f20958948392f8b0ee0de766784570b50c869afe4566728d2fe448f8d82e135821149e03993891e8253e603eec9ebc8b797183bceab097a5e31592ad9f3ce82b868f1b77a644a6c41c52a23fefbff968f29fa8314026573dcafc094a21f42447593b8950634ee52549d0b88af1f6385416d94cde1c54f30e4161a8902fb6becf55a6052d9b3f11ef79f0ad4452b6d7f888ff30bf9614a6309eae55615952af39f12078576ebc58933f32ca52aa6910f0873d05db7772e24a7565ec47e1d9475af913d9fbe33e2f3cd93a9e75f73f191c56f4e45780a4d4e5e513706f628bac1ae1d9d538872d712c359036181b69760a5fc1cff7d545e48aa6e96f4b89ae84708887f06ce09a78b83766184c2113de2694d24707adfe5cd0463699a4e8a0570083dc4f865f2243fe82bbb066ce024fb54124f2bf73e87a0144e43b3d0fc69e777b1ee8b969bb5321a0cb5de6487c1c4795ed8b52b2d70a7a1be574fadd2c97c31d48287a87f23a2cde106bdd17dff3b09a47cc3a2c0c8f380b175caf874bbda6a88f81742a3f6114acf882dd4df7fd149f063c89dc9429b3bbe92c5ac12747967afcf38f8a744141f5374c87b81c5f3cf5fdc8db5dbd2a0762f5df49fcf3d8b0b5fd4d810c923e29abf9a2015e25005236288dd00017184706e26eb72f16358a39e0ea5d73152c136ad9b43c411e79d34ad1f6ddac5ba00768c80d6f6f960d9edb1e79c2aaef8eb3eb957f80890792acf8b28d10b6f11f9a3b033e565ceeb989b6bb237bd79d032100dfebe20043bbbb4bd5a921a7bd51f1584444a17daf2dc36d377fbcb124b24baf1e792809c75d11a792a05c8d0f38ce1842b6a1cd18c99176d88eaab8669f34378304898f0cbc86c8955815676c619eb493764aeff4f4c96364a3d6f48cc3998fa9d0f9efa256080dfec909f570b871a2c65c9d90e7e67838a90036737a37b694cbd9bc0b4586b31ffc84cbe267fd574c4d655053eb87aa25b2d6cd3c173ad5de2303334e452f3065637ffb4ace7482745af9c62995c16c3b1f140b2a4af6c5e1d7a2d916b0c8dcb5f4fd2dcfaf51cc6252213b81e27b7a2c1eff21dba40bdf4c70f15560919e5db420ca3ea590f70729b7f1d59a880bb611c712112a92ba62fad5b392713aa8b7d28f265a458b234b84e4434405a5bc14aaa7e63a763a76bf75cfc4c105705de943541d99361d892eb049ca86f654195643a3b9fca77390ccca113668b2879a979fe0580d15a81a34d9983df92d923abbf8da71732127c0c3ab32b65c539dc8298bef3de901502c457a57cb3611a2f1298feb7219af1a4f465962e1e650547a9bab0b64956fefd71f28f87b907d14f7422134632326f79cfefafa877ae103d0e34754ad63cb4749e5bf7dad7fc4f05d924931ab2ec0d498fa1485d74c6d679d4f0d9ae5aad3077a841a2b8415d2fed5a3e831ee3c0d216ee867a2e4cb81584c3ea9d77b3b5fa30cfa19ab53369defae44b0267a8e0210d796698463e2a0ad06c5cbf9d197f3a954aaa96c4c41a7300d493cc617e7500d1c9f635fbb9a94f96c276aa418fd860f6b99a3e14114d051eac3ca51cd3509ca0ce8346d156ec0973e1d75d2ab7988f9d1e0d1329a4fb3841325849914354a24a589cb3be1acf54909bdf6306a292c553410b9fda00ba1f1b07f0979973ab0ce7cc8e68def5432233717f1be411aed5c06ff3b98f5888e31cde80dcc52ab038053fb50bb5bb244e56346324031a62c1cb492825fd1a0e3f293629eefe711656af2a283d693f54e30bf2e1afe4ae21aa1a439f5b2277ad49d28ba85b81ad0d86b8e6d62625f528818f2ce150160675fd39fc24f768c9fa6069d4a0d2b3893e0fa70c762a16e290553d4f9b094c7dc023a0e7c21345f80f60800eb5ee6d28d37f8bf4ad1f3a689092da39221c1fb539d5e98c146a29edadaa3454b9ff93e8a0dd7a3398315d6ee2899d10c870a5c4d1708d360f2944e7121703c8e2ea9ef54d73bc62019752fec5f2ccd38b203d625bbdd6b92d51540f9c55af9ec38f7a70e7de47fbbcca3788e07e4e64acf7806ad07454e57617965c2473258f9997387c20740ee28dc137f8283822d0efbe8b268a1fd170f095359d7814a51047344450cf58dc544e4f0b46497a847760819b345da05543ee76798cfb5b42e0f46510cf46f6bed7c089fb8816d663c38a4c27f29bc5f7cc210d0d49deae702c0cdce70d42360d78c18fd1cf065e50b6a0755a11b77c7e4d5ee62c2051d0155881939045b0fc33b0c46b5149054ebdd9da202d4cc7797aa43743f56746569fc1e7599a2ba18ba7352b2acef47ba3a2b8c7e1d1f6885cd290495888eedf290a699719b89d1535032bc1de33c60dbdc6ac0f401af899b92fcdf8748f19d902b191d52a253c46c15a47f075fef228561998b18e0f7f4a4e527c75509c59872d0b10022349ed5a6b113c9cdd75df0b5589761ff379b509eddf690f86afaca4d9fe85c44a0c8195b14b0d4b9c4d4bae2d59ab6c740d15b730b6290801ff66a5e6ee6d4c7356e0bc6e04987b2f833d25f4473b1cf663534a9023374b759afe1feaae799714c5cf94269c00634bec34f782be0d36039cb646decbca9542913817dcb559e7078f959d59a979d4318b88e05903aa9aef43e43e0a017616e097f3e026ff8bcbafca129b8efbd6806dd13c131bb3177e30851597806801ef16e2e58a427a7848f7627259a177c698cc7425d9b13964c3ec85c665fefd64b4b9098fee118d924c97bd9285ed52d3233d5510099bcf61e3558fb39260596cdeb3b01e0482f84d130d804d5e67622546bfd1c11548f814ecd61cb67507efa817afc86ff1249e910a065916658fc114fd513b9359edf8b33498f80584e880a827f2ebc979d36a82cc9789446f72ce491066f4abd4b03ee386d8eb813fe6eb606a1249ca6a592f12e7848fefb30294062926d953040c860fa851dac36ca8765082ba93e31eb1e2007df83252c8d062171a5e262a01297bd65d4ea3055721bd8afabc3ec1256df3b5149a426c5ce63866ce11f577b3faee2b3395d75ffd60025e34575b081c86989e9ec2789f7d8ec1044cae9599ee6b502edcdd6c00abaabd06b20aaea10ed998b8b80c65a6df8cd3a773ea80fc17093932d83bf38a8368a3917f8c2c9eb193f6f9587eb96bf69ed3384f107d7b28343b9f3705de33404c3770e40db07753ae554b764903a1a07319ad6d50c3bad320fff887bf902bd598b8574ce9428ccf5586e2954ac6bbaf2536211934737e5eaf0a1012903a543c024f34b58e4706cc55bf9687516bc96296a4e6adff507a0f0c72365796af72cadf785fc02fa0fa819f727953b29cf9bee2514217cf21f7fa15140cd7c4b519193f94c0d8efb753c6d76e1ade6ebfd71b5e521a6d6710b2f43035dc6000bafc71645f7f8a7da0bf3ff397a8098d2b924f36a046cb4d515387f57db5e32792df9e8a3488aac6e3960f1c9ea2cb16322e6bc94e44b13925671fe04e3a76729447ff18613feabf046b104e8f6b443822e92267e29a2b0ba7f6d415921c83137ab959681ed1aa6d3bcac9f53105a390b37482f1bb683d861757ab9cf4cfa8330809a5921a2167d7e97c877791ae0370e2dc2bdc3022cbcb35c2dcfd07088d91cabf15823bd057b8ac39b9bafeb88e8c17ac3a269855535a1e8d66b06b5497b8f6fbe125216c2c4ac4cece3baa85b3d8cc50a397195383cdde30580c0ec1b5098627e2af315a0dfe27b785a7579251b2f3c4b1dec5b6128c816482d0486b6af460e54fa38e50f5beabd3bfc9cdb2537a85bdcfef30886ce42a41c5f1416b656236cd52879e5f3a662dbe395f2d4fb9a70f0cb0a0682ac6dd03c8982233b15a2de1cd1ad4c29f1d2c60d4b50296ccd6d65516dc65fdd4e760f6c3825c3656aab20ec67a3bee64a0d05ed26b2d1212fde1e1ce421dd64e95740a7b12472332e47d468cfc0674d25cdebeb89902559ca82e07f2030240ac87ad65cc6a120f2e2cb6c6e091e17403b41f285ba82addbed90cec987e1e9091331330502429fb195c946925b5ae9f21b55612d5f6334945062dd51e1c2a16a17d09a1abd967008a26b6979c3b4afd87c2eafa259cf809ef86465f0717343280a0e8b0e48e756a10ec1aabb558e31b4baa40618c32d08797059d33d67a91d662d708e79b099d986531199195885615b8bc6a6ce331e7d96c31178ce2a0c8f5a6761a71dae83c4abf70ec5c4480c0cb9699df4031578546561397aed8e7d5c3d8339b0e59ec1ebcf75ab1b6d74c27ae6fa173aac297e3e6af4d52d9eea650d492b90885f75bf4aa82c3d747d4392ecab1f88f50100" "sk from a raw seed" + got=$(printf '%s' D67F9DB963E93302D0B7B996FDA537D31B1E7885F00C2675FE01E6F15595B4A6 | hex_out "$BC_RUST" --hex-input mldsa87 keygen-from-seed -x) + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cb66a75a14a2a7734b671939b9fc5da5e8f3badf60b2ef9aea78c9c262959344f780455f7f719fbcdde674b0d50446651a9766c0f2e5cd3db90758cf7c58807ec47e0d3b95b9e8fcfadfc1b8d56543b5f7184d9148f32ad8c12391827a54df4c300ca9905a4469c0c4904092259b00068c162801120221364883340858b46c00a04de4227054a0641c090d0229690c116554466491140e5b184c11c84083c280e3a4711a262121312d18924c2334680bb78198342020a59101047041308c5aa06c10402d18854d01224a1c114848a465e0c42c031949a08204891632520488913201034610a03649929831e0824154424603318c5c0292489244c3886d2313204106609c404a1c4628e4168e62969094889104102d24c6440ab14de2246e5ca66810042d5a4840c1c0311a826d4cc84dc43645010868a208861323456220460a26051431081405852407425ca86801a2680a04501202246316120c3680d3b624d2885160b688d2c4691841650015710cc50d63062d1c162913c630d8224941a2410892259cb471cb428859824120a50154b001a3400ac912660b972ddba0910c438e21426024061254244692b26d9b084d43b809d33288e3a085a0b00c18b2089ab804e0b46003306a8b24688286710948899b147103300d91947020b40511b12d9a0048102004d2343184045151104ad9b0809ac80090c8240b078501050d8a4442822669c2a02904a28454880008396854b84122432419360a22028a04c980e2a42c12314a92326ce186210b344e04498523b26802c74014206661266920982083322c9c4882c0c88d13144152c84c0b32305a146a0191050a2782a2a66121448010a86501976918342610330e53000e214629192948241422da120554340109428018a40849c444e2466183a450d2266013436880402543b4856006081381114ca48ddb02842211406108644c48119c34921b480e03142999249100939153b20d41a2298104851a346422170a20b3015236505a106012a481e104702413505c82294b1465cbc4495410880ab70598922404c588203044cb0006c2002d233951023040811446c3c02900112a54980824066144468c1913020316121ba24c22b28411c730e30005491850501680cb366249926dd4284a51366cd120881346481b82254b98250ab909c3205063184988268251b00840160613478819c4041b156282c66191227192b2654a364a0a372593a245184066cc8481cc0609d8260a1920501b05900b050a9b3650222612cbb6649022814086418ca63120421218c68c13090c89144e4a4421e03072110645989604d0848521a0081b26115c827142848c48c6252443842431310c9910d144814b369162b805e0480a9224451aa38482b8109b845062c65193266c8ab685228284e2900141844cdc002c943092249889922802204101e2c0605bc48021100c5ab09101186463086523299263c86190b620098964d4b00c03097119869110276623310960c44c18b0215c3208c3a270110411a2b049492066424661d4008dc432681b9300a3b800419225da448d111169caa84d4a864088462d034872a2386a00b929931842198380524865cc460418c96849086ad33226c9c28c0111319912329886905a8604db00280a202d08998999106a58b60453a869cb9208c2c00549962509c86c93b42112202211b97189369208100254c049912611a40002c414621cb671189428c3066063124d0a1729134212d810811245440c160a62363001c6014ab8440b02720ba22c09b4219a244e820026984862193105db98899c406421484e8246660032642419492193618834611b0650c4246c19b208d828848ac2285b1682c2002604b68c2183449c169110088988105221388e0b41068cc25081a04004a38591902d11420d02a00883c86d044742134485cc346a44244e21c19061222193469162c00d8c265109878993100101988460c06921118101044d2248004cc0809310609908691a079022c665a0404423152c8c166924382191b850d2946500396d8280001b4981203020c336062029308c06918412225b206e5ab64422919164b845d8060e09228c9b806ddb30905b142522370d094904c9080662a891d9100060a885ca022e0cb471088628039491e3ee3d00690918fbe73f35a5eb0f08f223c6ec9e9b4d3cf388c99910caa16d496f16913bb024f0b437ddd80c16fd412fcff3c240a4148e5092b6ff9f8694d5d0eeabc8be4158f161bfb4d6ebee8b1a5290d4e811c1eb77766c08dcbac587f8724d65ad856a12daed257365e54dccacae742ebe4e963d493dde59aa5e674392ca6e899bd40f983fdb8edb53c02dfa84d81563f1756cb495932c516b189748089d021933989bbee17d964474c5235bb25940d0fc48e15f0facf4cf0257b1bb3764f0079ec0052e45ac10bb43b6cd413c30710a8d4c252387394d6e43bb37802658c23d8cd8e6f5ff5050e14b7865f9f1b3a236bb8ee5554bd47ded627bf0e747239cf27cc2f20958948392f8b0ee0de766784570b50c869afe4566728d2fe448f8d82e135821149e03993891e8253e603eec9ebc8b797183bceab097a5e31592ad9f3ce82b868f1b77a644a6c41c52a23fefbff968f29fa8314026573dcafc094a21f42447593b8950634ee52549d0b88af1f6385416d94cde1c54f30e4161a8902fb6becf55a6052d9b3f11ef79f0ad4452b6d7f888ff30bf9614a6309eae55615952af39f12078576ebc58933f32ca52aa6910f0873d05db7772e24a7565ec47e1d9475af913d9fbe33e2f3cd93a9e75f73f191c56f4e45780a4d4e5e513706f628bac1ae1d9d538872d712c359036181b69760a5fc1cff7d545e48aa6e96f4b89ae84708887f06ce09a78b83766184c2113de2694d24707adfe5cd0463699a4e8a0570083dc4f865f2243fe82bbb066ce024fb54124f2bf73e87a0144e43b3d0fc69e777b1ee8b969bb5321a0cb5de6487c1c4795ed8b52b2d70a7a1be574fadd2c97c31d48287a87f23a2cde106bdd17dff3b09a47cc3a2c0c8f380b175caf874bbda6a88f81742a3f6114acf882dd4df7fd149f063c89dc9429b3bbe92c5ac12747967afcf38f8a744141f5374c87b81c5f3cf5fdc8db5dbd2a0762f5df49fcf3d8b0b5fd4d810c923e29abf9a2015e25005236288dd00017184706e26eb72f16358a39e0ea5d73152c136ad9b43c411e79d34ad1f6ddac5ba00768c80d6f6f960d9edb1e79c2aaef8eb3eb957f80890792acf8b28d10b6f11f9a3b033e565ceeb989b6bb237bd79d032100dfebe20043bbbb4bd5a921a7bd51f1584444a17daf2dc36d377fbcb124b24baf1e792809c75d11a792a05c8d0f38ce1842b6a1cd18c99176d88eaab8669f34378304898f0cbc86c8955815676c619eb493764aeff4f4c96364a3d6f48cc3998fa9d0f9efa256080dfec909f570b871a2c65c9d90e7e67838a90036737a37b694cbd9bc0b4586b31ffc84cbe267fd574c4d655053eb87aa25b2d6cd3c173ad5de2303334e452f3065637ffb4ace7482745af9c62995c16c3b1f140b2a4af6c5e1d7a2d916b0c8dcb5f4fd2dcfaf51cc6252213b81e27b7a2c1eff21dba40bdf4c70f15560919e5db420ca3ea590f70729b7f1d59a880bb611c712112a92ba62fad5b392713aa8b7d28f265a458b234b84e4434405a5bc14aaa7e63a763a76bf75cfc4c105705de943541d99361d892eb049ca86f654195643a3b9fca77390ccca113668b2879a979fe0580d15a81a34d9983df92d923abbf8da71732127c0c3ab32b65c539dc8298bef3de901502c457a57cb3611a2f1298feb7219af1a4f465962e1e650547a9bab0b64956fefd71f28f87b907d14f7422134632326f79cfefafa877ae103d0e34754ad63cb4749e5bf7dad7fc4f05d924931ab2ec0d498fa1485d74c6d679d4f0d9ae5aad3077a841a2b8415d2fed5a3e831ee3c0d216ee867a2e4cb81584c3ea9d77b3b5fa30cfa19ab53369defae44b0267a8e0210d796698463e2a0ad06c5cbf9d197f3a954aaa96c4c41a7300d493cc617e7500d1c9f635fbb9a94f96c276aa418fd860f6b99a3e14114d051eac3ca51cd3509ca0ce8346d156ec0973e1d75d2ab7988f9d1e0d1329a4fb3841325849914354a24a589cb3be1acf54909bdf6306a292c553410b9fda00ba1f1b07f0979973ab0ce7cc8e68def5432233717f1be411aed5c06ff3b98f5888e31cde80dcc52ab038053fb50bb5bb244e56346324031a62c1cb492825fd1a0e3f293629eefe711656af2a283d693f54e30bf2e1afe4ae21aa1a439f5b2277ad49d28ba85b81ad0d86b8e6d62625f528818f2ce150160675fd39fc24f768c9fa6069d4a0d2b3893e0fa70c762a16e290553d4f9b094c7dc023a0e7c21345f80f60800eb5ee6d28d37f8bf4ad1f3a689092da39221c1fb539d5e98c146a29edadaa3454b9ff93e8a0dd7a3398315d6ee2899d10c870a5c4d1708d360f2944e7121703c8e2ea9ef54d73bc62019752fec5f2ccd38b203d625bbdd6b92d51540f9c55af9ec38f7a70e7de47fbbcca3788e07e4e64acf7806ad07454e57617965c2473258f9997387c20740ee28dc137f8283822d0efbe8b268a1fd170f095359d7814a51047344450cf58dc544e4f0b46497a847760819b345da05543ee76798cfb5b42e0f46510cf46f6bed7c089fb8816d663c38a4c27f29bc5f7cc210d0d49deae702c0cdce70d42360d78c18fd1cf065e50b6a0755a11b77c7e4d5ee62c2051d0155881939045b0fc33b0c46b5149054ebdd9da202d4cc7797aa43743f56746569fc1e7599a2ba18ba7352b2acef47ba3a2b8c7e1d1f6885cd290495888eedf290a699719b89d1535032bc1de33c60dbdc6ac0f401af899b92fcdf8748f19d902b191d52a253c46c15a47f075fef228561998b18e0f7f4a4e527c75509c59872d0b10022349ed5a6b113c9cdd75df0b5589761ff379b509eddf690f86afaca4d9fe85c44a0c8195b14b0d4b9c4d4bae2d59ab6c740d15b730b6290801ff66a5e6ee6d4c7356e0bc6e04987b2f833d25f4473b1cf663534a9023374b759afe1feaae799714c5cf94269c00634bec34f782be0d36039cb646decbca9542913817dcb559e7078f959d59a979d4318b88e05903aa9aef43e43e0a017616e097f3e026ff8bcbafca129b8efbd6806dd13c131bb3177e30851597806801ef16e2e58a427a7848f7627259a177c698cc7425d9b13964c3ec85c665fefd64b4b9098fee118d924c97bd9285ed52d3233d5510099bcf61e3558fb39260596cdeb3b01e0482f84d130d804d5e67622546bfd1c11548f814ecd61cb67507efa817afc86ff1249e910a065916658fc114fd513b9359edf8b33498f80584e880a827f2ebc979d36a82cc9789446f72ce491066f4abd4b03ee386d8eb813fe6eb606a1249ca6a592f12e7848fefb30294062926d953040c860fa851dac36ca8765082ba93e31eb1e2007df83252c8d062171a5e262a01297bd65d4ea3055721bd8afabc3ec1256df3b5149a426c5ce63866ce11f577b3faee2b3395d75ffd60025e34575b081c86989e9ec2789f7d8ec1044cae9599ee6b502edcdd6c00abaabd06b20aaea10ed998b8b80c65a6df8cd3a773ea80fc17093932d83bf38a8368a3917f8c2c9eb193f6f9587eb96bf69ed3384f107d7b28343b9f3705de33404c3770e40db07753ae554b764903a1a07319ad6d50c3bad320fff887bf902bd598b8574ce9428ccf5586e2954ac6bbaf2536211934737e5eaf0a1012903a543c024f34b58e4706cc55bf9687516bc96296a4e6adff507a0f0c72365796af72cadf785fc02fa0fa819f727953b29cf9bee2514217cf21f7fa15140cd7c4b519193f94c0d8efb753c6d76e1ade6ebfd71b5e521a6d6710b2f43035dc6000bafc71645f7f8a7da0bf3ff397a8098d2b924f36a046cb4d515387f57db5e32792df9e8a3488aac6e3960f1c9ea2cb16322e6bc94e44b13925671fe04e3a76729447ff18613feabf046b104e8f6b443822e92267e29a2b0ba7f6d415921c83137ab959681ed1aa6d3bcac9f53105a390b37482f1bb683d861757ab9cf4cfa8330809a5921a2167d7e97c877791ae0370e2dc2bdc3022cbcb35c2dcfd07088d91cabf15823bd057b8ac39b9bafeb88e8c17ac3a269855535a1e8d66b06b5497b8f6fbe125216c2c4ac4cece3baa85b3d8cc50a397195383cdde30580c0ec1b5098627e2af315a0dfe27b785a7579251b2f3c4b1dec5b6128c816482d0486b6af460e54fa38e50f5beabd3bfc9cdb2537a85bdcfef30886ce42a41c5f1416b656236cd52879e5f3a662dbe395f2d4fb9a70f0cb0a0682ac6dd03c8982233b15a2de1cd1ad4c29f1d2c60d4b50296ccd6d65516dc65fdd4e760f6c3825c3656aab20ec67a3bee64a0d05ed26b2d1212fde1e1ce421dd64e95740a7b12472332e47d468cfc0674d25cdebeb89902559ca82e07f2030240ac87ad65cc6a120f2e2cb6c6e091e17403b41f285ba82addbed90cec987e1e9091331330502429fb195c946925b5ae9f21b55612d5f6334945062dd51e1c2a16a17d09a1abd967008a26b6979c3b4afd87c2eafa259cf809ef86465f0717343280a0e8b0e48e756a10ec1aabb558e31b4baa40618c32d08797059d33d67a91d662d708e79b099d986531199195885615b8bc6a6ce331e7d96c31178ce2a0c8f5a6761a71dae83c4abf70ec5c4480c0cb9699df4031578546561397aed8e7d5c3d8339b0e59ec1ebcf75ab1b6d74c27ae6fa173aac297e3e6af4d52d9eea650d492b90885f75bf4aa82c3d747d4392ecab1f88f50100" "sk from a hex seed" + "$BC_RUST" mldsa87 keygen-from-seed <"$TMP/seed" >"$TMP/sk" + got=$(hex_out "$BC_RUST" mldsa87 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cba4a6993379d25c4372a51cc76782ccb4bfd64c4f8b9a371e7e2bb6d4b4288b247c3a4ab2e2c96a4ee0c9ff8e388b543fa023067fb87255dfd537b16ddb7b50a83a4af98443fc0376ecebe059699ef8fc0b4605dd3b952ebc61c041da81f02af8ffa3ec95d292ce76493471b34dd2fc422015d8343baafb23338fc1b9e3742a38ae172c7674e3aab6c5dbaacee8143d342435a2ff3ea62038e907b9cb46e967f89b149a8d7b3f3088a1a77bc7d743fa26e2e238e68809771df6c6e6168f54fbff042be8af3ec36ee16d9f614154d329373107a240047350b9280f9f85423e70a87277bb3c0fec911d2f832d947ed75cbf0491545a98c710b8433ae0783c8f3533987d19da5d94b6eaccc8d19454e4d5d9bf1202fbde2e676e7d5dd6311bb75fef0231c7c866bfa0b5a43445d3daae7ce29934dd1e44ade1cd6f887bd480413178aae7a9f5fae7d8f47bd730b6695e49638bae0f9f047a3ff2eac96d633a7da388067917240ed2513684d7f3d43a3287e7a71ff8a50f4c1d83b427e3748ba11728c7c46cefdffd94d5a3cad19c98ae7d3c39cfd9cc57adb4d55efce506b6bce1d594b09a08138921570709821b7867bcb9a16369cd4988025d819152b42bb9be2cf1a0feaae3ba80a862aae4964503a3d7a1994f85813ff135e903e7eee2fffa9083e40166de96f312b2a27c3e91fd8b64590c06eb8541e30ddee206d37cdc200c202bc65464111eae3cee5f4a15fe14a84288099ddbcdec2d23bf456638d5471b0960f53d74b97bfd94441bf53246840a98a0120c8c78b266189d6992343581997147195cbf3b2c5f4c2de79f9a6421ec65f55af85e71be9a85a280dae87ac80835593d1461d9deb7e001378a018e637f6358683893432dc29ee98204a82d795427c0865df2e0cf71a65101f7f6f8e2f7f32359016299f121e29c07f488da275257f6433a6933b8c3df69669ba174ff6a1d9238d60702770c2b50d2c369d7149c0733996fddb1a866c475ade5a84f6a7d5fc7f795f03bd2f305fbc3fd770731eeb00041c20cd61f32c24f92ff3c20ae790de77615c189e2bc9bf41c12fcc066f6f3c6707987090d55b66ba8914b45ef834024be470f166f284e065834ab05550ea84e6addf82a502df243ee26351f6fe36826af07ccb8e92ce9354b8e42be9767bb03e4264aae3449d9084c955e75c9b1fceec4a314fde2203913a9ff1f0ec556d3130df20c3f6c8fcd6b1d33a7aa7f342b0c82327be9b5de468ea221f96bc3a146b24daf9ea27dbd55d4ae92ea1231ed0143da38a3bd857b411a16ef4d829116111d9c28dd5ee75825b9a5d96e517886ee49d9763942bb393108589720b2f132c2e1aab2d616f5986c885b76076cd4666b009672685fc3538a2872c96c62202a6ff7d00ef7b47401f731c079915541230a67085adf68258455972265ffe0e3a3c8cad8bb7e2e9255b70914d09b64f216435f1f65d89e41c623e9dbd02e849998d8fe49f53749205e0034dadd26f606b3dfe90c3f3a594c06e486ae0b9187bf1ff351379d3b456912957f27ef4c06bc8ed909996ffef694eb81c9d47e410acde2b81d51eed7d7539b70c93cd14bd0615b955c98fcb9ab2e441a61bd020760e6ddd5bea462eb60ac580a99dbad2f81d530b31d462f4b1d3e8b8029fcf50b7e9aa54fbac53820f1248c029fcb2f08f2cd66ed6b3d1d2da2b79dbc3b1a5f2c49b5a7b9902a8e1a9bb326303948c9b5797067b0fe6a8259f0ade7a3b309231a58c67d9d30f9b0e0045b403ec66788f9eaf603605f8bb6a45cd4f45b1767facf2c1063e5e199e24b23e59487cfc4f1e048bd7f76135f07b2898acf450089a746adb35efa41e97ea655475d2862ddb1ef9aba81a5fd34b855d691c291ec9da7752c4b0b05d45dfb122e168bae3263d7edcb1454897394afb1661320ef51c6750999de284c590a55477a0616023c924721557e37d2f7778706d925865595d4137d6d548112aad44f0a7715ffd87974b78665b98148b4c2546a68077893b0db40e5e94939933696b6499fc24a1cfddec19556775ab9e4b23b71a916d5e53f32923c3896f010c230e93f3651cf1d2a6cfba3e8e1a30a14895b2d58e3410fc603dde6a0b537a2fedc2480b98b236c00749fba3fb915130df64d9e5166bde7d204d9499a20ab4c5f57b2a84e86f6b2013a0355c98b82651459fe2304e89a85ee63300abe8a8d87157833d127ea8be07893b28540193d1b5690b230ad82f2f19c20a48846f0bd7b4f89b5c3087fc83d210a89ce60e1e94f2f0b46c4bb091fcc0cbbf8fea4baad75c8e97da61b20d02dd23666b97c9287e92ba41adda15859c285dfbb4ac6d4a1c9caae34eab338abf1f67ddc16722687b13999fcb43a64f807677f7b176ba1005ae388328d2e13adac44f1fb7061202c73513a21d6e862804c6ef084c61d627d2607d521204078ef50a6ea152ba7d50c7ca222fc4d8d315bc86e3dab25be0699cf038d927f97499799f1a80f894238386e1061cfca92fab77920eeb21a560786527431b17c2c9cd401cc36e7efaf86406f16716e0f48d113691366e730ff99d840707d3dba15048369e363823f28d5f754de09eaa22ae5d1f6b9e8e4ab8ee1be22d711a98a31a8ed7513eaf9dd4dd274215586d6fcf3df6eb1120a644e04c9463b87b745cfc2f3ab550dd0b7c2bdca5957e4f72f667dc18f467a0ae583943615f6e9c028c454410b25621200af24989473d75f2f8f1a32358fc3a2e7e5cfedcc7b023b0c3eba272ce5bcfa06d1c2c58a685303c920c848ae7f05fd25a900cd95a17cc3273d52871e2427d280b929cb92b999bcec0c23980d59832ecfe9456db5dd54ec63c2cfa9e444523cda66b136bd1140e87ced84704c7f8d3b9e65e5a56b2c7da3ffc702a8537e4b02d91108e8da31c2b5361bcbaa3b1a7c768903c2f2ac2996809dd12a827ec29fbcfd8de5dffd2033a0466b3ace84b68970f36217f9f9dd736e0bb53c81c759281e73307ba0ba958116a52937ce53b903fbe0fe8f97e50a4a540dea8794ccce67c5d843e4eaebe0248d27354e6c0fa526b789b2fd78a3a6bfd990e22d4e126ab952a716f6c13346ba8f602982a7060fa6683d518b9f59ac0c5218a956e2397bc8737f90ca844937ee3c3771216c887ed7a63fc5ad7e9aad75696503d37fe475ecf2794a4938d76fbcb973964fd32fbf4f1f00de680ce7e7fcf00c3411201f2767ae96af4a49f06df5b9b103b94f41618f0dcf932d10ab299f4888ea78d625053e380af3083ebffd1da44b050152204fa7499f165d4a7438d04976388175bf29ec6330d5b84a1d82e0f7def6bfa85738090bcbfa4c2d2f2fa1796ab5ddbdff9e87b8d3d855fb741d4ff5d2287335345df9c905c4d3fea7df69d500d2a634e79a2546ca980bf993e208064173b5ca7bddcf8af8ca3948c7beb4201fd7aad2d47cb0eab110e7dfc19fff1cea3374440db423ed6e4a882c2924c5dcf71b3b90edff3ec0578811702fa0d0a5cf3dceed14e3e883b9fd94dc89d7fadade73a9bc0650a38660f6d5bb0286b0e53c7e415aae21db4ae2c3f3f402e6c587a44c97aa9fe1bc64864f68f29d71c08672308f61daad0a7277544e7065" "pk from the sk" + expect_ok "the pair must be consistent" "$BC_RUST" mldsa87 check-consistency --pkfile <(unhex B09A7F9D1C3754BBC1470D2B91628D660630144BD75E7614BD210E4E9F1E14CBA4A6993379D25C4372A51CC76782CCB4BFD64C4F8B9A371E7E2BB6D4B4288B247C3A4AB2E2C96A4EE0C9FF8E388B543FA023067FB87255DFD537B16DDB7B50A83A4AF98443FC0376ECEBE059699EF8FC0B4605DD3B952EBC61C041DA81F02AF8FFA3EC95D292CE76493471B34DD2FC422015D8343BAAFB23338FC1B9E3742A38AE172C7674E3AAB6C5DBAACEE8143D342435A2FF3EA62038E907B9CB46E967F89B149A8D7B3F3088A1A77BC7D743FA26E2E238E68809771DF6C6E6168F54FBFF042BE8AF3EC36EE16D9F614154D329373107A240047350B9280F9F85423E70A87277BB3C0FEC911D2F832D947ED75CBF0491545A98C710B8433AE0783C8F3533987D19DA5D94B6EACCC8D19454E4D5D9BF1202FBDE2E676E7D5DD6311BB75FEF0231C7C866BFA0B5A43445D3DAAE7CE29934DD1E44ADE1CD6F887BD480413178AAE7A9F5FAE7D8F47BD730B6695E49638BAE0F9F047A3FF2EAC96D633A7DA388067917240ED2513684D7F3D43A3287E7A71FF8A50F4C1D83B427E3748BA11728C7C46CEFDFFD94D5A3CAD19C98AE7D3C39CFD9CC57ADB4D55EFCE506B6BCE1D594B09A08138921570709821B7867BCB9A16369CD4988025D819152B42BB9BE2CF1A0FEAAE3BA80A862AAE4964503A3D7A1994F85813FF135E903E7EEE2FFFA9083E40166DE96F312B2A27C3E91FD8B64590C06EB8541E30DDEE206D37CDC200C202BC65464111EAE3CEE5F4A15FE14A84288099DDBCDEC2D23BF456638D5471B0960F53D74B97BFD94441BF53246840A98A0120C8C78B266189D6992343581997147195CBF3B2C5F4C2DE79F9A6421EC65F55AF85E71BE9A85A280DAE87AC80835593D1461D9DEB7E001378A018E637F6358683893432DC29EE98204A82D795427C0865DF2E0CF71A65101F7F6F8E2F7F32359016299F121E29C07F488DA275257F6433A6933B8C3DF69669BA174FF6A1D9238D60702770C2B50D2C369D7149C0733996FDDB1A866C475ADE5A84F6A7D5FC7F795F03BD2F305FBC3FD770731EEB00041C20CD61F32C24F92FF3C20AE790DE77615C189E2BC9BF41C12FCC066F6F3C6707987090D55B66BA8914B45EF834024BE470F166F284E065834AB05550EA84E6ADDF82A502DF243EE26351F6FE36826AF07CCB8E92CE9354B8E42BE9767BB03E4264AAE3449D9084C955E75C9B1FCEEC4A314FDE2203913A9FF1F0EC556D3130DF20C3F6C8FCD6B1D33A7AA7F342B0C82327BE9B5DE468EA221F96BC3A146B24DAF9EA27DBD55D4AE92EA1231ED0143DA38A3BD857B411A16EF4D829116111D9C28DD5EE75825B9A5D96E517886EE49D9763942BB393108589720B2F132C2E1AAB2D616F5986C885B76076CD4666B009672685FC3538A2872C96C62202A6FF7D00EF7B47401F731C079915541230A67085ADF68258455972265FFE0E3A3C8CAD8BB7E2E9255B70914D09B64F216435F1F65D89E41C623E9DBD02E849998D8FE49F53749205E0034DADD26F606B3DFE90C3F3A594C06E486AE0B9187BF1FF351379D3B456912957F27EF4C06BC8ED909996FFEF694EB81C9D47E410ACDE2B81D51EED7D7539B70C93CD14BD0615B955C98FCB9AB2E441A61BD020760E6DDD5BEA462EB60AC580A99DBAD2F81D530B31D462F4B1D3E8B8029FCF50B7E9AA54FBAC53820F1248C029FCB2F08F2CD66ED6B3D1D2DA2B79DBC3B1A5F2C49B5A7B9902A8E1A9BB326303948C9B5797067B0FE6A8259F0ADE7A3B309231A58C67D9D30F9B0E0045B403EC66788F9EAF603605F8BB6A45CD4F45B1767FACF2C1063E5E199E24B23E59487CFC4F1E048BD7F76135F07B2898ACF450089A746ADB35EFA41E97EA655475D2862DDB1EF9ABA81A5FD34B855D691C291EC9DA7752C4B0B05D45DFB122E168BAE3263D7EDCB1454897394AFB1661320EF51C6750999DE284C590A55477A0616023C924721557E37D2F7778706D925865595D4137D6D548112AAD44F0A7715FFD87974B78665B98148B4C2546A68077893B0DB40E5E94939933696B6499FC24A1CFDDEC19556775AB9E4B23B71A916D5E53F32923C3896F010C230E93F3651CF1D2A6CFBA3E8E1A30A14895B2D58E3410FC603DDE6A0B537A2FEDC2480B98B236C00749FBA3FB915130DF64D9E5166BDE7D204D9499A20AB4C5F57B2A84E86F6B2013A0355C98B82651459FE2304E89A85EE63300ABE8A8D87157833D127EA8BE07893B28540193D1B5690B230AD82F2F19C20A48846F0BD7B4F89B5C3087FC83D210A89CE60E1E94F2F0B46C4BB091FCC0CBBF8FEA4BAAD75C8E97DA61B20D02DD23666B97C9287E92BA41ADDA15859C285DFBB4AC6D4A1C9CAAE34EAB338ABF1F67DDC16722687B13999FCB43A64F807677F7B176BA1005AE388328D2E13ADAC44F1FB7061202C73513A21D6E862804C6EF084C61D627D2607D521204078EF50A6EA152BA7D50C7CA222FC4D8D315BC86E3DAB25BE0699CF038D927F97499799F1A80F894238386E1061CFCA92FAB77920EEB21A560786527431B17C2C9CD401CC36E7EFAF86406F16716E0F48D113691366E730FF99D840707D3DBA15048369E363823F28D5F754DE09EAA22AE5D1F6B9E8E4AB8EE1BE22D711A98A31A8ED7513EAF9DD4DD274215586D6FCF3DF6EB1120A644E04C9463B87B745CFC2F3AB550DD0B7C2BDCA5957E4F72F667DC18F467A0AE583943615F6E9C028C454410B25621200AF24989473D75F2F8F1A32358FC3A2E7E5CFEDCC7B023B0C3EBA272CE5BCFA06D1C2C58A685303C920C848AE7F05FD25A900CD95A17CC3273D52871E2427D280B929CB92B999BCEC0C23980D59832ECFE9456DB5DD54EC63C2CFA9E444523CDA66B136BD1140E87CED84704C7F8D3B9E65E5A56B2C7DA3FFC702A8537E4B02D91108E8DA31C2B5361BCBAA3B1A7C768903C2F2AC2996809DD12A827EC29FBCFD8DE5DFFD2033A0466B3ACE84B68970F36217F9F9DD736E0BB53C81C759281E73307BA0BA958116A52937CE53B903FBE0FE8F97E50A4A540DEA8794CCCE67C5D843E4EAEBE0248D27354E6C0FA526B789B2FD78A3A6BFD990E22D4E126AB952A716F6C13346BA8F602982A7060FA6683D518B9F59AC0C5218A956E2397BC8737F90CA844937EE3C3771216C887ED7A63FC5AD7E9AAD75696503D37FE475ECF2794A4938D76FBCB973964FD32FBF4F1F00DE680CE7E7FCF00C3411201F2767AE96AF4A49F06DF5B9B103B94F41618F0DCF932D10AB299F4888EA78D625053E380AF3083EBFFD1DA44B050152204FA7499F165D4A7438D04976388175BF29EC6330D5B84A1D82E0F7DEF6BFA85738090BCBFA4C2D2F2FA1796AB5DDBDFF9E87B8D3D855FB741D4FF5D2287335345DF9C905C4D3FEA7DF69D500D2A634E79A2546CA980BF993E208064173B5CA7BDDCF8AF8CA3948C7BEB4201FD7AAD2D47CB0EAB110E7DFC19FFF1CEA3374440DB423ED6E4A882C2924C5DCF71B3B90EDFF3EC0578811702FA0D0A5CF3DCEED14E3E883B9FD94DC89D7FADADE73A9BC0650A38660F6D5BB0286B0E53C7E415AAE21DB4AE2C3F3F402E6C587A44C97AA9FE1BC64864F68F29D71C08672308F61DAAD0A7277544E7065) <"$TMP/sk" >/dev/null + got=$(hex_out "$BC_RUST" hash-mldsa87 pk-from-sk -x <"$TMP/sk") + assert_eq "$got" "b09a7f9d1c3754bbc1470d2b91628d660630144bd75e7614bd210e4e9f1e14cba4a6993379d25c4372a51cc76782ccb4bfd64c4f8b9a371e7e2bb6d4b4288b247c3a4ab2e2c96a4ee0c9ff8e388b543fa023067fb87255dfd537b16ddb7b50a83a4af98443fc0376ecebe059699ef8fc0b4605dd3b952ebc61c041da81f02af8ffa3ec95d292ce76493471b34dd2fc422015d8343baafb23338fc1b9e3742a38ae172c7674e3aab6c5dbaacee8143d342435a2ff3ea62038e907b9cb46e967f89b149a8d7b3f3088a1a77bc7d743fa26e2e238e68809771df6c6e6168f54fbff042be8af3ec36ee16d9f614154d329373107a240047350b9280f9f85423e70a87277bb3c0fec911d2f832d947ed75cbf0491545a98c710b8433ae0783c8f3533987d19da5d94b6eaccc8d19454e4d5d9bf1202fbde2e676e7d5dd6311bb75fef0231c7c866bfa0b5a43445d3daae7ce29934dd1e44ade1cd6f887bd480413178aae7a9f5fae7d8f47bd730b6695e49638bae0f9f047a3ff2eac96d633a7da388067917240ed2513684d7f3d43a3287e7a71ff8a50f4c1d83b427e3748ba11728c7c46cefdffd94d5a3cad19c98ae7d3c39cfd9cc57adb4d55efce506b6bce1d594b09a08138921570709821b7867bcb9a16369cd4988025d819152b42bb9be2cf1a0feaae3ba80a862aae4964503a3d7a1994f85813ff135e903e7eee2fffa9083e40166de96f312b2a27c3e91fd8b64590c06eb8541e30ddee206d37cdc200c202bc65464111eae3cee5f4a15fe14a84288099ddbcdec2d23bf456638d5471b0960f53d74b97bfd94441bf53246840a98a0120c8c78b266189d6992343581997147195cbf3b2c5f4c2de79f9a6421ec65f55af85e71be9a85a280dae87ac80835593d1461d9deb7e001378a018e637f6358683893432dc29ee98204a82d795427c0865df2e0cf71a65101f7f6f8e2f7f32359016299f121e29c07f488da275257f6433a6933b8c3df69669ba174ff6a1d9238d60702770c2b50d2c369d7149c0733996fddb1a866c475ade5a84f6a7d5fc7f795f03bd2f305fbc3fd770731eeb00041c20cd61f32c24f92ff3c20ae790de77615c189e2bc9bf41c12fcc066f6f3c6707987090d55b66ba8914b45ef834024be470f166f284e065834ab05550ea84e6addf82a502df243ee26351f6fe36826af07ccb8e92ce9354b8e42be9767bb03e4264aae3449d9084c955e75c9b1fceec4a314fde2203913a9ff1f0ec556d3130df20c3f6c8fcd6b1d33a7aa7f342b0c82327be9b5de468ea221f96bc3a146b24daf9ea27dbd55d4ae92ea1231ed0143da38a3bd857b411a16ef4d829116111d9c28dd5ee75825b9a5d96e517886ee49d9763942bb393108589720b2f132c2e1aab2d616f5986c885b76076cd4666b009672685fc3538a2872c96c62202a6ff7d00ef7b47401f731c079915541230a67085adf68258455972265ffe0e3a3c8cad8bb7e2e9255b70914d09b64f216435f1f65d89e41c623e9dbd02e849998d8fe49f53749205e0034dadd26f606b3dfe90c3f3a594c06e486ae0b9187bf1ff351379d3b456912957f27ef4c06bc8ed909996ffef694eb81c9d47e410acde2b81d51eed7d7539b70c93cd14bd0615b955c98fcb9ab2e441a61bd020760e6ddd5bea462eb60ac580a99dbad2f81d530b31d462f4b1d3e8b8029fcf50b7e9aa54fbac53820f1248c029fcb2f08f2cd66ed6b3d1d2da2b79dbc3b1a5f2c49b5a7b9902a8e1a9bb326303948c9b5797067b0fe6a8259f0ade7a3b309231a58c67d9d30f9b0e0045b403ec66788f9eaf603605f8bb6a45cd4f45b1767facf2c1063e5e199e24b23e59487cfc4f1e048bd7f76135f07b2898acf450089a746adb35efa41e97ea655475d2862ddb1ef9aba81a5fd34b855d691c291ec9da7752c4b0b05d45dfb122e168bae3263d7edcb1454897394afb1661320ef51c6750999de284c590a55477a0616023c924721557e37d2f7778706d925865595d4137d6d548112aad44f0a7715ffd87974b78665b98148b4c2546a68077893b0db40e5e94939933696b6499fc24a1cfddec19556775ab9e4b23b71a916d5e53f32923c3896f010c230e93f3651cf1d2a6cfba3e8e1a30a14895b2d58e3410fc603dde6a0b537a2fedc2480b98b236c00749fba3fb915130df64d9e5166bde7d204d9499a20ab4c5f57b2a84e86f6b2013a0355c98b82651459fe2304e89a85ee63300abe8a8d87157833d127ea8be07893b28540193d1b5690b230ad82f2f19c20a48846f0bd7b4f89b5c3087fc83d210a89ce60e1e94f2f0b46c4bb091fcc0cbbf8fea4baad75c8e97da61b20d02dd23666b97c9287e92ba41adda15859c285dfbb4ac6d4a1c9caae34eab338abf1f67ddc16722687b13999fcb43a64f807677f7b176ba1005ae388328d2e13adac44f1fb7061202c73513a21d6e862804c6ef084c61d627d2607d521204078ef50a6ea152ba7d50c7ca222fc4d8d315bc86e3dab25be0699cf038d927f97499799f1a80f894238386e1061cfca92fab77920eeb21a560786527431b17c2c9cd401cc36e7efaf86406f16716e0f48d113691366e730ff99d840707d3dba15048369e363823f28d5f754de09eaa22ae5d1f6b9e8e4ab8ee1be22d711a98a31a8ed7513eaf9dd4dd274215586d6fcf3df6eb1120a644e04c9463b87b745cfc2f3ab550dd0b7c2bdca5957e4f72f667dc18f467a0ae583943615f6e9c028c454410b25621200af24989473d75f2f8f1a32358fc3a2e7e5cfedcc7b023b0c3eba272ce5bcfa06d1c2c58a685303c920c848ae7f05fd25a900cd95a17cc3273d52871e2427d280b929cb92b999bcec0c23980d59832ecfe9456db5dd54ec63c2cfa9e444523cda66b136bd1140e87ced84704c7f8d3b9e65e5a56b2c7da3ffc702a8537e4b02d91108e8da31c2b5361bcbaa3b1a7c768903c2f2ac2996809dd12a827ec29fbcfd8de5dffd2033a0466b3ace84b68970f36217f9f9dd736e0bb53c81c759281e73307ba0ba958116a52937ce53b903fbe0fe8f97e50a4a540dea8794ccce67c5d843e4eaebe0248d27354e6c0fa526b789b2fd78a3a6bfd990e22d4e126ab952a716f6c13346ba8f602982a7060fa6683d518b9f59ac0c5218a956e2397bc8737f90ca844937ee3c3771216c887ed7a63fc5ad7e9aad75696503d37fe475ecf2794a4938d76fbcb973964fd32fbf4f1f00de680ce7e7fcf00c3411201f2767ae96af4a49f06df5b9b103b94f41618f0dcf932d10ab299f4888ea78d625053e380af3083ebffd1da44b050152204fa7499f165d4a7438d04976388175bf29ec6330d5b84a1d82e0f7def6bfa85738090bcbfa4c2d2f2fa1796ab5ddbdff9e87b8d3d855fb741d4ff5d2287335345df9c905c4d3fea7df69d500d2a634e79a2546ca980bf993e208064173b5ca7bddcf8af8ca3948c7beb4201fd7aad2d47cb0eab110e7dfc19fff1cea3374440db423ed6e4a882c2924c5dcf71b3b90edff3ec0578811702fa0d0a5cf3dceed14e3e883b9fd94dc89d7fadade73a9bc0650a38660f6d5bb0286b0e53c7e415aae21db4ae2c3f3f402e6c587a44c97aa9fe1bc64864f68f29d71c08672308f61daad0a7277544e7065" "hash-mldsa87 derives the same pk" +} + +# Wycheproof testvectors_v1/mldsa_87_verify_test.json tcId 1 ("baseline", external interface, empty +# context) must verify, and tcId 6 ("short signature") must not. The signature and key are given as +# hex files (--hex-input) and as binary ones; the message "Hello world" is stdin either way. +test_mldsa87_verify_matches_wycheproof() { + unhex 48656c6c6f20776f726c64 >"$TMP/msg" + printf '%s' 17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805 >"$TMP/pk.hex" + printf '%s' ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731 >"$TMP/sig.hex" + expect_ok "tcId 1 must verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" <"$TMP/msg" >/dev/null + unhex 17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805 >"$TMP/pk.bin"; unhex ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731 >"$TMP/sig.bin" + expect_ok "tcId 1 must verify from binary files" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk.bin" --sigfile "$TMP/sig.bin" <"$TMP/msg" >/dev/null + printf '%s' ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a1115182027 >"$TMP/sig-bad.hex" + expect_fail "tcId 6 must not verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig-bad.hex" <"$TMP/msg" + assert_stderr_has "invalid" + expect_fail "another message must not verify" "$BC_RUST" --hex-input mldsa87 verify --pkfile "$TMP/pk.hex" --sigfile "$TMP/sig.hex" < <(printf 'Hello World') +} + +# Signing is randomized (FIPS 204 Alg 2 draws rnd), so there is no signature known answer; the +# round trip pins the wiring: sign with the sk, verify with the derived pk, with and without a +# context, in the pure and the HashML-DSA (SHA-512) forms, and each form rejects the other's +# signature and the other context. +test_mldsa87_sign_verify_round_trip() { + "$BC_RUST" mldsa87 keygen >"$TMP/sk" + assert_size "$TMP/sk" 4896 "sk length of ML-DSA-87" + "$BC_RUST" mldsa87 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 3000 >"$TMP/msg" + "$BC_RUST" mldsa87 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig" + assert_size "$TMP/sig" 4627 "signature length of ML-DSA-87" + expect_ok "pure signature verifies" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" >/dev/null + printf 'ctx' >"$TMP/ctx" + expect_fail "a context the signer did not use must not verify" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" + "$BC_RUST" mldsa87 sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/sig-ctx" + expect_ok "signature with context verifies with it" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_fail "and not without it" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ctx" <"$TMP/msg" + "$BC_RUST" hash-mldsa87 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig-ph" + expect_ok "HashML-DSA signature verifies as HashML-DSA" "$BC_RUST" hash-mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" >/dev/null + expect_fail "and not as pure ML-DSA" "$BC_RUST" mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-ph" <"$TMP/msg" + expect_fail "nor the pure one as HashML-DSA" "$BC_RUST" hash-mldsa87 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" +} + +# FIPS 204 Alg 2 step 1: a context longer than 255 bytes is rejected. The CLI must report it +# (exit 255, an `Error:` line), not panic; and a seed of the wrong length likewise, for every +# parameter set and both forms. +test_bad_inputs_are_errors_not_panics() { + local cmd + head -c 256 /dev/zero >"$TMP/ctx256" + head -c 255 /dev/zero >"$TMP/ctx255" + rng 31 >"$TMP/seed31" + for cmd in mldsa44 mldsa65 mldsa87 hash-mldsa44 hash-mldsa65 hash-mldsa87; do + "$BC_RUST" $cmd keygen >"$TMP/sk" + expect_ok "$cmd: a 255-byte context is allowed" "$BC_RUST" $cmd sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx255" /dev/null + expect_fail "$cmd: a 256-byte context" "$BC_RUST" $cmd sign --skfile "$TMP/sk" --ctxfile "$TMP/ctx256" 255 must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" + expect_fail "$cmd: a 31-byte seed" "$BC_RUST" $cmd keygen-from-seed <"$TMP/seed31" + grep -q panicked "$LAST_STDERR" && fail "$cmd: a bad seed must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" + done +} + +# A signature file of the wrong length, a truncated public key and a missing file are errors. +test_bad_files_are_errors() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + printf 'hello' | "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" >"$TMP/sig" + head -c 100 "$TMP/sig" >"$TMP/sig-short" + expect_fail "short signature" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-short" < <(printf 'hello') + assert_stderr_has "invalid" + head -c 100 "$TMP/pk" >"$TMP/pk-short" + expect_fail "short pk" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk-short" --sigfile "$TMP/sig" < <(printf 'hello') + assert_stderr_has "Error" + expect_fail "missing sig file" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/nope" < <(printf 'hello') + assert_stderr_has "Error" +} + +# FIPS 204 Section 3.4: the deterministic variant signs with rnd all zero, so two signatures of +# one message agree, and it verifies like any other; the hedged default draws rnd, so two differ. +test_deterministic_signatures_repeat_and_hedged_ones_do_not() { + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 100 >"$TMP/msg" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig1" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig2" + assert_same "$TMP/sig1" "$TMP/sig2" "deterministic signatures must repeat" + expect_ok "a deterministic signature verifies" "$BC_RUST" mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig1" <"$TMP/msg" >/dev/null + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig3" + "$BC_RUST" mldsa44 sign --skfile "$TMP/sk" <"$TMP/msg" >"$TMP/sig4" + assert_differs "$TMP/sig3" "$TMP/sig4" "hedged signatures must differ" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig5" + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --deterministic <"$TMP/msg" >"$TMP/sig6" + assert_same "$TMP/sig5" "$TMP/sig6" "deterministic HashML-DSA signatures must repeat" +} + +# The external-mu interface: compute-mu gives the 64-byte representative of FIPS 204 Algorithm 7 +# line 6, sign-mu signs it, and the result verifies both as an ordinary signature over the message +# and with verify-mu; a context is part of mu, so the wrong one fails. +test_external_mu_round_trip() { + "$BC_RUST" mldsa65 keygen >"$TMP/sk" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 1000 >"$TMP/msg" + printf 'ctx' >"$TMP/ctx" + "$BC_RUST" mldsa65 compute-mu --pkfile "$TMP/pk" --ctxfile "$TMP/ctx" <"$TMP/msg" >"$TMP/mu" + assert_size "$TMP/mu" 64 "mu length" + "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" --mufile "$TMP/mu" "$TMP/sig" + assert_size "$TMP/sig" 3309 "signature length of ML-DSA-65" + expect_ok "a mu signature verifies over the message" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" --ctxfile "$TMP/ctx" <"$TMP/msg" >/dev/null + expect_ok "and with verify-mu" "$BC_RUST" mldsa65 verify-mu --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/mu" >/dev/null + expect_fail "not under another context" "$BC_RUST" mldsa65 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig" <"$TMP/msg" + "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" --deterministic <"$TMP/mu" >"$TMP/sig-det" + expect_ok "a deterministic mu signature verifies" "$BC_RUST" mldsa65 verify-mu --pkfile "$TMP/pk" --sigfile "$TMP/sig-det" --mufile "$TMP/mu" /dev/null + expect_fail "mu is 64 bytes" "$BC_RUST" mldsa65 sign-mu --skfile "$TMP/sk" < <(rng 63) + assert_stderr_has "64 bytes" + expect_fail "the mu actions are not HashML-DSA's" "$BC_RUST" hash-mldsa65 compute-mu --pkfile "$TMP/pk" <"$TMP/msg" + assert_stderr_has "hash-mldsa" +} + +# FIPS 204 Section 5.4: the pre-hash must be at least as strong as the parameter set. --hash +# selects it; sha256 and shake128 (RFC 8702's 32-byte digest) fit hash-mldsa44 only, sha512 and +# shake256 fit all three, and a signature only verifies under the hash it was made with. +test_hash_mldsa_pre_hash_choice() { + local h + "$BC_RUST" mldsa44 keygen >"$TMP/sk" + "$BC_RUST" mldsa44 pk-from-sk --skfile "$TMP/sk" >"$TMP/pk" + rng 500 >"$TMP/msg" + for h in sha256 sha512 shake128 shake256; do + "$BC_RUST" hash-mldsa44 sign --skfile "$TMP/sk" --hash $h <"$TMP/msg" >"$TMP/sig-$h" + expect_ok "hash-mldsa44 with $h verifies" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-$h" --hash $h <"$TMP/msg" >/dev/null + done + expect_fail "a sha256 signature is not a sha512 one" "$BC_RUST" hash-mldsa44 verify --pkfile "$TMP/pk" --sigfile "$TMP/sig-sha256" <"$TMP/msg" + "$BC_RUST" mldsa65 keygen >"$TMP/sk65" + "$BC_RUST" mldsa65 pk-from-sk --skfile "$TMP/sk65" >"$TMP/pk65" + for h in sha256 shake128; do + expect_fail "hash-mldsa65 refuses $h" "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk65" --hash $h <"$TMP/msg" + assert_stderr_has "PreHashTooWeak" + expect_fail "and on verify" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk65" --sigfile "$TMP/sig-sha256" --hash $h <"$TMP/msg" + assert_stderr_has "PreHashTooWeak" + done + "$BC_RUST" hash-mldsa65 sign --skfile "$TMP/sk65" --hash shake256 <"$TMP/msg" >"$TMP/sig65" + expect_ok "hash-mldsa65 with shake256 verifies" "$BC_RUST" hash-mldsa65 verify --pkfile "$TMP/pk65" --sigfile "$TMP/sig65" --hash shake256 <"$TMP/msg" >/dev/null +} + +run_all diff --git a/cli/tests/test_mlkem.sh b/cli/tests/test_mlkem.sh new file mode 100755 index 000000000..33acfd26b --- /dev/null +++ b/cli/tests/test_mlkem.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# The mlkem512 / mlkem768 / mlkem1024 subcommands against the ACVP ML-KEM known answers. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ACVP ML-KEM keyGen (bc-test-data/pqc/crypto/mlkem/ML-KEM-keyGen.txt, ML-KEM-512 tcId 1): the +# 64-byte seed is d || z (FIPS 203 Alg 19 draws d then z; Alg 16 takes (d, z)); `keygen-from-seed` +# must give the vector's dk and `pk-from-sk` its ek, from the seed as raw bytes and as hex. +test_mlkem512_keygen_from_seed_matches_acvp() { + local got + unhex 8A3D5F46A59A1B14E87C963A355D12D353074754FF15000172E6C8AE00A3AC1D5449ACE5765E87F252DD14DED32FD1973DF5B5DE53751A3723D5A52C8C6DF2FD >"$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem512 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "d2facfe0ab6bc16109fc167cabb16605052fd5e70b757ba6c3d70712929716d2be43b2c7baa2631b4c374218899a0ca1e3b370d518aa992b37cda8767a8728fc51a43abc2728775a82674fc5eabf6e79218864395b6cce4531983d88874890682c9c41e302ab6882bfa94c37dea34fc0d977d5a7cca52420396cbffc89a562c47cfe858360d0cf3d89349df0a4da26bbdbcc28cda811a885423dd130a3cc78ffe85257e030298a4c5c2c6d22924802a72e722c079b10b0c0806ca62c667f533c305b8445b8a64d8cbe05111d1ae798fa4816293a61a01975105b64318932b5c8c83e805e2a3678271c77b4918f8caa15063b9b4e264615ba32a3db4e3510714b566375396926e873693743bde8a4c6280f66054df9f2930d045bdebcb1752581a10241024558e6163b6324a7c13784e3d28e78352164ab137a54a3a5650eb2399c20d14c1f0ccca4cc3aae37217b791aae8c74fa085e72249313e8746de6316b664262a253f0a100caf38de2889a465264bdca258bf28ed660c6f27a93c6d6c6d9f9c10be80605126ff17a1b5ee3b2c25c76ce58ab21450cbac36aed192cc096006b5c0ca43640d11aa9b1587dedb06506c9c2dd882d30b73df318a57c09af614a4cd066586319771a106146469e21526a41b4a3a4c3386e490160ca9d2ab55ea6d401bcd01fa4a949283a878ae2105bc11cfd705e231018e4fc84c4e24bee263393d76b5cdb6309841aaefb609149af19b492ec4628fe6090ee5349eac519e1a226050244f70c5dd87b1a8c8c11c00ccf8d3844557c1333a02dbea5a690ac367c325094c53945fa31b168508951a19547bc422bcbd02a8db8ca9366cb617ad13236607968c93aab0c78e50217c96a0f76d64db09b4deca4a3aa574ef9210c3a6c7b21d5729ee07c781378680648a24aa3bef461e6ca012df9be515856dadb67d7835edeac4610c58f38fb4115831dbf29ba1aa980b7eacc7c430f70970256b1029ef27b6ca4a61f5093326ac4128a0536cc1bbb005b1158b0322899759761e0c5b9752c045c2cc3ffd81e526b86f0b03ca42355daf5a7f1c19a0dd517eb1744084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b47203c0abc212cd4230f2a993d12a11a0ac6e761a8d479f3b961d8e79ff834dbb3b5449ace5765e87f252dd14ded32fd1973df5b5de53751a3723d5a52c8c6df2fd" "dk from a raw seed" + got=$(printf '%s' 8A3D5F46A59A1B14E87C963A355D12D353074754FF15000172E6C8AE00A3AC1D5449ACE5765E87F252DD14DED32FD1973DF5B5DE53751A3723D5A52C8C6DF2FD | hex_out "$BC_RUST" --hex-input mlkem512 keygen-from-seed -x) + assert_eq "$got" "d2facfe0ab6bc16109fc167cabb16605052fd5e70b757ba6c3d70712929716d2be43b2c7baa2631b4c374218899a0ca1e3b370d518aa992b37cda8767a8728fc51a43abc2728775a82674fc5eabf6e79218864395b6cce4531983d88874890682c9c41e302ab6882bfa94c37dea34fc0d977d5a7cca52420396cbffc89a562c47cfe858360d0cf3d89349df0a4da26bbdbcc28cda811a885423dd130a3cc78ffe85257e030298a4c5c2c6d22924802a72e722c079b10b0c0806ca62c667f533c305b8445b8a64d8cbe05111d1ae798fa4816293a61a01975105b64318932b5c8c83e805e2a3678271c77b4918f8caa15063b9b4e264615ba32a3db4e3510714b566375396926e873693743bde8a4c6280f66054df9f2930d045bdebcb1752581a10241024558e6163b6324a7c13784e3d28e78352164ab137a54a3a5650eb2399c20d14c1f0ccca4cc3aae37217b791aae8c74fa085e72249313e8746de6316b664262a253f0a100caf38de2889a465264bdca258bf28ed660c6f27a93c6d6c6d9f9c10be80605126ff17a1b5ee3b2c25c76ce58ab21450cbac36aed192cc096006b5c0ca43640d11aa9b1587dedb06506c9c2dd882d30b73df318a57c09af614a4cd066586319771a106146469e21526a41b4a3a4c3386e490160ca9d2ab55ea6d401bcd01fa4a949283a878ae2105bc11cfd705e231018e4fc84c4e24bee263393d76b5cdb6309841aaefb609149af19b492ec4628fe6090ee5349eac519e1a226050244f70c5dd87b1a8c8c11c00ccf8d3844557c1333a02dbea5a690ac367c325094c53945fa31b168508951a19547bc422bcbd02a8db8ca9366cb617ad13236607968c93aab0c78e50217c96a0f76d64db09b4deca4a3aa574ef9210c3a6c7b21d5729ee07c781378680648a24aa3bef461e6ca012df9be515856dadb67d7835edeac4610c58f38fb4115831dbf29ba1aa980b7eacc7c430f70970256b1029ef27b6ca4a61f5093326ac4128a0536cc1bbb005b1158b0322899759761e0c5b9752c045c2cc3ffd81e526b86f0b03ca42355daf5a7f1c19a0dd517eb1744084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b47203c0abc212cd4230f2a993d12a11a0ac6e761a8d479f3b961d8e79ff834dbb3b5449ace5765e87f252dd14ded32fd1973df5b5de53751a3723d5a52c8c6df2fd" "dk from a hex seed" + "$BC_RUST" mlkem512 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem512 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "084c1a6f228372bb63e07632561746cd310ba0c09fcfac838556bf598168ee7a9e784c0f428a5aa6685bb23574d9d1330d2aaab4aa5cd3da3c90a9557140af72e813e17868e60b5e80d3c1afa7ae32a141a6a48fb05878f9b7cfa6e4939c4c8c240a8e337579e7958c59605f82e4088b705bf530451a968ff83c388615bf8c0c3470e52010e1226fc83c22d64167b35e0b946ddf40a17be90ac062c5d4a8587711c192b096adc64962dc99a3aa260d19143c99919e6bb7b6e129ff288b121b96d25b7822852fc6dcbbb47bbe47534ff58a80f7cc43a86bb96dac2001a60d8c95cd1926259f662f4a79160435378c37209092bdabba796beba290ab49585294dd6481deaa348a62a95dca8262197654278c6e06a1fedb9168249ae6cbc20310abd964279d477906cabdde6a9d3f2789a02a2e609a5d8d768ad4761c23ab5003c61145e715249560321a06b49461536b2f84f320a9741ccf33a09695694b102350baae6d085823608794f728c1e7bd35f3a8b3b68b4cd2aa7017310eda95db058f6983a366882e394949d4c8756fda372eec65219849caf069cf5b95bd0c43cbe00fa0125ea705bbfad35480c85d192035576abfe475c2ed5c2ee37ca705036e4ce92075b27943189442164b96816b63d6b3f8886af6dcb57d385206803b81d638965b6b90baac8c9c8c3db8587a7036c02b24cc5201bda802febb7eeba607df69568f8c7f44006ec95529ce297a48c8cad08120ff836135b3aefeb96b6c8248b5918a49d84e56766555198f730403abf840283106bc001ef92cbcac194c7873c675ca142e790711a38be1e73f94d40f8e4257d3741718f9452765757d1916b92acfd429b48ac00ad31c5212730a6a26987627c988db224e74afcb03a05be5aea29422a52788fcfa14a4eb847895c3aad81c90d65d480b53b910672a65c35c69a23130a862542b502c2e84a092ed32bdd8c703e4ab27fc3cca014269a3a0930505b1922c54eb8340e8132e17c36a2512872aa98f64f691f268326e083a4c488d861a74bddb78ead962daf9cbd1807c8cdb0b22456778698e634b0c0572b5d50627001eee88e35a3e01b7d3c81b948bbc338de3294a574aa46f45ce4c795400b472" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem512 check-consistency --pkfile <(unhex 084C1A6F228372BB63E07632561746CD310BA0C09FCFAC838556BF598168EE7A9E784C0F428A5AA6685BB23574D9D1330D2AAAB4AA5CD3DA3C90A9557140AF72E813E17868E60B5E80D3C1AFA7AE32A141A6A48FB05878F9B7CFA6E4939C4C8C240A8E337579E7958C59605F82E4088B705BF530451A968FF83C388615BF8C0C3470E52010E1226FC83C22D64167B35E0B946DDF40A17BE90AC062C5D4A8587711C192B096ADC64962DC99A3AA260D19143C99919E6BB7B6E129FF288B121B96D25B7822852FC6DCBBB47BBE47534FF58A80F7CC43A86BB96DAC2001A60D8C95CD1926259F662F4A79160435378C37209092BDABBA796BEBA290AB49585294DD6481DEAA348A62A95DCA8262197654278C6E06A1FEDB9168249AE6CBC20310ABD964279D477906CABDDE6A9D3F2789A02A2E609A5D8D768AD4761C23AB5003C61145E715249560321A06B49461536B2F84F320A9741CCF33A09695694B102350BAAE6D085823608794F728C1E7BD35F3A8B3B68B4CD2AA7017310EDA95DB058F6983A366882E394949D4C8756FDA372EEC65219849CAF069CF5B95BD0C43CBE00FA0125EA705BBFAD35480C85D192035576ABFE475C2ED5C2EE37CA705036E4CE92075B27943189442164B96816B63D6B3F8886AF6DCB57D385206803B81D638965B6B90BAAC8C9C8C3DB8587A7036C02B24CC5201BDA802FEBB7EEBA607DF69568F8C7F44006EC95529CE297A48C8CAD08120FF836135B3AEFEB96B6C8248B5918A49D84E56766555198F730403ABF840283106BC001EF92CBCAC194C7873C675CA142E790711A38BE1E73F94D40F8E4257D3741718F9452765757D1916B92ACFD429B48AC00AD31C5212730A6A26987627C988DB224E74AFCB03A05BE5AEA29422A52788FCFA14A4EB847895C3AAD81C90D65D480B53B910672A65C35C69A23130A862542B502C2E84A092ED32BDD8C703E4AB27FC3CCA014269A3A0930505B1922C54EB8340E8132E17C36A2512872AA98F64F691F268326E083A4C488D861A74BDDB78EAD962DAF9CBD1807C8CDB0B22456778698E634B0C0572B5D50627001EEE88E35A3E01B7D3C81B948BBC338DE3294A574AA46F45CE4C795400B472) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-512 tcId 26): dk and c give k. +test_mlkem512_decaps_matches_acvp() { + local got + unhex E4B91341E794B495639F98AA78A0A786DC5172213DE6866909E2050ED265C04927EB8805A1A2B9CADB0C16DB67DC4AB77D72A8A2F31EE723A38B214F1D1455EAB0866084C10A811AA806B9E9DC5757107F46A04AB8EC5C41888E40D385879B4B52C288B4044725F36FA3F43841D42ADF34AB9816022663108B821671D6A56EA2204A93C55E669CE66CB118F35858396B63A9CD08EBCE703293C40BAB2FC5485B8A0B595A7EA941C82FC361AA093237B9AC36D44AD25A785D596127B4B46627BC8237A29ED37FB888AB7CC84469FA972395701F864A4CC83A6397AC23255425DC1CA030B07DB09EB8B50DBFE0BE34C49F489BA3FEA2B2F13853679AB8B4D4456E688D972CB4063B5289D64347C81F256B91563AB443BABC59984643BC45CE34A0CEABB445BB0B96808F5972972E0C7458A655EBA95696354EC0422730302D322A2B0E60CE8593C054715E946116F0C69B81F9C26C64AD8A557C987651905505085581EB708D952018AF1CAE67ACCBA26882D8F6A6290A02F619775AB795494C0F12D2C73A28BA08BBA703074E941956CC032DA50B468ACB26A4DA82F8C507687AC50339384A4C233EE9B678E4964F63A704A0B4513B6C451BB0A8B92A54A188D5D74ECE9AAE90AB1C0841001E443CD0FBB4F907879B048A2E63025E160BC805CBDD0B0457D2A74BB440DDBCA3AD0B86FDEBB3BE1B215A3559992A76DC270E47DA63DF9C8DFFC530D51C88C7F9BB2FB0AEB29558B6152B57DB564F647E5109A510604F79BA4A3515BDB1F9150837560441535776273948878D1555161943547A48C3293E40261F5112A0D93664E280775CC7B83FFC673C4BB47E6856F72C62B05A4C98239E28944E9AB59B6B86B2AEA92DAAC92642354B18E9C1871A57ECAA06F6386EDCD092C7151AC965CB90893AC9F079CEA5CC02FBAABF90A95845CC6A9A426E0670D49C0CEFE6A350129BB8E72EE9A7229B87309305237D50C6F634686507ACF652C12D848C77D1683908498FA8203C08C7DFAB5CAF6A67F7D88CD58150997651EA74946B6B05BBF80E09470B5AD696CD426CF8C67E68D570C7E753E00A4B522403468534A253596F8B531CB4ADA263CC3EFB87D8E571AA699DB3D27D5961BA4AA5212914BF1747CC601544330CB1315796DBD46FBDAB4BA7A4199E8327C3E2472DF3C6060B0AF54A2E206485FC34B4DF372D2B586DBDD8B73367CB23DAC857562380A513DB6BAF68E0170C75293BA1A592F023BF2C1E66437CBE789584495ADD43B3905001F2270E21C622D9D256314B4CB731A9FAC841C0C547D7C5B396014B826A6831C2CF4EECBFB09B36F5356CB1E02D2AF988C63B69AA9B0F3D5683974C06C65847E3EC3C8D7458257978EF063BC2A23320C785CDE71DAAE7448AE2CAC9A65820BC957406A9C7D1812417C54FA813B55103F869CEE5C6229C4540CF77CCE125298A1133FBC8CD6081B437613CDB56A5563A7F3313B76BE42554A157B9F7BB1D37459ED849C4D6968CBC5443376296F1A8F2617406E40575906E12B42D1F974A9A3496301763B3F133B8107F08466F5381A4C0C41F48A993B0594741A4B0824A4C87695AAB32CAAAC3A9462C51B366B451B0A7C94281FDA1092BE5C4CF754829E26C7EE66B779627EC9BA08BC6093E7682701CCD6E847F774A9A2E791C2BCBC6ED74A8A6CC3762A520DD208887D75C4BEA035E8ACEF82C8B259BA8B91A832716A136C56C6CC397187010DFC603037388304BB622B1458A0B6DC0E108135C3A00D460E3225AFBB272A2DBB39C909F893A86654957D4A9684BC44FCA358C6430B37202CD0B43585A575E8D01536E930AFA668CF9C23FEF7A2B91B5CA5BD26CA4912E1AB20D6D176F58C61A25BB21E3569E6A3B3DEAA03253861ECD5A379E9819152612A0E7B6B380806C74310BA78F73CC2B6963468DC270388B02DD87B8181688F0304F46244403715395EB3B67659C19291615D4259A086304D993BA050459C04D433C88495B52492357BDC30AFCCC73922709479A0D0DB2A5C3720D4F12852C217BD954CEFACABD74A4CFC7C90DE360A1148A2C6D5644FB8C2490F29FF1009815EB5770B0B40B3A46E552C089E9B1AE0C0A27589A6191CCB054258EAA650936C3E7D2247582873F396A8357A09CD0A43DB4CC9207849E7AAC29E973FB05268CF468D8E5C2FA6E0291D4F3972EA189F4FAC1E15894798D2D2FC73D1D4F67538DAA1D7654AA2BAB8ECE6944BDB57FCD362D24717F0255E845A5D231E9ED0D32308DC009AB9B81A2E945FE72C1F7E30AC712617137E56D11C9A49D4B89 >"$TMP/dk" + unhex F120796127DA7ACC2C980427DFB6EEB2B229315CC4BB364B7A06B02780F98BB3EFFA46153E75B6364231BED99E846C3BBFA842FDE9738E3579E849B0E22FF9A2323978BA2BB8280AB052F83AE2D653E3CCD8A218130250BD02C373DCADE7D01D67E788E418E14E3C41BD26B01B6021472E983F2AE1D02BEC32DCB164942994853FBA123D4AF93B56234A5AC7F0AFC709A9AF10F568F02893835A32E2A81E88F74FF9A52D8A6F6EDB7A2AEC6A05E2F287EFC99CEC1C6FA934C40C53C97FFC58E6DA5673E716935C9EBCEDF361706A69E0048F0C7FE3C7465E008C2B2E5194DE124C4CD6EE0D43F02CEAA8D2997C56C95721D22247A579430BA08FF30FF6A4B26CD5F0B71DAD4B832B0D05BFF1177DA99910B544D0184DD66DC615C6C885842D2232A89E772B2C3F067682F6440F92936043B7F2D987D1FCDE7B5BD8803D9F4209577C7873532903B7C5D2631D6B930728FBF5740EDD77988D3AAE2A1ED4129B03584DD12ABD844BE7BFAA7C5449013982D18900B40D9CE6D9C6E186233E8D320AAD122895DE21B68ECD8578326475D42D1F3814265A6050E2A6E599A3417698064BB2EA003399001532E19ACD7D31C9DD653E6572301D3F5920FCBD4925AFEB88131E93D16CF62C74B269DDFDB3B0DC161A063BAFAF3FF23F438F7CE36A2F66558DB145A6A955AD72B56A1588315F091A6912EFF884B694A613DCA643B70D097571E553AACFC32C9066B0E767B4799C3C229AEFB0547DEA6D847B10DB38DBF91EE58C72863D859E9DCEC07B70F304A772EAFF549DAB568108C4201ABB4E81DEC2C070996976C88E64A9206CE2756BF1771C181EBF929470F56D148FE788AE33D24A06B1AB17C254B864ED3082B8484736D490AC89719D75DD0DFAD2813B50435FC07B5BCD8CFF2E60EFF7F0E7204F3A34A7EBE58AE93629D16866880BFC946DAC044002411A0871569E25BEDA6306F3301DDC10E83D51824E4E0BC70107FAB287C194513BAFE257CCC9E6FC86B07DCFF1B0BFC030A68A7757E842C6EA8F39BD6D37809DA7EED8FDA747E42296B6675B6132EAEC9874A55548DC9700F851A64A4B >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "afb1789cf2bf24b4d0cd0e94570fe0da6dcc7a082286cfad97c59195679cedda" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem512 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "afb1789cf2bf24b4d0cd0e94570fe0da6dcc7a082286cfad97c59195679cedda" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-512 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem512_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem512 keygen >"$TMP/dk" + assert_size "$TMP/dk" 1632 "dk length of ML-KEM-512" + "$BC_RUST" mlkem512 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 800 "ek length of ML-KEM-512" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem512 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem512 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem768 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "e0963a4ca3b67b75caf8650d12193989e9b0ee23b72c00c53f835fa5a5986c5a9d6b730e23c7720ba08641cac0f9489d0fcc39b7b3b54b33be045c4d5e54c3f1cb1eb8d722ecfab2c2209b3a8574fa4615d6e626d0c15ef392cda4f4153d1b55be83241f97a79b8a1bb6daab46d64948988c23ebcfe57134a66606b139b821b6a857c825d21080ce6a183799210f33cc65b7127c7cc071932dc80415701865966137c9bb52d8e14756b976775c97feb58702922c64f65a8c4c9fb378af484576c855322f5c7163e356c3386ce9f21ab7253b2d3a99126b207f6086a388cf89e94cfd89942907ca9a4510e02b707f5b32119186db2769e2daa07cc4a34950508aa1b0fea5a4d3833e9e2b11837202e7bb88bdb9842c78b56a8320ae950acd19c135f36c49cb20267c6b67a10b2d0256289bc90c6996c67198963149a7620dd7e1b7ac2c9c228c4e60419e6932c74e799df9085d92493b910ccd0b837b55f82c73da044b0305cb52bf54da86ebbaba245624cc78ba3296bd8a204a6d0ac9d039b8c2f45036f711c6bc516b936403fb051fd09be8c7c8fb9aa2c22cb9e0d312fd972271e56de755ab479bb9f2b183818b6cbbbaba1bd704812a3f1fa0c6066875caa83a5c926b4b88561066bb6924b44b97ce16d5b160f001bc66289e49cdf843856f3cab938c71308553c84a0fa0e53331fa38dd642c2c284b76c135d845003e1b7e18c59a91e43da9050fbf74324b110ec3d756b02031b0cb3ef70186dcc42947f160cd8891acfc169b802243b9b27df799722b6477c093b0252a047a6a1fe65c7fb7c2c853b4a81376ea0651186242bb295e8355c47f868c35d5862adac4f8427b63745140f35570a58553e962ece42619922fd0953ad5cac2efca6b86e68389b51e663348f49631ae06a9969b9086bac11ad742837940267a187229018517a658777a4964674ccc8e3c2b8bb615cc7885af2cf5865193725023180558258f48b619b80032f7c521b95ff6325ee7acb22edaa772e5a5cc166bfbf439e762405c84987530735ba7ba4b1b583a6bc504d28e62c448fc6a541b5a5a3f382c356a3b2bc35e831255fa902196b669d7a12bbeaa02930434d75c01a02801f8f9aecb885ccf8727448854ca450cc561c394917c7dc58dae989cd5620b18f657ed193822b676fccb49cf705055ec62cc9800e3546e1f78253594a563176338f49484aaa7a6faa402c659a320a975f824e3da1313d1ca588b00e9e2685e690fb31a867deb5fac69610fd99631555bef428f8fe29509caa04af6638d0a200d05ba94c626c97b4c26d550caf1b489f590e8a99332f5b712a8872022381df32b28951ffd71450cf28f82cc0ed5d9240114917d18219498513cbb30c980576039228551c78153a5d9a8008e559246c645508657be02a48cb0c6e826ca2ce076ec76b915c40c5134ac626735d7b21cbcc6984b8c80f6a8ca616b547a666c8c8c9cdd0ba0b38a4465ea23820140b6744dc1f8a78558b6b699af0fe96158dc206161bf0adc04273947dae81af241a61352847eccadf48b6bed0196b6c63e7a33022adb1ce4e968fedab7d6f65824732ffcb325483599a6f886b79b5cbcb0477f8906c535ace37b784bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8ae3c90829b99d1f0a55382eaf301bb5f288c67687a2eb61060b3aafd1bbe680371c1ca2a5b29d73a2cdd1955dc19785d8c3f313bc8c65e1edde23293bc0724496" "dk from a raw seed" + got=$(printf '%s' 4A39718BC71DA57200A3458849B66A584B1654517FC01CD71F73936EC637AFDB1C1CA2A5B29D73A2CDD1955DC19785D8C3F313BC8C65E1EDDE23293BC0724496 | hex_out "$BC_RUST" --hex-input mlkem768 keygen-from-seed -x) + assert_eq "$got" "e0963a4ca3b67b75caf8650d12193989e9b0ee23b72c00c53f835fa5a5986c5a9d6b730e23c7720ba08641cac0f9489d0fcc39b7b3b54b33be045c4d5e54c3f1cb1eb8d722ecfab2c2209b3a8574fa4615d6e626d0c15ef392cda4f4153d1b55be83241f97a79b8a1bb6daab46d64948988c23ebcfe57134a66606b139b821b6a857c825d21080ce6a183799210f33cc65b7127c7cc071932dc80415701865966137c9bb52d8e14756b976775c97feb58702922c64f65a8c4c9fb378af484576c855322f5c7163e356c3386ce9f21ab7253b2d3a99126b207f6086a388cf89e94cfd89942907ca9a4510e02b707f5b32119186db2769e2daa07cc4a34950508aa1b0fea5a4d3833e9e2b11837202e7bb88bdb9842c78b56a8320ae950acd19c135f36c49cb20267c6b67a10b2d0256289bc90c6996c67198963149a7620dd7e1b7ac2c9c228c4e60419e6932c74e799df9085d92493b910ccd0b837b55f82c73da044b0305cb52bf54da86ebbaba245624cc78ba3296bd8a204a6d0ac9d039b8c2f45036f711c6bc516b936403fb051fd09be8c7c8fb9aa2c22cb9e0d312fd972271e56de755ab479bb9f2b183818b6cbbbaba1bd704812a3f1fa0c6066875caa83a5c926b4b88561066bb6924b44b97ce16d5b160f001bc66289e49cdf843856f3cab938c71308553c84a0fa0e53331fa38dd642c2c284b76c135d845003e1b7e18c59a91e43da9050fbf74324b110ec3d756b02031b0cb3ef70186dcc42947f160cd8891acfc169b802243b9b27df799722b6477c093b0252a047a6a1fe65c7fb7c2c853b4a81376ea0651186242bb295e8355c47f868c35d5862adac4f8427b63745140f35570a58553e962ece42619922fd0953ad5cac2efca6b86e68389b51e663348f49631ae06a9969b9086bac11ad742837940267a187229018517a658777a4964674ccc8e3c2b8bb615cc7885af2cf5865193725023180558258f48b619b80032f7c521b95ff6325ee7acb22edaa772e5a5cc166bfbf439e762405c84987530735ba7ba4b1b583a6bc504d28e62c448fc6a541b5a5a3f382c356a3b2bc35e831255fa902196b669d7a12bbeaa02930434d75c01a02801f8f9aecb885ccf8727448854ca450cc561c394917c7dc58dae989cd5620b18f657ed193822b676fccb49cf705055ec62cc9800e3546e1f78253594a563176338f49484aaa7a6faa402c659a320a975f824e3da1313d1ca588b00e9e2685e690fb31a867deb5fac69610fd99631555bef428f8fe29509caa04af6638d0a200d05ba94c626c97b4c26d550caf1b489f590e8a99332f5b712a8872022381df32b28951ffd71450cf28f82cc0ed5d9240114917d18219498513cbb30c980576039228551c78153a5d9a8008e559246c645508657be02a48cb0c6e826ca2ce076ec76b915c40c5134ac626735d7b21cbcc6984b8c80f6a8ca616b547a666c8c8c9cdd0ba0b38a4465ea23820140b6744dc1f8a78558b6b699af0fe96158dc206161bf0adc04273947dae81af241a61352847eccadf48b6bed0196b6c63e7a33022adb1ce4e968fedab7d6f65824732ffcb325483599a6f886b79b5cbcb0477f8906c535ace37b784bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8ae3c90829b99d1f0a55382eaf301bb5f288c67687a2eb61060b3aafd1bbe680371c1ca2a5b29d73a2cdd1955dc19785d8c3f313bc8c65e1edde23293bc0724496" "dk from a hex seed" + "$BC_RUST" mlkem768 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem768 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "4bc58d18dc9cf6973257a12a7aeb62cd91944dd173f85c571d64277b7263fa37b0f8b5aaffaa7419998442e55fd0bc1af7b0287ada9ab174ae4fa78f2266158bc83e0ee33aaa7086cf3c010a4a4d56b42ed5172169827729293745844ae757af6e95c2a6744797710f2fe9c78bfa15c13aaee8e9b69b021f142144d398600a579cc9cc2c7520b7259c2528a9966e8cc5efba8668d885145b052bdc0ad4f0be38bc9144b20509f25d942a88c9789650c76d6806791109c647bb842e314946f251dcb5b71cfab81ad81908472cb7307e87e4a29cc48ae0b3cb7e82a57dc92fb95126988b11d506c35e40228687099af3aef53baf6b3686ce3695b4270692567afb367cdb7129b2b83551d47420a1c938016f4671a354259787f69534798839eb1464d749dc883ad8d4072561056501c76bf2a4c8385897302acc76cf5609ccdad74b9bea745b78321c975d0f72789feb32da404ced223f7b838d10fa6bf7057384cb1e5ec863f2d906e274c4eee6b9388b5ac9e4575d48747d371cecd58dadf256ba9b278fd8a1535708d8b78862b27f080c7b8a057fc08c231a41bf428941be82a307644297d825a386182ff32f8f3c30493638e16b8a8edc9346183262e50a4474416402628be4a7ccb3431ab70d324910a39198a25468eb95ccdad0948d450213b06bb83430283538c9234cb1181fd6689cc07c8d125cb45fb2b78de6071d8895b614cd47e7a253aaa1ce31bd955974117809f9391cd8a9113178273c5a8a65e7b66ca77b0710ab112144e77058e5da498ce0199967c39b8a447aab7f8b714ddcd694c2f28b2295762a3ba880acba260384ff8a7c64a8211e7c4f81165723e3bb261198a2a1c8086662f7940abc4ba1ef10460526621a79470db22427bb10f0115c17b79763c5488bdb3d305a4e411797cb620b29c3a7c076bd3c405db2b81fec85adfe7ac9ba7702fbc3bebdc0b308149f327c18c7037ee5234c37f581dfd94837554673a39379e03ac6aaa38ca87ea2b940478811edd41c07968b15c112cf64b667b3bd6dd5bcdd5a5cb065787aba70d6926b30259c77e3b4ef2cc8b25153fbb38c1cdc8ef81358893861a888851b54256e37a0e8360ce22c9ccc3822b718becd7bc83422cc61ea9f918b986f15b6d8e1b32d472d394ca40ed7a5c5611548e294c5bc09b64367fcc86f5fd417786c3c7b864c4f7b24ecab8a32c40a993769093b36fb2b465b2924816314fc1063d9606d7277a8fff5b5dba5769c5c8986e4adae1965b652064e0570d6a36dc28a45df2c4c5f6aa201ea7d8d672a7222648b32672dcba0a68459dcb484b14c18603831003d15da1b4a87d0579d196bb069477ff3bef0059ad38c78d7da94413a69f8274e93726dca420f75043e55236abc4921f6489c14043d2cbab64d68a0083bb0d2257a4750459780c623a12d8eb522b8b62214c75bcaa41899b8cbe36b1a071200652bae06e25808296151964c89a3bb7d5b88e8b23260964b3cd23f6bb7655b0834b9ecc9bcac1185603d0af11216a9aae4ca80ee0c714618bf0afc73293c9d592872ca72c887d4447e65b751368ea081294a43777cb599c416516cb09ebc436f352ba11a3962fd5745214133b72b67ea855f380e4661920c003ce639a7f02609835f00f09ab1a80f0e1cd0d5b5fd8a" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem768 check-consistency --pkfile <(unhex 4BC58D18DC9CF6973257A12A7AEB62CD91944DD173F85C571D64277B7263FA37B0F8B5AAFFAA7419998442E55FD0BC1AF7B0287ADA9AB174AE4FA78F2266158BC83E0EE33AAA7086CF3C010A4A4D56B42ED5172169827729293745844AE757AF6E95C2A6744797710F2FE9C78BFA15C13AAEE8E9B69B021F142144D398600A579CC9CC2C7520B7259C2528A9966E8CC5EFBA8668D885145B052BDC0AD4F0BE38BC9144B20509F25D942A88C9789650C76D6806791109C647BB842E314946F251DCB5B71CFAB81AD81908472CB7307E87E4A29CC48AE0B3CB7E82A57DC92FB95126988B11D506C35E40228687099AF3AEF53BAF6B3686CE3695B4270692567AFB367CDB7129B2B83551D47420A1C938016F4671A354259787F69534798839EB1464D749DC883AD8D4072561056501C76BF2A4C8385897302ACC76CF5609CCDAD74B9BEA745B78321C975D0F72789FEB32DA404CED223F7B838D10FA6BF7057384CB1E5EC863F2D906E274C4EEE6B9388B5AC9E4575D48747D371CECD58DADF256BA9B278FD8A1535708D8B78862B27F080C7B8A057FC08C231A41BF428941BE82A307644297D825A386182FF32F8F3C30493638E16B8A8EDC9346183262E50A4474416402628BE4A7CCB3431AB70D324910A39198A25468EB95CCDAD0948D450213B06BB83430283538C9234CB1181FD6689CC07C8D125CB45FB2B78DE6071D8895B614CD47E7A253AAA1CE31BD955974117809F9391CD8A9113178273C5A8A65E7B66CA77B0710AB112144E77058E5DA498CE0199967C39B8A447AAB7F8B714DDCD694C2F28B2295762A3BA880ACBA260384FF8A7C64A8211E7C4F81165723E3BB261198A2A1C8086662F7940ABC4BA1EF10460526621A79470DB22427BB10F0115C17B79763C5488BDB3D305A4E411797CB620B29C3A7C076BD3C405DB2B81FEC85ADFE7AC9BA7702FBC3BEBDC0B308149F327C18C7037EE5234C37F581DFD94837554673A39379E03AC6AAA38CA87EA2B940478811EDD41C07968B15C112CF64B667B3BD6DD5BCDD5A5CB065787ABA70D6926B30259C77E3B4EF2CC8B25153FBB38C1CDC8EF81358893861A888851B54256E37A0E8360CE22C9CCC3822B718BECD7BC83422CC61EA9F918B986F15B6D8E1B32D472D394CA40ED7A5C5611548E294C5BC09B64367FCC86F5FD417786C3C7B864C4F7B24ECAB8A32C40A993769093B36FB2B465B2924816314FC1063D9606D7277A8FFF5B5DBA5769C5C8986E4ADAE1965B652064E0570D6A36DC28A45DF2C4C5F6AA201EA7D8D672A7222648B32672DCBA0A68459DCB484B14C18603831003D15DA1B4A87D0579D196BB069477FF3BEF0059AD38C78D7DA94413A69F8274E93726DCA420F75043E55236ABC4921F6489C14043D2CBAB64D68A0083BB0D2257A4750459780C623A12D8EB522B8B62214C75BCAA41899B8CBE36B1A071200652BAE06E25808296151964C89A3BB7D5B88E8B23260964B3CD23F6BB7655B0834B9ECC9BCAC1185603D0AF11216A9AAE4CA80EE0C714618BF0AFC73293C9D592872CA72C887D4447E65B751368EA081294A43777CB599C416516CB09EBC436F352BA11A3962FD5745214133B72B67EA855F380E4661920C003CE639A7F02609835F00F09AB1A80F0E1CD0D5B5FD8A) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-768 tcId 26): dk and c give k. +test_mlkem768_decaps_matches_acvp() { + local got + unhex 94AAA7ABEC0BAB887A8C485D3E33AA35305B8088B25CCB4487EC7B2DB4AB29F440BA5231BD724A7A397F9AD3813D393A1786271CB49068A6451FDC230AB3186BBC0290E2362F5AB1EBE58CBE1B3371A0818E26A0A58C59DC96B90C3C45B483ABBA410208F78D275203F4A40EB7DBC42D191553341F98672057D3CEBE668B6C397BC9707092776FCC450FFDA31C5E2C929C253FFCC184D2D98C25F11AACA44145BC1AAC54077F77AAA77B9EC35BBC9856CE30E70FEB77BFD1F42F0421B9F3F55719E78502141263B4CF432830B943BF1CE83F1BF12DA931505A293000D977B6E33C25584869009BD4CA44F3BC19CE041984D0CE9110735D637D7E027606940E6A05BE090078532065750A5FD953302C44A530B432DBBC73E946076512A90142C16130BA9E2413A9F0C526737137B5BB113524D4B5747D79A325298F1B9819A0C2058D199BF93781098B1363243B6110C0EDB1BD750B92B3513482E62572C66687652B34819F0FF0557F022381B39D7797C14D8C8011F96C39CB44D0B039D3745A71C541D7937C4C7BA75B3470961117D6B22E24BC52660158150B654C220F4C5992891720CA5779F89426F78A3949397B7D10977F18909B247A5848096F43689BE199AA7A2D16927C54E6191241CE27706B7444674CC10050E14197A11623370C15372466716BDF6825361BBD4642A277458C80532DA4213A1510A57425CD6C5B21282B168DDA6A095B122A4A175F95B8311A1151133F44D57A78BC7A934BCAA38097D3808B7BCA17B7EB4F20165EB4983EE7D588C2B19F3CF5790CFC9B4688379E4C91DBF7878DC626AE11308AF19D73A59675E8A0D9E5662529BFCA457848861FBC5392C9453C33434CA8A307EC30854F50115A6C939991AAE3DB749C689208772864C9AD091389F3030F1AE45E3A528B5F9248F47727507389A40270E8EC7085133642282102C3992E829CEC8976B5C102082B911A6126F99A35E5834BDC2A3C58E96F5D44B6FAC845B113573FE9580E469E4393B3520695E22113AB02014981A647B1B2B7B8935FD53565B6B83CA1CB285899D8968C37E866904A6DDDE1A0F3C17CE7C702BDB520CF7696052B5F74525B2C8A740D4B3BB85ABB8EE5081B77B789B25ACA006C3C75A57DDAA47D742283C9BABD5B35D424789822AC6821A1BF6A4D5EB7CB70B2013FDA913C6C02DDE856CC624B875587042C1B82C8946D6123EE07572BF4BF6B4381074BCB96693879A87E13640F102A5A838B26C5BAC121C4BD63E44EE2121F21EC6016351B4FD6BCE835B67EB55D0E24895FBC2F92F447D05855BFA9BACF2A044AC32E2F6B7367458C1AF74F24025F4DC1B647B82578E29D93317BCF6A00C1892FC712BB61328102EB6A23491210408E1418B31BC4ADBF287889414FFAD3C654A3B0E473832D1355364C8303259142D0207FF302C7FC7238398F9249C9DDF69D21A57630B120CFAA3E0A3142AD481194D471B8F919EE131615369D77D033C65AAC2CC1AD87F582AA8C1FCBEB7C010CBD422C0A9BE9844ECA5A0FCAAF4AC2ACCF50760C341492A6B0CE712B7B362BE397AB892B9DFB064A60F396AB963F9CE837C483456D055DE3F25545139F06696780377BF6C5813A607E4039820364598108920300822E1C2CDB97899AA2A9EB4409CEA5A24F959CD204A12DE532066AB5619897408655E6259D7579C6A509314CA2C210F2224CC9691C734F1929974CB5B3859683322AAD9D3953521C607110386939329F92122EC03E0F02961FAB5578741C600307CEACB54B21C4BAE847C8A479577077E6920E933C4E3882B515BBC03934BA86D4BE3E96895FB3CBA7E9B4AC54C2731B678CA908C7502500D8A782B73C79FAC137F56193662FB0D30AC90131ABF6B109A2B4AED62BF94A923C808D6EE9ACCE4B2466B2B0A10552C0645C7D271A9D71A9025117E6E46FE1D66F4BD306D1075898627AA3B3BC70C1B1ABCC694A86180498B17F9147091172E019734E732CFB6A3744591246D5575351A7F7AB593F5A7C1876009C46516D95C7408A78A17548E5BB2313DA178D027175496AFC59266DEA825C8140FBD51494D0CBC9F7457DD57237C5CFF4CCAB4E04A0FA4141F0422E7E85CD0A94274BDB3C6CF10F6415BB48606DFF5C98FE551BD96B4A06459F45C358A4284EA87844534189077C10899331C134230478C0E7A35442CC13C6D4AB0D928041156E6F7A70F6438EE1746196E245B2F31AE07221AD9C3CCC542A79D960D9178259F51315371D85428B227228B7F38B6C543C51074B31184C16E143E0F18E4004668BB32DF9386E347395F47C38A857ABBBE342C761326F713B5730BD241200A230C4ECB4C5E05A0D0F5C28B42B9D715386DEC74E5DC9373E7BCE096C56CCD8C0D48910A3D96D61449B71B1A319B1B3A0826AB289B5A66280593C437DA08A5A610D50EA4BC401542CC83DB5436882810BA60C5B673A1175258563542EFFA3146339A8461A343DF59A06B362D5E762053962E2B441CA1742CB92304F6B1F104BCCE1783566C6CD56F0BF1C15460CD3B32574386DB122E9F23F2715230622C45306033BC9501357B9BAB6822772823412447D965080C04F29AB36D49A0CF37C893202846EB9A1EFB86A23EA3269D2769601C67962488AF0A61937CCB0AC892DB2A28710935A5C82B929C732B3226A746923AB1ADFB81ED165692041BE874C7D8E1A056ED2CF5BE8BD87D5C595521E2BF65749E98D27003A6C000C3F87B544C224343763272047376B420DB7688E710B4308953D792B060C30AFB7044BAC134D77C306B365DF1B315EE536F223663A023507C23A6BAAB46BBB1BC40BA408E9309D430006178517D41F9D1364A6C722C7B737BC62BB270202F2799F0CA50B5853A60ED789A2FC4742F6CFFFBBAA400A2F8CA01ACA044E2830AD65B78BCDD8CEDAEC451DD093C0627FBA3C365C2A47EDFA6582BB1DE899355E0C64820646E07703B4390A6DF27633654490BA927699981A34528EB7427ED70FBB9C3A4BE5BFCEB70516768A5222C08079352D6A181BBCB69FAC13917AB0F3B221505B350754B7614C4120E01A9167091E84AC48A7C00ABA212672535C8608B5531364BC656386B42B8B41C777B82DD808FBF76559764D01D55F35CB594C19AA0918571F9C4AAC65595949135DDCB8AE8C7C4ECB2220D51C56655CCF13A7BF88784DC74C33C4390658CE75A59BA24776EE96686231AF8AD007BDD3CC6657674998719D2C7180481518F809855C114B8D8F80EF081ECB4CDF02F3634147386FF2C8FDC972FA395AE103C61030221B90D5B8400C1B570298312D9E8057FBAB908D88E3F7E826900C7FD53615ABAD0EE1227965788DE2A02017CA6828D3BEC5F665D9FF36CE804564AF8DA275FC4634 >"$TMP/dk" + unhex FA014F168757DD2CEE22644E799A9592644E886C29BF70E3ABFF5C540F6DAF1561DEE71948A3782CB834CF96FDB205E6BCE9B5B151C8F01AE6326868B57C2CCBDE03FC23F9A3DD7422BD0DA2C53BC961821E927941854C3D55DE42643422F1BD31AEEFF355B88391E0A4A8416DE41328D71EA2E36FA6839C800BCC2212B91B2D485267960085A20A088FD2C48F748BFB56C285E83E1A1C0A2E4A2138595F857B8EBBB2B43FD2584BC24BFB1AF8BB5D2D4BFCFD5D610F0E73D12E817D4D00A643BFD9AD53587EB5F610BAEB81EC59A93F24E81B16C0258A56A3FDF4BC5FF67BE67CFAAA2C38CF5E4FF909763BE8060610E74445050CA99D21C1505CE717B615084D37CFA10827986F91C719695284B5EBFA7720D0C3A4099576C50E9BE2A71DB8DBDC6B247A47E9130FFA5E4007BCB8D095998EAA826BE2D3EAD7F787CFE08ED6394DB72EDC160B0D3EDA203674B4F96A5A2DBF052AD8F803CAF6572BC5A2A540F72927F05BFF5EC2DDE529C982E379E12D4260C3780C7F70DE4B6E6CA451117045ADBFA6D77F7538807F221B3AEEA18263E649ABEB3143F60A8FDCA3A6414537F5824932CEC4F5859C4827F9CBC30DCB21DBE909BB1188FF8553875D0A4328094E7A6DE57EB72407F4870FC2771201F70F19A796F8F8907E1DFB1080659389D8B900235D5C53398D5CA487B5D5A86686D6A016B4FAE247BE9D473474FD04FB7BA6DC54D00824779965022A4946C0418760ED5DCC97AD4B68EB4C29A279A9DF962CCC7ED81259D4B1EA6E772532E16925648C5C04B08272FEB81EDF280FBECCC5CD62B21A74EAA828A4473C20873A1668873464C57129C2BBA570B6037508AB3A6AE2702052759F9FBE620D775FEBCDEA9B8F0FDBAB0FBE730E9D43E414A5B29D949E27BDE8E08E9B2260B9CA274924224386505DB5CB43689CF5A6C495824DC7E4C43D970D7636CBD0B4A7B3664C13D383897989DA503ED18238BF37514D0E426EABAC4F5DC85172DFC0260EAA9A83DC72A22A88CF8EEC7DC1AD771BC6ED5C6D45F1203C86E80DCD38F4C3F5959A1EC45B1035A6C5263D026B3016A51F1D8216DC6517C38667AC13BB2C42FFAEB5464533B3BAD89325F1DB37774D9136B0F5DB38FDAEB4D3104D08657FD380441156097AD602C46F54F27E41C2F9451087776E0FF7BBB90F5F497B9241F9FB4AA598E76BFC62D9C59F1EA4EAC29E3B03E46C0D66D134D3B451315CBA69E0EF05E49C4211B2941F779FE2DC4E65AE37D2FA5A3AC117CC6A3439B0ACDA4255DD57347AF7A9FA6F995AC9811CAF7BFE64427335C1D3DED4C0A57DF4F03AD1166D17163CA2EC16BCE65B47DB0243326D143058FC8D8D619EA7D9B08C28A66B1D87D05FF336CED546627AB8AE8904942E3448B01F9030D415DDBAFFEBE0E5141FFB43DACB29A626C7942E8A8F3D539FEF5FDB9CF3E5DE83105C4E154971403A125FEE8F42E2EF7783B7C3ADD9047F661469C61048ECFD2A7945179C773AF46DFD70E5BB09F45A7498403CDDDBA9E2A58A1DC9A2470B >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem768 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "f347ebcf8e03cc1195b1a7a02899249b7ff9f5fc48144013989bb94b9ab8d1d3" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem768 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem768 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "f347ebcf8e03cc1195b1a7a02899249b7ff9f5fc48144013989bb94b9ab8d1d3" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-768 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem768_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem768 keygen >"$TMP/dk" + assert_size "$TMP/dk" 2400 "dk length of ML-KEM-768" + "$BC_RUST" mlkem768 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 1184 "ek length of ML-KEM-768" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem768 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem768 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/seed" + assert_size "$TMP/seed" 64 "seed length" + got=$(hex_out "$BC_RUST" mlkem1024 keygen-from-seed -x <"$TMP/seed") + assert_eq "$got" "e40b1a2c116bfac97fb37b0d20c13f436c0ba5e348eab3815cb61b2118970779ad1a539abeb3948fac2e5748a3e2866df4fa7c83b277a4b102eb6911b6642589107060d67779d59628665951168e2748b2a4ab56716887366427b31777912c70421380299619a4a64e61a60c12376885f6cd5d98c80ac35d1f293d2c643d90e790144b1d673496d9c98f4a44047215989a18655b767352e1049bc71fb6e375b16a2ca15559c7ec12396b08445731b7f497e6b02411b18d56a2cf0e099e8611cdff86b778720225d72726f8122d6b2b360014f7d8cdac58171c78af0aa37e1624509f435a5e4984dbb254f13bbe1deb0d4e04047bd14e341788308358c2453df02b879a16590cd61709d2c77fdbc4a5ccbb817cb70649255e050f12441507d442c78262ab98030a13311ad81a406474cbc729b4c2459d2ab0f0ea0911448ef456c37cac70ab890ebde72369628f14235a2838afe3c6912256a970279cb61c8234226f1f153d5f3b924b10b154bbb5ac576ab7f7482cc06e6fd533f367328bd4a4340c06f8c639a6303165f40fce564d3cb187a51a5224e94bbfb817a0e03cefab77bb1c5ed887a9a00c51f2b34741f078dd625fd7a49fbc0c7e5544455e599ede0a92e9fbc583f132b62b8c8286cfd4c81d65e785b7b44fec127dd6a46885d409fe254cfb787107b201ba80ca63e85abc5a7e49f421e7a6b34b62ce9bd6ce55c28153fc7f55f00b4169a574bcb61ca08f02f17b10dc298e5ba19aa8c6a4c74c5fb9a196caac201937dee779c2d11fe6fa9d6ff5570f70cfae93c216484ff0634dbd1647f40ccd164ab1cfb67216b5a6eb2b3417078ab2c386a63471003aaf233cacc4679c684ca557ba3a275577582103af34257d01806a166b57229c63c040778678776a9ca981b7fa62451e82476f44ce07fb0d115328159469c88a3f2a3a47dd3484f48885802948e6853706757bedfa8f95565dccb1685732b601879572f45eca5a4baf84b8b72623d4450c34c7c92255932580c18d71cf3d8255efc38aab4b5370cc044d6c3b322a6d8f79355c229b674b8a3bea8670433985f7b5de01aa95332cbd1c2706ea716c904f45e88419a04241ea3d32d5c30f732a13071b34f33b91c742c14662e752865bf63e65f3bdcae19a8aa5408ebb9bacbb6e20a8062e1b954692a61668755209890f191dbd0ca3b3d11260e0560ab72787507221cb1a38e0a48d7539c7e249ddb519cfc0974a242b81a5469dab08cb50c3884c14ed8bc177c78e397782e544ab889c42cfa78bc263c0682470d6908f930903a0e719fd701b60fb3218c84609738099d09aea5c142ea443cf536980d72e37b16b4aa8a81cc16ea9b7b4e5904f97375ab883be601ca0932974cdc60574f2c50222a38015157818281aa32d24cc1126238260ba8d60b3a3d38912c558ac3134ac4a8298c5052661b71a430aacd6d7bec0f70425726071f2b39cb5030e71007d5257c588200dfbb915557943b629bbf3ae55f0cd546013ed7ccecc6475b5fc60adf6633b63537f4c95db609358515da9217daef1c30e5455a66b30d4b1b1a1d4746e62caba887d7341807a9c99bbf4880a5896e225b13b443ec99a2726f3bd9473a813913846d2b67c403018867781eac0b8045ae0e02e03160f865b8cba0140f83744dbd70bdaabbea4f31911565deee4aef6b3ac32eb65793ac115e7a798e80a2a856731f56f50218b6e35a5d882032732a62906bbf00a82c6249fe7824664b3b3f8633dbb7aa83fc3c0a6812efc6a238b449aa8494ee0dc28e36a42d4ca509f62038e4b2f73e3a603096777161289542c83f64abbd70c810a3ccc31a81be9b6a45a4b333540b554c0ddf423360491e22998b22b1ff582545ae43c41651aa95b31f7892d54dc2c9f1024d768c70d6597919c749ed9b45e62aab143ae1e4c7334d07a9c235247bbadb7c04b188a7bb8706ca7263f76191e99c5675a547acd1a746bb09a6aa4c8dcd844efd5b321c7486bcb6d299719b6388574166722516249ecb678b390629ba45339a746da0532bc60cce08fcaf0c4048c951a035057c6b594ba94a5071db32284dab54e1c664835438370fa535835b97ad4093668949a156667926890d42adb587e501c496a903b0e66c5be619ad7c165ba920a9069b69c004d99170eca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16d651fd545328c20d5a2c0e139a49460a9f63cb77cea230165de90fcb5f5aba64ba7d101c1f1215068d3665997c3446b16c0ce29d95f0ed17434e620bb95a4045" "dk from a raw seed" + got=$(printf '%s' 07262F4FF6B3CC09E050D438FF648D9C8AAD96B3479EE81D4AD88E6B7E1829ACBA7D101C1F1215068D3665997C3446B16C0CE29D95F0ED17434E620BB95A4045 | hex_out "$BC_RUST" --hex-input mlkem1024 keygen-from-seed -x) + assert_eq "$got" "e40b1a2c116bfac97fb37b0d20c13f436c0ba5e348eab3815cb61b2118970779ad1a539abeb3948fac2e5748a3e2866df4fa7c83b277a4b102eb6911b6642589107060d67779d59628665951168e2748b2a4ab56716887366427b31777912c70421380299619a4a64e61a60c12376885f6cd5d98c80ac35d1f293d2c643d90e790144b1d673496d9c98f4a44047215989a18655b767352e1049bc71fb6e375b16a2ca15559c7ec12396b08445731b7f497e6b02411b18d56a2cf0e099e8611cdff86b778720225d72726f8122d6b2b360014f7d8cdac58171c78af0aa37e1624509f435a5e4984dbb254f13bbe1deb0d4e04047bd14e341788308358c2453df02b879a16590cd61709d2c77fdbc4a5ccbb817cb70649255e050f12441507d442c78262ab98030a13311ad81a406474cbc729b4c2459d2ab0f0ea0911448ef456c37cac70ab890ebde72369628f14235a2838afe3c6912256a970279cb61c8234226f1f153d5f3b924b10b154bbb5ac576ab7f7482cc06e6fd533f367328bd4a4340c06f8c639a6303165f40fce564d3cb187a51a5224e94bbfb817a0e03cefab77bb1c5ed887a9a00c51f2b34741f078dd625fd7a49fbc0c7e5544455e599ede0a92e9fbc583f132b62b8c8286cfd4c81d65e785b7b44fec127dd6a46885d409fe254cfb787107b201ba80ca63e85abc5a7e49f421e7a6b34b62ce9bd6ce55c28153fc7f55f00b4169a574bcb61ca08f02f17b10dc298e5ba19aa8c6a4c74c5fb9a196caac201937dee779c2d11fe6fa9d6ff5570f70cfae93c216484ff0634dbd1647f40ccd164ab1cfb67216b5a6eb2b3417078ab2c386a63471003aaf233cacc4679c684ca557ba3a275577582103af34257d01806a166b57229c63c040778678776a9ca981b7fa62451e82476f44ce07fb0d115328159469c88a3f2a3a47dd3484f48885802948e6853706757bedfa8f95565dccb1685732b601879572f45eca5a4baf84b8b72623d4450c34c7c92255932580c18d71cf3d8255efc38aab4b5370cc044d6c3b322a6d8f79355c229b674b8a3bea8670433985f7b5de01aa95332cbd1c2706ea716c904f45e88419a04241ea3d32d5c30f732a13071b34f33b91c742c14662e752865bf63e65f3bdcae19a8aa5408ebb9bacbb6e20a8062e1b954692a61668755209890f191dbd0ca3b3d11260e0560ab72787507221cb1a38e0a48d7539c7e249ddb519cfc0974a242b81a5469dab08cb50c3884c14ed8bc177c78e397782e544ab889c42cfa78bc263c0682470d6908f930903a0e719fd701b60fb3218c84609738099d09aea5c142ea443cf536980d72e37b16b4aa8a81cc16ea9b7b4e5904f97375ab883be601ca0932974cdc60574f2c50222a38015157818281aa32d24cc1126238260ba8d60b3a3d38912c558ac3134ac4a8298c5052661b71a430aacd6d7bec0f70425726071f2b39cb5030e71007d5257c588200dfbb915557943b629bbf3ae55f0cd546013ed7ccecc6475b5fc60adf6633b63537f4c95db609358515da9217daef1c30e5455a66b30d4b1b1a1d4746e62caba887d7341807a9c99bbf4880a5896e225b13b443ec99a2726f3bd9473a813913846d2b67c403018867781eac0b8045ae0e02e03160f865b8cba0140f83744dbd70bdaabbea4f31911565deee4aef6b3ac32eb65793ac115e7a798e80a2a856731f56f50218b6e35a5d882032732a62906bbf00a82c6249fe7824664b3b3f8633dbb7aa83fc3c0a6812efc6a238b449aa8494ee0dc28e36a42d4ca509f62038e4b2f73e3a603096777161289542c83f64abbd70c810a3ccc31a81be9b6a45a4b333540b554c0ddf423360491e22998b22b1ff582545ae43c41651aa95b31f7892d54dc2c9f1024d768c70d6597919c749ed9b45e62aab143ae1e4c7334d07a9c235247bbadb7c04b188a7bb8706ca7263f76191e99c5675a547acd1a746bb09a6aa4c8dcd844efd5b321c7486bcb6d299719b6388574166722516249ecb678b390629ba45339a746da0532bc60cce08fcaf0c4048c951a035057c6b594ba94a5071db32284dab54e1c664835438370fa535835b97ad4093668949a156667926890d42adb587e501c496a903b0e66c5be619ad7c165ba920a9069b69c004d99170eca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16d651fd545328c20d5a2c0e139a49460a9f63cb77cea230165de90fcb5f5aba64ba7d101c1f1215068d3665997c3446b16c0ce29d95f0ed17434e620bb95a4045" "dk from a hex seed" + "$BC_RUST" mlkem1024 keygen-from-seed <"$TMP/seed" >"$TMP/dk" + got=$(hex_out "$BC_RUST" mlkem1024 pk-from-sk -x <"$TMP/dk") + assert_eq "$got" "ca975b8ad48f9f3132e47784ead421e056238af4916f5b326529800584098ee0b4c6d2430f02296826c09c987fa75061a543abecdc6b3287acec29cb818b336b2c65fa099684a179c9c7b24d5c00a4a09cac4300536430d1f05a9eb4ba31682aef0a91adbb0555680c112c5709b8b0c52683651c0df4d8c9f6c09d74d45465da78743455b9b2803bea86ca41531f556128cccfd8c8ae70b057e090ac2a347f57d185326c2bee8c3819d340301071a077ad84d296c095359c97010ec6a42693340636268d48204f707610387862152c805bac1370bd2c99a3f3c2842ae17c69b98152c112e98aa703b9526e8c138875a873f598e689bc22b6cb51f7a7c7b760507b36c6cbbe0052030d593fb3b9073f446ec0a010616a0aac2baeaeda262e9bcca8c7911273ccbe3832937707ec22591a9bc62d2114bcfa1614bb85b3c7934d53c47695a1a4087e85d00b301cad6ffb70d38836bf916015757b38fa60dada5f0a99892fea429443b17c5715c9c75726f63d528565ea587d1b5a3065344f4660466c7c80f8c4213f5246a3c75b03624cf11967926a21297c13929baaa0639dc3128633d34e081c5c4792374bc58e24c6375e0314b79947e6794a255852ac67300bb8086e5088df849ce4722cfe0420c8b18bd25c9da378874542b33e233f1ef37eb7b7167349cdd1e70c50a115082824e8d4963a022691136be606695fc385794bcff394bb42754be21228b7c32456d5bde7bb5ce4ca2e419c2f0f5c3d53058393105eb89359400a87a1e8c57037551461a09653990762651e964552599c115578ebd1926a64ccdca75ce358991058cd57a5b8719468d69a53d7304ddf69c4bdd0c15a94015cf54009054f41dc4346235742ab4ea261952642c579b204a6c2a6c436554fc605d9f9cb43099c4fba5b978437391b3cfd73a89528c92ca69a7f7b30e2263eeb462c3b530b88206e990030076aa6ec386f7bf517002ba3c4180c3d75362e188e3a0372ecc87267e9ac40ba7b5b79c55980aec82bc2b9921c84f2643b298ed0dc2b5dcb0a3ee15e7ef959ff6272c6d10b299102b13959dd8bb1129c8d38862bddc75e16a3b4cd097d3ec2848a28aa3f0bc742d1ba2afb478a0635275775f245be96a8494d62120c7069f3d21a19f40879a521df87b1cb465da07a69ebc0c6b43626dc1c225ba97fc09b0d6c7189b1f669da9ace3c2a7b3151741643cee5d35df471a26e053ad2474521f5ae56e062b8727e206571263465fafc9103aa4b556438e8a9a31c65a22f0249d081538fd63ecf15ce400224bd5654717b0e977a2d802486c989c4ca23bf0ff24415622e4f6b63f2047c2aa0a57d0753347a455e605aeb5392466a52c2e5a73789bd96c46a86f1552ce511b7a1a64f7a215b582b987a344005224c771b885301797608cdc2a89505a47946715072754599377e06139361cadeeba59a948a9a36ca2ccb3da1d040bee237c8d81c35d0b2381666a2351a373230c22393dac770ca62403c6a925ef2bd629638ec5478cf2c4cfa5b715cdb7256d365971183ed1320874bc56428cd12412dc9fa790e71aeec077e07d91f268bc38f334e6b6068888818171a71fd360a6e7505c5d37eb0b3101290a689414878b62629217cbe0125474a4d90a59d50093dcce45f2ebb551f6205fffb7b16b0c952fb72ddc723aeb9cb1fa074a1e07d2ef4c4a7612b5f14a4d2e807da7349a035c7cc85b332f263cef387a5d36bfb55930ef96a04fa2bdbf508c94aa071e7699ba96b59798c0096b0a491a0125c0179a8940a8795c7bc6a44f34a7af212f5e5244a92a41a0b339d63377208151154408d3c3c127a5641e1002246a747678694a4c316d7951bd370d9104a15c7584ee60c019ca9f0a965fb0c7517a1c333f470bac01c252a8ef075664dd1173f2829b854b6f75b535ddc09314b31fd62370e676bdd569948215aba79b1afa11c47344e21f02dbc60351607bc22aab412d024d08002be73a49677c77fe7ac4872645166b1b9963641a106e9bb7092ac1a16498cdbf37de579aa95c9227baaa32ae74bc2b1240ab50bfba4645bf7682b1ca6e424adb9e1c56e7b7380ba32ffdb2b314ab46517bfe501a7d1f60a06b53ea82308a3fa69036603dbc45f61f9be4f171016212491c2c6f48327ef32cc9c28b4939c3d17a6f8643d51c76c84c6eddfac7a1d03818d3fb856ff960fe9ce96fe16" "ek from the dk" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem1024 check-consistency --pkfile <(unhex CA975B8AD48F9F3132E47784EAD421E056238AF4916F5B326529800584098EE0B4C6D2430F02296826C09C987FA75061A543ABECDC6B3287ACEC29CB818B336B2C65FA099684A179C9C7B24D5C00A4A09CAC4300536430D1F05A9EB4BA31682AEF0A91ADBB0555680C112C5709B8B0C52683651C0DF4D8C9F6C09D74D45465DA78743455B9B2803BEA86CA41531F556128CCCFD8C8AE70B057E090AC2A347F57D185326C2BEE8C3819D340301071A077AD84D296C095359C97010EC6A42693340636268D48204F707610387862152C805BAC1370BD2C99A3F3C2842AE17C69B98152C112E98AA703B9526E8C138875A873F598E689BC22B6CB51F7A7C7B760507B36C6CBBE0052030D593FB3B9073F446EC0A010616A0AAC2BAEAEDA262E9BCCA8C7911273CCBE3832937707EC22591A9BC62D2114BCFA1614BB85B3C7934D53C47695A1A4087E85D00B301CAD6FFB70D38836BF916015757B38FA60DADA5F0A99892FEA429443B17C5715C9C75726F63D528565EA587D1B5A3065344F4660466C7C80F8C4213F5246A3C75B03624CF11967926A21297C13929BAAA0639DC3128633D34E081C5C4792374BC58E24C6375E0314B79947E6794A255852AC67300BB8086E5088DF849CE4722CFE0420C8B18BD25C9DA378874542B33E233F1EF37EB7B7167349CDD1E70C50A115082824E8D4963A022691136BE606695FC385794BCFF394BB42754BE21228B7C32456D5BDE7BB5CE4CA2E419C2F0F5C3D53058393105EB89359400A87A1E8C57037551461A09653990762651E964552599C115578EBD1926A64CCDCA75CE358991058CD57A5B8719468D69A53D7304DDF69C4BDD0C15A94015CF54009054F41DC4346235742AB4EA261952642C579B204A6C2A6C436554FC605D9F9CB43099C4FBA5B978437391B3CFD73A89528C92CA69A7F7B30E2263EEB462C3B530B88206E990030076AA6EC386F7BF517002BA3C4180C3D75362E188E3A0372ECC87267E9AC40BA7B5B79C55980AEC82BC2B9921C84F2643B298ED0DC2B5DCB0A3EE15E7EF959FF6272C6D10B299102B13959DD8BB1129C8D38862BDDC75E16A3B4CD097D3EC2848A28AA3F0BC742D1BA2AFB478A0635275775F245BE96A8494D62120C7069F3D21A19F40879A521DF87B1CB465DA07A69EBC0C6B43626DC1C225BA97FC09B0D6C7189B1F669DA9ACE3C2A7B3151741643CEE5D35DF471A26E053AD2474521F5AE56E062B8727E206571263465FAFC9103AA4B556438E8A9A31C65A22F0249D081538FD63ECF15CE400224BD5654717B0E977A2D802486C989C4CA23BF0FF24415622E4F6B63F2047C2AA0A57D0753347A455E605AEB5392466A52C2E5A73789BD96C46A86F1552CE511B7A1A64F7A215B582B987A344005224C771B885301797608CDC2A89505A47946715072754599377E06139361CADEEBA59A948A9A36CA2CCB3DA1D040BEE237C8D81C35D0B2381666A2351A373230C22393DAC770CA62403C6A925EF2BD629638EC5478CF2C4CFA5B715CDB7256D365971183ED1320874BC56428CD12412DC9FA790E71AEEC077E07D91F268BC38F334E6B6068888818171A71FD360A6E7505C5D37EB0B3101290A689414878B62629217CBE0125474A4D90A59D50093DCCE45F2EBB551F6205FFFB7B16B0C952FB72DDC723AEB9CB1FA074A1E07D2EF4C4A7612B5F14A4D2E807DA7349A035C7CC85B332F263CEF387A5D36BFB55930EF96A04FA2BDBF508C94AA071E7699BA96B59798C0096B0A491A0125C0179A8940A8795C7BC6A44F34A7AF212F5E5244A92A41A0B339D63377208151154408D3C3C127A5641E1002246A747678694A4C316D7951BD370D9104A15C7584EE60C019CA9F0A965FB0C7517A1C333F470BAC01C252A8EF075664DD1173F2829B854B6F75B535DDC09314B31FD62370E676BDD569948215ABA79B1AFA11C47344E21F02DBC60351607BC22AAB412D024D08002BE73A49677C77FE7AC4872645166B1B9963641A106E9BB7092AC1A16498CDBF37DE579AA95C9227BAAA32AE74BC2B1240AB50BFBA4645BF7682B1CA6E424ADB9E1C56E7B7380BA32FFDB2B314AB46517BFE501A7D1F60A06B53EA82308A3FA69036603DBC45F61F9BE4F171016212491C2C6F48327EF32CC9C28B4939C3D17A6F8643D51C76C84C6EDDFAC7A1D03818D3FB856FF960FE9CE96FE16) <"$TMP/dk" >/dev/null +} + +# ACVP ML-KEM encapDecap, decapsulation (ML-KEM-encapDecap.txt, ML-KEM-1024 tcId 26): dk and c give k. +test_mlkem1024_decaps_matches_acvp() { + local got + unhex A2E32D4F2C80368140FCBCA14F841FE27635A1384B402C9D79037A58F04546C7A1EF3A610AA21E977214BD97BBE9840294E267F2A97FD8AB6220789CCAB5C710A9ABFCB34D89936182D52C63C273FAD72C59606B06B4C0AE73897C0BAD2893775930C108016EE573944E8934C7707330A5B44EE854F2F82DF2F06E4AB97FEE4C3DFB966911A729E8697C57642D19283B040983ECF9361D566B48B702642CA0FA756BA03C13BE11ADE7C20422F07DE40A6507253D8F82756FD29D6F601624B8A832461436F441A5BC249A466F880B6377593B340514995A1A8D74BC41C449ED5A53818A8DA62021323CBEBD81BA7D1B6B0E28B66D284791F76FD7494741504BFAF319F009359A80654007BC45563C26B4444F29C9ADEC967C4C439A88B9CCE165A0DB48FEF80F7C770D9799B5B330405E819FCF85CF01F22C63E518C376281C3C35FB8A2CF929502720261467093051CD0DCC3BEEAA987C5A0D60226981B1865FC3B5D0476B0FC17BD8F647FC28336DFC605A231A1FFAB13933AC5D9A82D7A1ACD672CB02653964C10193949F9EF91F93105ED0822D8CA37BF6B74A8DC63739872F3F273360AA352BEB2C931B65C6A5A45175557E72561121B4038958C4C52459D890BDC2CB4E841CCFAC20B9233730C6A5D6864683397D40333D7B537373C953F6725E2FE40B938381293B4D7ACA6982E6CF4E982958188296EC531F535433B13C19BB8AD091AF648495DE7BCEA7282112C74CD868B086C0408EF60357059232526EC4871B2070632A151424B6CE1E53BAB2A62E63018915B1A40FA57E6A1A623B963FF3DA42FCE6B682FA0EE243165E62907067CE3FA331AA796034C5532866C70581A1012446DD1A1BDDEC4A37F24365E33A7EFA23333B869461C697111BEDA6CC24571E7C18BD99D95933348B4D4098E7C592A91A1A7193AD893C0A91790F04588315D98D6169685B63A845C148EFC318D1D9315366A0FB93B5EED25839A25FC05AB985E8B2CA46CB238B57C77217389667F80A209C8B7D7DAA1B63A4A549A57A05294A9443688A755F66D9A45F225B1475C4F8CA7DAF5A63EA55A49E2342B1DB9191B1CBF7F6536BF40A15710E9ED00CF2891378F5850024B8B3284D726C8F95989A253BB0C49AC7EBEAC6B8ACB966F74FCDD6078F1BBEB02651B019C68B1C27622564B833A3046768EAC5C37E17C51286890D52A7A6763444BC7BFF24322498896765202E71CF10416170E483DD6189AAD3A83CB3B66FC7489EF00D38C3C741492D2730760706B3BE8C5ACE9372C1AA78127B309AF753DCB3A565C78045019B4F809F6326C3A3166228C57F87E3B423536C650355B9828FBB712E0CC9C163810A4E4A5B32F310F5A2510E41A6AFBB8C2B6C11F8E91DC0D38A831868ED12A3CFC25B38F88B17708C99F86B31CC6CCEAB8FCB265E239BA0B1165A4D9B1D0F858E756A2388C3C2F9CC4D8B37C0A913B8C961CC6848B371F3A86A9191B9548C30E502FAE3051523B3867219AE91C8F5F4C8A75B8115F5BC8CA69EAC900C260A214DB6C657564DFFC968F335886B89224E3A45A78C047BC17749305230C9B7E864304D328E13B26300042851840AF3098C51A911EEA6377A5C70853800835A0FA72B4137BA885CB905966660D8987646E7BCBB68A2A1C85EE07C103F702D5FF87025E70AF4DA765458328C415A2FEA9164A087C9904AA132BC96D08C38D5AB17947822BA6894FB772B05898A734CF6DC173BD03D35DAA51067C76E200420AC87E672AFD98053BC943493F35068926D01F9A8112756773090547807A2EC8C573799D00817121B29D01626B5937B2180B0A63AC7645436E5291ABD3083E582AB13E3882293C3D47B8B89C8007134736E703276F3BD95D899C04B0F0046AB6FDC88B098C3114438A412C91991727E8A223DDB9C23D54046F92B1E642BAA497C2031434E5999B9B51E18549E86F08AF368CA818A1A295498BDA5AA90A3B38CCC8B7E051C1FC207A9A619D8305EFF1824F59283B0634460B67A4B2B30C80636745A5A4CE7321700403833B6A85978469B04FF819B19A87A205C0C70B263D8FC514958968CE8603A9643627847A1131646C6307BFA3B55D7A9DC23495537BD32070D17E39327C12C399B7E047762320C4CD74C98B041019EC5CC1A451E7C0120532394C3349057F508F3129367EC75B4574E18457B9D245C5D5B480F514D01D6895E81B82A37A9AD83C4A120B9F86CA0810BBC30D29EF6E13E9C1A6EA7D833B2E7B5AB855C6B1B33202510EC0A2B50D8CDB1B583514B990EE8062E18CA261A6DDE7342DBFA637F645B27C7645A497A43D10CBFE5C60D2A4B2135BC6710C7B6F07443E369A6F14858BB24EB4A7FA741920B8B6E88000FD4F1383A5385BCAB90FD6719C9DBCBFE9B271F5B37B94054D706ACCFA164A4C8656A33B6BAE9697159398729C7B817A0EA14CD41931D02E0669FC680B2802CED844436E1C1E6F2A3E0C2C53619944A8B1B81E942989125FECC1ACA166B5E5777D4C543A8B5448B2428475A969D7C6F7297C19E307887FB4FE470213B102351447FCE5728DD8A30883ACD62E903DD9C68179C0B1EB99D0BF5B200323730EA0408C463BB7AA009CA0474E26A9B354CC29C1CEC6147D68CA29EA130CA29C8AE74C5AA850325494B52437960489896872C38D3841EC9A00B25CD5C906BB55214C0E1302C7B1C3F416BAC2861A40BAF3D97B570982CAFB39D05561783298758D434288014025572CD932DDCAA25E3227E1382CCB337A9EE6CCC573595E1BC5FBCE6C1EEE71D68E1BA39C337D6F92A966C76433158EF06BF7FD959F22969DDD418BA432B06D01E5B0886F7D7BFB462728E0BBEE5152264965DA944C4D87097A1CB3D4539990A75666FE32457927711A22999145502080DE5D13E25D1907009058CF6C73906CE81F7C9CF08449BD78BB6ECAE19F8AE511B70B12268251499D1244BA253026501A2C3BA24C71A4101ECBD497BC3E9A57C8E1657A0499F1F6B0DB25455B23C88D9FB3F2316472E696EA30A4EA2A008A5DABEB3EA5A81A384795008DFDBB21A6274931B5A86A5962ED44DEA007C9F62AFD891AD39CC66B5CB2C2998297E349DEC6A4140F922F3851649377A6E685EEB5B423D52C15586C10288AF9A4935D27C1F92AB8ADF38080656BB9EAB9D57C3414A1A130F6C90773375757C433479AA41D64F65F31D23774AA4289924099713C76AADACBB93F441FF24AD09B99940E49942E1B1A088293C58815CA72832B007D0708E103BAD61899DCF203E419A72BBEC614AE098171B89BCD2231725A2C5C38AA2E4C2F0C1C7F164579F5C75ABC3B0ACD60F5F38A720420934B5CDF02C7B77678681203F3386AD4AC72354F4A37DC6B8BF1ACFD0305B30560E04E531705876052A14B5E298A5D34226A0ACCEF92287B2B567055527D906BAA388A4262F447C78338B92CF5957266228AA747F9ABA9F0D57A32AB6C37BC464BC8922E7781FD6607CAC5134F9C5CC1784BFC68B7B93596D20958B1F977DC9D580CC461841F029F1C4C45D0554D3747796399464F337A9218293ABA0D3677F44B608B8C9A66EEAB7311C8BE8B45C8E6AC064096AF6D15573D4B0FEA5B1412B6E40F7B950A62C864B1FDC774498F1AD2D868077F0C8FC920517E77D844639A5B515CF602135889C20084EDEABC1ACCAAE5DDC724F875F64474CC2873635B48D7F1BBA69E64683A4A739AACEA8EBBE919683CF7B7B581996555A9721D14A1F542D08641D8F74C333E38AE3B30DDBD9974592923715772F4B0760C3B0FF8CC977E433779620D9CB21AE036443C4AAFE1C1B15209866B20F4E2407575A8F94C344AF1794861C6F89B52B9B1A5B14D39127C74F66AA185B482D14C933DC95C235FB026E617250187C606A1A81280079062C72AB9326D371526427EA7061F8C4AF8E4346B80112AD5BBE307290FD2A40E29CB5EC6A4582396AA3B68E03144EBB5497EB04B286B68C9D35B1FF2B6C86B1105CF3C6FEE47D4A07CB897990C5685E4A08828A9ACF05991C37A685480ACB7A4A7403CC56BC332291C81BD9F344BD13824A9111BA72CDC0228677B8B433E693C1C87E4E2505776AC38699A5C8D80CFD4A11BE047519978E35E34D4D52A7B3593FEA2756E95155C1D794EA00783594633F135B8B28648E3616597CC194C7A522F11E34842F7EA868CDB7340BF2B2CA45BCE0F83D17FCA935399B2DB2C4D686460AE6A4297092FF4633C5D181FC86AABDEB5ED07B634E11A22DD080F5A851CB7AAF4A23327F55BCB908A516981641B85E0550A29413CBB60024066A26BA0B53E949142CB89070D4A1F47A7BE9A1CDD640982E344C998B79B69867F6A3FC6DA567B342E2EFAD0434B5202A86047159F0C1011B08FB226C23F73AC5E8BD666F91A9ACADCDB47D9CBE921EAEDF0E8B4AAC3318C0D8407ADFAAD51CCACC4ABD50AFEB9B6C216EDC3098DBDC65CD0792669DB8E48544638E097D >"$TMP/dk" + unhex B5A1AC0A3BBAE8F621A52CA15EE6C0B21FECB8E7D3C788CDAB60FF155D2C2FB58AC61BED65BE35800A9E82BFFC89BCC11398DFAAED1074B3DC59DC87AC13A16B3C4883ED188F181DD041EC6F9BF678F5E27FF4323B4A6D57CAC1C5C70679E295D4F3DFEE539BDDC2DFAD183F0A3F5A3B12A13B39410CA529095CE7183AEC29ABBE6EC303A100AC821C02291A39B4728C9F38FC3E0AE80B718246158C31A0E35FA5F6398946A79682AAFE0E3E7663668A56B7FCAD85A7A74BECAEF6CB70B2E8E645B9807061CDEAA78D3A9A818E7DAA4B2E4FE735165FF7F0097A506AE603E9B630FF2F0701662B8CB6F4D5374C3E017BB2678C9CF4C0564B96D5FE23C5051D93F420A404277B0181AD4171E0C888DCCE329E700461A6250DD7E335B8FB6E27793789775F54F31104919E0C73C3802966C0A1D867CFB1E2D02B9A6D0E919D509F1622E3DF865A5E5F907A702918949094A785F7778E6EEA66D2E9239F42BC8F83ED58B7480B0B60951181C550F6FFDD10491483C2D342C848E5216F7465FD58657646263A9159970D2019BB603BAB375A18F82C763D640262C0B7DEB01ADA00D03CA6AD9A1B65FA3491A43A6E5D4FA366259766E46A5C0C44C8CCE94D787A7511EE5E7E303AE2C386E6D7E0A5DA21504A90D68B5EDDEDE1788ABD426DC43D071CE84DE34BF6354355E4223F6EDEED6DCE2C8851412133FD498DC5BCEE5C9B59AC3588EE96D8362D7835DAA90132EDCB1F6AAF99551F1C8271BF23D773CF336F334081EE1CC8E37E4EACE29E756902A9BBA77E6E32208B1791C41124B8F4DC7096B5084DADC55ABD9AE533FDFFD75AE2EE0A43CD99D4950C548FFBB46ED3135E70C0DFC0E9EE3EA5BF71E2C3B37675E83F4954A78F7F79899812764DB8560EFD303FF3928894436CF0D2AFC0F50F718C65645DC6BADD9C948F3B1C84E58B85BFA7F902FA45CA800A86D544FBABA14B0A809985CEA155C93FA3E9823BC7A418D70EB0AC9A259DC673E4287EAEA83DC85480EC79CEEBC4A5D00925B1C444059203F940647CFA9C26A6762BC9DFE45C0A33D505F712ADEBC20DA9C03970750966C078465EA2A76ED8158158EB198E9377FE4E3D15F0FFD3F0A92115028666C073C13DE3AD8679E8CF8FA5921FCE9A11660942E6379807FEDB3EC07F1658150160DC4217FCCFEE39F1B1FF91A4A5C081E10C009CBF166E47E0CAD337EA7FC0571B9A44DBE10A8B4A8BCDB7025370D865AB551FCB1AC4B7DDE85D751F37B91DA3CB75FC8ED7BF0BF557DB563CE4DE93159051E132AD44F68B18A29DD76EDB8BB03B0318C79A72EE67501867AE577BE1E36D76534E1A52ED1B1FD8CEEFA84F5781D8AB555DD41A31665B48F7699A04DCD1AB2ED59726116F37808486061DA4A9C99455CF1D1B773FD8802C5CC7189C14806119595E6990862F2B0D576031D5D76FF4E7DB0AF6977638DCD4C777BB281645666B59CDA5C10BAED0900C684DDD38D6BDB20425B2D31851F71048DA3F26CB890C9FB8940ADD4B2DCA8C0925A6FCA11CEE9A813A43BCC416B34D12770D9B64A01C917BDE03AD0C7075A148F3A61C40B468B26304F3EEEC1B96CC7B13A271D4DF33471F834CAD503FDA8EECEF125054B81ED1FE1F02B4D70CFAEBD7FD5402EEBA35E321B602C9919F9ED26548E1A144A5A0136752060B72E93A75E8BC6FB8FABCE5B9D84CB0E167C5A448BD16AD19AB52AD529EBB2EFAB32BEAAA5167CE1411C7ABC31195299F7F895D4FE301D4B1568DDA730CF8EAB35A973E64F6B96E8E411A892D547B466496273261FFA11EF56122D43B41E0540CE278C19E8312C8DCBB2DC9704F2C2D1AF7ED04676F61D31B51185DE079ED085A259E4C74974E6FD539416A7FE45C6B835769C7D8153841D8A906C45FF8EB151D1FE9C88C21B89D2C2B1938D41BCF468D5C935701F99E5930419FFF3337A2684FBE508EAC73471A1F87059DE5E49E9FE5E65DE1D10615F3127DC633360BF99AD6111FE21F15B2D6A258E63BB4138B97D85731FC0D3D5F291117A5F7E0D5F56CA325183A63EE489FA4C81811905741B35E702D741F7B8FA7D719D3727C097D889430DCEAF4097055ED2005A2BED2912C4DCABF98EFF609362925CDD8CED954131B54D3D041D7A3B06EBAF21FBB85A30A380CD1A112A6FA4890543B3273FAD3AED569BB4CA37799904DCEC77765AF6EADB6DF2A2AD8A4424A79D678172354 >"$TMP/c" + got=$(hex_out "$BC_RUST" mlkem1024 decaps --skfile "$TMP/dk" -x <"$TMP/c") + assert_eq "$got" "09f3db05409636d3d9e3ebc09790fc6feb7b37b23a321a7f3fb9e10ebb1c5c0a" "shared secret (ct on stdin)" + got=$(hex_out "$BC_RUST" mlkem1024 decaps --skfile "$TMP/dk" --ctfile "$TMP/c" -x "$TMP/dk.hex" + got=$(hex "$TMP/c" | hex_out "$BC_RUST" --hex-input mlkem1024 decaps --skfile "$TMP/dk.hex" -x) + assert_eq "$got" "09f3db05409636d3d9e3ebc09790fc6feb7b37b23a321a7f3fb9e10ebb1c5c0a" "shared secret (hex dk file and hex ct, with --hex-input)" +} + +# `keygen` draws from the OS; its output must be a ML-KEM-1024 key (FIPS 203 Table 3 dk length), and +# encaps/decaps with it must agree on the shared secret through --ctfile and through stdout. +test_mlkem1024_keygen_encaps_decaps_round_trip() { + local ss_enc ss_dec lines + "$BC_RUST" mlkem1024 keygen >"$TMP/dk" + assert_size "$TMP/dk" 3168 "dk length of ML-KEM-1024" + "$BC_RUST" mlkem1024 pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + assert_size "$TMP/ek" 1568 "ek length of ML-KEM-1024" + expect_ok "the pair must be consistent" "$BC_RUST" mlkem1024 check-consistency --skfile "$TMP/dk" --pkfile "$TMP/ek" /dev/null + # `-x` would also render the --ctfile as hex ("the ciphertext to file also in binary or hex"), so the + # secret is hex-encoded here and the ciphertext file stays binary. + ss_enc=$("$BC_RUST" mlkem1024 encaps --pkfile "$TMP/ek" --ctfile "$TMP/ct" "$TMP/dk" + rng 700 >"$TMP/short-ct" + expect_fail "short ct" "$BC_RUST" mlkem512 decaps --skfile "$TMP/dk" <"$TMP/short-ct" + assert_stderr_has "Error" + rng 100 >"$TMP/junk" + expect_fail "junk dk" "$BC_RUST" mlkem512 pk-from-sk --skfile "$TMP/junk" "$TMP/seed33" + expect_fail "33-byte seed" "$BC_RUST" mlkem512 keygen-from-seed <"$TMP/seed33" + grep -q panicked "$LAST_STDERR" && fail "a bad seed must not panic: $(head -1 "$LAST_STDERR")" + assert_stderr_has "Error" +} + +# A hex key file is read with --hex-input; without the flag it is raw bytes, however hex-like, +# and is refused as a key of the wrong length rather than guessed at. A trailing newline on the +# hex is fine; a comment line is not hex and is refused cleanly. +test_hex_key_files_need_the_hex_input_flag() { + local cmd + for cmd in mlkem512 mlkem768 mlkem1024; do + "$BC_RUST" $cmd keygen-from-seed < <(rng 64) >"$TMP/dk" + "$BC_RUST" $cmd pk-from-sk --skfile "$TMP/dk" >"$TMP/ek" + { hex "$TMP/ek"; printf '\n'; } >"$TMP/ek.hex" + expect_ok "$cmd encaps with a hex ek and --hex-input" "$BC_RUST" --hex-input $cmd encaps --pkfile "$TMP/ek.hex" --ctfile "$TMP/ct" /dev/null + expect_fail "$cmd encaps with a hex ek and no --hex-input" "$BC_RUST" $cmd encaps --pkfile "$TMP/ek.hex" --ctfile "$TMP/ct" "$TMP/ek.commented" + expect_fail "$cmd: a comment line is not hex" "$BC_RUST" --hex-input $cmd encaps --pkfile "$TMP/ek.commented" --ctfile "$TMP/ct" "$TMP/out" + assert_size "$TMP/out" "$len" "rng --len $len" + done +} + +test_hex_output_is_two_chars_per_byte_plus_a_newline() { + local len + for len in 0 1 1024 1025; do + "$BC_RUST" rng --len "$len" -x >"$TMP/out" + assert_size "$TMP/out" $((2 * len + 1)) "rng --len $len -x" + [ "$(tail -c 1 "$TMP/out" | od -An -c | tr -d ' ')" = '\n' ] || fail "hex output must end in a newline" + [ "$(head -c $((2 * len)) "$TMP/out" | tr -d '0-9a-f' | wc -c | tr -d ' ')" -eq 0 ] || fail "hex output must be lower-case hex" + done +} + +test_successive_draws_differ() { + "$BC_RUST" rng --len 32 >"$TMP/a" + "$BC_RUST" rng --len 32 >"$TMP/b" + assert_differs "$TMP/a" "$TMP/b" "two 32-byte draws" +} + +test_len_is_validated_by_clap() { + expect_fail "negative" "$BC_RUST" rng --len -1 + expect_fail "non-numeric" "$BC_RUST" rng --len abc + expect_fail "over u32" "$BC_RUST" rng --len 4294967296 +} + +# Without --len the command streams until the reader goes away; a closed pipe is a quiet exit 0. +test_streaming_until_the_reader_closes_exits_zero() { + local status + "$BC_RUST" rng 2>"$TMP/err" | head -c 100000 >"$TMP/out" + status=${PIPESTATUS[0]} + assert_eq "$status" 0 "exit status on a closed pipe" + assert_size "$TMP/out" 100000 "the reader got what it asked for" +} + +# --seed-file instantiates the DRBG from the file (SP 800-90A Rev. 1 Section 10.1.1.2 with the +# file as the entropy input), so the same file gives the same bytes, another file different ones, +# and a file too short for the instance's strength is refused with a message. +test_seed_file_gives_a_reproducible_stream() { + rng 32 >"$TMP/seed" + rng 32 >"$TMP/seed2" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed" >"$TMP/a" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed" >"$TMP/b" + assert_same "$TMP/a" "$TMP/b" "the same seed file must give the same stream" + "$BC_RUST" rng --len 1000 --seed-file "$TMP/seed2" >"$TMP/c" + assert_differs "$TMP/a" "$TMP/c" "another seed file must give another stream" + assert_eq "$(hex_out "$BC_RUST" --hex-input rng --len 16 --seed-file <(hex "$TMP/seed") -x)" "$(head -c 16 "$TMP/a" | hex_out "$BC_RUST" hex-encode)" "a hex seed file with --hex-input is the same seed" + rng 16 >"$TMP/short" + expect_fail "16 bytes is below the 256-bit instance" "$BC_RUST" rng --len 16 --seed-file "$TMP/short" + assert_stderr_has "Error" + "$BC_RUST" rng --len 16 --hash sha256 --seed-file "$TMP/short" >"$TMP/d" + assert_size "$TMP/d" 16 "16 bytes seeds the 128-bit SHA-256 instance" +} + +test_hash_selects_the_drbg() { + "$BC_RUST" rng --len 100 --hash sha256 >"$TMP/a" + assert_size "$TMP/a" 100 "rng --hash sha256" + "$BC_RUST" rng --len 100 --hash sha512 >"$TMP/b" + assert_size "$TMP/b" 100 "rng --hash sha512" + expect_fail "an unknown hash" "$BC_RUST" rng --len 1 --hash md5 +} + +run_all diff --git a/cli/tests/test_sha2.sh b/cli/tests/test_sha2.sh new file mode 100755 index 000000000..065dc6d5c --- /dev/null +++ b/cli/tests/test_sha2.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# The sha224 / sha256 / sha384 / sha512 / sha512-224 / sha512-256 subcommands against the CAVP SHS known answers. +# +# Generated for the 2026-11-03 CLI review (plan 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# CAVP SHS byte-oriented SHA224ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha224 -x <"$TMP/msg") + assert_eq "$got" "bdf21ff325f754157ccf417f4855360a72e8fd117d28c8fe7da3ea38" "sha224 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA256ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg") + assert_eq "$got" "963bb88f27f512777aab6c8b1a02c70ec0ad651d428f870036e1917120fb48bf" "sha256 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA384ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha384_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha384 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha384 -x <"$TMP/msg") + assert_eq "$got" "a3d861d866c1362423eb21c6bec8e44b74ce993c55baa2b6640567560ebecdaeda07183dbbbd95e0f522caee5ddbdaf0" "sha384 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512 -x <"$TMP/msg") + assert_eq "$got" "a3941def2803c8dfc08f20c06ba7e9a332ae0c67e47ae57365c243ef40059b11be22c91da6a80c2cff0742a8f4bcd941bdee0b861ec872b215433ce8dcf3c031" "sha512 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512_224ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512-224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512-224 -x <"$TMP/msg") + assert_eq "$got" "21f6c373637e6a5e89d6e88811110c5c3fa12e497144912914c546e1" "sha512-224 of an 8-byte message" +} + +# CAVP SHS byte-oriented SHA512_256ShortMsg.rsp (bc-test-data/crypto/sha2/byte-oriented), Len = 0 and Len = 64. +test_sha512_256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha512-256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha512-256 -x <"$TMP/msg") + assert_eq "$got" "d25265bf9cbc0dd2f108a2f5e8f69db7d15e5b8fe9100fe887dae20b6e054fe8" "sha512-256 of an 8-byte message" +} + +# CAVP SHA256LongMsg.rsp, the first message of at least 3 KiB (Len = 25064 bits = 3133 bytes), so the +# 1 KiB stdin chunking (`cli/src/sha2_cmd.rs:29`) is crossed several times; fed both from a file and +# through a pipe. +test_sha256_matches_cavp_long_msg_across_chunk_boundaries() { + local got + unhex df753c3c6eb6f4e34dde6ee5388b5b818196c4e7a951439e2d0d7223a2e0a4d304a5d30a4c43da8d4033e4258f3c258d1d81c88e9cbe28695ccf427564726b09c14d3e9c1e33d92b95a349dcc59142e02569139d9752da0d41b0f46198ecb8ebbd77653a0d0c5887f436835001131a4240578f5cc493c15283b8712f67d7f53fa12a302c4158c16703f57bc96c48157b16522313490b45ffcaef9f27f8b989ce146f87b9e19e7b946de719912eba9c525062b7094e42cfdec05e6791741ddc60bf4d522b1567eed6d81446ef6ef11590968f3487fcbfc5b50b0cb1b4028856bd4acc79c8073a82f4ed85e82a5474eafd8fe280d57a0a914d530166d9cb845fac5cf4e4d9dc9f9fb85341e88b0acb8161e462843bf1ab996e41466c48f6985ff69dce7ca5bf1eade8b2502f5ab57e44843d182801892a47df7ce2ad49ac4dbe63b7612776d640144db4bba4df8015992e400a556e6910026aaddd10241529e4bb4fb5ce92498c3cafff892e619119ea269842f1cbbae531d57c407c2058b639c610d35a42418b8ed63bc2b72a10129e35ebc8e560a32f3ee99012db869e7c264c9fc2b1cee6c37089116f268026861d23e2a21a754d78906e925b149e0e021114a78abd7e5fbe4b92289763acfbffcfb824da7752e0478f2c29230fb0d1d063e7ce34649ad50488b72a4255f5b6041cbb5a78e33f8325a3ebfe73b4040e6e858dbcc790696f1b8dc1bc78e6a57f1e6cf62b8248835e993472b952ebe2ee65ddce1a4b2834568dc4778842fd0380d3272f40028d679a2baa8a01c99c91993bd57290c91443b1e293a336a78256deaeb52f40ff9a335f636dfb7edfaf5a367bd440c5473cdea9a0640b6eb05ee7c4347ebed482572fd4cae58dd99c0c85edaa410162d0884d66519e78d76477fe58efeba3a492aca222e77b07ba089b5ae852867d0e69e0d70f7ca9b319cbb6e2c26662a8573781d670429533c82c45204cfa0a7c721c2cc6ccaaed81dded03dd2b214c939579be11e7649dd7ac0ff021442b35c636f05460c762d2adcbd34735bba9122d08614b9cafd4f9bc0eb985a3f56c6f2201ee40ee252cc0fd8a84683399851a3e56f610bfef1a13aa9433edef8a45205a7b1cd8b711901f3bc308501fee3dce8e2741f5c1d0bf5169e9840e4d293eb0344d840505b117572d1d68e541e15a95e42448e3ec794a1dc0b3b4d204fbe088791f2ead1e2d48d09c41ab11e09b63d60af415c0861243de789bcc205e27b20ef19820b94baeacf24fd6d9bb4643e0fbc640b076d8c533fb5948d3dfa1d53dab63616412d45cc9261085fbcbb902596be1b01feb2ab8a1de2b63d1f9162f07c79822029e015e2ad2a5f617288200578e23eb6c09c41315eeb693cf55055a9acb598200471add5460d7d038bce96461cc539a9fb6edff1793c281d3dbb6817e1d6c2968f46600d1366ebb636e3557008d3b64bedceea4303516d4e87370ca392799c0428d2ef027be3261a226b000bb39a1f2d3620f29e73c7b3513057d5d5550aaef94f9bad4e15eeb67fccca0881a384eebe53098b6c511b94c5e7923635aff655d682252d5848060787a494e16a5f20af8fd2ed175511a98c0d0b7ab04ce9e94b3c5ef3e1b9a8b5a3a228302d3e5d09cc12244028c13c0e03a71a85d673b94fd98b448c5cdabe7a155fe6304e66138ceed5c5a7223936b58614a3280b5284969b53b1531133dfbfd7216d19b78a1936625b3586a635a84c9de623e5e151e259fb2653ec816da31363970b610b12e4a2d1030263273ef71fb58c2b53db0a490693637acd3fe09f0135116f8abb88fc3eb57daac127cda9cb678707d66ae42f28de9bb4599f353a15d631081b4b64c6868a1bad352a6a46bc2e67680213680b3d474f8f2641c3eaadc16643773d2eea06b97d6813918d448e649dca4b570e73b067ff69c7aa1547458131c043035fcd2bee1389f10fbf29aeb49a8d36c85fef2ebb1be2924ab1d67990140cf3206fe682726e8161f7686c99a8fe4f48c9d4d414ab35783aa6220eb6689d680c26e7dc96f0eecccaeb2193db9c397ef5edf5d8eed2cc99054ede5698953e8fe2895005d5e434eab05d7325d108dcaac2a74b5429ac51d3a6a74f447c8067d33aca20f8cbb6a169a6c0c5a93699384123884dec61a265a673321d6efec4a9e0696be32be502e4a802a7918478a80cd0bcf365e8135f68801f81a12c7bdc9525f2d10f25be334f4478847f4df3cfaad7f38b2bfdab2deb0ef018730390228a126bac1d6cb3d271bd6ce9e76c447a92d54bc21883b5b85d8199691b8ac8512934385f8677626adc81544a5275e73375473a1ab420bf6940b67ff6033880c2d319fd6de2bc5656e02ec31858e1588492bc42f7774b14cb24ec1831b293460218220f59bac1ec414d143df6534524d8ab82fb0aea6972588ab0f6cea201a49978eaf295672ea09443d02946e9f7b8b9f059d6a77612449bd293900f6a2a18e375e35bc370392fda84f1199c859fa0a331af4a6fb2bc07ffad02788673631f9a8f998f467e97c68e80e58369aea3592ecfd1eac618fd390e7a9c24b656532509367c21a0eac1212ac83c0b20cd896eb72b801c4d212c5452bbbf09317b50c5c9fb1997553d2bbc29bb42f5748ad83dc6e538736763eff646f0f9aa6a5b028e575de074f5fe5de425dab8e6b73f0662f88d49749749ae7b55aaba9cd38deecb3bcf6f3ee5a6de5589b70c637b82aa6aa67451847f5aee60295990350f8c6c18d1d02449b6bf037cdfd09bd033de2ab16b4adf47620036511c7e192770cf0103c4009e49ebaf83c682a805d172aa0dd3a1615121f1e20caab99893c8a9ce43f89cc13ba3f700c5cf3cbdd8dbaa8eda4e036845a89168d7e98b39bebd0d22bb8396a29aec6b5b5ff3487263435df4f68cebb02b4fe31d3eaa0bc1e8692d44ce117c151a87ab0798df507653cd0f65091cd4e9808c49779758b5d1bf518cacf24ea7a9590a58ae36efcf2a18cd3157887a5974c3e246f0786203d9920aadd3de8793cfd4e8d6a780a11f1e30c86664eb21e3d283e66e106872805a0ef90341c948db090cb7a79c2cafeac32a4310e5a7b00e934f0d217fddf6c5c394a11f9f792ee7d56edb6df48148aca963c1b3824481242773db03674f6b1eb6a1977515349644c579d88c392517cf417bc8d0a35821df1f90b4769f334c2593d440866ba21b7c59cc4374d29ea9dd1be0ccfac1492e99dc244e0d60301664fbab30d60b382ae8f2c480ca79445b50a9215294749de3eefdbcec3a52bc56475e84ecd67c7d413530f134ff65bcc8c469bbde3035dd0e48f4b9338fff4e88572cffe92a17c7facb84e0b486507e6e92ee2ed4407d7cfa2241a79870ce5d291ef77a9a2395deb4267f01a3e191ffa95696e8e957c3f424256506af7f386eb5093e9384655ef819c07fd277b3d2b7259a048861f891e938acff64dd1dd5f9c89072c94c61ebca784417e1945f7b3b5fb7b76bf2757fa29433414095ef1f94b06f248a36abb4bc6c0bd7adae56f402b43a1021ff80e5e6c3530b088e358db628dfd464f7a5424471922f951a3cf593cfbbbf390d9d5cfc42769d4b1ab398d47e7d02d7f3d13d09057190366c63c8750e97052f911d4d799e287876cfdbd9864dc12051bfc1ed60b4249a10198e051cfd7692641e934fd532f33d2a1200b831f336bd60aff9dbe1fa15bda0a5fe33f218b4e7752a91dc95ad415bb385d4dd6e2b685a9368eb911333ef6b769646dd9aecdc64e1399b6c41799d44d1afca81a142b058586f19955b1dfc33e07efa4a8df6779b26c002875f048188d2d4546d61c5b9673e26f6715004d6979ca47b3331a1f10ab45289a654ea78b855a7f4f92640ede7a325248d6885091709bb6c002c8bf33418657351a9a80d33c8af4ebd6530b85a25ee06622b1afe32a6382f49e520d897d18d6211e3a9998baa3ea402b511c34f34f62e980e33406af08f4768e1ed2bb66e1ed85f998ba83088eaa7918c6079376eb1ff97986a5a30774546f5a96d57004cae389311aad3b2d347cbc261a9549321b61bc9402cb613b8dea068b21e05160bb0257502a3969317ca731dbb68eff2fc86e5d59bf6ee9513dfd64a826b906819d9014de2f25b51d4d7502c200ba5c76d89002502656e2546ad7b0390f29367056d6eb61913ac6f8912c546061e1090e350cd4029d4af549febe13c743f88933d01c0263b74045ca2f15523f42caadffc52dfaf68d14ca3ae0fbb5d92aeea9d4f1aa816b0bffd99b0f7821e6093ef152723a9cb45f7a082ef8d6bdf72cd33b5aa3c79102f43e2b74199decdd20057d0e227ae4c57945582e2e9653a9b16eeacecdbc5aaedac7e35c35cbd9adede7f83bbf36f8b0453d61416a85a17821885b3757d203fa2560a85c4b4c10 >"$TMP/msg" + assert_size "$TMP/msg" 3133 "message length" + got=$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg") + assert_eq "$got" "176b0c71e213031a29f56009aac7ebec591ba24a8b162d80506b2df8f59e11a2" "from a file" + got=$(cat "$TMP/msg" | hex_out "$BC_RUST" sha256 -x) + assert_eq "$got" "176b0c71e213031a29f56009aac7ebec591ba24a8b162d80506b2df8f59e11a2" "through a pipe" +} + +# Binary output is the same digest without the hex rendering; both end in one newline. +test_sha256_binary_output_matches_hex_output() { + rng 100 >"$TMP/msg" + "$BC_RUST" sha256 <"$TMP/msg" >"$TMP/bin" + assert_size "$TMP/bin" 33 "32 digest bytes plus a newline" + assert_eq "$(head -c 32 "$TMP/bin" | "$BC_RUST" hex-encode)" "$(hex_out "$BC_RUST" sha256 -x <"$TMP/msg")" "binary and hex agree" +} + +test_the_subcommands_are_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + local cmd + for cmd in sha224 sha256 sha384 sha512 sha512-224 sha512-256; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +run_all diff --git a/cli/tests/test_sha3.sh b/cli/tests/test_sha3.sh new file mode 100755 index 000000000..eb41a306c --- /dev/null +++ b/cli/tests/test_sha3.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# The sha3-*, shake*, cshake*, kmac*, kmacxof*, tuplehash*, tuplehashxof*, parallelhash* and +# parallelhashxof* subcommands against the CAVP and SP 800-185 known answers. +# +# Written for the 2026-11-03 CLI review (report 15) from the vector files named in each test; +# every expected value is copied from that file, none is typed by hand. Follows lib.sh's +# conventions: each test_* runs in its own subshell under `set -e`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# CAVP SHA3_224ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_224_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-224 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-224 -x <"$TMP/msg") + assert_eq "$got" "7dd1a8e3ffe8c99cc547a69af14bd63b15ac26bd3d36b8a99513e89e" "sha3-224 of an 8-byte message" +} + +# CAVP SHA3_256ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_256_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-256 -x <"$TMP/msg") + assert_eq "$got" "dbb8be5dec1d715bd117b24566dc3f24f2cc0c799795d0638d9537481ef1e03e" "sha3-256 of an 8-byte message" +} + +# CAVP SHA3_384ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_384_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-384 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-384 -x <"$TMP/msg") + assert_eq "$got" "60ad40f964d0edcf19281e415f7389968275ff613199a069c916a0ff7ef65503b740683162a622b913d43a46559e913c" "sha3-384 of an 8-byte message" +} + +# CAVP SHA3_512ShortMsg.rsp (bc-test-data/crypto/sha3/byte-oriented), Len = 0 and Len = 64. +test_sha3_512_matches_cavp_short_msg() { + local got + got=$(hex_out "$BC_RUST" sha3-512 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-512 -x <"$TMP/msg") + assert_eq "$got" "03c2ac02de1765497a0a6af466fb64758e3283ed83d02c0edb3904fd3cf296442e790018d4bf4ce55bc869cebb4aa1a799afc9d987e776fef5dfe6628e24de97" "sha3-512 of an 8-byte message" +} + +# CAVP SHA3_256LongMsg.rsp, the first message of at least 3 KiB (Len = 25200 bits), crossing the 1 KiB +# chunking of `helpers::stream_hash` and the 136-byte rate many times. +test_sha3_256_matches_cavp_long_msg() { + local got + unhex d9c7762f278a6ecb070bca56c4f7d7fea31590df217906d47dfb058c76e7f4e056f6fd632f7d6e3b65e55f306c5b9603d3c8a70182045fd7404763a878e0155d3c29b73d8abad3bdceddda99a9420b23f1f496dbf98c024112a5cce7518f51ca9348ede2bfa765f84bdb82b80214ff070480a6970e79b5b8fbfd86718b5e6fcf643ae87d56aeeb95e3c7a1b6ff393a5714541c5a493341e40437da6dadb43913b6e9ed34d8362f3b9f897dba281a84ba2a58434f33226e6f343b100340f8753f913c472fcca6f79385095eed061da5d84c74629b53af03fe94f1705dcb94ecfafd1b3c97ba680c45a0308e7720ab645a8590c0693140ca3c2a4142a0d6ef66ed036e16942ae336f8f5e4547ffe2d8ae8da94a6df563f89ce0014cdf7ea71abc0aa1d1b4da57f3c548e0ef72d2909df2955685c254912095f1e505a888e82821afb1194ebb2a4e8037297c0aa28a92bc6fdf42a64922312958adf317b4a8ab4a3fc30c895daba00aaa965f71e83733666da2158c4bad86c184ea79af9a6f10a04b7630174a4294df43c62e4b1c3d1c8b2f5d52d6c489bde917292dd2a2b1f49e5349385b0985a97863274ce896f2aa85255f9f285c4d331a8fc874135607d3ced7aa69e703eb3a60b9385ffd10fe59fed0276f036b7e72d04f66d0f42cd71aac5918691dc1f9d4129677cbdaf2c6c752b05326ca8a8419a4e672e907bfb645a158119a91ec2813288b741514b4d26f2b66517b1021f48402d58b1090671bf158452492d5bafc53fd18abc03cefa7bdd332a0c066da464e74ad0dec50bb7e8a3ba0dfc64be6fd331ace9d51a60bbd3004d5df8b211c0fd564cd79d0bb35649cc60ba1c976c8911cfc0db74e028199621aa05c5fe15fa7b56dc75d62225d548581e5f900f9085e9e3b668819b4f9b2c09f22a5a32a2db47afa2b371538abc4f0e9b06401150ecc2333598e494fccafe80ced49f96dfeac729459856e60a94c5b780b614e8d4450389e6748513582c724ee60c7c71f5af648b6e2d6e23cce4121b7478f4db451816ab71034c5f8b4bf13ae1d9d90d0bb2869fc4799f51f9349d022053c831cbee62617d4e22c2bcafe40d67449eb04a7c962bf084d2bab80dd0342b4f78338d4d4f75b25bed8214deb18f2254b3a3da94faf89956f0a432f512783e74ec29b4c045adba3497e8ba62c288b711002ee2821cce68f8df588f76cc9801cb0d5b67ccacd33ae1063cd6c37dc0d1836e988acf63750571891ef618645a1b5bc110cffbecaddd6824c692874cff16b3e32bfc0236b417c9d43d8f624387352cf19114d46d0448d3d7cd1438960c2ea8482d5da3ff544608aaff83dcd1e7f64786275ddf989f262a099b845dc2b0c26a86e7d83a251e3c37f2aafa0e764107b36618d2a5d3481d73a1760b7f3ab37a0283a1925010d79e5e94871b819b5e0f787bac9dad87c5d5b887a7d12565ddfd7729a3b66c274a178377de0fbca607b79fab2de37f1ddff800a376fdd7abf5f4d15f346a17d43e4db085f7fe470102a72fe0e1cfa4fb5e2b54dd2ab71e74c506190c9dd6d87f7ae8eca5190fab12178630011286a38b0a18bb1d0d29802813dc561a2724378ec79140bf8e6a6f4310fdabf606330434ab673d4b6578872fa81d90701779bc6aedf0b2bc9c381bfbb4b3a6a705fc505d08c0e24f7bcfbbf24c72cff6b800f07bb4ac4d828ca138a1ca512cfc59090e70eac9b3119754a3d7c6831c9451de5a29e46d16faa6cc4d9f1b7e42dd4983c6d4192db779012fc747e0f77e32293e83dd44dd170c31e2e25c6fe3f2321c9201f15d92f6700089afab6077feb88946f1fd9da257a1770a7f408d2743e31b5d452f7ea179dcdd8ae4690faaa30eb91e2ef3b764de9fbe6dadb33ff33ed88e0d1df6430147a7e159edebb539751d01efc2255accb5f04f32f31d4befe9f9bdc90eae755a68b7511f52971acb7ccba9e133fd4e81e37a7282dae67ac24e2fefbaa2bfdc56e5f678039cd70509c101f81b0be15a2c70276e85e976941575606d086091409bc54a9d2e5212409a4ba837c6d13b487565dd8d62a836618f647580dad08c69d1109d07c7fcaf912822a6af090e72de03c98615429f8ca9d925576e587b191fa1cfd6fff72218c1584fe5a50d49eee57fa4094f4c57e1339ac0ec7face89e29b309ddcde64db498d4d94b64f90a172490fe4e83a9579ec87b3b1efdfb9f8550aec69ab9bb04ebdd44c8c233f6da5565dfe03df98027293024a0d42e7029345c3a0ef50b9fd0255a4f2531592c3f106e1451bef8fa50ec333cf7f65c300d151500ead60ab938cad59ef77f1fc2c3b008d65ab9c93f594d986c466bcd66d38e42a86125ff32c44217d94696cde1ee9dfbcd0216b576cab6295b971dc9380409adda7cca3231fcf6e8a71a5dabb0473f0fa251bd02c82c8eacb1f1f39ace27ed1c3d42adebdf694c0fb07cbc005856ff0cfd4f96f85c053dea185dd58751c4a25879bbb6726392ad569d739b1600a343b985058ae15be02bab36e1abd7e964902937efce0ee6ff82831608e233d1b774dfe37a7f03ec42c0b7b3885dd3bd8e1f561fe7db099039315fc24279a0f319fb46bfb65ea838b564d53857d4c3f6cab7a9d8aac6a988c724b24a2949a58d513eb6bfc2101666c07fcf6d58f6efbba0c0e80b407837f731c14455c53889c7e8a7f78ea57c959fec796bf992ac9e3599a2dc5fb2d8fa040072dc8c1f2f96ca659dd0d267896f0bbfba72850ddf198c3cc54d7d07cd3cd8bb9e44973e1712be50870b341121ead1c3aaabb8d275a9e71191030c4741439aeaac421d2ad646cb32cf6e5baa2b501a80b1e03be858946ea16fc5b57a1b597316069fc6ec567a1791dcc2f03a8ee70d7d38d7b5576e224b73c55c20a943c48bc613223c8b7df6cdf816ce9aca34f8d342a559cf28a19da310fc91f4dca0cc09823b95240f350113ece55c026004ca9357622e3162d74ba1fe7d2ad9191cbb16d2e6be298165e411f7b132cf07e6a77645d51e820dfa766ac7afef796a9ed7d93f7c67f1e8f5e8f1a0e219aac4c2d5a49368f16a32f16429f2f9aa79d60f6a7b506556244ccc52d86fe0d93ba1809d44c9826e93d7a45d0408c521846555ba238e0a546bb5d46df0ed143a29a509477e6f8a5fa207fbab23907a1d40205910fa7fd1286e2cf48061a1e4041dd8431b7c0eea8a42c7de95af127f66550e03b328ad239cd4b814d25314b9febe431761632d09b719213562d0173ec46c383cedd4a0697645621cacedccfcf2b5ed6d605e554fb0cf1ce85bad261eff2da4ea0426de097f6b31f556575313a6a9df2e61920d243c0dc0dfc86fc787de3f5cf64147f16d7feb2d8518ba3168e2431f40ac2addf0227b1603f13d82e12b4b6849ec5f61fb3e705eb53a3eec61702bb78d894c4d1b2bc0253a1d40e095acf8ceca4d95b41d83a0cd6e030a0e681708aefc4021e60d1f84f66d95a068092aef9860ab12743a17238196bd471fe49e820d7418d02f2fc6718f85a284a3a66cd7cef550fe28b9c3a28216ae1ca53dce42dbdf5a81b1e7e72dfa688a1417d43c597670828059044dc5d9d71eb22ada6ff020244f5ef29e4c603f00957a4efc601d9ab88ac08e827d338dc0573035254934e52b3c8c7cbdbbb364330ddbd084a1ca41b7b0e74f9b1d95a6b8a0bf4f8523b431dc6c36d7aafdcd73eb386223d6c70874543b1d25b7f3922da94087fe62c61b0c1f268289cfda58730f6034d1589a813a777589963ec9d3297a80afcd25fb0f7edcb1981c9319f96d87cb7c6274e2da9b08a4542c9865ffa213a8604134883890fb56a1185d845a683bc1605c9166be9ff011a56f2e8fbfa05c00c1e4f8354049645dc50397e1236c138c084916efd34f1504a5e66611bb2c3dcffb3dd9ccccb2ff0e2b67a8f9fc82f7739db06d45c1b260c47c19a9b29a4d9e5f35d4401387d1b5b0fccbf8948bb0cfbd471afd3031d0b99b51906039050c67f6983293d416b06c8d5fb8ef7a7b0083b32d0e2e78cbb209cf4cd4f16fa36f80dd4cde3bd524570deb7db5f572727afb9d781468e4365f36a9976cc61c1d81d15c7860a1df57cc81137e4a724518251fc34fb4133d1bdf2a7157e045081a4742ba44412cb95719ae4d89ffcebb1709d86cdb77974b213bcb6fb7b9d8fa43cbbc41d908432dcd467ee1c65d719c142e0ce144e45dd6f17a03ebdd7ee6bb401425375f3d50016159d8165eebc3b0ba9da46bb184cba632a4b92cf1b7d714b82a67e5fd69bafeba5ca33579312dbfae5d89504add4a828dc2d57b1cec12ca53166893dc13e9ec8156c9863eb815909b76a1d839794334aadb3e2085a601a8badf128924c345935d503151d607e4ec27f1d64da3e01c8230f7de63566c0b75958d4b0efc0136baf751c3de69a7039bfad50ff5b3983f35c81e9c8bb4c798 >"$TMP/msg" + got=$(hex_out "$BC_RUST" sha3-256 -x <"$TMP/msg") + assert_eq "$got" "20e819aa8988e4a3c491673504b59ca44db6703f01094e0d87a85ce9c4d9ac4f" "long message" +} + +# CAVP SHAKE128ShortMsg.rsp ([Outputlen = 128]), Len = 0 and Len = 64, and SHAKE128VariableOut.rsp's +# longest output (COUNT = 1116, Outputlen = 1120 bits = 140 bytes): the +# positional length argument selects the squeeze length. +test_shake128_matches_cavp() { + local got + got=$(hex_out "$BC_RUST" shake128 16 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" shake128 16 -x <"$TMP/msg") + assert_eq "$got" "5ef5578b89c50532131b7843de7329a3" "shake128 of an 8-byte message" + unhex a8ded9816defca8327c194a48a88ae4e >"$TMP/msg" + got=$(hex_out "$BC_RUST" shake128 140 -x <"$TMP/msg") + assert_eq "$got" "ed7397b2215c6c412bf444b1b96fc55c531aef025c6dd13fb4ab53fcc20c91917d82c8d6710a8d7b4c24d18b54150490e98ee01b4a4b9790d1878810a8392d3fa203b066327c0c67cace3a08c57d0d30b62ba43121d8d715637884f055c55ad6689692249885dce01c96979f1a2b309943e14abb0ba8e227bceb381667579e10694bd442b99064aa88501431" "shake128 variable-length output" +} + +# CAVP SHAKE256ShortMsg.rsp ([Outputlen = 256]), Len = 0 and Len = 64, and SHAKE256VariableOut.rsp's +# longest output (COUNT = 1240, Outputlen = 2000 bits = 250 bytes): the +# positional length argument selects the squeeze length. +test_shake256_matches_cavp() { + local got + got=$(hex_out "$BC_RUST" shake256 32 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" shake256 32 -x <"$TMP/msg") + assert_eq "$got" "54f5dddb85f62dba7dc4727d502bdee959fb665bd482bd0ce31cbdd1a042e4b5" "shake256 of an 8-byte message" + unhex dc886df3f69c49513de3627e9481db5871e8ee88eb9f99611541930a8bc885e0 >"$TMP/msg" + got=$(hex_out "$BC_RUST" shake256 250 -x <"$TMP/msg") + assert_eq "$got" "00648afbc5e651649db1fd82936b00dbbc122fb4c877860d385c4950d56de7e096d613d7a3f27ed8f26334b0ccc1407b41dccb23dfaa529818d1125cd5348092524366b85fabb97c6cd1e6066f459bcc566da87ec9b7ba36792d118ac39a4ccef6192bbf3a54af18e57b0c146101f6aeaa822bc4b4c9708b09f0b3bab41bcce964d999d1107bd7c27af989ebe1e104a35478df362ec4c9628f5ea29cc1164b92960d42068a59b1ddcb8875722ed59df36ea654d97b6bf39e5075121ec93f8a8ea6ee5d923997ae8a9d4c315a3d074e3ff83dfa26490f0f5b00a3cc3f110f0f761eaac885a3d1eee302014191733bf77b4d58cca49258897d6460" "shake256 variable-length output" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 0 (kmac128, S = "", +# 32-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac128_sample_count_0() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" "KMAC128 COUNT 0" + expect_ok "the sample tag must verify" "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac128 33 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "e5780b0d3ea6f7d3a429c5706aa43a00fadbd7d49628839e3187243f456ee14e" <"$TMP/msg" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 1 (kmac128, S = 'My Tagged Application', +# 32-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac128_sample_count_1() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac128 32 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" "KMAC128 COUNT 1" + expect_ok "the sample tag must verify" "$BC_RUST" kmac128 32 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac128 33 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "3b1fba963cd8b0b59e8c1a6d71888b7143651af8ba0a7070c0979e2811324aa5" <"$TMP/msg" +} + +# SP 800-185 KMAC sample, bc-test-data/crypto/sp800-185/KMAC.rsp COUNT = 3 (kmac256, S = 'My Tagged Application', +# 64-byte tag). The key is the sample's 32-byte key, passed as hex; the customization string is +# passed as the characters themselves (`-s`), since the CLI takes it as a string. +test_kmac256_sample_count_3() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmac256 64 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" "KMAC256 COUNT 3" + expect_ok "the sample tag must verify" "$BC_RUST" kmac256 64 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" <"$TMP/msg" + expect_fail "a different length must not verify the same tag" "$BC_RUST" kmac256 65 -s 'My Tagged Application' --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f --verify "20c570c31346f703c9ac36c61c03cb64c3970d0cfc787e9b79599d273a68d2f7f69d4cc3de9d104a351689f27cf6f5951f0103f33f4f24871024d9c27773a8dd" <"$TMP/msg" +} + +# SP 800-185 TupleHash sample, TupleHash.rsp COUNT = 0 (tuplehash128, S = "", 32 bytes). +test_tuplehash128_sample_count_0() { + local got + got=$(hex_out "$BC_RUST" tuplehash128 32 -e 000102 -e 101112131415 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash128 32 -b 8 -x <"$TMP/msg") + assert_eq "$got" "ba8dc1d1d979331d3f813603c67f72609ab5e44b94a0b8f9af46514454a2b4f5" "ParallelHash128 COUNT 0" +} + +# SP 800-185 ParallelHash sample, ParallelHash.rsp COUNT = 1 (parallelhash128, B = 8, S = 'Parallel Data', 32 bytes). +test_parallelhash128_sample_count_1() { + local got + unhex 000102030405060710111213141516172021222324252627 >"$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash128 32 -b 8 -s 'Parallel Data' -x <"$TMP/msg") + assert_eq "$got" "fc484dcb3f84dceedc353438151bee58157d6efed0445a81f165e495795b7206" "ParallelHash128 COUNT 1" +} + +# SP 800-185 ParallelHash sample, ParallelHash.rsp COUNT = 3 (parallelhash256, B = 8, S = "", 64 bytes). +test_parallelhash256_sample_count_3() { + local got + unhex 000102030405060710111213141516172021222324252627 >"$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhash256 64 -b 8 -x <"$TMP/msg") + assert_eq "$got" "bc1ef124da34495e948ead207dd9842235da432d2bbc54b4c110e64c451105531b7f2a3e0ce055c02805e7c2de1fb746af97a1dd01f43b824e31b87612410429" "ParallelHash256 COUNT 3" +} + +# Shorter XOF output is a prefix of longer output (FIPS 202 Sec 6.2 defines SHAKE as a function of +# the output length d: the first d bits). +test_shake128_output_is_a_prefix_of_a_longer_squeeze() { + local full short + full=$(hex_out "$BC_RUST" shake128 200 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" cshake128 32 -s 'Email Signature' -x <"$TMP/msg") + assert_eq "$got" "c1c36925b6409a04f1b504fcbca9d82b4017277cb5ed2b2065fc1d3814d5aaf5" "cSHAKE128 COUNT 0" + got=$(hex_out "$BC_RUST" cshake256 64 -s 'Email Signature' -x <"$TMP/msg") + assert_eq "$got" "d008828e2b80ac9d2218ffee1d070c48b8e4c87bff32c9699d5b6896eee0edd164020e2be0560858d9c00c037e34a96937c561a74c412bb4c746469527281c8c" "cSHAKE256 COUNT 2" + got=$(hex_out "$BC_RUST" cshake128 32 --customization-hex 456d61696c205369676e6174757265 -x <"$TMP/msg") + assert_eq "$got" "c1c36925b6409a04f1b504fcbca9d82b4017277cb5ed2b2065fc1d3814d5aaf5" "the customization given in hex" + assert_eq "$(hex_out "$BC_RUST" cshake128 32 -x <"$TMP/msg")" "$(hex_out "$BC_RUST" shake128 32 -x <"$TMP/msg")" "cSHAKE128 with empty N and S is SHAKE128" + assert_eq "$(hex_out "$BC_RUST" cshake256 32 -x <"$TMP/msg")" "$(hex_out "$BC_RUST" shake256 32 -x <"$TMP/msg")" "cSHAKE256 with empty N and S is SHAKE256" + expect_fail "both customization forms at once" "$BC_RUST" cshake128 32 -s a --customization-hex 61 <"$TMP/msg" +} + +# SP 800-185 KMACXOF sample, KMACXOF.rsp COUNT = 0 (kmacxof128, S = "", 256 bits), and that the XOF +# form is not the fixed-length KMAC of the same length (Sec 4.3.1 encodes L = 0). +test_kmacxof128_sample_count_0() { + local got + unhex 00010203 >"$TMP/msg" + got=$(hex_out "$BC_RUST" kmacxof128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg") + assert_eq "$got" "cd83740bbd92ccc8cf032b1481a0f4460e7ca9dd12b08a0c4031178bacd6ec35" "KMACXOF128 COUNT 0" + [ "$got" != "$(hex_out "$BC_RUST" kmac128 32 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg")" ] || fail "KMACXOF128 must differ from KMAC128" + assert_eq "${got:0:32}" "$(hex_out "$BC_RUST" kmacxof128 16 --key 404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f -x <"$TMP/msg")" "a shorter KMACXOF output is a prefix" +} + +# SP 800-185 TupleHashXOF sample, TupleHashXOF.rsp COUNT = 0 (tuplehashxof128, S = "", 256 bits). +test_tuplehashxof128_sample_count_0() { + local got + got=$(hex_out "$BC_RUST" tuplehashxof128 32 -e 000102 -e 101112131415 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" parallelhashxof128 32 -b 8 -x <"$TMP/msg") + assert_eq "$got" "fe47d661e49ffe5b7d999922c062356750caf552985b8e8ce6667f2727c3c8d3" "ParallelHashXOF128 COUNT 0" +} + +# A XOF squeezes to stdout in pieces, so a long output is a long run rather than an allocation: +# 3 MB from shake128 arrives as exactly that many bytes, and the fixed-length SP 800-185 forms, +# which are computed whole, refuse a request over the command's cap with a pointer to the XOF form. +test_long_xof_output_streams_and_fixed_output_is_capped() { + "$BC_RUST" shake128 3000000 "$TMP/out" + assert_size "$TMP/out" 3000001 "3 MB and a newline" + "$BC_RUST" kmacxof256 2000000 --key 00 "$TMP/out" + assert_size "$TMP/out" 2000001 "2 MB and a newline" + expect_fail "kmac over the cap" "$BC_RUST" kmac128 268435457 --key 00 "$TMP/msg" + "$BC_RUST" sm3 <"$TMP/msg" >"$TMP/bin" + assert_size "$TMP/bin" 33 "32 digest bytes plus a newline" + assert_eq "$(head -c 32 "$TMP/bin" | "$BC_RUST" hex-encode)" "$(hex_out "$BC_RUST" sm3 -x <"$TMP/msg")" "binary and hex agree across chunk boundaries" +} + +run_all diff --git a/crypto/aes/src/bitslice.rs b/crypto/aes/src/bitslice.rs index dbf2fb6d1..258c581eb 100644 --- a/crypto/aes/src/bitslice.rs +++ b/crypto/aes/src/bitslice.rs @@ -72,8 +72,14 @@ //! byte-lane, block B in the odd), which ties every mask to one width. Keeping each block in its //! own lane instead is what lets one set of `u16` patterns serve `u16`, `u32` and `u64` planes. +use bouncycastle_utils::ct; use core::ops::{BitAnd, BitOr, BitXor, BitXorAssign, Not, Shl, Shr}; +/// Targets whose integer multiply is fixed-latency, so the round-key widening in +/// [`PlaneWord::splat`] may be left for LLVM to fold into one. A `const` rather than a `cfg` on +/// the two bodies, so both compile on every target. +const MULTIPLY_IS_FIXED_LATENCY: bool = cfg!(any(target_arch = "x86_64", target_arch = "aarch64")); + /// One 16-byte AES block, in the order of FIPS 197 Eq (3.6): `block[r + 4c] == s[r,c]`. pub type Block = [u8; crate::AES_BLOCK_LEN]; @@ -190,12 +196,24 @@ impl PlaneWord for u32 { type Blocks = [Block; 2]; /// Shift-and-or rather than the equivalent `u32::from(pattern) * 0x0001_0001`, because - /// because integer multiplication is not constant-time on all architectures. - /// A compiler may still emit a multiply where its cost model - /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + /// integer multiplication is not constant-time on every target. LLVM knows the + /// zero-extended `x` has no bits above 16, so the ORs are disjoint, so it may rewrite them + /// into that multiply anyway: it does on aarch64 and x86-64, where `mul` / `imul` are + /// fixed-latency and nothing is leaked, and (checked in a `no_std` harness, 2026-11-04) on + /// AVR, Cortex-M0 and RISC-V 32I, where the multiply is a `muls` or a `__mulsi3` call. So + /// everywhere but the two fixed-latency targets `x` first goes through + /// [`ct::value_barrier`], which hides its known-zero bits; the ORs then stay ORs (none of + /// the five harness targets emits a multiply or a branch). On x86-64 and aarch64 the + /// barrier would cost about 60 % of the 2-block and 190 % of the 4-block throughput + /// (measured), for a multiply that is already constant-time, so there the fold is allowed. + /// The gate is by architecture alone, so 32-bit Arm, x86 and wasm32 pay the barrier too, + /// whether or not their multiply is fixed-latency: conservative, and cheap off the hot + /// 64-bit targets. `dev_scripts/check_aes_ct_asm.sh` rebuilds the harness and greps its + /// assembly; CI runs it on the Cortex-M0 and RISC-V 32I targets. #[inline(always)] fn splat(pattern: u16) -> Self { let x = u32::from(pattern); + let x = if MULTIPLY_IS_FIXED_LATENCY { x } else { ct::value_barrier(x) }; x | (x << 16) } @@ -224,13 +242,13 @@ impl PlaneWord for u32 { impl PlaneWord for u64 { type Blocks = [Block; 4]; - /// Shift-and-or equivalent of `u64::from(pattern) * 0x0001_0001_0001_0001`, - /// because integer multiplication is not constant-time on all architectures. - /// A compiler may still emit a multiply where its cost model - /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + /// Shift-and-or equivalent of `u64::from(pattern) * 0x0001_0001_0001_0001`, with the same + /// barrier as the `u32` version, for the same reason (on AVR and RISC-V 32I the fold is a + /// `__muldi3` call). #[inline(always)] fn splat(pattern: u16) -> Self { let x = u64::from(pattern); + let x = if MULTIPLY_IS_FIXED_LATENCY { x } else { ct::value_barrier(x) }; x | (x << 16) | (x << 32) | (x << 48) } diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs index 2df3dfc34..e0f7a6234 100644 --- a/crypto/aes/src/gcm.rs +++ b/crypto/aes/src/gcm.rs @@ -4,9 +4,8 @@ use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::Gcm; -// Imports needed for docs -#[allow(unused_imports)] -use bouncycastle_cipher::modes::GCM_NONCE_LEN; +pub use bouncycastle_cipher::modes::GCM_NONCE_LEN; +// Imported for the docs #[allow(unused_imports)] use bouncycastle_cipher::{Decrypting, Encrypting}; diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs index 4437b4f10..561128d22 100644 --- a/crypto/aes/src/hazmat/aes_internal.rs +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -287,7 +287,12 @@ impl AES128Internal { /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. pub(crate) fn new(key: &KeyMaterial<16>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 16, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 16] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } @@ -295,7 +300,12 @@ impl AES192Internal { /// Expands a 24-byte key into an AES-192 schedule. See [`AES128Internal::new`] for the error cases. pub(crate) fn new(key: &KeyMaterial<24>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 24, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 24] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } @@ -303,7 +313,12 @@ impl AES256Internal { /// Expands a 32-byte key into an AES-256 schedule. See [`AES128Internal::new`] for the error cases. pub(crate) fn new(key: &KeyMaterial<32>) -> Result { Self::validate(key)?; - Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + // `validate` checked key_len() == 32, which is exactly what ref_to_bytes() spans. + let bytes: &[u8; 32] = key + .ref_to_bytes() + .try_into() + .map_err(|_| SymmetricCipherError::KeyMaterialError(KeyMaterialError::InvalidLength))?; + Ok(Self { schedule: expand::(bytes) }) } } diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index f203062ee..52388f51d 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -102,8 +102,9 @@ //! Per-call stack usage is set by the plane width: 16, 32 or 64 bytes of bit-sliced state for //! one, two or four blocks, the same again for the round key widened from its stored one-block //! form, plus the S-box circuit's spills. Only key expansion depends on the key length. Measured -//! as the deepest frame chain below each entry point in the release build (x86-64, return -//! addresses included): +//! as the deepest frame chain below each entry point in the release build on x86-64 (return +//! addresses included); another target lays its frames out differently, and aarch64's are +//! smaller: //! //! | Entry point | AES-128 | AES-192 | AES-256 | //! |---|---|---|---| @@ -143,7 +144,14 @@ //! //! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the //! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual -//! ways of forcing the issue, but the property is not contractual. +//! ways of forcing the issue, but the property is not contractual. The one place LLVM is known +//! to rewrite the arithmetic, the widening of a round-key word into the 2- and 4-block lanes, +//! folds into an integer multiply of a secret; on x86-64 and aarch64 that multiply is +//! fixed-latency and is allowed, everywhere else the word passes through +//! `bouncycastle_utils::ct::value_barrier` first so the widening stays shifts and ORs (see +//! `bitslice.rs`). Debug builds are not covered by any of this. +//! * The "no secret-dependent branch" claim is about release builds: a `debug_assert` can +//! short-circuit on a secret, so the crate keeps none over key-derived values. //! * The working state (16, 32 or 64 bytes, by the entry point) is not scrubbed after a call. //! Only the key schedule is wrapped in `Secret`, and so only it is guaranteed to be zeroized on //! drop. @@ -189,7 +197,7 @@ mod round; mod sbox; mod schedule; -use bouncycastle_utils::secret::ZeroizablePrimitive; +use bouncycastle_utils::secret::{NoPadding, ZeroizablePrimitive}; /// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). pub const AES_BLOCK_LEN: usize = 16; @@ -223,8 +231,8 @@ pub trait AESParams: AESParamsInternalTrait { /// The algorithm name, as reported by `Algorithm::ALG_NAME`. const ALG_NAME: &'static str; /// `[u32; 4 * (NR + 1)]` -- the bit-sliced schedule; its layout is - /// described in `schedule.rs`. - type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; + /// described in `schedule.rs`. `NoPadding` lets keys compare their schedules in constant time. + type Schedule: ZeroizablePrimitive + NoPadding + AsRef<[u32]> + AsMut<[u32]>; } /// AES-128 parameters: 16-byte key, `Nk` = 4, `Nr` = 10 (FIPS 197 Table 3). @@ -271,6 +279,6 @@ pub use cfb::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; pub use cfb8::{AES_CFB8_128, AES_CFB8_192, AES_CFB8_256}; pub use cmac::{AES_CMAC_128, AES_CMAC_192, AES_CMAC_256}; pub use ctr::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN}; -pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; +pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256, GCM_NONCE_LEN}; pub use gmac::{AES_GMAC_128, AES_GMAC_192, AES_GMAC_256}; pub use key::{AES128Key, AES192Key, AES256Key, AESKey}; diff --git a/crypto/aes/src/sbox.rs b/crypto/aes/src/sbox.rs index e4df6d75d..2c513b6e6 100644 --- a/crypto/aes/src/sbox.rs +++ b/crypto/aes/src/sbox.rs @@ -222,7 +222,7 @@ pub(crate) fn sbox(q: &mut Planes) { /// `iS(S(y)) = B(A(I(B(A(I(y)) ^ {63} ^ {63}))) ^ {63} ^ {63}) = y`. /// /// So applying [`inv_affine`], then the forward circuit, then [`inv_affine`] again yields the -/// inverse S-box, at the cost of 16 extra XORs and 8 complements instead of a whole second +/// inverse S-box, at the cost of 32 extra XORs and 8 complements instead of a whole second /// circuit. Verified against Table 6 by the decryption known-answer tests in `tests/` /// (SP 800-38A F.1.2/4/6 and the ACVP decrypt vectors), and against [`sbox`] by the ECB /// conformance suite's inverse checks. diff --git a/crypto/aes/src/schedule.rs b/crypto/aes/src/schedule.rs index e0c38b653..24fa86c69 100644 --- a/crypto/aes/src/schedule.rs +++ b/crypto/aes/src/schedule.rs @@ -80,31 +80,37 @@ fn sub_word(word: u32) -> u32 { ortho(&mut q); sbox_out_of_line(&mut q); ortho(&mut q); - // The word was broadcast into all eight planes, so all eight must carry the same answer. - debug_assert!(q.iter().all(|&plane| plane == q[0]), "the eight broadcast planes must agree"); + // The word was broadcast into all eight planes, so all eight carry the same answer and plane + // 0 is it; `test_sub_word_fills_every_plane` below pins that. (Not a `debug_assert` over the + // planes: `Iterator::all` short-circuits, which is a branch on S-box output of key bytes in a + // debug build.) q[0] } /// KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2), returning the bit-sliced schedule. /// -/// `key` must be exactly `P::KEY_LEN` bytes; [`crate::hazmat::AESInternal`] checks that before calling, so this -/// cannot fail and takes no `Result`. +/// `key` is `P::KEY_LEN` bytes by its type (`KEY_LEN == P::KEY_LEN` is asserted at compile time), +/// so this cannot fail and takes no `Result`; [`crate::hazmat::AESInternal`]'s constructors hand +/// over the array once `validate` has passed. /// /// Algorithm 2 is followed literally -- lines 2-6 copy the key into `w[0..Nk]`, lines 7-16 derive /// the rest -- and then the finished schedule is rewritten in place into the storage form /// described in the module docs. Verified against the worked expansions in FIPS 197 /// Appendix A.1, A.2 and A.3 by the tests at the bottom of this file, which unpack the stored /// schedule and compare every `w[i]`. -pub(crate) fn expand(key: &[u8]) -> Secret { - debug_assert_eq!(key.len(), P::KEY_LEN); +pub(crate) fn expand( + key: &[u8; KEY_LEN], +) -> Secret { + const { assert!(KEY_LEN == P::KEY_LEN, "the key array must be P::KEY_LEN bytes") }; let mut schedule = Secret::::new(); let w = (*schedule).as_mut(); - // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. - for i in 0..P::NK { - // Cannot fail: `key` is P::KEY_LEN == 4 * P::NK bytes, so this window is in bounds. - w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. KEY_LEN == 4 * NK, so the + // array splits into exactly NK whole words with no remainder. + let (words, _) = key.as_chunks::<4>(); + for (w_i, bytes) in w[..P::NK].iter_mut().zip(words) { + *w_i = u32::from_le_bytes(*bytes); } // Algorithm 2 lines 7-16. @@ -252,8 +258,12 @@ mod tests { /// Appendix A prints a word as the byte sequence `[a0,a1,a2,a3]` left to right, so the /// tabulated `u32` has `a0` in its *most* significant byte; words are held little-endian /// here, so `swap_bytes` is the conversion. - fn assert_expansion_matches(key: &[u8], expected: &[u32], label: &str) { - let schedule = expand::

(key); + fn assert_expansion_matches( + key: &[u8; KEY_LEN], + expected: &[u32], + label: &str, + ) { + let schedule = expand::(key); assert_eq!(expected.len(), 4 * (P::NR + 1), "{label}: table length"); for (i, &want) in expected.iter().enumerate() { let got = classical_word::

(&schedule, i).swap_bytes(); @@ -267,7 +277,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); + assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); } #[test] @@ -276,7 +286,7 @@ mod tests { 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, 0x79, 0xe5, 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, ]; - assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); + assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); } #[test] @@ -286,7 +296,7 @@ mod tests { 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, ]; - assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); + assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); } #[test] @@ -297,7 +307,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - let schedule = expand::(&key); + let schedule = expand::(&key); for i in 0..AES128Params::NK { let got = classical_word::(&schedule, i); assert_eq!(got.to_le_bytes(), key[4 * i..4 * i + 4]); @@ -343,7 +353,7 @@ mod tests { 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, ]; - let schedule = expand::(&key); + let schedule = expand::(&key); // Recompute the classical schedule without the compression step. let mut w = [0u32; 44]; diff --git a/crypto/aes/tests/ccm_key_tests.rs b/crypto/aes/tests/ccm_key_tests.rs index 8776eb42c..5b47934cb 100644 --- a/crypto/aes/tests/ccm_key_tests.rs +++ b/crypto/aes/tests/ccm_key_tests.rs @@ -245,3 +245,22 @@ fn a_refused_update_leaves_no_payload_in_the_output_buffer() { )); assert_eq!(out, [0u8; 32], "a refused decrypt must leave only zeros behind"); } + +/// The drawing in-place one-shot, `encrypt_detached_in_place(rng, ..)`, is the `_nonce` form +/// under the nonce it drew: the one form the suite above does not reach. +#[test] +fn the_drawing_in_place_form_is_the_nonce_form_under_the_drawn_nonce() { + let km = material::<16>(); + let key = AES128Key::>::from_key_material(&km).unwrap(); + for len in LENS { + let msg = message(len); + let mut buf = msg.clone(); + let (nonce, tag) = key.encrypt_detached_in_place(&mut rng(), AAD, &mut buf).unwrap(); + assert_eq!(reference(&km, &nonce, &msg), (buf.clone(), tag), "len {len}"); + let mut again = msg.clone(); + assert_eq!(key.encrypt_detached_in_place_nonce(&nonce, AAD, &mut again).unwrap(), tag); + assert_eq!(again, buf, "len {len}: the nonce form agrees"); + key.decrypt_detached_in_place(&nonce, AAD, &mut buf, &tag).unwrap(); + assert_eq!(buf, msg, "len {len}: round trip"); + } +} diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs index 69e019a0e..fe97d8e86 100644 --- a/crypto/ascon/src/ascon_aead128.rs +++ b/crypto/ascon/src/ascon_aead128.rs @@ -36,11 +36,11 @@ use crate::ASCON_AEAD128_NAME; use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; /// Length in bytes of the Ascon-AEAD128 key. -pub const KEY_LEN: usize = 16; +pub const ASCON_AEAD128_KEY_LEN: usize = 16; /// Length in bytes of the Ascon-AEAD128 nonce. -pub const NONCE_LEN: usize = 16; +pub const ASCON_AEAD128_NONCE_LEN: usize = 16; /// Length in bytes of the Ascon-AEAD128 authentication tag. -pub const TAG_LEN: usize = 16; +pub const ASCON_AEAD128_TAG_LEN: usize = 16; const RATE: usize = 16; /// Ascon-AEAD128 initial value (SP 800-232 Table 14). @@ -116,17 +116,17 @@ impl Phase { /// /// The encrypting side implements [`AEADCipherEncryptor`] and [`SymmetricCipherEncryptor`]; the /// nonce is drawn by the key's `create_encryptor` and returned, or supplied to -/// `create_encryptor_nonce` by a caller who takes on its uniqueness. `FINAL_LEN` is `TAG_LEN`: -/// nothing is held back, so the -/// inline [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag, and the detached +/// `create_encryptor_nonce` by a caller who takes on its uniqueness. `FINAL_LEN` is +/// `ASCON_AEAD128_TAG_LEN`: nothing is held back, so the inline +/// [`SymmetricCipherEncryptor::do_encrypt_final`] writes only the tag, and the detached /// [`AEADCipherEncryptor::do_encrypt_final_detached_out`] only zeroes its buffer. /// /// The decrypting side implements [`AEADCipherDecryptor`] and [`SymmetricCipherDecryptor`]. The -/// cipher emits every byte as it is seen, but this side holds back the last `TAG_LEN` bytes of -/// ciphertext, since until the stream ends it cannot know whether they are the inline tag +/// cipher emits every byte as it is seen, but this side holds back the last `ASCON_AEAD128_TAG_LEN` +/// bytes of ciphertext, since until the stream ends it cannot know whether they are the inline tag /// ([`SymmetricCipherDecryptor::do_decrypt_final`]) or ciphertext with the tag carried separately -/// ([`AEADCipherDecryptor::do_decrypt_final_detached_out`]). They are ciphertext, not plaintext, -/// so they need no [`Secret`] wrapper; the encrypting side carries the same fields unused. +/// ([`AEADCipherDecryptor::do_decrypt_final_detached_out`]). They are ciphertext, not plaintext, so +/// they need no [`Secret`] wrapper; the encrypting side carries the same fields unused. /// /// ```compile_fail /// # use bouncycastle_ascon::AsconAEAD128Key; @@ -151,8 +151,8 @@ pub struct Ascon_AEAD128

{ // Where the call sequence is, for enforcing the call order. phase: Phase, /// Decrypting only: the most recent `held_len` bytes of ciphertext, not yet given to the - /// cipher. Always `min(TAG_LEN, total ciphertext seen)`. - held: [u8; TAG_LEN], + /// cipher. Always `min(ASCON_AEAD128_TAG_LEN, total ciphertext seen)`. + held: [u8; ASCON_AEAD128_TAG_LEN], held_len: usize, _dir: PhantomData, } @@ -176,7 +176,9 @@ impl Ascon_AEAD128 { /// Validate a [`KeyMaterial`] for use with Ascon-AEAD128 and return its key words. /// The key must be tagged as a [`KeyType::SymmetricCipherKey`] and carry at least the /// algorithm's 128-bit security strength (SP 800-232 R1/R2). - fn checked_key(key: &KeyMaterial) -> Result<[u64; 2], SymmetricCipherError> { + fn checked_key( + key: &KeyMaterial, + ) -> Result<[u64; 2], SymmetricCipherError> { if key.key_type() != KeyType::SymmetricCipherKey { return Err(KeyMaterialError::InvalidKeyType( "Ascon-AEAD128 requires a SymmetricCipherKey", @@ -190,14 +192,14 @@ impl Ascon_AEAD128 { .into()); } let bytes = key.ref_to_bytes(); - if bytes.len() != KEY_LEN { + if bytes.len() != ASCON_AEAD128_KEY_LEN { return Err(KeyMaterialError::InvalidLength.into()); } Ok([load_u64_le(bytes, 0), load_u64_le(bytes, 8)]) } /// Step 1 from already-validated key words: what [`AsconAEAD128Key`] calls per message. - fn from_key_words(key_words: &[u64; 2], nonce: &[u8; NONCE_LEN]) -> Self { + fn from_key_words(key_words: &[u64; 2], nonce: &[u8; ASCON_AEAD128_NONCE_LEN]) -> Self { let mut key_secret: Secret<[u64; 2]> = Secret::new(); *key_secret = *key_words; @@ -218,7 +220,7 @@ impl Ascon_AEAD128 { state, pos: 0, phase: Phase::Init, - held: [0u8; TAG_LEN], + held: [0u8; ASCON_AEAD128_TAG_LEN], held_len: 0, _dir: PhantomData, } @@ -312,14 +314,14 @@ impl Ascon_AEAD128 { // Finalization (SP 800-232 §4.1.1 step 4 / §4.1.2 step 4, Eq. 30-32 / 49-51): re-add the key, // permute with Ascon-p[12], and add the key again; the tag is the resulting last 128 bits. - fn finish_data(&mut self) -> [u8; TAG_LEN] { + fn finish_data(&mut self) -> [u8; ASCON_AEAD128_TAG_LEN] { self.state[2] ^= self.key[0]; self.state[3] ^= self.key[1]; p12(&mut self.state); self.state[3] ^= self.key[0]; self.state[4] ^= self.key[1]; - let mut tag = [0u8; TAG_LEN]; + let mut tag = [0u8; ASCON_AEAD128_TAG_LEN]; store_u64_le(&mut tag, 0, self.state[3]); store_u64_le(&mut tag, 8, self.state[4]); tag @@ -357,6 +359,11 @@ impl Ascon_AEAD128 { /// Encrypt `data` in place (SP 800-232 §4.1.1 step 3). Every byte is transformed and emitted /// immediately; nothing is buffered across calls. fn encrypt_bytes(&mut self, data: &mut [u8]) { + // An empty call is a no-op that leaves the AAD phase open: the family's rule, held by + // the AEAD suite in the test framework; data begins with its first byte. + if data.is_empty() { + return; + } self.check_data(); let mut data = data; @@ -380,7 +387,7 @@ impl Ascon_AEAD128 { /// Finish encryption; returns the 128-bit tag (SP 800-232 §4.1.1 steps 3-4). Pads the final /// (possibly empty) plaintext block; no further bytes are emitted here since every /// plaintext/ciphertext byte was already written by `encrypt_bytes`. - fn finish_tag(mut self) -> [u8; TAG_LEN] { + fn finish_tag(mut self) -> [u8; ASCON_AEAD128_TAG_LEN] { self.check_data(); // Padding of the final (possibly empty) plaintext block (Eq. 27). self.xor_state_byte(self.pos, 0x01); @@ -392,6 +399,11 @@ impl Ascon_AEAD128 { /// Decrypt `data` in place (SP 800-232 §4.1.2 step 3). Every byte is transformed and emitted /// immediately; the plaintext is **not** authenticated until `verify_tag` returns `Ok`. fn decrypt_bytes(&mut self, data: &mut [u8]) { + // An empty call is a no-op that leaves the AAD phase open: the family's rule, held by + // the AEAD suite in the test framework; data begins with its first byte. + if data.is_empty() { + return; + } self.check_data(); let mut data = data; @@ -413,7 +425,7 @@ impl Ascon_AEAD128 { } /// Finish decryption, checking `tag` in constant time (SP 800-232 §4.1.2 steps 3-4). - fn verify_tag(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + fn verify_tag(mut self, tag: &[u8; ASCON_AEAD128_TAG_LEN]) -> Result<(), SymmetricCipherError> { self.check_data(); // Padding of the final (possibly empty) ciphertext block (Eq. 47). self.xor_state_byte(self.pos, 0x01); @@ -431,7 +443,9 @@ impl Algorithm for Ascon_AEAD128 { const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } -impl SymmetricCipherEncryptor for Ascon_AEAD128 { +impl SymmetricCipherEncryptor + for Ascon_AEAD128 +{ /// Ascon-AEAD128 never buffers: every byte given is a byte returned. fn do_encrypt_out_len(&self, input_len: usize) -> usize { input_len @@ -453,17 +467,21 @@ impl SymmetricCipherEncryptor for Ascon_AEAD128 } /// The inline layout: nothing is held back, so the final buffer is exactly the tag. - fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - Ok((self.finish_tag(), TAG_LEN)) + fn do_encrypt_final( + self, + ) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), SymmetricCipherError> { + Ok((self.finish_tag(), ASCON_AEAD128_TAG_LEN)) } /// The ciphertext, which is as long as the plaintext, followed by the tag. fn encrypt_out_len(plaintext_len: usize) -> usize { - plaintext_len + TAG_LEN + plaintext_len + ASCON_AEAD128_TAG_LEN } } -impl AEADCipherEncryptor for Ascon_AEAD128 { +impl AEADCipherEncryptor + for Ascon_AEAD128 +{ fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { self.absorb_aad(aad) } @@ -471,17 +489,19 @@ impl AEADCipherEncryptor for Ascon_AEAD128 Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext: &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result<(usize, [u8; ASCON_AEAD128_TAG_LEN]), SymmetricCipherError> { ciphertext.fill(0); Ok((0, self.finish_tag())) } } -impl SymmetricCipherDecryptor for Ascon_AEAD128 { - /// Everything but the last `TAG_LEN` bytes seen so far is released. +impl SymmetricCipherDecryptor + for Ascon_AEAD128 +{ + /// Everything but the last `ASCON_AEAD128_TAG_LEN` bytes seen so far is released. fn do_decrypt_out_len(&self, input_len: usize) -> usize { - (self.held_len + input_len).saturating_sub(TAG_LEN) + (self.held_len + input_len).saturating_sub(ASCON_AEAD128_TAG_LEN) } fn do_decrypt_out( @@ -500,11 +520,15 @@ impl SymmetricCipherDecryptor for Ascon_AEAD128 let out = &mut plaintext[..release]; out[..from_held].copy_from_slice(&self.held[..from_held]); out[from_held..].copy_from_slice(&ciphertext[..from_input]); - // Called even when `release` is 0: that is what ends the AAD phase, so a later non-empty - // `do_update_aad` is refused however little ciphertext has been seen. + // Data has started once any ciphertext has been seen, even while every byte of it is + // still held back as a possible tag, so the AAD phase ends here and a later non-empty + // `do_update_aad` is refused. An empty call, though, is a no-op that leaves it open. + if !ciphertext.is_empty() { + self.check_data(); + } self.decrypt_bytes(out); - // Keep the newest `TAG_LEN` (or fewer) bytes: what is left of `held`, then the tail of - // `ciphertext`. + // Keep the newest `ASCON_AEAD128_TAG_LEN` (or fewer) bytes: what is left of `held`, then + // the tail of `ciphertext`. let kept = self.held_len - from_held; self.held.copy_within(from_held..self.held_len, 0); let new_len = kept + ciphertext.len() - from_input; @@ -517,24 +541,28 @@ impl SymmetricCipherDecryptor for Ascon_AEAD128 /// release. /// /// # Errors - /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `TAG_LEN` bytes were seen in all; - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. - fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { - if self.held_len < TAG_LEN { + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `ASCON_AEAD128_TAG_LEN` bytes were + /// seen in all; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_decrypt_final( + self, + ) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), SymmetricCipherError> { + if self.held_len < ASCON_AEAD128_TAG_LEN { return Err(SymmetricCipherError::DecryptionFailed); } let tag = self.held; self.verify_tag(&tag)?; - Ok(([0u8; TAG_LEN], 0)) + Ok(([0u8; ASCON_AEAD128_TAG_LEN], 0)) } /// Everything but the trailing tag. fn decrypt_out_len(ciphertext_len: usize) -> usize { - ciphertext_len.saturating_sub(TAG_LEN) + ciphertext_len.saturating_sub(ASCON_AEAD128_TAG_LEN) } } -impl AEADCipherDecryptor for Ascon_AEAD128 { +impl AEADCipherDecryptor + for Ascon_AEAD128 +{ /// # Errors /// [`SymmetricCipherError::StateError`] if `aad` is non-empty and /// [`SymmetricCipherDecryptor::do_decrypt_out`] has already been called. @@ -547,8 +575,8 @@ impl AEADCipherDecryptor for Ascon_AEAD128 Result { plaintext.fill(0); let n = self.held_len; @@ -575,11 +603,16 @@ impl Ascon_AEAD128 { impl Ascon_AEAD128 { /// Decrypts in place, in one buffer: the in-place twin of /// [`SymmetricCipherDecryptor::do_decrypt_out`], with the same hold-back. The plaintext - /// released by this call, the oldest `(held + data.len()) - TAG_LEN` bytes of ciphertext seen - /// so far, is written to the front of `data` and its length returned; the newest `TAG_LEN` - /// bytes are held back for the final, so either final can follow. Whatever is in `data` past - /// the returned count is not plaintext. + /// released by this call, the oldest `(held + data.len()) - ASCON_AEAD128_TAG_LEN` bytes of + /// ciphertext seen so far, is written to the front of `data` and its length returned; the + /// newest `ASCON_AEAD128_TAG_LEN` bytes are held back for the final, so either final can + /// follow. Whatever is in `data` past the returned count is not plaintext. pub fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> usize { + // The same phase rule as `do_decrypt_out`: data has started once any ciphertext has + // been seen, even while all of it is held back, and an empty call is a no-op. + if !data.is_empty() { + self.check_data(); + } let release = self.do_decrypt_out_len(data.len()); let from_held = release.min(self.held_len); let from_input = release - from_held; @@ -588,7 +621,7 @@ impl Ascon_AEAD128 { // `data`, saved before the shift below overwrites the tail. let kept = self.held_len - from_held; let input_kept = data.len() - from_input; - let mut new_held = [0u8; TAG_LEN]; + let mut new_held = [0u8; ASCON_AEAD128_TAG_LEN]; new_held[..kept].copy_from_slice(&self.held[from_held..self.held_len]); new_held[kept..kept + input_kept].copy_from_slice(&data[from_input..]); @@ -596,7 +629,6 @@ impl Ascon_AEAD128 { // the input moves right to make room for the held-back bytes ahead of it. data.copy_within(..from_input, from_held); data[..from_held].copy_from_slice(&self.held[..from_held]); - // Called even when `release` is 0, which is what ends the AAD phase. self.decrypt_bytes(&mut data[..release]); self.held = new_held; @@ -645,21 +677,30 @@ impl PartialEq for AsconAEAD128Key { impl Eq for AsconAEAD128Key {} -impl FromKeyMaterial for AsconAEAD128Key { +impl FromKeyMaterial for AsconAEAD128Key { const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; /// # Errors /// * [`KeyMaterialError::InvalidKeyType`] if the key is not a `SymmetricCipherKey`. /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. - fn from_key_material(key: &KeyMaterial) -> Result { + fn from_key_material( + key: &KeyMaterial, + ) -> Result { let mut key_secret: Secret<[u64; 2]> = Secret::new(); *key_secret = Ascon_AEAD128::::checked_key(key)?; Ok(Self { key: key_secret }) } } -impl AeadEncryptionKey for AsconAEAD128Key { +impl + AeadEncryptionKey< + ASCON_AEAD128_KEY_LEN, + ASCON_AEAD128_NONCE_LEN, + ASCON_AEAD128_TAG_LEN, + ASCON_AEAD128_TAG_LEN, + > for AsconAEAD128Key +{ type Encryptor<'k> = Ascon_AEAD128 where @@ -670,26 +711,39 @@ impl AeadEncryptionKey for AsconAEAD128Key fn create_encryptor<'k>( &'k self, rng: &mut dyn RNG, - ) -> Result<(Self::Encryptor<'k>, [u8; NONCE_LEN]), SymmetricCipherError> { - let mut nonce = [0u8; NONCE_LEN]; + ) -> Result<(Self::Encryptor<'k>, [u8; ASCON_AEAD128_NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; ASCON_AEAD128_NONCE_LEN]; rng.next_bytes_out(&mut nonce)?; Ok((Ascon_AEAD128::from_key_words(&self.key, &nonce), nonce)) } /// Under a nonce the caller supplies, who takes on the obligation that it never repeat /// under this key (SP 800-232 R3). - fn create_encryptor_nonce<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Encryptor<'k> { + fn create_encryptor_nonce<'k>( + &'k self, + nonce: &[u8; ASCON_AEAD128_NONCE_LEN], + ) -> Self::Encryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } -impl AeadDecryptionKey for AsconAEAD128Key { +impl + AeadDecryptionKey< + ASCON_AEAD128_KEY_LEN, + ASCON_AEAD128_NONCE_LEN, + ASCON_AEAD128_TAG_LEN, + ASCON_AEAD128_TAG_LEN, + > for AsconAEAD128Key +{ type Decryptor<'k> = Ascon_AEAD128 where Self: 'k; - fn create_decryptor<'k>(&'k self, nonce: &[u8; NONCE_LEN]) -> Self::Decryptor<'k> { + fn create_decryptor<'k>( + &'k self, + nonce: &[u8; ASCON_AEAD128_NONCE_LEN], + ) -> Self::Decryptor<'k> { Ascon_AEAD128::from_key_words(&self.key, nonce) } } @@ -713,8 +767,9 @@ impl Display for Ascon_AEAD128 { pub const SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN: usize = CIPHER_STATE_LEN; /// Length in bytes of the serialized state of `Ascon_AEAD128`: the encrypting -/// side's layout, then the up to `TAG_LEN` bytes it holds back and their count. -pub const SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN: usize = CIPHER_STATE_LEN + TAG_LEN + 1; +/// side's layout, then the up to `ASCON_AEAD128_TAG_LEN` bytes it holds back and their count. +pub const SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN: usize = + CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN + 1; /// The cipher core's serialized state: the whole of the encrypting side's and the head of the /// decrypting side's. @@ -762,15 +817,15 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN] { let mut out = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; - out[CIPHER_STATE_LEN..CIPHER_STATE_LEN + TAG_LEN].copy_from_slice(&self.held); - debug_assert!(self.held_len <= TAG_LEN); - out[CIPHER_STATE_LEN + TAG_LEN] = self.held_len as u8; + out[CIPHER_STATE_LEN..CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN].copy_from_slice(&self.held); + debug_assert!(self.held_len <= ASCON_AEAD128_TAG_LEN); + out[CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN] = self.held_len as u8; out[..CIPHER_STATE_LEN].copy_from_slice(&self.suspend_as(DEC_DIR_BITS)); out } - /// A state suspended by the encrypting side, or a held-back count above `TAG_LEN`, is - /// [`SuspendableError::InvalidData`]. + /// A state suspended by the encrypting side, a held-back count above `ASCON_AEAD128_TAG_LEN`, + /// or held-back bytes before any ciphertext was seen, is [`SuspendableError::InvalidData`]. fn from_suspended( serialized_state: [u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN], key: &'k Self::Key, @@ -779,9 +834,15 @@ impl<'k> SuspendableKeyed<'k, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN> let cipher_state: [u8; CIPHER_STATE_LEN] = serialized_state[..CIPHER_STATE_LEN].try_into().unwrap(); let mut dec = Self::resume_as(cipher_state, key, DEC_DIR_BITS)?; - dec.held.copy_from_slice(&serialized_state[CIPHER_STATE_LEN..CIPHER_STATE_LEN + TAG_LEN]); - dec.held_len = serialized_state[CIPHER_STATE_LEN + TAG_LEN] as usize; - if dec.held_len > TAG_LEN { + dec.held.copy_from_slice( + &serialized_state[CIPHER_STATE_LEN..CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN], + ); + dec.held_len = serialized_state[CIPHER_STATE_LEN + ASCON_AEAD128_TAG_LEN] as usize; + if dec.held_len > ASCON_AEAD128_TAG_LEN { + return Err(SuspendableError::InvalidData); + } + // Bytes are held back only by `decrypt_bytes`' callers, which move the phase to `Data`. + if dec.held_len != 0 && dec.phase != Phase::Data { return Err(SuspendableError::InvalidData); } Ok(dec) @@ -850,7 +911,7 @@ impl Ascon_AEAD128 { state: s, pos, phase, - held: [0u8; TAG_LEN], + held: [0u8; ASCON_AEAD128_TAG_LEN], held_len: 0, _dir: PhantomData, }) diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs index 0f1a76e51..d3ca3be3a 100644 --- a/crypto/ascon/src/ascon_cxof128.rs +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -86,17 +86,10 @@ impl AsconCXof128 { Ok(Self { sponge }) } - /// Produces `output.len()` bytes from the XOF stream. - /// - /// The first call ends the message-absorb phase by padding and absorbing the - /// final message block. Subsequent calls continue the same output stream. + /// Produces the next `output.len()` bytes of the XOF stream; the absorb phase was ended by + /// [`XOF::into_squeezer`]. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { output.fill(0); - - if !self.sponge.squeezing() { - self.sponge.pad_and_absorb(); - } - self.sponge.squeeze(output); output.len() } @@ -217,7 +210,10 @@ impl Hash for AsconCXof128 { impl XOF for AsconCXof128 { type Squeezer = AsconCXof128Squeezer; - fn into_squeezer(self) -> Self::Squeezer { + fn into_squeezer(mut self) -> Self::Squeezer { + // The squeezer is squeezing from the start, so a squeezer suspended before its first + // output resumes as a squeezer. + self.sponge.finish_absorb(); AsconCXof128Squeezer { xof: self } } diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs index 5717f774a..2f906febc 100644 --- a/crypto/ascon/src/ascon_hash256.rs +++ b/crypto/ascon/src/ascon_hash256.rs @@ -35,7 +35,7 @@ impl AsconHash256 { // Algorithm 5). The 32-byte digest is exactly RATE * 4 bytes, so a single generic // `Sponge::squeeze()` call over the whole output produces all four blocks with no leftover. fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { - self.sponge.pad_and_absorb(); + self.sponge.finish_absorb(); self.sponge.squeeze(output); } } @@ -86,19 +86,44 @@ impl Hash for AsconHash256 { fn do_final_partial_bits( self, - _partial_byte: u8, - _num_partial_bits: usize, + partial_byte: u8, + num_bits: usize, ) -> Result, HashError> { - Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-Hash256 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) } fn do_final_partial_bits_out( self, - _partial_byte: u8, - _num_partial_bits: usize, - _output: &mut [u8], + partial_byte: u8, + num_bits: usize, + output: &mut [u8], ) -> Result { - Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + output.fill(0); + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-Hash256 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) } } diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs index 0d27ff2b1..afeb3dc73 100644 --- a/crypto/ascon/src/ascon_xof128.rs +++ b/crypto/ascon/src/ascon_xof128.rs @@ -39,17 +39,10 @@ impl AsconXof128 { } } - /// Produces `output.len()` bytes from the XOF stream. - /// - /// The first call ends the absorb phase by padding and absorbing the final - /// message block. Subsequent calls continue the same output stream. + /// Produces the next `output.len()` bytes of the XOF stream; the absorb phase was ended by + /// [`XOF::into_squeezer`]. fn squeeze_into(&mut self, output: &mut [u8]) -> usize { output.fill(0); - - if !self.sponge.squeezing() { - self.sponge.pad_and_absorb(); - } - self.sponge.squeeze(output); output.len() } @@ -170,7 +163,10 @@ impl Hash for AsconXof128 { impl XOF for AsconXof128 { type Squeezer = AsconXof128Squeezer; - fn into_squeezer(self) -> Self::Squeezer { + fn into_squeezer(mut self) -> Self::Squeezer { + // The squeezer is squeezing from the start, so a squeezer suspended before its first + // output resumes as a squeezer. + self.sponge.finish_absorb(); AsconXof128Squeezer { xof: self } } diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs index 4d936ff58..baeda293a 100644 --- a/crypto/ascon/src/lib.rs +++ b/crypto/ascon/src/lib.rs @@ -183,12 +183,22 @@ //! //! - **Nonce uniqueness (SP 800-232 R3):** a (key, nonce) pair must never be reused for two //! different Ascon-AEAD128 encryptions. Nonce reuse breaks confidentiality. +//! - **Data limit (SP 800-232 R6, R7):** the total data processed under one key, encryption and +//! decryption together and including the nonces, must not exceed 2^54 bytes; the key must be +//! replaced before then. The limit spans every message under the key, so the caller must track +//! it; this crate does not count. +//! - **Decryption failures (SP 800-232 R5):** with the full 128-bit tag, no more than 2^96 +//! decryption failures may be accepted under one key. //! - **Tag length:** this crate always produces and verifies the full 128-bit tag. Truncated tags //! (SP 800-232 §4.2.1) are not exposed. +//! - **Cloning a cipher:** a clone of an [`Ascon_AEAD128`] continues with the same +//! key, nonce and state, so encrypting different data on the two copies reuses the keystream, +//! exactly as nonce reuse does. //! - **No partial-byte input:** Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 are byte-oriented; //! their `do_final_partial_bits`/`do_final_partial_bits_out` (and the equivalent XOF methods) -//! always return `HashError::InvalidInput`, including when reached through `HashFactory`. A -//! caller that needs a partial-byte final block should reach for SHA-3, which supports one. +//! return `HashError::InvalidInput` for 1 to 7 bits, including when reached through +//! `HashFactory`; 0 bits is an ordinary final. A caller that needs a partial-byte final block +//! should reach for SHA-3, which supports one. //! - **Decryption tag check failure:** a ciphertext decryption whose finalization returns //! `Err(SymmetricCipherError::AEADTagCheckFailed)` must be treated as tampered, and the entire //! plaintext rejected. The key's one-shots @@ -216,9 +226,9 @@ mod ascon_hash256; mod ascon_xof128; pub use ascon_aead128::{ - Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, - SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, - TAG_LEN, + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, Ascon_AEAD128, + AsconAEAD128Key, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, + SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, }; pub use ascon_cxof128::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; pub use ascon_hash256::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; diff --git a/crypto/ascon/src/sponge.rs b/crypto/ascon/src/sponge.rs index c1618b6d6..60be607d8 100644 --- a/crypto/ascon/src/sponge.rs +++ b/crypto/ascon/src/sponge.rs @@ -122,15 +122,23 @@ impl Sponge { self.s[0] ^= 0x01u64 << final_bits; } - /// Squeeze `output.len()` bytes. May be called multiple times; the first call must follow - /// [`Sponge::pad_and_absorb`] and ends the absorb phase. + /// Ends the absorb phase: pads the final block and marks the sponge as squeezing with no + /// squeezed bytes buffered, so the first [`Sponge::squeeze`] begins with the permutation. The + /// buffered message bytes are cleared, since nothing reads them again. + pub(crate) fn finish_absorb(&mut self) { + self.pad_and_absorb(); + self.buf.fill(0); + self.buf_pos = RATE; + self.squeezing = true; + } + + /// Squeeze `output.len()` bytes. May be called multiple times, after + /// [`Sponge::finish_absorb`]. pub(crate) fn squeeze(&mut self, output: &mut [u8]) { + debug_assert!(self.squeezing, "squeeze before finish_absorb"); let mut output = output; - if !self.squeezing { - self.squeezing = true; - self.buf_pos = RATE; - } else if self.buf_pos < RATE { + if self.buf_pos < RATE { let available = RATE - self.buf_pos; if output.len() <= available { let end_pos = self.buf_pos + output.len(); diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs index 68bd3a461..09d813ecc 100644 --- a/crypto/ascon/tests/aead128_tests.rs +++ b/crypto/ascon/tests/aead128_tests.rs @@ -495,6 +495,29 @@ fn aead128_key_enforces_the_material_policy() { .test::<16, AsconAEAD128Key>(ASCON_AEAD128_NAME, SecurityStrength::_128bit); } +/// The AAD phase ends with the first non-empty data call on either decrypt entry point, even +/// while every byte of it is still held back as a possible tag, and an empty call on either +/// leaves it open: `do_decrypt_inplace` and `do_decrypt_out` run one phase machine, so a caller +/// mixing them sees the same answer from `do_update_aad` whichever it used. +#[test] +fn both_decrypt_entry_points_close_the_aad_phase_on_the_first_non_empty_call() { + let km = aead_key(&KEY); + let held = [0x5au8; 16]; + + let mut d = km.create_decryptor(&NONCE); + assert_eq!(d.do_decrypt_inplace(&mut []), 0); + d.do_update_aad(b"still open").unwrap(); + assert_eq!(d.do_decrypt_inplace(&mut held.clone()), 0, "all held back"); + assert!(matches!(d.do_update_aad(b"late"), Err(SymmetricCipherError::StateError(_)))); + + let mut d = km.create_decryptor(&NONCE); + let mut out = [0u8; 16]; + assert_eq!(d.do_decrypt_out(&[], &mut out).unwrap(), 0); + d.do_update_aad(b"still open").unwrap(); + assert_eq!(d.do_decrypt_out(&held, &mut out).unwrap(), 0, "all held back"); + assert!(matches!(d.do_update_aad(b"late"), Err(SymmetricCipherError::StateError(_)))); +} + /// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by /// streaming [`Ascon_AEAD128`] and taking the tag from `do_encrypt_final_detached_out`, /// must equal what the inline layout produces for the same key, nonce (driven by the same RNG @@ -698,6 +721,19 @@ fn aead128_suspendable_keyed_state() { Err(SuspendableError::InvalidData) )); + // Bytes are held back only once ciphertext has been seen: a decryptor still taking AAD + // resumes with nothing held, and not with a held-back count. + let mut d_aad = key.create_decryptor(&NONCE); + d_aad.do_update_aad(ad).unwrap(); + let aad_state = d_aad.suspend(); + assert!(Ascon_AEAD128::::from_suspended(aad_state, &key).is_ok()); + let mut held_in_aad = aad_state; + held_in_aad[SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN - 1] = 5; + assert!(matches!( + Ascon_AEAD128::::from_suspended(held_in_aad, &key), + Err(SuspendableError::InvalidData) + )); + // A state suspended while encrypting must not resume as a decryptor, nor the reverse: the // call-state byte carries the direction and the resuming type checks it. let mut enc_as_dec = [0u8; SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN]; diff --git a/crypto/ascon/tests/ascon_wycheproof.rs b/crypto/ascon/tests/ascon_wycheproof.rs index 798a001f3..ebea69c8c 100644 --- a/crypto/ascon/tests/ascon_wycheproof.rs +++ b/crypto/ascon/tests/ascon_wycheproof.rs @@ -9,7 +9,10 @@ //! A `valid` case must encrypt to exactly `ct || tag` and decrypt back to `msg`. An `invalid` case //! must be rejected with `AEADTagCheckFailed`, leaving the output buffer zeroized. -use bouncycastle_ascon::{Ascon_AEAD128, AsconAEAD128Key, KEY_LEN, NONCE_LEN, TAG_LEN}; +use bouncycastle_ascon::{ + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, Ascon_AEAD128, + AsconAEAD128Key, +}; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::do_hazardous_operations; @@ -21,9 +24,12 @@ use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wych /// Wraps the vector's key bytes as a cipher key, promoting them if `KeyMaterial`'s entropy /// heuristic declined to (as `ascon_bc-test-data.rs` does for the NIST KAT keys). -fn cipher_key(bytes: &[u8]) -> KeyMaterial { - let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) - .expect("a 16-byte key"); +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + let mut key = KeyMaterial::::from_bytes_as_type( + bytes, + KeyType::SymmetricCipherKey, + ) + .expect("a 16-byte key"); do_hazardous_operations(&mut key, |k| { k.set_key_type(KeyType::SymmetricCipherKey)?; k.set_security_strength(SecurityStrength::_128bit) @@ -44,7 +50,11 @@ fn wycheproof_ascon_aead128() { let (mut valid_count, mut invalid_count) = (0usize, 0usize); for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { - for (field, len) in [("keySize", KEY_LEN), ("ivSize", NONCE_LEN), ("tagSize", TAG_LEN)] { + for (field, len) in [ + ("keySize", ASCON_AEAD128_KEY_LEN), + ("ivSize", ASCON_AEAD128_NONCE_LEN), + ("tagSize", ASCON_AEAD128_TAG_LEN), + ] { let bits = group.get(field).and_then(Value::as_u64).expect(field); assert_eq!(bits as usize, 8 * len, "every group uses Ascon-AEAD128's fixed {field}"); } @@ -52,7 +62,7 @@ fn wycheproof_ascon_aead128() { for test in group.get("tests").and_then(Value::as_array).expect("tests") { let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); let key = aead_key(&hex_field(test, "key", tc_id)); - let nonce: [u8; NONCE_LEN] = + let nonce: [u8; ASCON_AEAD128_NONCE_LEN] = hex_field(test, "iv", tc_id).try_into().expect("a 16-byte nonce"); let aad = hex_field(test, "aad", tc_id); let msg = hex_field(test, "msg", tc_id); diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs index b8a4d3949..52d9b61dd 100644 --- a/crypto/ascon/tests/cxof128_tests.rs +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -121,6 +121,20 @@ fn cxof128_prefix_property_and_streaming() { assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + // Reads smaller than what is left of the last squeezed block are served from it. + for n in [1usize, 2, 3, 5, 7] { + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + while piecewise.len() < full.len() { + let mut part = vec![0u8; n.min(full.len() - piecewise.len())]; + assert_eq!(squeezer.do_output_out(&mut part), part.len()); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "squeeze in {n}-byte pieces mismatch"); + } + // Absorbing the message in chunks must equal absorbing it in one call. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconCXof128::with_customization(z).unwrap(); @@ -144,6 +158,16 @@ fn cxof128_hash_view_metadata() { assert_eq!(x.block_bitlen(), 64); assert_eq!(x.output_len(), 32); assert_eq!(x.hash(b"").len(), 32); + + // hash_out writes the 32-byte Hash view and returns how many bytes it wrote. + let expected = AsconCXof128::new().hash(b"abc"); + let mut out = [0xEEu8; 40]; + assert_eq!(AsconCXof128::new().hash_out(b"abc", &mut out), 32); + assert_eq!(&out[..32], &expected[..]); + assert_eq!(&out[32..], &[0u8; 8]); + let mut short = [0u8; 10]; + assert_eq!(AsconCXof128::new().hash_out(b"abc", &mut short), 10); + assert_eq!(&short[..], &expected[..10]); } #[test] @@ -321,6 +345,18 @@ fn cxof128_suspendable_state() { combined.extend_from_slice(&tail); assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); + // A squeezer suspended before its first output is a squeezing state too: it resumes as the + // squeezer, not as the absorbing type, and continues the same stream. + let mut fresh = AsconCXof128::with_customization(z).unwrap(); + fresh.do_update(&data); + let fresh_state = fresh.into_squeezer().suspend(); + assert!(matches!( + AsconCXof128::from_suspended(fresh_state), + Err(SuspendableError::InvalidData) + )); + let mut out = [0u8; 40]; + AsconCXof128Squeezer::from_suspended(fresh_state).unwrap().do_output_out(&mut out); + assert_eq!(out, expected, "a squeezer resumed before its first output must give the stream"); } #[test] diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs index 0fc25f4ad..19e86e9a9 100644 --- a/crypto/ascon/tests/hash256_tests.rs +++ b/crypto/ascon/tests/hash256_tests.rs @@ -4,6 +4,7 @@ //! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. use bouncycastle_ascon::AsconHash256; +use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Hash, HashAlgParams, HashOneShot}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_hex as hex; @@ -116,9 +117,44 @@ fn hash256_hash_out_zeroizes_past_output_len() { #[test] fn hash256_unsupported_partial_ops_return_err() { - assert!(AsconHash256::new().do_final_partial_bits(0, 3).is_err()); - let mut o = [0u8; 32]; - assert!(AsconHash256::new().do_final_partial_bits_out(0, 3, &mut o).is_err()); + // num_bits == 0 is byte-aligned input and must behave like ordinary finalization. + let data = b"abc"; + let expected = AsconHash256::hash(data); + let mut h = AsconHash256::new(); + h.do_update(data); + assert_eq!(h.do_final_partial_bits(0xFF, 0).unwrap(), expected); + let mut h = AsconHash256::new(); + h.do_update(data); + let mut o = [0xEEu8; 40]; + assert_eq!(h.do_final_partial_bits_out(0xFF, 0, &mut o).unwrap(), 32); + assert_eq!(&o[..32], &expected[..]); + assert_eq!(&o[32..], &[0u8; 8]); + + for num_bits in [1usize, 3, 7] { + assert!(matches!( + AsconHash256::new().do_final_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + let mut o = [0xEEu8; 32]; + assert!(matches!( + AsconHash256::new().do_final_partial_bits_out(0xA0, num_bits, &mut o), + Err(HashError::InvalidInput(_)) + )); + assert_eq!(o, [0u8; 32]); + } + + for num_bits in [8usize, 9] { + assert!(matches!( + AsconHash256::new().do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + let mut o = [0xEEu8; 32]; + assert!(matches!( + AsconHash256::new().do_final_partial_bits_out(0xFF, num_bits, &mut o), + Err(HashError::InvalidLength(_)) + )); + assert_eq!(o, [0u8; 32]); + } } #[test] diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs index 85d915e55..5701ac5b0 100644 --- a/crypto/ascon/tests/xof128_tests.rs +++ b/crypto/ascon/tests/xof128_tests.rs @@ -88,6 +88,20 @@ fn xof128_prefix_property_and_streaming() { assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + // Reads smaller than what is left of the last squeezed block are served from it. + for n in [1usize, 2, 3, 5, 7] { + let mut x = AsconXof128::new(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + while piecewise.len() < full.len() { + let mut part = vec![0u8; n.min(full.len() - piecewise.len())]; + assert_eq!(squeezer.do_output_out(&mut part), part.len()); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "squeeze in {n}-byte pieces mismatch"); + } + // Absorbing in chunks equals one-shot input. for chunk in [1usize, 8, 9, 64] { let mut xc = AsconXof128::new(); @@ -287,4 +301,14 @@ fn xof128_suspendable_state() { combined.extend_from_slice(&tail); assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); + + // A squeezer suspended before its first output is a squeezing state too: it resumes as the + // squeezer, not as the absorbing type, and continues the same stream. + let mut fresh = AsconXof128::new(); + fresh.do_update(&data); + let fresh_state = fresh.into_squeezer().suspend(); + assert!(matches!(AsconXof128::from_suspended(fresh_state), Err(SuspendableError::InvalidData))); + let mut out = [0u8; 40]; + AsconXof128Squeezer::from_suspended(fresh_state).unwrap().do_output_out(&mut out); + assert_eq!(out, expected, "a squeezer resumed before its first output must give the stream"); } diff --git a/crypto/base64/src/lib.rs b/crypto/base64/src/lib.rs index a6a4350f9..1b8cf3644 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -109,6 +109,14 @@ pub enum Base64Error { /// Input contained a character that was not in the base64 alphabet. The index of the illegal character is included in the output. InvalidB64Character(usize), + + /// Padding where the data cannot end, or something other than the rest of the padding (and, + /// when whitespace is skipped, whitespace) after it. The index of the offending character in + /// the input to [`Base64Decoder::do_final`] is included. + InvalidPadding(usize), + + /// The input ended one symbol into a block: six bits, which encode no whole byte. + TruncatedInput, } /// The stateful base64 encoder that supports streaming. @@ -255,14 +263,16 @@ impl Base64Decoder { /// Streaming API that performs Base64 encoding of the provided input, but does not apply /// the final padding and will hold an incomplete block while waiting for more input. pub fn do_update>(&mut self, input: T) -> Result, Base64Error> { - self.decode_internal(input, true) + self.decode_internal(input, true).map(|(out, _)| out) } + /// Decodes up to the first padding character, returning the output and, if there was one, the + /// padding character's index. fn decode_internal>( &mut self, input: T, rollback_if_padding: bool, - ) -> Result, Base64Error> { + ) -> Result<(Vec, Option), Base64Error> { // copy the current state so that we can restore it if we encounter a padding character. let starting_state: [u8; 4] = self.buf.clone(); let starting_vals_in_block: usize = self.vals_in_buf; @@ -296,7 +306,7 @@ impl Base64Decoder { self.vals_in_buf = starting_vals_in_block; return Err(Base64Error::PaddingEncounteredDuringDoUpdate); } - return Ok(out); + return Ok((out, Some(i))); } i += 1; @@ -313,35 +323,59 @@ impl Base64Decoder { } } - Ok(out) + Ok((out, None)) } /// As can be expected, do_final() consumes the object. + /// + /// Missing padding is tolerated, but padding must come where the data can end, after two or + /// three symbols of a block, and may be followed only by the rest of that padding (and + /// whitespace, when it is skipped): anything else is [`Base64Error::InvalidPadding`]. Input + /// ending one symbol into a block is [`Base64Error::TruncatedInput`]. pub fn do_final>(mut self, input: T) -> Result, Base64Error> { + let input = input.as_ref(); // process as much as we can the usual way. - let mut out = match self.decode_internal(input, false) { - Ok(out) => out, + let (mut out, pad_at) = match self.decode_internal(input, false) { + Ok(decoded) => decoded, Err(Base64Error::PaddingEncounteredDuringDoUpdate) => { - panic!( + unreachable!( "rollback_if_padding = false should not produce a Base64Error::PaddingEncounteredDuringDoUpdate" ); } Err(e) => return Err(e), }; - // now, a single block containing padding remains to be dealt with. - if self.vals_in_buf != 0 { - // be tolerant of missing padding - // if it is not a complete block at the end, the infer the byte count from the number of leftover symbols - let pad_count: u8 = 3 - (self.vals_in_buf as u8 - 1); + // RFC 4648 Section 4: a final quantum of 8 bits is two symbols and "==", of 16 bits three + // symbols and "=". Section 3.3: implementations "MUST reject the encoded data if it + // contains characters outside the base alphabet", which includes anything after the + // padding, such as a second message. + if let Some(pad_at) = pad_at { + let pads_due = match self.vals_in_buf { + 2 => 2, + 3 => 1, + _ => return Err(Base64Error::InvalidPadding(pad_at)), + }; + let mut pads = 0; + for (offset, &c) in input[pad_at..].iter().enumerate() { + // Only padding and whitespace may follow, both public, so branching is fine. + match Self::ct_b64_to_bin(c) { + 0x81 if pads < pads_due => pads += 1, + 0x80 if self.skip_whitespace => {} + _ => return Err(Base64Error::InvalidPadding(pad_at + offset)), + } + } + } - out.push(self.buf[0] << 2 | self.buf[1] >> 4); - if pad_count != 2 { + // now, a single block containing padding remains to be dealt with. + match self.vals_in_buf { + 0 => {} + // Six bits encode no whole byte. + 1 => return Err(Base64Error::TruncatedInput), + 2 => out.push(self.buf[0] << 2 | self.buf[1] >> 4), + _ => { + out.push(self.buf[0] << 2 | self.buf[1] >> 4); out.push(self.buf[1] << 4 | self.buf[2] >> 2); } - if pad_count == 0 { - out.push(self.buf[2] << 6 | self.buf[3]); - } } Ok(out) diff --git a/crypto/base64/tests/base64_tests.rs b/crypto/base64/tests/base64_tests.rs index aa700dd62..f919ebc55 100644 --- a/crypto/base64/tests/base64_tests.rs +++ b/crypto/base64/tests/base64_tests.rs @@ -122,3 +122,74 @@ fn do_update_refuses_padding_and_leaves_the_state_restorable() { assert!(matches!(decoder.do_update("=="), Err(Base64Error::PaddingEncounteredDuringDoUpdate))); assert_eq!(decoder.do_final("==").unwrap(), b"D"); } + +/// RFC 4648 Section 10's test vectors, each also with its padding dropped (which `do_final` +/// tolerates) and with whitespace around and inside it. The two- and three-symbol tails carry +/// low bits in their last sextet. +#[test] +fn rfc4648_section_10_vectors() { + let vectors: [(&[u8], &str); 7] = [ + (b"", ""), + (b"f", "Zg=="), + (b"fo", "Zm8="), + (b"foo", "Zm9v"), + (b"foob", "Zm9vYg=="), + (b"fooba", "Zm9vYmE="), + (b"foobar", "Zm9vYmFy"), + ]; + for (raw, enc) in vectors { + assert_eq!(base64::encode(raw), enc); + assert_eq!(base64::decode(enc).unwrap(), raw, "{enc}"); + assert_eq!(base64::decode(enc.trim_end_matches('=')).unwrap(), raw, "{enc} unpadded"); + let spaced = format!(" {} \n", enc.replace('=', " = ")); + assert_eq!(base64::decode(&spaced).unwrap(), raw, "{spaced:?}"); + } +} + +/// A block of 4k + 1 symbols ends six bits into a byte, which is no whole byte: it is refused +/// rather than finished from whatever an earlier block left in the decoder. +#[test] +fn a_single_trailing_symbol_is_truncated_input() { + for input in ["R", "QUJDR", "QUJDR ", "QUJD R", "R="] { + assert!( + matches!( + base64::decode(input), + Err(base64::Base64Error::TruncatedInput | base64::Base64Error::InvalidPadding(_)) + ), + "{input:?}" + ); + } + assert!(matches!(base64::decode("QUJDR"), Err(base64::Base64Error::TruncatedInput))); +} + +/// Padding ends the data: RFC 4648 Section 3.3 requires rejecting anything after it other than +/// the rest of the padding, and padding is only valid after two or three symbols of a block. +#[test] +fn nothing_but_padding_and_whitespace_may_follow_the_padding() { + use base64::Base64Error::InvalidPadding; + + // (input, index of the offending character) + let cases = [ + ("QQ==garbage!!", 4), + ("QQ==QQ==", 4), + ("QUJD=RA==", 4), + ("QQ===", 4), + ("QUI==", 4), + ("=", 0), + ("QUJD=", 4), + ("Q=", 1), + ]; + for (input, at) in cases { + match base64::decode(input) { + Err(InvalidPadding(i)) => assert_eq!(i, at, "{input:?}"), + other => panic!("{input:?}: expected InvalidPadding({at}), got {other:?}"), + } + } + + // Whitespace after the padding is fine only when whitespace is skipped. + assert_eq!(base64::decode("QQ== \n").unwrap(), b"A"); + assert!(matches!( + Base64Decoder::new(false).do_final("QQ==\n"), + Err(base64::Base64Error::InvalidPadding(4)) + )); +} diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 2ad71174c..3f900bc38 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -683,6 +683,13 @@ where const MAX_COUNTER: u64 = if Self::Q_LEN >= 8 { u64::MAX } else { (1u64 << (8 * Self::Q_LEN)) - 1 }; + /// The largest `next_ctr` a state of this type can hold: the payload is at most `2^8q - 1` + /// bytes (`MAX_COUNTER` is that same value), so at most `ceil(that / BLOCK_LEN)` counter + /// values from `S1` are ever used and the index stops one past the last. Tighter than + /// `MAX_COUNTER` for `q = 8`, where that is `u64::MAX` and a resumed index at it would + /// overflow the next increment and `remaining_blocks`. + const MAX_NEXT_CTR: u64 = Self::MAX_COUNTER / (BLOCK_LEN as u64) + 2; + /// Formats the counter template and positions the keystream at `S1`. fn from_perm(perm: &'k P, nonce: &[u8; NONCE_LEN]) -> Self { // A.3, Tables 3 and 4: `Ctr_i` is `Flags || N || [i]_8q`, and its flags octet has both @@ -1529,11 +1536,12 @@ where let mut r = Cursor::new(state); let ctr_template = r.array::(); // A.3: the flags octet is `[q-1]_3` and nothing else, and the counter field is zero in - // the template; `next_ctr` starts at 1 (`S0` is the tag mask) and stops at `MAX_COUNTER`. + // the template; `next_ctr` starts at 1 (`S0` is the tag mask) and stops at + // `MAX_NEXT_CTR`, one past the last counter value the largest payload uses. let next_ctr = r.u64(); let flags_ok = ctr_template[0] == (Self::Q_LEN - 1) as u8; let counter_field_zero = ctr_template[BLOCK_LEN - Self::Q_LEN..].iter().all(|&b| b == 0); - let ctr_ok = next_ctr >= 1 && next_ctr - 1 <= Self::MAX_COUNTER; + let ctr_ok = next_ctr >= 1 && next_ctr <= Self::MAX_NEXT_CTR; if !(flags_ok && counter_field_zero && ctr_ok) { return Err(SuspendableError::InvalidData); } diff --git a/crypto/cipher/src/modes/cmac.rs b/crypto/cipher/src/modes/cmac.rs index dcdc91278..bd8daac7a 100644 --- a/crypto/cipher/src/modes/cmac.rs +++ b/crypto/cipher/src/modes/cmac.rs @@ -64,7 +64,8 @@ //! 2^21 with a 64-bit one. //! * **Key separation** (Sec 5.2): the key "shall be used exclusively for the CMAC mode of the //! chosen block cipher", so the key's `from_key_material` accepts only a [`KeyType::MACKey`] -//! key (or an all-zero key, as HMAC does), never a [`KeyType::SymmetricCipherKey`]. +//! key, never a [`KeyType::SymmetricCipherKey`] and not an all-zero (`Zeroized`) one; resume +//! goes through the same key object. //! * **Replay** (Appendix C): a valid tag says nothing about freshness; that is the protocol's job. //! //! [`KeyType::MACKey`]: bouncycastle_core::key_material::KeyType::MACKey diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs index 9a2f6823c..7daf12888 100644 --- a/crypto/cipher/src/modes/gcm.rs +++ b/crypto/cipher/src/modes/gcm.rs @@ -498,6 +498,11 @@ where /// plaintext into ciphertext in place) and the decryptor (which calls this *before* GCTR turns /// the ciphertext back into plaintext): either way GHASH must see ciphertext, never plaintext. fn absorb_data(&mut self, data: &[u8]) -> Result<(), SymmetricCipherError> { + // An empty call is a no-op that leaves the AAD phase open, the rule CCM and the AEAD + // suite in the test framework hold the family to; data begins with its first byte. + if data.is_empty() { + return Ok(()); + } self.data_len = self.data_len_after(data.len())?; self.begin_data_if_needed(); self.ghash.update(data); @@ -714,8 +719,11 @@ where // Data has started even if every byte is still held back as a possible tag, so the AAD // phase ends here rather than at the first byte released: otherwise a `do_update_aad` // after a first call shorter than `TAG_LEN` would be accepted, and absorbed as if it came - // before the ciphertext (Algorithm 5 absorbs `A` before `C`). - self.begin_data_if_needed(); + // before the ciphertext (Algorithm 5 absorbs `A` before `C`). An empty call, though, is + // a no-op that leaves the phase open (see `absorb_data`). + if !ciphertext.is_empty() { + self.begin_data_if_needed(); + } // Bytes of the old tail that are now known to be ciphertext, then bytes of the new input // that are also released this call. @@ -837,6 +845,13 @@ where gcm.ctr = as SuspendableComponent<'k>>::read_state( ctr, &key.perm, )?; + // The CTR state's own check is only the top of the counter's range. GCTR starts at + // inc32(J0), counter 2 (Algorithm 4 step 3); J0 itself, counter 1, is the tag mask + // (step 6) and 0 is never used, so a resumed counter below 2 would emit CIPH_K(J0) as + // keystream. Read through the keystream's own layout, after its own range check. + if CtrKeyStream::<'k, P, 16, GCM_NONCE_LEN>::counter_from_state(ctr) < 2 { + return Err(SuspendableError::InvalidData); + } gcm.ghash.restore_state(ghash)?; let mut r = Cursor::new(rest); diff --git a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs index 2919bd1e6..6b000c088 100644 --- a/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -109,6 +109,15 @@ where Cursor::new(state).array::() } + /// The next counter value of a suspended keystream, read out of the same state: the `u64` + /// after the nonce. For a composite whose rule on the counter is tighter than this type's + /// (GCM starts at 2), so that the rule is tied to this layout and not to a copy of it. + pub(crate) fn counter_from_state(state: &[u8]) -> u64 { + let mut r = Cursor::new(state); + r.array::(); + r.u64() + } + /// `Tj = N | [j]m`: the nonce followed by the counter `j`, big-endian, in the trailing /// `CTR_LEN` bytes. /// diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs index 29367ed00..cf733c49f 100644 --- a/crypto/cipher/tests/ccm_suspend_tests.rs +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -126,15 +126,45 @@ fn owed_is_held_to_the_frame() { } /// The counter index runs from 1 (`S0` is the tag mask, step 7 starts the payload keystream at -/// `S1`) to one past the last counter value, which is where it stands once every block has been -/// used; 0 and anything beyond are not states the mode can have been in. +/// `S1`) to one past the last counter value the largest payload (`2^8q - 1` bytes, so +/// `ceil(LIMIT / 16)` blocks) can use, which is where it stands once every block has been used; +/// 0 and anything beyond are not states the mode can have been in. The bound is the payload's, +/// not the counter field's: for `q = 8` the field runs to `u64::MAX`, where one more increment +/// would overflow. #[test] fn the_counter_index_is_held_to_its_range() { let state = mid_payload(); + let last = LIMIT / BLOCK_LEN as u64 + 2; assert!(resumes(with_u64(state, NEXT_CTR, 1)).is_ok()); - assert!(resumes(with_u64(state, NEXT_CTR, LIMIT + 1)).is_ok(), "every counter used"); + assert!(resumes(with_u64(state, NEXT_CTR, last)).is_ok(), "every counter used"); assert_eq!(resumes(with_u64(state, NEXT_CTR, 0)), Err(SuspendableError::InvalidData)); - assert_eq!(resumes(with_u64(state, NEXT_CTR, LIMIT + 2)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, last + 1)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, u64::MAX)), Err(SuspendableError::InvalidData)); +} + +/// The same bound at `q = 8` (a 7-byte nonce), where `MAX_COUNTER` is `u64::MAX` and a check of +/// the form `next_ctr - 1 <= MAX_COUNTER` holds for every value: the index stops one past the +/// last of the `ceil(u64::MAX / 16)` counter values a payload can use, and `u64::MAX` itself, +/// which the next increment would overflow, is refused. +#[test] +fn the_counter_index_is_held_to_its_range_at_q_8() { + const NONCE_LEN_7: usize = 7; + type Enc7<'k> = + CcmPacketEncryptor<'k, ToyBlockCipher, BLOCK_LEN, NONCE_LEN_7, 16, AAD_LEN, DATA_LEN>; + const N7: usize = Enc7::SUSPENDED_STATE_LEN; + let key: ToyKey> = + ToyKey::from_key_material(&key()).unwrap(); + let mut enc = key.create_encryptor_nonce(&[0x24u8; NONCE_LEN_7]); + enc.do_update_aad(b"aad").unwrap(); + let mut out = [0u8; 16]; + enc.do_encrypt_out(&[0x5Au8; 16], &mut out).unwrap(); + let state: [u8; N7] = enc.suspend(); + let resumes = |state: [u8; N7]| Enc7::from_suspended(state, key.state()).map(|_| ()); + let last = u64::MAX / BLOCK_LEN as u64 + 2; + assert!(resumes(with_u64(state, NEXT_CTR, 1)).is_ok()); + assert!(resumes(with_u64(state, NEXT_CTR, last)).is_ok(), "every counter used"); + assert_eq!(resumes(with_u64(state, NEXT_CTR, last + 1)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, u64::MAX)), Err(SuspendableError::InvalidData)); } /// A.3 Table 4 fixes the template's flags octet at `[q-1]_3` with every other bit zero, and the diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs index 71256137b..690557d18 100644 --- a/crypto/cipher/tests/suspend_tests.rs +++ b/crypto/cipher/tests/suspend_tests.rs @@ -15,6 +15,7 @@ use bouncycastle_cipher::modes::{ }; use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SuspendableError; use bouncycastle_core::hazmat::{EcbDecryptionKey, EcbEncryptionKey, Mode}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ @@ -27,6 +28,7 @@ use bouncycastle_core::traits::{ SymmetricCipherEncryptor, }; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; +use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; #[path = "common/toy_block_cipher.rs"] mod toy_block_cipher; @@ -226,6 +228,35 @@ fn ctr_mid_block_both_directions() { assert_eq!(plain, msg[7..]); } +/// GCTR starts at counter 2 (SP 800-38D Algorithm 4 step 3, `inc32(J0)`); J0 itself, counter 1, +/// is the tag mask and 0 is never used, so a state claiming either would emit `CIPH_K(J0)` as +/// keystream. The counter is the little-endian u64 after the nonce in the CTR component. +#[test] +fn gcm_refuses_a_resumed_counter_below_two() { + let key = key_for::>(); + let (mut enc, _) = key.create_encryptor(&mut os_rng()).unwrap(); + enc.do_update_aad(b"aad").unwrap(); + let mut out = [0u8; 20]; + enc.do_encrypt_out(&message(20), &mut out).unwrap(); + let good: [u8; ToyGcm::::SUSPENDED_STATE_LEN] = enc.suspend(); + let counter_at = LIB_VERSION_LEN + 12; + assert_eq!(&good[counter_at..counter_at + 8], &4u64.to_le_bytes(), "20 bytes: 2 blocks used"); + for bad_counter in [0u64, 1] { + let mut bad = good; + bad[counter_at..counter_at + 8].copy_from_slice(&bad_counter.to_le_bytes()); + assert!( + matches!( + ToyGcm::::from_suspended(bad, key.state()), + Err(SuspendableError::InvalidData) + ), + "counter {bad_counter} must be refused" + ); + } + let mut edge = good; + edge[counter_at..counter_at + 8].copy_from_slice(&2u64.to_le_bytes()); + assert!(ToyGcm::::from_suspended(edge, key.state()).is_ok(), "2 is the start"); +} + #[test] fn gcm_both_directions_with_aad() { let msg = message(45); diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs index c5d1e5e8d..3370d8935 100644 --- a/crypto/core-test-framework/src/aead.rs +++ b/crypto/core-test-framework/src/aead.rs @@ -376,6 +376,37 @@ impl TestFrameworkAEADCipher { assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); } + // an empty data call is a no-op that leaves the AAD phase open, in both directions: AAD + // given after it is absorbed as if the call had not happened. The family's rule, so that + // a generic caller may forward an empty buffer without closing the phase. + { + let (first, second) = aad.split_at(aad.len() / 2); + let (mut enc, nonce) = key.create_encryptor(&mut fixed_rng()).unwrap(); + enc.do_update_aad(first).unwrap(); + assert_eq!(enc.do_encrypt_out(&[], &mut []).unwrap(), 0); + enc.do_update_aad(second).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detached_out(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + assert_eq!(ct, ct_ref, "an empty data call must leave the AAD phase open (encrypt)"); + assert_eq!(tag, tag_ref, "an empty data call must leave the AAD phase open (encrypt)"); + + let mut dec = key.create_decryptor(&nonce); + dec.do_update_aad(first).unwrap(); + assert_eq!(dec.do_decrypt_out(&[], &mut []).unwrap(), 0); + dec.do_update_aad(second).unwrap(); + let mut pt = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + pt.truncate(n); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detached_out(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "an empty data call must leave the AAD phase open (decrypt)"); + } + // a wrong detached tag is refused at the final let mut wrong_tag = tag_ref; wrong_tag[0] ^= 0xFF; diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 211cb9220..485550db8 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -137,8 +137,14 @@ pub use bouncycastle_utils::suspendable_state::SuspendableError; pub enum SignatureError { /// GenericError(&'static str), - /// - ConsistencyCheckFailed(), + /// The public and private key of a pair do not belong together. + ConsistencyCheckFailed, + /// The algorithm bounds its rejection-sampling loop (FIPS 204 Section 6.2 allows ML-DSA to) + /// and the bound was reached without a signature; nothing was written. The hedged variant + /// draws fresh randomness on a retry; the deterministic variant fails the same way again. + RejectionLimitExceeded, + /// The pre-hash is weaker than the parameter set (FIPS 204 Section 5.4 for HashML-DSA). + PreHashTooWeak, /// EncodingError(&'static str), /// diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index ea41754e3..bf585fc62 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -221,7 +221,6 @@ pub trait KeyMaterialTrait: KeyMaterialInternalTrait { /// A wrapper for holding bytes-like key material (symmetric keys or seeds) which aims to apply a /// strict typing system to prevent many kinds of mis-use mistakes. /// The capacity of the internal buffer can be set at compile-time via the param. -#[derive(Clone)] pub struct KeyMaterial { buf: Secret<[u8; KEY_LEN]>, key_len: Secret, @@ -230,6 +229,20 @@ pub struct KeyMaterial { allow_hazardous_operations: bool, } +/// A clone never carries an open hazardous-operations window: the window belongs to the +/// `do_hazardous_operations` call that opened it, which closes only the original. +impl Clone for KeyMaterial { + fn clone(&self) -> Self { + Self { + buf: self.buf.clone(), + key_len: self.key_len.clone(), + key_type: self.key_type, + security_strength: self.security_strength, + allow_hazardous_operations: false, + } + } +} + // The explicit `#[repr(u8)]` discriminants are the stable on-the-wire encoding used by // `SerializableState` implementations (see the `TryFrom` impl below). Pin each value to its // variant name: reordering variants is fine, but never reuse or renumber an existing discriminant, @@ -305,23 +318,28 @@ impl KeyMaterial { } /// Creates a new instance of KeyMaterial containing random bytes from the provided random number generator. + /// + /// The key is tagged at the lesser of the RNG's strength and what `KEY_LEN` bytes can carry; + /// a `KeyMaterial<0>` comes back [`KeyType::Zeroized`] without consulting the RNG, so an RNG + /// that would refuse (reseed required) is not reported for it. pub fn from_rng(rng: &mut (impl RNG + ?Sized)) -> Result { let mut key = Self::new(); + if KEY_LEN == 0 { + return Ok(key); + } do_hazardous_operations(&mut key, |key| { - rng.next_bytes_out(&mut key.ref_to_bytes_mut().unwrap()) + rng.next_bytes_out(key.ref_to_bytes_mut()?) .map_err(|_| KeyMaterialError::GenericError("RNG failed."))?; - Ok(()) + key.set_key_len(KEY_LEN)?; + key.set_key_type(KeyType::CryptographicRandom)?; + // SP 800-90A r1 Section 8.4: "Security_strength_of_output = min(output_length, + // DRBG_security_strength)". + key.set_security_strength(*min( + &rng.security_strength(), + &SecurityStrength::from_bytes(KEY_LEN), + )) })?; - - *key.key_len = KEY_LEN; - key.key_type = KeyType::CryptographicRandom; - // The RNG's strength, capped at what `KEY_LEN` bytes can carry: a 16-byte key drawn from - // a 256-bit DRBG is a 128-bit key, and `set_security_strength` would refuse the higher - // figure. - let by_len = SecurityStrength::from_bits(KEY_LEN * 8); - let by_rng = rng.security_strength(); - key.security_strength = if by_rng < by_len { by_rng } else { by_len }; Ok(key) } @@ -461,49 +479,24 @@ impl KeyMaterialTrait for KeyMaterial { } fn set_key_type(&mut self, key_type: KeyType) -> Result<(), KeyMaterialError> { - if self.allow_hazardous_operations { - // just do it - self.key_type = key_type; - return Ok(()); - } - - match self.key_type { - KeyType::Zeroized => { - return Err(KeyMaterialError::ActingOnZeroizedKey); - } - KeyType::CryptographicRandom => { + if !self.allow_hazardous_operations { + match self.key_type { + KeyType::Zeroized => return Err(KeyMaterialError::ActingOnZeroizedKey), // raw full entropy can be safely converted to anything. - self.key_type = key_type; - } - KeyType::Unknown => match key_type { - KeyType::Unknown => { /* No change */ } - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::MACKey => match key_type { - KeyType::MACKey => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::SymmetricCipherKey => match key_type { - KeyType::SymmetricCipherKey => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { - return Err(KeyMaterialError::HazardousOperationNotPermitted); - } - }, - KeyType::Seed => match key_type { - KeyType::Seed => { /* No change */ } - // Else: Once a KeyMaterial is typed, it should stay that way. - _ => { + KeyType::CryptographicRandom => {} + // Else: once a KeyMaterial is typed, it should stay that way. + current if current != key_type => { return Err(KeyMaterialError::HazardousOperationNotPermitted); } - }, + _ => { /* No change */ } + } } + self.key_type = key_type; + // A Zeroized or Unknown key has no security strength (see `KeyType::Unknown`). + if key_type <= KeyType::Unknown { + self.security_strength = SecurityStrength::None; + } Ok(()) } @@ -576,6 +569,7 @@ impl KeyMaterialTrait for KeyMaterial { self.buf.zeroize(); self.key_len.zeroize(); self.key_type = KeyType::Zeroized; + self.security_strength = SecurityStrength::None; } fn equals(&self, other: &dyn KeyMaterialTrait) -> bool { @@ -599,16 +593,17 @@ impl KeyMaterialTrait for KeyMaterial { // set the metadata into.set_key_len(bytes_to_copy)?; into.set_key_type(self.key_type)?; - into.set_security_strength( - min(&self.security_strength(), &SecurityStrength::from_bytes(bytes_to_copy)) - .clone(), - )?; + into.set_security_strength(*min( + &self.security_strength(), + &SecurityStrength::from_bytes(bytes_to_copy), + ))?; Ok(()) }) - // Swallow all errors generated inside the hazardous operations closure. - // The set_* calls here are all infallible inside the hazardous closure. - .unwrap(); + // Infallible: inside the window the setters refuse only a strength the key cannot carry. + // The strength is capped at what `bytes_to_copy` bytes carry, and a Zeroized or Unknown + // source has strength None, because set_key_type and zeroize keep it so. + .expect("truncate's metadata is consistent by construction"); } } diff --git a/crypto/core/src/signature_key.rs b/crypto/core/src/signature_key.rs index 1cedb7984..99d4435b3 100644 --- a/crypto/core/src/signature_key.rs +++ b/crypto/core/src/signature_key.rs @@ -91,7 +91,8 @@ pub trait SigningKey { /// written, always `SIG_LEN`. /// /// # Errors - /// Those of [`create_signer`](Self::create_signer) and of the signer's final step. + /// Those of [`create_signer`](Self::create_signer) and of the signer's final step ([`SignatureError::RejectionLimitExceeded`] + /// for an algorithm that bounds its rejection loop). fn sign_out( &self, rng: &mut dyn RNG, @@ -219,7 +220,7 @@ pub trait PreHashSigningKey: /// signs the digest. /// /// # Errors - /// As [`SigningKey::create_signer`]; also [`SignatureError::GenericError`] if the algorithm + /// As [`SigningKey::create_signer`]; also [`SignatureError::PreHashTooWeak`] if the algorithm /// does not accept `H` (FIPS 204 Section 5.4 requires a hash of at least the parameter set's /// strength for HashML-DSA). fn create_ph_signer<'k, H: PreHash>( @@ -274,7 +275,7 @@ pub trait DeterministicPreHashSigningKey( &'k self, ctx: Option<&[u8]>, @@ -324,7 +325,7 @@ pub trait PreHashVerifyingKey: /// and verifies over the digest. /// /// # Errors - /// As [`VerifyingKey::create_verifier`]; also [`SignatureError::GenericError`] if the + /// As [`VerifyingKey::create_verifier`]; also [`SignatureError::PreHashTooWeak`] if the /// algorithm does not accept `H`. fn create_ph_verifier<'k, H: PreHash>( &'k self, diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index fd4d965c4..4859c05db 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -54,8 +54,9 @@ pub trait AEADCipherDecryptor Result<(), SymmetricCipherError>; /// Finishes the decryption with the tag detached, consuming the decryptor: decrypts whatever @@ -178,9 +179,10 @@ pub trait AEADCipherEncryptor Result<(), SymmetricCipherError>; @@ -865,8 +867,9 @@ pub trait SignatureSigner: AlgorithmInfo + Sized { /// Produces the signature. Consumes the worker. /// /// # Errors - /// Whatever the algorithm's own signing step can fail with; the key, the context and any - /// randomness were checked when the worker was made. + /// [`SignatureError::RejectionLimitExceeded`] for an algorithm that bounds its rejection loop, + /// and whatever else its signing step can fail with; the key, the context and any randomness + /// were checked when the worker was made. fn do_final(self) -> Result<[u8; SIG_LEN], SignatureError>; /// As [`do_final`](Self::do_final), into `output`, which is zeroized first; returns diff --git a/crypto/core/tests/key_material_tests.rs b/crypto/core/tests/key_material_tests.rs index dea104962..da5b605f3 100644 --- a/crypto/core/tests/key_material_tests.rs +++ b/crypto/core/tests/key_material_tests.rs @@ -233,6 +233,90 @@ mod test_key_material { assert_eq!(key.security_strength(), rng_strength, "64 bytes keep the RNG's strength"); } + /// SP 800-90A r1 Section 8.4: the strength of a draw is the lesser of the RNG's and the + /// draw's length, so a short key from a 256-bit RNG is tagged at what its length carries. + #[test] + fn from_rng_strength_is_capped_by_the_key_length() { + use bouncycastle_rng as rng; + + let mut rng = rng::DefaultRNG::default(); + use bouncycastle_core::traits::RNG; + assert_eq!(rng.security_strength(), SecurityStrength::_256bit); + let k32 = KeyMaterial256::from_rng(&mut rng).unwrap(); + assert_eq!(k32.security_strength(), SecurityStrength::_256bit); + let k24 = KeyMaterial::<24>::from_rng(&mut rng).unwrap(); + assert_eq!(k24.security_strength(), SecurityStrength::_192bit); + let k16 = KeyMaterial128::from_rng(&mut rng).unwrap(); + assert_eq!(k16.security_strength(), SecurityStrength::_128bit); + let k14 = KeyMaterial::<14>::from_rng(&mut rng).unwrap(); + assert_eq!(k14.security_strength(), SecurityStrength::_112bit); + let k13 = KeyMaterial::<13>::from_rng(&mut rng).unwrap(); + assert_eq!(k13.key_type(), KeyType::CryptographicRandom); + assert_eq!(k13.security_strength(), SecurityStrength::None); + let k0 = KeyMaterial0::from_rng(&mut rng).unwrap(); + assert_eq!(k0.key_len(), 0); + assert_eq!(k0.key_type(), KeyType::Zeroized); + assert_eq!(k0.security_strength(), SecurityStrength::None); + } + + /// A key re-typed or zeroized to Zeroized/Unknown carries no strength, so truncating it is + /// fine rather than a panic. + #[test] + fn retyping_to_unknown_drops_the_strength() { + use bouncycastle_rng as rng; + + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + key.set_key_type(KeyType::Unknown).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::None); + let mut short = KeyMaterial128::new(); + key.clone().truncate(&mut short); + assert_eq!(short.key_type(), KeyType::Unknown); + assert_eq!(short.security_strength(), SecurityStrength::None); + assert_eq!(short.ref_to_bytes(), &key.ref_to_bytes()[..16]); + + // The same inside a hazardous window. + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + do_hazardous_operations(&mut key, |k| k.set_key_type(KeyType::Unknown)).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::None); + + // And zeroize. + let mut key = KeyMaterial256::from_rng(&mut rng::DefaultRNG::default()).unwrap(); + key.zeroize(); + assert_eq!(key.security_strength(), SecurityStrength::None); + + // A typed key still refuses to be re-typed outside a window. + let mut key = + KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + assert!(matches!( + key.set_key_type(KeyType::Unknown), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert_eq!(key.security_strength(), SecurityStrength::_256bit); + } + + /// The window is closed on a clone taken inside it. + #[test] + fn a_clone_taken_inside_a_window_has_it_closed() { + let mut key = + KeyMaterial256::from_bytes_as_type(&DUMMY_KEY[..32], KeyType::MACKey).unwrap(); + let mut copy = None; + do_hazardous_operations(&mut key, |k| { + copy = Some(k.clone()); + Ok(()) + }) + .unwrap(); + let mut copy = copy.unwrap(); + assert!(matches!( + copy.set_key_type(KeyType::Seed), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert!(matches!( + copy.ref_to_bytes_mut(), + Err(KeyMaterialError::HazardousOperationNotPermitted) + )); + assert_eq!(copy.ref_to_bytes(), key.ref_to_bytes()); + } + #[test] fn zeroize() { let mut key = KeyMaterial256::from_bytes(&DUMMY_KEY[..32]).unwrap(); diff --git a/crypto/factory/src/kdf_factory.rs b/crypto/factory/src/kdf_factory.rs index ff0866ee9..48076aa4d 100644 --- a/crypto/factory/src/kdf_factory.rs +++ b/crypto/factory/src/kdf_factory.rs @@ -60,6 +60,7 @@ use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{ SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME, SHAKE128_NAME, SHAKE256_NAME, }; +use bouncycastle_sm3::hkdf::{HKDF_SM3, HKDF_SM3_NAME}; /// Wrapper object for all algorithms that impl [`KDF`]. #[non_exhaustive] @@ -73,6 +74,9 @@ pub enum KDFFactory { /// #[allow(non_camel_case_types)] HKDF_SHA512(HKDF_SHA512), + /// HKDF over SM3. + #[allow(non_camel_case_types)] + HKDF_SM3(HKDF_SM3), /// SHA3_224(sha3::SHA3_224), /// @@ -110,6 +114,7 @@ impl AlgorithmFactory for KDFFactory { HKDF_SHA256_NAME => Ok(Self::HKDF_SHA256(HKDF_SHA256::new())), HKDF_SHA384_NAME => Ok(Self::HKDF_SHA384(HKDF_SHA384::new())), HKDF_SHA512_NAME => Ok(Self::HKDF_SHA512(HKDF_SHA512::new())), + HKDF_SM3_NAME => Ok(Self::HKDF_SM3(HKDF_SM3::new())), SHA3_224_NAME => Ok(Self::SHA3_224(sha3::SHA3_224::new())), SHA3_256_NAME => Ok(Self::SHA3_256(sha3::SHA3_256::new())), SHA3_384_NAME => Ok(Self::SHA3_384(sha3::SHA3_384::new())), @@ -134,6 +139,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key(key, additional_input), Self::HKDF_SHA384(h) => h.derive_key(key, additional_input), Self::HKDF_SHA512(h) => h.derive_key(key, additional_input), + Self::HKDF_SM3(h) => h.derive_key(key, additional_input), Self::SHA3_224(h) => h.derive_key(key, additional_input), Self::SHA3_256(h) => h.derive_key(key, additional_input), Self::SHA3_384(h) => h.derive_key(key, additional_input), @@ -153,6 +159,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key_out(key, additional_input, output_key), Self::HKDF_SHA384(h) => h.derive_key_out(key, additional_input, output_key), Self::HKDF_SHA512(h) => h.derive_key_out(key, additional_input, output_key), + Self::HKDF_SM3(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_224(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_256(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_384(h) => h.derive_key_out(key, additional_input, output_key), @@ -171,6 +178,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => h.derive_key_from_multiple(keys, additional_input), Self::HKDF_SHA384(h) => h.derive_key_from_multiple(keys, additional_input), Self::HKDF_SHA512(h) => h.derive_key_from_multiple(keys, additional_input), + Self::HKDF_SM3(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_224(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_256(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_384(h) => h.derive_key_from_multiple(keys, additional_input), @@ -196,6 +204,7 @@ impl KDF for KDFFactory { Self::HKDF_SHA512(h) => { h.derive_key_from_multiple_out(keys, additional_input, output_key) } + Self::HKDF_SM3(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_224(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_256(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), Self::SHA3_384(h) => h.derive_key_from_multiple_out(keys, additional_input, output_key), @@ -212,6 +221,7 @@ impl AlgorithmInfo for KDFFactory { Self::HKDF_SHA256(h) => h.alg_name(), Self::HKDF_SHA384(h) => h.alg_name(), Self::HKDF_SHA512(h) => h.alg_name(), + Self::HKDF_SM3(h) => h.alg_name(), Self::SHA3_224(h) => h.alg_name(), Self::SHA3_256(h) => h.alg_name(), Self::SHA3_384(h) => h.alg_name(), @@ -226,6 +236,7 @@ impl AlgorithmInfo for KDFFactory { Self::HKDF_SHA256(h) => h.max_security_strength(), Self::HKDF_SHA384(h) => h.max_security_strength(), Self::HKDF_SHA512(h) => h.max_security_strength(), + Self::HKDF_SM3(h) => h.max_security_strength(), Self::SHA3_224(h) => h.max_security_strength(), Self::SHA3_256(h) => h.max_security_strength(), Self::SHA3_384(h) => h.max_security_strength(), diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index b6df9e8b5..9500beca1 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -95,12 +95,12 @@ use bouncycastle_sm3 as sm3; use bouncycastle_sm3::hmac::HMAC_SM3_NAME; /*** Defaults ***/ -/// +/// The default MAC: HMAC-SHA256, a 128-bit MAC. pub const DEFAULT_MAC_NAME: &str = HMAC_SHA256_NAME; -/// +/// The default 128-bit MAC: HMAC-SHA256. pub const DEFAULT_128BIT_MAC_NAME: &str = HMAC_SHA256_NAME; -/// -pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; +/// The default 256-bit MAC: HMAC-SHA512, as the 256-bit KDF default is HKDF-SHA512. +pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA512_NAME; /// A MAC key selected by name: the key object of whichever algorithm was named, which hands out /// [`MACFactoryWorker`]s through [`MacGenerationKey`] and [`MacVerificationKey`]. diff --git a/crypto/factory/tests/kdf_factory_tests.rs b/crypto/factory/tests/kdf_factory_tests.rs index 6ab26ee85..fbebc2b17 100644 --- a/crypto/factory/tests/kdf_factory_tests.rs +++ b/crypto/factory/tests/kdf_factory_tests.rs @@ -10,6 +10,7 @@ mod kdf_factory_tests { use bouncycastle_factory::AlgorithmFactory; use bouncycastle_factory::kdf_factory::KDFFactory; use bouncycastle_sha2::hkdf::HKDF_SHA384; + use bouncycastle_sm3::hkdf::HKDF_SM3; use bouncycastle_utils::ct; /// `AlgorithmInfo` answers for the algorithm the factory selected, not for the factory. @@ -21,10 +22,12 @@ mod kdf_factory_tests { SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME, SHAKE128_NAME, SHAKE256_NAME, }; + use bouncycastle_sm3::hkdf::HKDF_SM3_NAME; let table = [ (HKDF_SHA256_NAME, _128bit), (HKDF_SHA384_NAME, _192bit), (HKDF_SHA512_NAME, _256bit), + (HKDF_SM3_NAME, _128bit), (SHA3_224_NAME, _112bit), (SHA3_256_NAME, _128bit), (SHA3_384_NAME, _192bit), @@ -110,6 +113,42 @@ mod kdf_factory_tests { assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), expected_key.ref_to_bytes())); assert_eq!(derived_key.key_len(), 48, "HKDF-SHA384 derives a hash-length key"); + /* HKDF-SM3 */ + // As for HKDF-SHA384: HKDF_SM3 is checked against known answers in bouncycastle-sm3, so + // here the factory's output is compared with the alias used directly. + let key_material = KeyMaterial256::from_bytes(&DUMMY_SEED[..32]).unwrap(); + let derived_key = + KDFFactory::new("HKDF-SM3").unwrap().derive_key(&key_material, b"info").unwrap(); + let expected_key = HKDF_SM3::new().derive_key(&key_material, b"info").unwrap(); + assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), expected_key.ref_to_bytes())); + assert_eq!(derived_key.key_len(), 32, "HKDF-SM3 derives a hash-length key"); + + // The _out forms fill a longer buffer and report how much they wrote. + let mut out = KeyMaterial512::new(); + let n = KDFFactory::new("HKDF-SM3") + .unwrap() + .derive_key_out(&key_material, b"info", &mut out) + .unwrap(); + let mut expected_out = KeyMaterial512::new(); + let expected_n = + HKDF_SM3::new().derive_key_out(&key_material, b"info", &mut expected_out).unwrap(); + assert_eq!(n, 64); + assert_eq!(n, expected_n); + assert!(ct::ct_eq_bytes(out.ref_to_bytes(), expected_out.ref_to_bytes())); + + let salt = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::MACKey).unwrap(); + let n = KDFFactory::new("HKDF-SM3") + .unwrap() + .derive_key_from_multiple_out(&[&salt, &key_material], b"info", &mut out) + .unwrap(); + let expected_n = HKDF_SM3::new() + .derive_key_from_multiple_out(&[&salt, &key_material], b"info", &mut expected_out) + .unwrap(); + assert_eq!(n, 64); + assert_eq!(n, expected_n); + assert!(ct::ct_eq_bytes(out.ref_to_bytes(), expected_out.ref_to_bytes())); + /* HKDF-SHA512 */ // Note: this value is not checked against any external reference implementation, // The value is hard-coded to ensure consistency. diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index 76d1d9c34..7d0042211 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -8,6 +8,33 @@ mod hash_factory_tests { use bouncycastle_hex as hex; /// `AlgorithmInfo` answers for the algorithm the factory selected, not for the factory. + /// Each default is at least as strong as its name says, and so refuses a key below that. + #[test] + fn defaults_meet_their_strength() { + use bouncycastle_factory::mac_factory::{ + DEFAULT_128BIT_MAC_NAME, DEFAULT_256BIT_MAC_NAME, DEFAULT_MAC_NAME, + }; + let key32 = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::MACKey).unwrap(); + let key16 = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::MACKey).unwrap(); + + let k = MACFactory::default_256_bit(&key32).unwrap(); + let m = k.create_generator(); + assert_eq!(m.max_security_strength(), SecurityStrength::_256bit); + assert_eq!(m.alg_name(), DEFAULT_256BIT_MAC_NAME); + assert!(MACFactory::default_256_bit(&key16).is_err()); + assert!(MACFactory::new("Default256Bit", &key16).is_err()); + + let k = MACFactory::default_128_bit(&key16).unwrap(); + let m = k.create_generator(); + assert!(m.max_security_strength() >= SecurityStrength::_128bit); + assert_eq!(m.alg_name(), DEFAULT_128BIT_MAC_NAME); + + let k = MACFactory::default(&key16).unwrap(); + let m = k.create_generator(); + assert!(m.max_security_strength() >= SecurityStrength::_128bit); + assert_eq!(m.alg_name(), DEFAULT_MAC_NAME); + } + #[test] fn alg_name_and_strength_follow_the_variant() { use SecurityStrength::*; diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index bed49ab0e..a42aae2c2 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -1,5 +1,7 @@ //! The generic HMAC-based Extract-and-Expand Key Derivation Function (HKDF) construction, as per -//! RFC 5869, as allowed by NIST SP 800-56Cr2. +//! RFC 5869, which NIST SP 800-56Cr2 Section 5.1 describes as "a version of the above +//! extraction-then-expansion key-derivation procedure using HMAC for both the extraction and +//! expansion steps". //! //! This is a utility crate and is not intended to be used directly. It provides [`HKDF`] -- the //! construction, generic over any hash that implements [`HMACParams`], the extension point through @@ -87,7 +89,10 @@ //! * HKDF is keyed by its `salt`, which keys the extract-phase HMAC. The salt is deliberately //! excluded from the suspended state and must be re-supplied on resume; resuming with a different //! salt cannot be detected and silently produces a different PRK. -//! * Entropy is credited only for input supplied via [`HKDF::do_extract_update_key`]. Bytes supplied +//! * Entropy is credited only for input supplied via [`HKDF::do_extract_update_key`], and for a +//! salt typed [`KeyType::MACKey`], which is how a caller marks it as the secret salt of SP +//! 800-56Cr2 Section 5.1. A salt of any other type is RFC 5869 Section 2.2's "non-secret random +//! value" and credits nothing: anyone who knows it can compute the PRK from the IKM. Bytes supplied //! via [`HKDF::do_extract_update_bytes`] are treated as uncredited context, so a PRK derived only //! from raw bytes will not be tagged as full-entropy key material even if those bytes were random. //! [`HKDF::do_extract_update_key`] is rejected after the first call to @@ -176,7 +181,9 @@ struct HkdfExtract { inner: H, } -// Note: does not need to impl Drop because HKDF itself does not hold any sensitive state data. +// No Drop impl of its own: the sensitive state is the salt-keyed pad states inside `hmac` (RFC +// 2104 Section 4 says they "need to be treated and protected the same as secret keys"), and the +// hash types zeroize those in their own Drop. #[derive(Clone, Debug, PartialOrd, PartialEq)] #[repr(u8)] @@ -218,6 +225,10 @@ struct HkdfEntropyTracker { } impl HkdfEntropyTracker { + /// The most the label can say: HMAC's strength is bounded by half the hash output (the + /// `min(K, 2C)` rule), so no input, and no resumed state, raises it past this. + const MAX_STRENGTH: SecurityStrength = SecurityStrength::from_bytes(H::OUTPUT_LEN / 2); + fn new() -> Self { Self { _phantomhash: PhantomData, entropy: 0, security_strength: SecurityStrength::None } } @@ -229,8 +240,7 @@ impl HkdfEntropyTracker { // Saturating: the counter is bookkeeping, and a resumed state may carry any value. self.entropy = self.entropy.saturating_add(additional_entropy); self.security_strength = max(&self.security_strength, &key.security_strength()).clone(); - self.security_strength = - min(&self.security_strength, &SecurityStrength::from_bytes(H::OUTPUT_LEN / 2)).clone(); + self.security_strength = min(&self.security_strength, &Self::MAX_STRENGTH).clone(); additional_entropy } @@ -304,13 +314,16 @@ impl /// Output: /// PRK a pseudorandom key (of HashLen octets) /// - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the IKM can be of any type, and credits entropy only + /// when it is a full-entropy one. The salt credits entropy only when it is typed + /// [`KeyType::MACKey`], a secret salt; see the crate's Security Considerations. The type of the + /// output is set from what was credited. /// The output KeyMaterial will be of fixed size, with a capacity large enough to cover any /// underlying hash function, but the actual key length will be appropriate to the underlying hash function. /// /// Salt is optional, which is indicated by providing an uninitialized KeyMaterial object of length zero, - /// the capacity is irrelevant, so KeyMateriol256::new() or KeyMaterial_internal::<0>::new() would both count as an absent salt. + /// the capacity is irrelevant, so KeyMaterial256::new() or KeyMaterial0::new() would both count as an absent salt. pub fn extract( salt: &impl KeyMaterialTrait, ikm: &impl KeyMaterialTrait, @@ -358,8 +371,9 @@ impl /// therefore this function is provided only as expand_out(), filling the provided KeyMaterial object, /// and no analogous expand() is provided. /// - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the IKM can be of any type. The type of the output is + /// set from what was credited, as [`Self::extract_out`] says. /// /// L is the output length. [`KDFError::InvalidLength`] is returned if L exceeds 255 * HashLen, /// if `okm` cannot hold L bytes, or if `prk` is shorter than HashLen (RFC 5869 Section 2.3). @@ -392,7 +406,7 @@ impl let hash_len = H::OUTPUT_LEN; if L > 255 * hash_len { return Err(KDFError::InvalidLength( - "HMAC can not produce more than 255*HashLen bytes out output", + "HKDF-Expand cannot produce more than 255*HashLen bytes of output", )); } @@ -499,7 +513,7 @@ impl } /// Salt is optional, which is indicated by providing an uninitialized KeyMaterial object of length zero, - /// the capacity is irrelevant, so KeyMateriol256::new() or KeyMaterial_internal::<0>::new() would both count as an absent salt. + /// the capacity is irrelevant, so KeyMaterial256::new() or KeyMaterial0::new() would both count as an absent salt. #[allow(non_snake_case)] pub fn extract_and_expand_out( salt: &impl KeyMaterialTrait, @@ -554,16 +568,17 @@ impl /// This, together with [`HKDF::do_extract_update_key`], [`HKDF::do_extract_update_bytes`] and [`HKDF::do_extract_final`] /// provide a streaming interface for very long values of `ikm`. - /// In this mode, the entropy of `ikm` is untracked, and so only the entropy ef `salt` is taken into account - /// when computing the entropy of the output `prk`. - /// The KeyMaterial input parameters can be of any [`KeyType`] but [`KeyType::Unknown`], which is - /// refused as [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from the inputs. + /// In this mode, entropy is credited for the keys given to [`HKDF::do_extract_update_key`] and + /// for a salt typed [`KeyType::MACKey`] (a secret salt); a salt of any other type credits + /// nothing, since RFC 5869 Section 2.2's salt is "a non-secret random value". + /// The salt can be of any [`KeyType`] but [`KeyType::Unknown`], which is refused as + /// [`KeyMaterialError::InvalidKeyType`]; the type of the output is set from what was credited. /// The output KeyMaterial will be of fixed size, with a capacity large enough to cover any /// underlying hash function, but the actual key length will be appropriate to the underlying hash function. /// /// Salt is optional; to omit it, provide a KeyMaterial0, which will cause HKDF to use the default all-zero salt. /// - /// Returns the number of bits of entropy credited to this input key material. + /// Returns the number of bytes of entropy credited to this input key material. pub fn do_extract_init(&mut self, salt: &impl KeyMaterialTrait) -> Result { if self.state >= HkdfStates::Initialized { return Err(MACError::InvalidState("Initialized twice")); @@ -576,7 +591,12 @@ impl let inner = hmac_hazmat::inner_state(&key); self.hmac = Some(HkdfExtract { key, inner }); - let additional_entropy = self.entropy.credit_entropy(salt); + // RFC 5869 Section 2.2 defines the salt as "a non-secret random value", and SP 800-133r2 + // Section 6.3 note (g) counts the component keys' entropy, not the salt's: a salt the + // caller has not marked as a key credits nothing, or a public salt alone would label the + // PRK full-entropy key material. A MACKey salt is SP 800-56Cr2 Section 5.1's secret salt. + let additional_entropy = + if salt.key_type() == KeyType::MACKey { self.entropy.credit_entropy(salt) } else { 0 }; self.state = HkdfStates::Initialized; Ok(additional_entropy) @@ -587,7 +607,7 @@ impl /// This function may be called zero or more times in a workflow. /// In particular, this function may be called multiple times to add more than one IKM. /// - /// Returns the number of bits of entropy credited to this input key material. + /// Returns the number of bytes of entropy credited to this input key material. pub fn do_extract_update_key( &mut self, ikm: &impl KeyMaterialTrait, @@ -620,7 +640,7 @@ impl /// State machine: this function must be called after [`HKDF::do_extract_init`], followed by /// zero or more calls of [`HKDF::do_extract_update_key`], and before [`HKDF::do_extract_final`]. /// - /// Returns the number of bits of entropy credited to this input key material, which is always 0 for this function. + /// Returns the number of bytes of entropy credited to this input key material, which is always 0 for this function. pub fn do_extract_update_bytes(&mut self, ikm_chunk: &[u8]) -> Result { if self.state == HkdfStates::Uninitialized { return Err(MACError::InvalidState( @@ -642,7 +662,7 @@ impl } /// Finish the HKDF-Extract phase and fill the provided `prk`. - /// Note that the provided KeyMaterial must be correctly sized to the HMAC block length. + /// The PRK is `HashLen` bytes (RFC 5869 Section 2.2), which the provided KeyMaterial always holds. #[allow(non_snake_case)] pub fn do_extract_final_out( self, @@ -683,8 +703,10 @@ impl } } -/// HKDF's extract-then-expand is the two-step key derivation of NIST SP 800-56Cr2 Section 5.1, with -/// HMAC as the auxiliary MAC. The default salt is empty: RFC 5869 Section 2.2 sets an absent salt +/// HKDF's extract-then-expand is RFC 5869's, which NIST SP 800-56Cr2 Section 5.1 names as "a +/// version of" its two-step key derivation "using HMAC for both the extraction and expansion +/// steps"; the expansion step's input layout (`T(i-1) || info || i`) is RFC 5869's own rather than +/// SP 800-108r1 Section 4.2's feedback mode. The default salt is empty: RFC 5869 Section 2.2 sets an absent salt /// to "a string of HashLen zeros", SP 800-56Cr2 Section 5.1 to an all-zero string of one hash input /// block, and HMAC zero-pads its key to a block, so the three key HMAC identically (see /// `salt_as_mac_key`). All key material and additional_input is mapped to HKDF's ikm input. @@ -876,6 +898,20 @@ where usize::MAX, )?; let security_strength = SecurityStrength::try_from(state[13 + HASH_STATE_LEN])?; + // The bookkeeping is checked against what this type writes: the label never exceeds the + // hash's cap (credit_entropy applies it), and a state that has not been initialized + // carries no entropy and no strength (new() has none). Otherwise a state whose two + // bookkeeping bytes were edited would label a PRK derived from nothing as full-entropy + // material at a strength the hash cannot give. A state past Uninitialized whose entropy + // and strength were edited within the cap still resumes with that label: whether a key + // was credited is not recorded in the state, so the rule cannot be tighter than the cap + // without a new state field (2026-11-04 verification, P190-hkdf-1). + if security_strength > HkdfEntropyTracker::::MAX_STRENGTH + || (hkdf_state == HkdfStates::Uninitialized + && (entropy != 0 || security_strength != SecurityStrength::None)) + { + return Err(SuspendableError::InvalidData); + } Ok(HKDF { hmac, diff --git a/crypto/hkdf/tests/hkdf_tests.rs b/crypto/hkdf/tests/hkdf_tests.rs index 7fa4bb569..f08d52ffa 100644 --- a/crypto/hkdf/tests/hkdf_tests.rs +++ b/crypto/hkdf/tests/hkdf_tests.rs @@ -963,6 +963,80 @@ mod hkdf_tests { /// The entropy counter is bookkeeping a resumed state may set to anything: the next credit /// saturates rather than overflowing. + /// A salt is RFC 5869 Section 2.2's "non-secret random value" unless the caller types it as a + /// key: a `CryptographicRandom` salt credits nothing, so a PRK made from it and an empty IKM, + /// which anyone who knows the salt can compute, is `Unknown`; the same bytes typed `MACKey` + /// are the secret salt of SP 800-56Cr2 Section 5.1 and are credited. + #[test] + fn a_public_salt_credits_no_entropy() { + let empty = KeyMaterial0::new(); + let public = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::CryptographicRandom) + .unwrap(); + let prk = HKDF_SHA256::extract(&public, &empty).unwrap(); + assert_eq!(prk.key_type(), KeyType::Unknown); + assert_eq!(prk.security_strength(), SecurityStrength::None); + + let secret = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap(); + let prk_secret = HKDF_SHA256::extract(&secret, &empty).unwrap(); + assert_eq!(prk_secret.key_type(), KeyType::CryptographicRandom); + assert_eq!(prk_secret.security_strength(), SecurityStrength::_128bit); + assert_eq!(prk_secret.ref_to_bytes(), prk.ref_to_bytes(), "the same PRK bytes either way"); + + let mut hkdf = HKDF_SHA256::new(); + assert_eq!(hkdf.do_extract_init(&public).unwrap(), 0, "nothing credited for a public salt"); + assert_eq!(hkdf.get_entropy(), 0); + let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::Seed).unwrap(); + assert_eq!(hkdf.do_extract_update_key(&seed).unwrap(), 32, "the IKM is credited"); + assert!(hkdf.is_fully_seeded()); + } + + /// The two bookkeeping fields of a suspended state are checked on resume: a strength label + /// above what the hash can give (half its output), or any entropy or strength on a state that + /// was never initialized, is refused, where it used to label a PRK derived from nothing as + /// `CryptographicRandom` at `_256bit`. + #[test] + fn a_forged_state_is_refused() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_sha2::hkdf::SUSPENDED_HKDF_SHA256_STATE_LEN; + + let salt = KeyMaterial128::from_bytes_as_type(&DUMMY_SEED[..16], KeyType::MACKey).unwrap(); + // Layout: [version(3) | present flag | hmac blob | state | entropy(8) | strength]. + let entropy_idx = SUSPENDED_HKDF_SHA256_STATE_LEN - 9; + let strength_idx = SUSPENDED_HKDF_SHA256_STATE_LEN - 1; + let refused = |state, what: &str| { + assert!( + matches!( + HKDF_SHA256::from_suspended(state, &salt), + Err(SuspendableError::InvalidData) + ), + "{what} must be refused" + ); + }; + + let mut hkdf = HKDF_SHA256::new(); + hkdf.do_extract_init(&salt).unwrap(); + let good = hkdf.suspend(); + assert_eq!(good[strength_idx], SecurityStrength::_128bit as u8, "layout check"); + let mut forged = good; + forged[strength_idx] = SecurityStrength::_256bit as u8; + refused(forged, "a strength above HMAC-SHA256's 128-bit cap"); + let mut edge = good; + edge[strength_idx] = SecurityStrength::_128bit as u8; + assert!(HKDF_SHA256::from_suspended(edge, &salt).is_ok(), "the cap itself resumes"); + + let pre_init = HKDF_SHA256::new().suspend(); + assert!(HKDF_SHA256::from_suspended(pre_init, &salt).is_ok(), "an honest pre-init state"); + let mut forged = pre_init; + forged[entropy_idx..entropy_idx + 8].copy_from_slice(&64u64.to_le_bytes()); + refused(forged, "entropy on a state that was never initialized"); + let mut forged = pre_init; + forged[strength_idx] = SecurityStrength::_128bit as u8; + refused(forged, "a strength on a state that was never initialized"); + } + #[test] fn a_resumed_entropy_counter_saturates() { use bouncycastle_core::traits::SuspendableKeyed; diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 9c7638859..7dd5f7ef6 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -1,5 +1,5 @@ //! The generic Hash-Based Message Authentication Code (HMAC) construction, as specified in RFC 2104, -//! taking into account NIST Implementation Guidance in FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! taking into account NIST Implementation Guidance in FIPS 140-3 IG C.L. //! //! This is a utility crate and is not intended to be used directly. It provides [`HmacKey`] -- the //! key object, which holds the two precomputed pad states RFC 2104 Section 4 describes and hands @@ -10,13 +10,14 @@ //! //! | Hash family | Instantiations | //! |-------------|------------------------------------------------------------------------------| -//! | SHA-2 | `bouncycastle_sha2::hmac` -- `HMAC_SHA224` .. `HMAC_SHA512_256` | -//! | SHA-3 | `bouncycastle_sha3::hmac` -- `HMAC_SHA3_224` .. `HMAC_SHA3_512` | +//! | SHA-2 | `bouncycastle_sha2::hmac` -- `HMAC_SHA224`, `HMAC_SHA256`, `HMAC_SHA384`, `HMAC_SHA512`, `HMAC_SHA512_224`, `HMAC_SHA512_256` | +//! | SHA-3 | `bouncycastle_sha3::hmac` -- `HMAC_SHA3_224`, `HMAC_SHA3_256`, `HMAC_SHA3_384`, `HMAC_SHA3_512` | +//! | SM3 | `bouncycastle_sm3::hmac` -- `HMAC_SM3` | //! //! Although users are free to implement [`Hash`] and [`HMACParams`] for a a hash function not included with the library, //! and will then be able to instantiate [`HMAC`] for it as well. //! -//! # Usage +//! # Usage Examples //! //! A key is built once, from key material of any length, and used for any number of messages: //! @@ -105,9 +106,9 @@ //! * Verification via [`MAC::verify`] / [`MAC::do_verify_final`] uses a constant-time comparison. //! Recomputing the MAC and comparing it with `==` leaks how many leading bytes matched. //! * [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is the shortest truncation this crate will produce, per -//! FIPS 140-2 IG A.8 / NIST SP 800-107-r1 Section 5.3.3. It is a floor, not a recommendation: -//! RFC 2104 Section 5 recommends that the output length "be not less than half the length of the -//! hash output ... and not less than 80 bits". +//! FIPS 140-3 IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). It is a floor, +//! not a recommendation: RFC 2104 Section 5 recommends that the output length "be not less than +//! half the length of the hash output ... and not less than 80 bits". //! * The key is deliberately excluded from the suspended state and the [`HmacKey`] must be //! re-supplied on resume. Resuming with the wrong key cannot be detected and silently produces a //! wrong MAC, computed with different keys in the inner and outer pad. @@ -341,7 +342,7 @@ impl HmacKey { out.fill(0); if out.len() < MIN_FIPS_DIGEST_LEN { return Err(MACError::InvalidLength( - "HMAC truncation too short for FIPS 140-2 guidelines", + "HMAC truncation too short for FIPS 140-3 guidelines", )); } let mut ihash = vec![0u8; inner.output_len()]; @@ -389,13 +390,13 @@ pub struct HMAC<'k, HASH: HMACParams> { impl Debug for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "HMAC-{} instance", HASH::ALG_NAME,) + write!(f, "{} instance", HASH::HMAC_ALG_NAME) } } impl Display for HMAC<'_, HASH> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "HMAC-{} instance", HASH::ALG_NAME,) + write!(f, "{} instance", HASH::HMAC_ALG_NAME) } } @@ -403,11 +404,10 @@ impl Display for HMAC<'_, HASH> { const IPAD_BYTE: u8 = 0x36; const OPAD_BYTE: u8 = 0x5C; -/// Per FIPS 140-2 IG A.8 Use of a truncated HMAC (matching NIST SP 800-107-r1 -/// Section 5.3.3. Truncation of HMAC), says that the minimum truncation of a -/// HMAC for tagging should be 32 bits; this exceeds the lower bound set by -/// IETF RFC 2104 Section 5 Truncated output, which sets the lower bound to be -/// half of the hash's length and no fewer than 80 bits. +/// FIPS 140-3 IG C.L Resolution C.4, which carries over NIST SP 800-107r1 Section 5.3.3 while +/// that document is being withdrawn, says a truncated HMAC output "shall be no less than 32 bits"; +/// this is below the lower bound set by IETF RFC 2104 Section 5 Truncated output, which sets the +/// lower bound to be half of the hash's length and no fewer than 80 bits. /// /// However, as we feel there should be a minimum limit (and have an author /// work around this via explicit truncation manually afterwards), but not diff --git a/crypto/hmac/tests/hmac_key_tests.rs b/crypto/hmac/tests/hmac_key_tests.rs index d30cbd02f..db460a95c 100644 --- a/crypto/hmac/tests/hmac_key_tests.rs +++ b/crypto/hmac/tests/hmac_key_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey, Mac use bouncycastle_core::traits::{MAC, RNG, SuspendableKeyed}; use bouncycastle_hmac::{HMAC, HMACParams, HmacKey}; use bouncycastle_rng::FixedSeedRNG; -use bouncycastle_sha2::{SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; +use bouncycastle_sha2::{SHA224, SHA256, SHA512, SUSPENDED_SHA256_STATE_LEN}; use bouncycastle_sha3::SHA3_256; fn mac_key(fill: u8) -> KeyMaterial { @@ -176,6 +176,14 @@ fn generate_draws_a_key_of_the_hash_output_length_at_the_rng_strength() { HmacKey::::generate(&mut weak_rng), Err(MACError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) )); + + // A hash output shorter than the RNG's strength: the 28-byte SHA-224 key is tagged at what + // 28 bytes carry rather than refused for claiming the RNG's 256 bits. + let key = HmacKey::::generate(&mut FixedSeedRNG::new([0x5Au8; 32])) + .expect("a 256-bit RNG suffices for HMAC-SHA224"); + let tag = key.mac(MSG); + assert_eq!(tag.len(), 28); + assert!(key.verify(MSG, &tag)); } #[test] diff --git a/crypto/mldsa-common/src/aux_functions.rs b/crypto/mldsa-common/src/aux_functions.rs index 636c20fce..b5c2f2de5 100644 --- a/crypto/mldsa-common/src/aux_functions.rs +++ b/crypto/mldsa-common/src/aux_functions.rs @@ -687,12 +687,14 @@ pub fn make_hint(low: i32, high: i32) -> i32 { // // 3: return [[𝑟1 ≠ 𝑣1]] // if r1 != v1 { 1 } else { 0 } - // mutants note: we do not have KATs that exercise all branches of this if - if low <= P::gamma2 || low > q - P::gamma2 || (low == q - P::gamma2 && high == 0) { - 0 - } else { - 1 - } + // Branch-free form of `low <= γ2 || low > q - γ2 || (low == q - γ2 && high == 0)`: each + // comparison is the sign bit of a difference of values in [0, q), so nothing overflows, and + // the combined mask is all ones exactly when the hint is 0. + let le = (low - P::gamma2 - 1) >> 31; // low <= γ2 + let gt = (q - P::gamma2 - low) >> 31; // low > q - γ2 + let eq = ((low ^ (q - P::gamma2)) - 1) >> 31; // low == q - γ2 + let high_zero = (high - 1) >> 31; // high == 0 + (le | gt | (eq & high_zero)) + 1 } /// Algorithm 40 UseHint(ℎ, 𝑟) diff --git a/crypto/mldsa-common/src/polynomial.rs b/crypto/mldsa-common/src/polynomial.rs index 009549768..4e187ee0f 100644 --- a/crypto/mldsa-common/src/polynomial.rs +++ b/crypto/mldsa-common/src/polynomial.rs @@ -132,13 +132,6 @@ impl Polynomial { /// type parameter cannot be used as a const generic argument. It is still a constant after /// monomorphization, so this costs nothing. pub fn check_norm(&self, bound: i32) -> bool { - // It is acceptable that this function is not constant-time (returns true early) - // The reason being because it is used in a rejection loop. - // That is, the early quit here leads to rejection, dropping the secret values and - // continuing to the top of the rejection loop with generating new secret values, - // or failing the signature validation. - // So the i32 that we just checked in a non-constant-time manner is about to get thrown away. - // Note: this formulation of the check_norm function usually requires this bounds check // if bound > (q - 1) / 8 { // return true; @@ -147,16 +140,16 @@ impl Polynomial { // instead to make sure the value is what we expect. debug_assert!(bound <= (q - 1) / 8); - let mut t: i32; + // Branch-free: the early return of the reference implementation reveals only that a + // candidate was rejected, but also the index of the first coefficient over the bound, + // a position-dependent fact about the rejected y + c*s1. |x| < 2^31 - 1 and bound > 0, so + // `bound - 1 - t` cannot overflow and is negative exactly when t >= bound. + let mut acc: i32 = 0; for x in self.coeffs.iter() { - t = *x >> 31; - t = *x - (t & (2 * *x)); - - if t >= bound { - return true; - } + let t = *x - ((*x >> 31) & (2 * *x)); + acc |= (bound - 1 - t) >> 31; } - false + acc != 0 } /// Multiplies every coefficient by 2^𝑑. diff --git a/crypto/mldsa-common/tests/rounding_tests.rs b/crypto/mldsa-common/tests/rounding_tests.rs new file mode 100644 index 000000000..e3f5847dc --- /dev/null +++ b/crypto/mldsa-common/tests/rounding_tests.rs @@ -0,0 +1,95 @@ +//! The branch-free forms of `make_hint` and `Polynomial::check_norm` against the comparisons they +//! replace, over every input the signer can feed them. These pin the value, which the comparison +//! forms also gave, so they pass on the old code too: that the new forms compile without a +//! branch on the secret is shown by reading the release assembly, which no in-tree test can do. + +use bouncycastle_mldsa_common::aux_functions::make_hint; +use bouncycastle_mldsa_common::params::{MLDSA44Params, MLDSA65Params, MLDSAParamsBase}; +use bouncycastle_mldsa_common::polynomial::Polynomial; +use bouncycastle_mldsa_common::q; + +/// `make_hint` as the comparisons of its doc comment, which are FIPS 204 Algorithm 39 in the +/// reference implementation's (low, high) form. +fn make_hint_by_comparison(low: i32, high: i32) -> i32 { + if low <= P::gamma2 || low > q - P::gamma2 || (low == q - P::gamma2 && high == 0) { + 0 + } else { + 1 + } +} + +fn make_hint_grid() { + let high_max = (q - 1) / (2 * P::gamma2); + for high in [0, 1, 2, high_max - 1, high_max] { + for low in 0..q { + assert_eq!( + make_hint::

(low, high), + make_hint_by_comparison::

(low, high), + "low {low} high {high}" + ); + } + } +} + +#[test] +fn make_hint_matches_the_comparisons_for_gamma2_88() { + make_hint_grid::(); +} + +#[test] +fn make_hint_matches_the_comparisons_for_gamma2_32() { + make_hint_grid::(); +} + +/// `check_norm` as FIPS 204 reads it: true when any |coefficient| >= bound. +fn check_norm_by_comparison(p: &Polynomial, bound: i32) -> bool { + (0..256).any(|i| p[i].abs() >= bound) +} + +#[test] +fn check_norm_matches_the_comparison_at_every_index_and_edge() { + let bounds = [ + MLDSA44Params::gamma1_minus_beta, + MLDSA44Params::gamma2_minus_beta, + MLDSA44Params::gamma2, + MLDSA65Params::gamma1_minus_beta, + MLDSA65Params::gamma2_minus_beta, + MLDSA65Params::gamma2, + 1, + ]; + for bound in bounds { + for i in 0..256 { + for v in [bound - 1, bound, bound + 1, -(bound - 1), -bound, -(bound + 1), 0] { + let mut p = Polynomial::new(); + p[i] = v; + assert_eq!( + p.check_norm(bound), + check_norm_by_comparison(&p, bound), + "bound {bound} index {i} value {v}" + ); + } + } + } +} + +#[test] +fn check_norm_matches_the_comparison_on_mixed_polynomials() { + // A small LCG so the test needs no RNG crate; values spread over (-q, q). + let mut x: u32 = 0x2545_F491; + let mut next = || { + x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + (x % (2 * q as u32)) as i32 - q + }; + let bound = MLDSA44Params::gamma1_minus_beta; + for _ in 0..200 { + let mut p = Polynomial::new(); + for i in 0..256 { + // Mostly in range, so that both outcomes occur. + p[i] = next() % bound; + } + if next() & 1 == 0 { + p[(next().unsigned_abs() % 256) as usize] = next(); + } + assert_eq!(p.check_norm(bound), check_norm_by_comparison(&p, bound)); + } +} diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 3570b9691..4ad8254c2 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -18,7 +18,7 @@ pub(crate) fn unpack_c_tilde(sig: &[u8]) -> P::SigCTilde { pub(crate) fn unpack_z_row( idx: usize, sig: &[u8; SIG_LEN], -) -> Result { +) -> Option { debug_assert!(idx < P::l); // skip to the start of the z's @@ -29,7 +29,7 @@ pub(crate) fn unpack_z_row( // Perform the norm check from // Alg 8; Line 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] - if z.check_norm(P::gamma1_minus_beta) { Err(()) } else { Ok(z) } + if z.check_norm(P::gamma1_minus_beta) { None } else { Some(z) } } /// Part of unpacking the sig value pub(crate) fn unpack_h_row( diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 58a9ac67a..29500a9fc 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -2,8 +2,10 @@ //! message, for the case where the message is hashed before the signing key is known or on a //! different machine. //! -//! The hash is a type parameter of the key's pre-hash methods, so any approved hash goes with any -//! ML-DSA key: +//! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the +//! parameter set's strength goes with any ML-DSA key, SHAKE128 and SHAKE256 among them as the +//! fixed-length digests of RFC 8702 Section 3.1 (Algorithm 4's third case is SHAKE128); that it +//! is an approved one (FIPS 204 Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; @@ -125,9 +127,7 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// FIPS 204 Section 5.4: the hash must give at least the parameter set's strength. fn check_hash_strength() -> Result<(), SignatureError> { if H::MAX_SECURITY_STRENGTH < P::MAX_SECURITY_STRENGTH { - return Err(SignatureError::GenericError( - "HashML-DSA: the pre-hash is weaker than the ML-DSA parameter set (FIPS 204 Section 5.4)", - )); + return Err(SignatureError::PreHashTooWeak); } Ok(()) } @@ -226,7 +226,7 @@ fn sign_ph_with_rnd< check_hash_strength::()?; check_ph_len::(ph)?; // Algorithm 4, lines 1 and 23, and Algorithm 7, line 6 - let mu = compute_mu_ph(&sk.tr(), ctx, H::OID_DER, ph)?; + let mu = compute_mu_ph(sk.tr(), ctx, H::OID_DER, ph)?; // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀′, 𝑟𝑛𝑑) sk.sign_mu_with_rnd_out(&mu, rnd, output) } diff --git a/crypto/mldsa-lowmemory/src/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index 80d05a946..3c7307c66 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -217,13 +217,23 @@ //! One caveat: `ctx` can still be security-relevant at the protocol level (domain separation, replay and //! cross-protocol binding), so choosing it incorrectly can weaken those properties. //! +//! ## When signing fails +//! +//! FIPS 204 Section 6.2 lets an implementation bound the rejection-sampling loop, and this one +//! does, at 1000·k iterations. Reaching it has negligible probability; when it happens, signing +//! returns [`SignatureError::RejectionLimitExceeded`] and writes nothing. The hedged variant +//! succeeds on a retry with fresh randomness; the deterministic one fails the same way again. +//! +//! [`SignatureError::RejectionLimitExceeded`]: bouncycastle_core::errors::SignatureError::RejectionLimitExceeded +//! //! ## What is zeroized //! //! FIPS 204 Section 3.6.3 asks that "any potentially sensitive intermediate data is destroyed as //! soon as it is no longer needed". Here the private key's `K` and `rho'`, the packed `s1` and -//! `s2` it expands on demand, and the seed are `Secret` or `KeyMaterial`, wiped on drop. The -//! per-row vectors signing works through are plain stack values that are not wiped when their -//! scope ends. +//! `s2` it expands on demand, the seed, and the signing randomizer `rho''` are `Secret` or +//! `KeyMaterial`, wiped on drop, on the error paths included. The per-row vectors signing works +//! through, and the `s1` and `s2` rows unpacked for them, are plain stack values that are not +//! wiped when their scope ends. #![no_std] #![forbid(unsafe_code)] @@ -273,7 +283,7 @@ pub use hash_mldsa::HASH_ML_DSA_44_with_SHA512_NAME; pub use hash_mldsa::HASH_ML_DSA_65_with_SHA512_NAME; pub use hash_mldsa::HASH_ML_DSA_87_with_SHA512_NAME; -pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_TR_LEN}; +pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_SEED_LEN, MLDSA_TR_LEN}; pub use mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; pub use mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; pub use mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; diff --git a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs index b6235f1b1..cdb3d1f53 100644 --- a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs @@ -4,7 +4,6 @@ use crate::aux_functions::unpack_z_row; use crate::params::MLDSAParams; -use bouncycastle_core::errors::SignatureError; use bouncycastle_mldsa_common::aux_functions::{ bit_unpack_eta_out, expand_mask_poly, rej_ntt_poly, }; @@ -51,7 +50,7 @@ pub(crate) fn compute_wp_approx_row( t1: &Polynomial, c_hat: &Polynomial, idx: usize, -) -> Result { +) -> Option { // Algorithm 8: line 9: 𝐰′_approx ← NTT−1(𝐀_hat ∘ NTT(𝐳) − NTT(𝑐) ∘ NTT(𝐭1 ⋅ 2^𝑑)) // broken out for clarity: // NTT−1( @@ -99,7 +98,7 @@ pub(crate) fn compute_wp_approx_row( Az_acc.inv_ntt(); Az_acc.conditional_add_q(); - Ok(Az_acc) + Some(Az_acc) } pub(crate) fn compute_z_component( @@ -108,7 +107,7 @@ pub(crate) fn compute_z_component( c_hat: &Polynomial, kappa: u16, col: usize, -) -> Result, SignatureError> { +) -> Option { let y = expand_mask_poly::

(rho_p_p, kappa + col as u16); let mut s1_hat = *s1; s1_hat.ntt(); @@ -118,7 +117,7 @@ pub(crate) fn compute_z_component( let mut z = cs1; z.add_ntt(&y); - if z.check_norm(P::gamma1_minus_beta) { Ok(None) } else { Ok(Some(z)) } + if z.check_norm(P::gamma1_minus_beta) { None } else { Some(z) } } pub(crate) fn compute_w0cs2_component( diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index b46bae0cd..02d5a9256 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -533,10 +533,10 @@ impl MLDSATrait

for MLDSA

{ sk: &MLDSASeedPrivateKey

, ) -> Result<(), SignatureError> { // tr = H(pk) was computed when the key was built, so no derivation is needed here. - if bouncycastle_utils::ct::ct_eq_bytes(&sk.tr(), &pk.compute_tr()) { + if bouncycastle_utils::ct::ct_eq_bytes(sk.tr(), &pk.compute_tr()) { Ok(()) } else { - Err(SignatureError::ConsistencyCheckFailed()) + Err(SignatureError::ConsistencyCheckFailed) } } } @@ -586,14 +586,14 @@ pub(crate) fn sign_internal< // skip: mu has already been provided // Alg 7; 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) - let rho_p_p: [u8; 64] = { + let rho_p_p: Secret<[u8; 64]> = { let mut h = H::new(); h.do_update(sk.K()); h.do_update(&rnd); h.do_update(mu); - let mut rho_p_p = [0u8; 64]; + let mut rho_p_p: Secret<[u8; 64]> = Secret::new(); let mut h = h.into_squeezer(); - h.do_output_out(&mut rho_p_p); + h.do_output_out(&mut *rho_p_p); rho_p_p }; @@ -610,9 +610,11 @@ pub(crate) fn sign_internal< // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't have access to a KAT that will exceed this limit. if kappa > 1000 * P::k as u16 { - return Err(SignatureError::GenericError( - "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", - )); + // "the signing algorithm shall return a constant that represents an error and no + // other output, destroying the results of the unsuccessful signing attempts" + // (FIPS 204 s. 6.2): the rejected candidate's c~ and z are in `output`. + output.fill(0); + return Err(SignatureError::RejectionLimitExceeded); } // 11-15: derive c_tilde without materializing y_hat or w as full vectors. @@ -657,7 +659,7 @@ pub(crate) fn sign_internal< &c_hat, kappa, col, - )? { + ) { Some(z) => z, None => { rejected = true; @@ -789,9 +791,9 @@ pub(crate) fn verify_internal< &c_hat, row, ) { - Ok(wp_approx) => wp_approx, + Some(wp_approx) => wp_approx, // means the norm check on z failed - Err(_) => return Err(SignatureError::SignatureVerificationFailed), + None => return Err(SignatureError::SignatureVerificationFailed), }; let h_i = match unpack_h_row::(row, sig) { @@ -877,9 +879,6 @@ pub(crate) fn draw_rnd(rng: &mut dyn RNG) -> Result<[u8; MLDSA_RND_LEN], Signatu Ok(rnd) } -/// An ML-DSA signature in progress: FIPS 204 Algorithm 7, with the message representative -/// (line 6) built as the message streams in. Made by [`SigningKey::create_signer`] on an ML-DSA -/// private key, which it borrows; the key is expanded from its seed only when the signature is /// An ML-DSA signature in progress: FIPS 204 Algorithm 7, with the message representative /// (line 6) built as the message streams in. Made by [`SigningKey::create_signer`] on an ML-DSA /// private key, which it borrows; the signing randomizer is fixed when the signer is made, and diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 5bc7e7f86..347d853c7 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -209,7 +209,8 @@ impl Eq for MLDSAPublicKey

{} impl PartialEq for MLDSAPublicKey

{ fn eq(&self, other: &Self) -> bool { - self.rho == other.rho && self.t1_packed.as_ref() == other.t1_packed.as_ref() + bouncycastle_utils::ct::ct_eq_bytes(&self.rho, &other.rho) + & bouncycastle_utils::ct::ct_eq_bytes(self.t1_packed.as_ref(), other.t1_packed.as_ref()) } } @@ -259,7 +260,7 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ fn seed(&self) -> Option<&KeyMaterial<32>>; /// Get a copy of the key hash `tr`, computed once when the key was built. - fn tr(&self) -> [u8; 64]; + fn tr(&self) -> &[u8; 64]; /// Derives the full public key from the seed: a key generation's worth of work. fn derive_pk(&self) -> MLDSAPublicKey

; /// This produces the full private key in the encoding specified in FIPS 204 Algorithm 24 skEncode() @@ -294,7 +295,7 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// key's `tr` and to `ctx`, which then takes the message in chunks. `ctx` longer than 255 /// bytes is the error of FIPS 204 Algorithm 2, line 1. fn create_mu_builder(&self, ctx: Option<&[u8]>) -> Result { - MuBuilder::from_tr(&self.tr(), ctx) + MuBuilder::from_tr(self.tr(), ctx) } /// Signs an externally computed message representative `mu`: FIPS 204 Algorithm 7 with @@ -308,6 +309,10 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// signature it verifies with a standard ML-DSA verifier, which need not know how `mu` was /// made. The hedged variant: `rnd` is drawn from `rng`. /// + /// # Errors + /// [`SignatureError::RejectionLimitExceeded`] if the bounded rejection loop (FIPS 204 + /// Section 6.2) runs out, which has negligible probability; nothing is written. + /// /// [`SigningKey::create_signer`]: bouncycastle_core::signature_key::SigningKey::create_signer fn sign_mu( &self, @@ -436,7 +441,7 @@ impl MLDSASeedPrivateKey

{ || seed.key_len() != 32 { return Err(SignatureError::KeyGenError( - "Seed must be 32 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 32 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -576,8 +581,8 @@ impl MLDSAPrivateKeyTrait

for MLDSASeedPrivateKey

{ Some(&self.seed) } - fn tr(&self) -> [u8; 64] { - self.tr + fn tr(&self) -> &[u8; 64] { + &self.tr } fn derive_pk(&self) -> MLDSAPublicKey

{ @@ -620,7 +625,7 @@ impl MLDSAPrivateKeyTrait

for MLDSASeedPrivateKey

{ out[0..32].copy_from_slice(&self.rho); // K is protected inside a Secret<[u8]>, but here we really do need to deref and copy it out. out[32..64].copy_from_slice(&*self.K); - out[64..128].copy_from_slice(&self.tr()); + out[64..128].copy_from_slice(self.tr()); off += 128; // 2: for 𝑖 from 0 to ℓ − 1 do diff --git a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs index 21941b3fb..516a8e217 100644 --- a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs @@ -19,9 +19,11 @@ mod hash_mldsa_tests { MLDSATrait, }; use bouncycastle_mldsa_lowmemory::{MLDSA44_SIG_LEN, MLDSA65_SIG_LEN, MLDSA87_SIG_LEN}; + use bouncycastle_mldsa_lowmemory::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait}; use bouncycastle_rng::DefaultRNG; use bouncycastle_rng::FixedSeedRNG; use bouncycastle_sha2::{SHA256, SHA512}; + use bouncycastle_sha3::{SHAKE128, SHAKE256}; #[test] fn core_framework_signature() { @@ -61,19 +63,19 @@ mod hash_mldsa_tests { let ph = SHA256::hash(b"msg"); assert!(matches!( sk.create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph, None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.create_ph_verifier::(None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.verify_ph::(&ph, None, &[0u8; MLDSA65_SIG_LEN]), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); } @@ -342,4 +344,52 @@ mod hash_mldsa_tests { let sig = sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph512, None).unwrap(); pk.verify_ph::(&ph512, None, &sig).unwrap(); } + + /// FIPS 204 Algorithm 4, case SHAKE128 (lines 17 to 19): the pre-hash is SHAKE128(M, 256) + /// under OID 2.16.840.1.101.3.4.2.11, the 32-byte digest of RFC 8702 Section 3.1. Its + /// 128-bit strength fits ML-DSA-44 only; SHAKE256's 64-byte digest fits all three. + #[test] + fn shake_pre_hashes() { + let tf = TestFrameworkSignature::new(true); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA65::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA87::keygen(&mut DefaultRNG::default()), false); + + let (pk, sk) = MLDSA65::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + let (pk, sk) = MLDSA87::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + + // The signature is Algorithm 4's: M' = 0x01 || |ctx| || ctx || OID || SHAKE128(M, 256) + // (line 23, with the OID's DER bytes as line 18 prints them) and mu = H(tr || M', 64) + // (Algorithm 7 line 6), checked through the external-mu path. + let (pk, sk) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); + let (msg, ctx) = (b"pre-hashed with SHAKE128".as_slice(), b"ctx".as_slice()); + let ph: [u8; 32] = SHAKE128::hash(msg).try_into().unwrap(); + let mut tr_m_prime = pk.compute_tr().to_vec(); + tr_m_prime.extend_from_slice(&[0x01, ctx.len() as u8]); + tr_m_prime.extend_from_slice(ctx); + tr_m_prime + .extend_from_slice(&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B]); + tr_m_prime.extend_from_slice(&ph); + let mu: [u8; 64] = SHAKE256::hash(&tr_m_prime).try_into().unwrap(); + let by_mu: [u8; MLDSA44_SIG_LEN] = sk.sign_mu_deterministic(&mu).unwrap(); + assert_eq!(sk.sign_ph_deterministic::(&ph, Some(ctx)).unwrap(), by_mu); + pk.verify_ph::(&ph, Some(ctx), &by_mu).unwrap(); + } } diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 6fe827290..80910c835 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -561,7 +561,7 @@ mod mldsa_tests { let tr: [u8; MLDSA_TR_LEN]; { let (_, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - tr = sk.tr(); + tr = *sk.tr(); } // BEGIN expected values @@ -844,11 +844,11 @@ mod mldsa_tests { // failure case: different but valid key let (pk2, sk2) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -857,7 +857,7 @@ mod mldsa_tests { pk_bytes[17] ^= 0x01; let pk2 = MLDSA44PublicKey::from_bytes(&pk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -865,7 +865,7 @@ mod mldsa_tests { sk_bytes[17] ^= 0x01; let sk2 = MLDSA44PrivateKey::from_bytes(&sk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; } diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index 56ffe1aa7..0b0c0b84a 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -70,7 +70,7 @@ pub(crate) type DecodedSig

= /// Output: (c_tilde, z, h) pub(crate) fn sig_decode( sig: &[u8; SIG_LEN], -) -> Result, ()> { +) -> Option> { debug_assert_eq!(SIG_LEN, P::SIG_LEN); let mut c_tilde = ::ZEROED; let mut z = P::VecL::new(); @@ -101,7 +101,7 @@ pub(crate) fn sig_decode( if sig[pos + (P::omega as usize) + i] < (idx as u8) || sig[pos + (P::omega as usize) + i] > P::omega as u8 { - return Err(()); + return None; } // 6: First ← Index @@ -112,7 +112,7 @@ pub(crate) fn sig_decode( // 9: if 𝑦[Index − 1] ≥ 𝑦[Index] then return ⊥ // ▷ malformed input if j > idx && sig[pos + j - 1] >= sig[pos + j] { - return Err(()); + return None; } // 12: 𝐡[𝑖]_𝑦[Index] ← 1 h.elems_mut()[i][sig[pos + j] as usize] = 1; @@ -127,11 +127,11 @@ pub(crate) fn sig_decode( // ▷ read any leftover bytes in the first 𝜔 bytes of 𝑦 for malformed (nonzero) bytes for j in idx..P::omega as usize { if sig[pos + j] != 0 { - return Err(()); + return None; } } - Ok((c_tilde, z, h)) + Some((c_tilde, z, h)) } /// Algorithm 32 ExpandA(𝜌) diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 6a55e8bec..e9c753ec7 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -2,8 +2,10 @@ //! message, for the case where the message is hashed before the signing key is known or on a //! different machine. //! -//! The hash is a type parameter of the key's pre-hash methods, so any approved hash goes with any -//! ML-DSA key: +//! The hash is a type parameter of the key's pre-hash methods, so any hash of at least the +//! parameter set's strength goes with any ML-DSA key, SHAKE128 and SHAKE256 among them as the +//! fixed-length digests of RFC 8702 Section 3.1 (Algorithm 4's third case is SHAKE128); that it +//! is an approved one (FIPS 204 Section 5.4) is the caller's to ensure: //! //! ```rust //! use bouncycastle_core::errors::SignatureError; @@ -128,9 +130,7 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// FIPS 204 Section 5.4: the hash must give at least the parameter set's strength. fn check_hash_strength() -> Result<(), SignatureError> { if H::MAX_SECURITY_STRENGTH < P::MAX_SECURITY_STRENGTH { - return Err(SignatureError::GenericError( - "HashML-DSA: the pre-hash is weaker than the ML-DSA parameter set (FIPS 204 Section 5.4)", - )); + return Err(SignatureError::PreHashTooWeak); } Ok(()) } diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 17ca47679..10aa38e0c 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -120,13 +120,23 @@ //! One caveat: `ctx` can still be security-relevant at the protocol level (domain separation, replay and //! cross-protocol binding), so choosing it incorrectly can weaken those properties. //! +//! ## When signing fails +//! +//! FIPS 204 Section 6.2 lets an implementation bound the rejection-sampling loop, and this one +//! does, at 1000·k iterations. Reaching it has negligible probability; when it happens, signing +//! returns [`SignatureError::RejectionLimitExceeded`] and writes nothing. The hedged variant +//! succeeds on a retry with fresh randomness; the deterministic one fails the same way again. +//! +//! [`SignatureError::RejectionLimitExceeded`]: bouncycastle_core::errors::SignatureError::RejectionLimitExceeded +//! //! ## What is zeroized //! //! FIPS 204 Section 3.6.3 asks that "any potentially sensitive intermediate data is destroyed as //! soon as it is no longer needed". Here the private key's `K`, `s1_hat` and `s2_hat` are `Secret` -//! and the seed is `KeyMaterial`, both wiped on drop, and the signing randomizer `rho''` is zeroized -//! by hand when signing finishes. The per-signature vectors `y`, `w`, `cs1`, `cs2`, `ct0` and `r0` -//! are plain stack values that are not wiped when their scope ends. +//! and the seed is `KeyMaterial`, and the key-generation seed `rho'` and the signing randomizer +//! `rho''` are `Secret`, all wiped on drop, on the error paths included. The per-signature vectors +//! `y`, `w`, `cs1`, `cs2`, `ct0` and `r0`, and the polynomials and buffer `sk_encode_out` packs +//! through, are plain stack values that are not wiped when their scope ends. #![no_std] #![forbid(unsafe_code)] diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index fc070f8bb..f3488093d 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -565,7 +565,7 @@ impl MLDSA

{ || seed.key_len() != 32 { return Err(SignatureError::KeyGenError( - "Seed must be 32 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 32 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -589,8 +589,8 @@ impl MLDSA

{ let mut h = h.into_squeezer(); let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); - let mut rho_prime: [u8; 64] = [0u8; 64]; - let bytes_written = h.do_output_out(&mut rho_prime); + let mut rho_prime: Secret<[u8; 64]> = Secret::new(); + let bytes_written = h.do_output_out(&mut *rho_prime); debug_assert_eq!(bytes_written, 64); let bytes_written = h.do_output_out(&mut *K); debug_assert_eq!(bytes_written, 32); @@ -681,16 +681,16 @@ pub(crate) fn sign_internal< // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) // skip: mu has already been provided - let mut rho_p_p: [u8; 64] = { + let rho_p_p: Secret<[u8; 64]> = { // scope for h // 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let mut h = H::new(); h.do_update(&**sk.K()); h.do_update(&rnd); h.do_update(mu); - let mut rho_p_p = [0u8; 64]; + let mut rho_p_p: Secret<[u8; 64]> = Secret::new(); let mut h = h.into_squeezer(); - h.do_output_out(&mut rho_p_p); + h.do_output_out(&mut *rho_p_p); rho_p_p }; @@ -715,9 +715,7 @@ pub(crate) fn sign_internal< // mutants note: there is no test for this because, at this point, // we don't know of a KAT that will exceed this limit. if kappa > 1000 * P::k as u16 { - return Err(SignatureError::GenericError( - "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", - )); + return Err(SignatureError::RejectionLimitExceeded); } // 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) @@ -835,9 +833,6 @@ pub(crate) fn sign_internal< break; } - // zeroize rho_p_p before returning it to the OS - rho_p_p.fill(0u8); - // sig_encode does not necessarily write to all bytes of the output, so just to be safe: output.fill(0u8); @@ -868,7 +863,7 @@ pub(crate) fn verify_internal< // ▷ signer’s commitment hash c_tilde, response 𝐳, and hint 𝐡 // 3: if 𝐡 = ⊥ then return false let (c_tilde, z, h) = - sig_decode::(sig).map_err(|_| SignatureError::SignatureVerificationFailed)?; + sig_decode::(sig).ok_or(SignatureError::SignatureVerificationFailed)?; // 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] if z.check_norm(P::gamma1_minus_beta) { @@ -978,10 +973,10 @@ impl MLDSATrait

for MLDSA

{ ) -> Result<(), SignatureError> { // This is maybe a computationally heavy way to compare them, but it works let derived_pk = sk.derive_pk(); - if derived_pk.compute_tr() == pk.compute_tr() { + if bouncycastle_utils::ct::ct_eq_bytes(&derived_pk.compute_tr(), &pk.compute_tr()) { Ok(()) } else { - Err(SignatureError::ConsistencyCheckFailed()) + Err(SignatureError::ConsistencyCheckFailed) } } } diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 20cd4b17d..1557db4a5 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -611,6 +611,10 @@ pub trait MLDSAPrivateKeyTrait: Sized + SignMuWithRnd

{ /// signature it verifies with a standard ML-DSA verifier, which need not know how `mu` was /// made. The hedged variant: `rnd` is drawn from `rng`. /// + /// # Errors + /// [`SignatureError::RejectionLimitExceeded`] if the bounded rejection loop (FIPS 204 + /// Section 6.2) runs out, which has negligible probability; nothing is written. + /// /// [`SigningKey::create_signer`]: bouncycastle_core::signature_key::SigningKey::create_signer fn sign_mu( &self, diff --git a/crypto/mldsa/tests/hash_mldsa_tests.rs b/crypto/mldsa/tests/hash_mldsa_tests.rs index ea6c4c63e..23813c14d 100644 --- a/crypto/mldsa/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa/tests/hash_mldsa_tests.rs @@ -16,10 +16,12 @@ mod hash_mldsa_tests { MLDSATrait, }; use bouncycastle_mldsa::{MLDSA44_SIG_LEN, MLDSA65_SIG_LEN, MLDSA87_SIG_LEN}; + use bouncycastle_mldsa::{MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait}; use bouncycastle_rng::DefaultRNG; use bouncycastle_rng::FixedSeedRNG; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha2::{SHA256, SHA512}; + use bouncycastle_sha3::{SHAKE128, SHAKE256}; #[test] fn core_framework_signature() { @@ -65,19 +67,19 @@ mod hash_mldsa_tests { let ph = SHA256::hash(b"msg"); assert!(matches!( sk.create_ph_signer::(&mut HashDRBG_SHA512::new_from_os(), None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph, None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.create_ph_verifier::(None), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); assert!(matches!( pk.verify_ph::(&ph, None, &[0u8; MLDSA65_SIG_LEN]), - Err(SignatureError::GenericError(_)) + Err(SignatureError::PreHashTooWeak) )); // ML-DSA-44 accepts it sk_44_accepts_sha256(); @@ -511,4 +513,52 @@ mod hash_mldsa_tests { let sig = sk.sign_ph::(&mut HashDRBG_SHA512::new_from_os(), &ph512, None).unwrap(); pk.verify_ph::(&ph512, None, &sig).unwrap(); } + + /// FIPS 204 Algorithm 4, case SHAKE128 (lines 17 to 19): the pre-hash is SHAKE128(M, 256) + /// under OID 2.16.840.1.101.3.4.2.11, the 32-byte digest of RFC 8702 Section 3.1. Its + /// 128-bit strength fits ML-DSA-44 only; SHAKE256's 64-byte digest fits all three. + #[test] + fn shake_pre_hashes() { + let tf = TestFrameworkSignature::new(true); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA44::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA65::keygen(&mut DefaultRNG::default()), false); + tf.test_ph_signature::(|| MLDSA87::keygen(&mut DefaultRNG::default()), false); + + let (pk, sk) = MLDSA65::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + let (pk, sk) = MLDSA87::keygen(&mut DefaultRNG::default()).unwrap(); + assert!(matches!( + sk.create_ph_signer::(&mut DefaultRNG::default(), None), + Err(SignatureError::PreHashTooWeak) + )); + assert!(matches!( + pk.create_ph_verifier::(None), + Err(SignatureError::PreHashTooWeak) + )); + + // The signature is Algorithm 4's: M' = 0x01 || |ctx| || ctx || OID || SHAKE128(M, 256) + // (line 23, with the OID's DER bytes as line 18 prints them) and mu = H(tr || M', 64) + // (Algorithm 7 line 6), checked through the external-mu path. + let (pk, sk) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); + let (msg, ctx) = (b"pre-hashed with SHAKE128".as_slice(), b"ctx".as_slice()); + let ph: [u8; 32] = SHAKE128::hash(msg).try_into().unwrap(); + let mut tr_m_prime = pk.compute_tr().to_vec(); + tr_m_prime.extend_from_slice(&[0x01, ctx.len() as u8]); + tr_m_prime.extend_from_slice(ctx); + tr_m_prime + .extend_from_slice(&[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B]); + tr_m_prime.extend_from_slice(&ph); + let mu: [u8; 64] = SHAKE256::hash(&tr_m_prime).try_into().unwrap(); + let by_mu: [u8; MLDSA44_SIG_LEN] = sk.sign_mu_deterministic(&mu).unwrap(); + assert_eq!(sk.sign_ph_deterministic::(&ph, Some(ctx)).unwrap(), by_mu); + pk.verify_ph::(&ph, Some(ctx), &by_mu).unwrap(); + } } diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index de6badd53..e4b5cf187 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -947,11 +947,11 @@ mod mldsa_tests { // failure case: different but valid key let (pk2, sk2) = MLDSA44::keygen(&mut DefaultRNG::default()).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -960,7 +960,7 @@ mod mldsa_tests { pk_bytes[17] ^= 0x01; let pk2 = MLDSA44PublicKey::from_bytes(&pk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk2, &sk) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; @@ -968,7 +968,7 @@ mod mldsa_tests { sk_bytes[17] ^= 0x01; let sk2 = MLDSA44PrivateKey::from_bytes(&sk_bytes).unwrap(); match MLDSA44::keypair_consistency_check(&pk, &sk2) { - Err(SignatureError::ConsistencyCheckFailed()) => { /* good */ } + Err(SignatureError::ConsistencyCheckFailed) => { /* good */ } _ => panic!("Expected error for different key"), }; } diff --git a/crypto/mlkem-common/src/polynomial.rs b/crypto/mlkem-common/src/polynomial.rs index e085203d3..93b2a4e1b 100644 --- a/crypto/mlkem-common/src/polynomial.rs +++ b/crypto/mlkem-common/src/polynomial.rs @@ -55,7 +55,7 @@ impl Polynomial { /// decryption noise margin. The mapping is computed branchlessly (constant-time) /// via a bit-derived all-ones / all-zeros mask, and bits are read LSB-first. This /// is the exact inverse of [`to_msg`]. - pub fn from_msg(m: [u8; 32]) -> Self { + pub fn from_msg(m: &[u8; 32]) -> Self { let mut w = Polynomial::new(); for (i, b) in m.iter().enumerate() { @@ -146,7 +146,7 @@ impl Polynomial { } } - /// Barrett-reduces every coefficient into \[0, 𝑞). + /// Barrett-reduces every coefficient into \[0, 𝑞] (see [`barrett_reduce`] for the closed end). pub fn poly_reduce(&mut self) { for i in 0..N { self[i] = barrett_reduce(self[i]); @@ -381,11 +381,13 @@ pub fn montgomery_reduce(a: i32) -> i16 { /// Barrett reduction into \[0, 𝑞]: the floor variant, not pq-crystals' centered one, which the /// compression code relies on for its unsigned casts. The result is 𝑞 rather than 0 for a -/// negative multiple of 𝑞; [`cond_sub_q`] folds that when a canonical value is needed. +/// negative multiple of 𝑞; [`cond_sub_q`] folds that when a canonical value is needed. Valid for +/// every `i16`: the subtraction is done in `i32`, since for `a <= -9q` the product `t * q` does +/// not fit an `i16` and a narrowed subtraction overflowed in debug builds. pub fn barrett_reduce(a: i16) -> i16 { - let v = (((1u32 << 26) + ((q / 2) as u32)) / (q as u32)) as i16; - let t = (((v as i32) * (a as i32)) >> 26) as i16; - a - (((t as i32) * q as i32) as i16) + let v = (((1u32 << 26) + ((q / 2) as u32)) / (q as u32)) as i32; + let t = (v * (a as i32)) >> 26; + (a as i32 - t * q as i32) as i16 } /// Folds 𝑞 to 0: for `a` in \[0, 𝑞], the canonical representative in \[0, 𝑞). @@ -416,11 +418,12 @@ mod tests { #[test] fn barrett_reduce_lands_in_zero_to_q_inclusive() { - // The floor variant: for the sums the NTT arithmetic produces, |a| < 8q, the result is - // congruent to a and in [0, q], reaching q only at negative multiples of q, which the - // compression masks fold and the encoders never see (their inputs are reduced sums of - // Montgomery products, each in (-q, q)). - for a in -(8 * q)..=(8 * q) { + // The floor variant, over every i16 (the NTT arithmetic produces |a| < 8q; the whole + // range is checked so that no input can overflow): the result is congruent to a and in + // [0, q], reaching q only at negative multiples of q, which the compression masks fold + // and the encoders never see (their inputs are reduced sums of Montgomery products, + // each in (-q, q)). + for a in i16::MIN..=i16::MAX { let r = barrett_reduce(a); assert!((0..=q).contains(&r), "barrett_reduce({a}) = {r} not in [0, q]"); assert_eq!((r as i32 - a as i32).rem_euclid(q as i32), 0, "barrett_reduce({a}) = {r}"); diff --git a/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs index 95d909151..f02a42bfb 100644 --- a/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs +++ b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs @@ -55,6 +55,8 @@ impl< ek: &PK, m: [u8; MLKEM_RND_LEN], ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { - Self::encaps_internal(ek, m) + // The public contract returns `K` by value; the internal path keeps it in a `Secret`. + let (K, ct) = Self::encaps_internal(ek, &m); + (*K, ct) } } diff --git a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs index a0990a767..c9c900fb8 100644 --- a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs @@ -78,6 +78,9 @@ pub(crate) fn compute_A_hat_dot_y_hat( A_ji.base_mult_montgomery(&y_j); u_i.add(&A_ji); } + // The k-term sum is in (-2kq, 2kq); `inv_ntt`'s first butterfly adds two coefficients in + // i16, so it is reduced first (see `dot_product` in bouncycastle-mlkem for the bound). + u_i.poly_reduce(); u_i.inv_ntt(); u_i diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index c14e1c135..44b756568 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -221,7 +221,7 @@ impl< fn pke_encrypt( t_hat_packed: &P::TPacked, rho: &[u8; 32], - m: [u8; 32], + m: &[u8; 32], r: &[u8; 32], ) -> [u8; CT_LEN] { let mut ct = [0u8; CT_LEN]; @@ -297,20 +297,26 @@ impl< /// Reachable from outside the crate only through /// [`EncapsWithRandomness`](crate::hazmat::EncapsWithRandomness), which carries the security /// notes on supplying `m`. - pub(crate) fn encaps_internal(ek: &PK, m: [u8; 32]) -> ([u8; 32], [u8; CT_LEN]) { + pub(crate) fn encaps_internal( + ek: &PK, + m: &[u8; 32], + ) -> (Secret<[u8; MLKEM_SS_LEN]>, [u8; CT_LEN]) { // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) - // ▷ derive shared secret key 𝐾 and randomness 𝑟 - let K: [u8; MLKEM_SS_LEN]; - let r: [u8; 32]; - (K, r) = { + // ▷ derive shared secret key 𝐾 and randomness 𝑟. Both are secret and held in `Secret`, + // as is the G output they are split from. + let (K, r) = { let mut g = G::new(); - g.do_update(&m); + g.do_update(m); g.do_update(&ek.compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K.copy_from_slice(&buf[..32]); + let mut r = Secret::<[u8; 32]>::new(); + r.copy_from_slice(&buf[32..64]); + (K, r) }; // 2: 𝑐 ← K-PKE.Encrypt(ek, 𝑚, 𝑟) @@ -329,7 +335,7 @@ impl< fn pke_decrypt>( dk: &DK, ct: &[u8; CT_LEN], - ) -> [u8; 32] { + ) -> Secret<[u8; 32]> { // 1: 𝑐1 ← 𝑐[0 ∶ 32𝑑𝑢𝑘] // 3: 𝐮′ ← Decompress_𝑑𝑢(ByteDecode_𝑑𝑢(𝑐1)) @@ -380,8 +386,11 @@ impl< }; // 7: 𝑚 ← ByteEncode1(Compress1(𝑤)) - // ▷ decode plaintext 𝑚 from polynomial 𝑤 - w.to_msg() + // ▷ decode plaintext 𝑚 from polynomial 𝑤, into a `Secret`: it is an internal value + // (FIPS 203 Sec 3.3) that must not outlive the decapsulation. + let mut m = Secret::<[u8; 32]>::new(); + *m = w.to_msg(); + m } /// Algorithm 18 ML-KEM.Decaps_internal(dk, 𝑐) @@ -395,7 +404,7 @@ impl< >( dk: &DK, c: &[u8; CT_LEN], - ) -> [u8; MLKEM_SS_LEN] { + ) -> Secret<[u8; MLKEM_SS_LEN]> { // I have tried to keep this as clean as possible for correspondence with the FIPS, // but I have moved things around so that I can use unnamed scopes to limit how many // stack variables are alive at the same time. @@ -411,19 +420,19 @@ impl< // Compute the trial shared secret key // 6: (𝐾′, 𝑟′) ← G(𝑚′‖ℎ)̄ - let K_prime: Secret<[u8; MLKEM_SS_LEN]>; - let r_prime: [u8; 32]; - (K_prime, r_prime) = { + let (K_prime, r_prime) = { let mut buf: Secret<[u8; 64]> = Secret::new(); let mut g = G::new(); - g.do_update(&m_prime); + g.do_update(&*m_prime); g.do_update(dk.pk_hash()); let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); let mut K_prime: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); K_prime.copy_from_slice(&buf[..32]); - (K_prime, buf[32..64].try_into().unwrap()) + let mut r_prime: Secret<[u8; 32]> = Secret::new(); + r_prime.copy_from_slice(&buf[32..64]); + (K_prime, r_prime) }; // 7: 𝐾_bar ← J(𝑧‖𝑐) @@ -449,12 +458,12 @@ impl< // A seed key holds no 𝐭̂, so it derives one here (Algorithm 13 again, most of what makes // this crate's decapsulation slower than its encapsulation); an expanded key hands over // the copy it holds. - let c_prime = Self::pke_encrypt(&dk.t_hat_packed(), dk.rho(), m_prime, &r_prime); + let c_prime = Self::pke_encrypt(&dk.t_hat_packed(), dk.rho(), &m_prime, &r_prime); // 9: if 𝑐 ≠ 𝑐′ then // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" - let mut K_out = [0u8; MLKEM_SS_LEN]; + let mut K_out = Secret::<[u8; MLKEM_SS_LEN]>::new(); conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(c, &c_prime)); K_out @@ -502,7 +511,10 @@ impl< ) -> Result<(PK, SK), KEMError> { let (pk, sk) = Self::keygen_internal(seed)?; - if !ct_eq_bytes(&sk.encode_full_sk(), encoded_sk) { + // The full encoding is private-key material: made in a `Secret` buffer for the comparison. + let mut full = Secret::<[u8; FULL_SK_LEN]>::new(); + sk.encode_full_sk_out(&mut full); + if !ct_eq_bytes(&*full, encoded_sk) { return Err(KEMError::KeyGenError("Encoded key does not match generated key")); } @@ -600,13 +612,13 @@ impl< if rng.security_strength() < P::MAX_SECURITY_STRENGTH { Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } - let mut m = [0u8; 32]; - rng.next_bytes_out(&mut m)?; + let mut m = Secret::<[u8; 32]>::new(); + rng.next_bytes_out(&mut *m)?; - let (ss_bytes, ct) = Self::encaps_internal(pk, m); + let (ss_bytes, ct) = Self::encaps_internal(pk, &m); let mut ss_keymaterial = - KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; + KeyMaterial::::from_bytes_as_type(&*ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; @@ -670,7 +682,7 @@ impl< let ss_bytes = Self::decaps_internal(dk, ct); let mut ss_keymaterial = - KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; + KeyMaterial::::from_bytes_as_type(&*ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; diff --git a/crypto/mlkem-lowmemory/src/mlkem_keys.rs b/crypto/mlkem-lowmemory/src/mlkem_keys.rs index d0c4784d3..a06bbb6ce 100644 --- a/crypto/mlkem-lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem-lowmemory/src/mlkem_keys.rs @@ -266,7 +266,7 @@ impl { fn eq(&self, other: &Self) -> bool { - let self_encoded = self.encode(); - let other_encoded = other.encode(); - bouncycastle_utils::ct::ct_eq_bytes(self_encoded.as_ref(), other_encoded.as_ref()) + // The encodings are the seed: held in `Secret` buffers for the comparison. + let mut self_encoded = Secret::<[u8; SK_LEN]>::new(); + let mut other_encoded = Secret::<[u8; SK_LEN]>::new(); + self.encode_out(&mut self_encoded); + other.encode_out(&mut other_encoded); + bouncycastle_utils::ct::ct_eq_bytes(&*self_encoded, &*other_encoded) } } diff --git a/crypto/mlkem/src/hazmat/encaps_with_randomness.rs b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs index 7826ecc2c..125760761 100644 --- a/crypto/mlkem/src/hazmat/encaps_with_randomness.rs +++ b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs @@ -65,6 +65,8 @@ impl< A_hat: Option<&P::MatrixA>, m: [u8; MLKEM_RND_LEN], ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { - Self::encaps_internal(ek, A_hat, m) + // The public contract returns `K` by value; the internal path keeps it in a `Secret`. + let (K, ct) = Self::encaps_internal(ek, A_hat, &m); + (*K, ct) } } diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index d92105946..9d62754ec 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -189,9 +189,13 @@ impl VectorTrait for Vector { let w1 = polynomial::base_mult_montgomery(&self[i], &v[i]); w.add(&w1); } - // Note: This function DOES NOT perform modular reduction, as the current - // construction of ML-KEM only reduces modulo q when it's necessary. - // w.poly_reduce(); + // Reduced before it is returned: each product coefficient is a sum of two Montgomery + // reductions in (-q, q), so the k-term sum is in (-2kq, 2kq), up to 16q for ML-KEM-1024, + // and the callers feed it to `inv_ntt`, whose first butterfly adds two coefficients in + // i16 (limit 9.84q). FIPS 203 Sec 3.3 allows deferring a reduction only while the result + // is right for every input; without this one the margin was empirical (a search reached + // 8.9q on a real key), and bc-java and the pq-crystals reference reduce at this point. + w.poly_reduce(); w } @@ -462,4 +466,32 @@ mod tests { fn compress_and_decompress_match_fips_203_for_du_11() { check::(); } + + /// `dot_product` returns reduced coefficients, in [0, q]: the k-term sum of Montgomery + /// products is in (-2kq, 2kq) and the callers' `inv_ntt` adds two coefficients in i16, so + /// the reduction at the end of `dot_product` is what keeps the sum in range (the review's + /// M1, which arose from that reduction being commented out). An LCG spreads the inputs over + /// (-q, q), the range NTT-domain coefficients take. + #[test] + fn dot_product_is_reduced() { + let mut x: u32 = 0x2545_F491; + let mut next = || { + x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + ((x % (2 * q as u32 - 1)) as i32 - (q as i32 - 1)) as i16 + }; + for _ in 0..64 { + let mut a = Vector::<4>::new(); + let mut b = Vector::<4>::new(); + for i in 0..4 { + for j in 0..N { + a[i][j] = next(); + b[i][j] = next(); + } + } + let w = a.dot_product(&b); + for j in 0..N { + assert!((0..=q).contains(&w[j]), "coefficient {j} is {}", w[j]); + } + } + } } diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index abdea9b47..1deb2e609 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -293,7 +293,7 @@ impl< || seed.key_len() != 64 { return Err(KEMError::KeyGenError( - "Seed must be 64 bytes and KeyType::Seed or KeyType::BytesFullEntropy.", + "Seed must be 64 bytes and KeyType::Seed or KeyType::CryptographicRandom.", )); } @@ -304,7 +304,9 @@ impl< } // 1: (ekPKE, dkPKE) ← K-PKE.KeyGen(𝑑) - let (pk, s_hat) = Self::pke_keygen(&seed.ref_to_bytes()[..32].try_into().unwrap()); + let mut d = Secret::<[u8; 32]>::new(); + d.copy_from_slice(&seed.ref_to_bytes()[..32]); + let (pk, s_hat) = Self::pke_keygen(&d); // 2: ek ← ekPKE ▷ KEM encaps key is just the PKE encryption key // 3: dk ← (dkPKE‖ek‖H(ek)‖𝑧) ▷ KEM decaps key includes PKE decryption key @@ -329,15 +331,19 @@ impl< // ▷ expand 32+1 bytes to two pseudorandom 32-byte seeds1 // rho: public seed // sigma: noise seed - let (rho, mut sigma) = { + // sigma is the noise seed: secret, and scrubbed when it drops, with the G output it + // came from. rho is public (it is part of ek). + let (rho, sigma): ([u8; 32], Secret<[u8; 32]>) = { let mut g = G::new(); g.do_update(d); g.do_update(&[P::k as u8]); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut sigma = Secret::<[u8; 32]>::new(); + sigma.copy_from_slice(&buf[32..64]); + (buf[..32].try_into().unwrap(), sigma) }; // 2: 𝑁 ← 0 @@ -387,9 +393,6 @@ impl< t_hat.add_vector_ntt(&e); } - // Clear the secret data before returning memory to the OS - sigma.fill(0u8); - // 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 ▷ run ByteEncode12 𝑘 times, then append 𝐀-seed // 20: dkPKE ← ByteEncode12(𝐬)̂ ▷ run ByteEncode12 𝑘 times // Note: The encoding is skipped at this stage and left expanded for future efficiency when it's used. @@ -403,7 +406,7 @@ impl< /// Input: message 𝑚 ∈ 𝔹32 . /// Input: randomness 𝑟 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). - fn pke_encrypt(ek: &PK, A_hat: &P::MatrixA, m: [u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { + fn pke_encrypt(ek: &PK, A_hat: &P::MatrixA, m: &[u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { // 1: 𝑁 ← 0 // since the number of loops here is static, the N values can be hard-coded rather than using a counter @@ -489,21 +492,24 @@ impl< pub(crate) fn encaps_internal( ek: &PK, A_hat: Option<&P::MatrixA>, - m: [u8; 32], - ) -> ([u8; 32], [u8; CT_LEN]) { + m: &[u8; 32], + ) -> (Secret<[u8; MLKEM_SS_LEN]>, [u8; CT_LEN]) { // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) - // ▷ derive shared secret key 𝐾 and randomness 𝑟 - let K: [u8; MLKEM_SS_LEN]; - let r: [u8; 32]; - (K, r) = { + // ▷ derive shared secret key 𝐾 and randomness 𝑟. Both are secret and held in `Secret`, + // as is the G output they are split from. + let (K, r) = { let mut g = G::new(); - g.do_update(&m); + g.do_update(m); g.do_update(&ek.compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K.copy_from_slice(&buf[..32]); + let mut r = Secret::<[u8; 32]>::new(); + r.copy_from_slice(&buf[32..64]); + (K, r) }; // 2: 𝑐 ← K-PKE.Encrypt(ek, 𝑚, 𝑟) @@ -524,7 +530,7 @@ impl< /// Input: decryption key dkPKE ∈ 𝔹384𝑘. /// Input: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). /// Output: message 𝑚 ∈ 𝔹32 . - fn pke_decrypt(dk: &SK, ct: &[u8; CT_LEN]) -> [u8; 32] { + fn pke_decrypt(dk: &SK, ct: &[u8; CT_LEN]) -> Secret<[u8; 32]> { // 1: 𝑐1 ← 𝑐[0 ∶ 32𝑑𝑢𝑘] // 2: 𝑐2 ← 𝑐[32𝑑𝑢𝑘 ∶ 32(𝑑𝑢𝑘 + 𝑑𝑣)] // 3: 𝐮′ ← Decompress_𝑑𝑢(ByteDecode_𝑑𝑢(𝑐1)) @@ -552,8 +558,11 @@ impl< }; // 7: 𝑚 ← ByteEncode1(Compress1(𝑤)) - // ▷ decode plaintext 𝑚 from polynomial 𝑤 - w.to_msg() + // ▷ decode plaintext 𝑚 from polynomial 𝑤, into a `Secret`: it is an internal value + // (FIPS 203 Sec 3.3) that must not outlive the decapsulation. + let mut m = Secret::<[u8; 32]>::new(); + *m = w.to_msg(); + m } /// Algorithm 18 ML-KEM.Decaps_internal(dk, 𝑐) @@ -565,7 +574,7 @@ impl< dk: &SK, A_hat: Option<&P::MatrixA>, c: &[u8; CT_LEN], - ) -> [u8; MLKEM_SS_LEN] { + ) -> Secret<[u8; MLKEM_SS_LEN]> { // Structured to mirror the FIPS as closely as possible, with unnamed scopes // used to limit the number of live stack variables at any given time. @@ -580,17 +589,21 @@ impl< // Compute the trial shared secret key // 6: (𝐾′, 𝑟′) ← G(𝑚′‖ℎ)̄ - let K_prime: [u8; MLKEM_SS_LEN]; - let r_prime: [u8; 32]; - (K_prime, r_prime) = { + // K' and r' are the candidate shared secret and the re-encryption randomness: secret, + // held in `Secret` with the G output they are split from (FIPS 203 Sec 3.3, Sec 6.3). + let (K_prime, r_prime) = { let mut g = G::new(); - g.do_update(&m_prime); + g.do_update(&*m_prime); g.do_update(&dk.pk().compute_hash()); - let mut buf = [0u8; 64]; - let bytes_written = g.do_final_out(&mut buf); + let mut buf = Secret::<[u8; 64]>::new(); + let bytes_written = g.do_final_out(&mut *buf); debug_assert_eq!(bytes_written, 64); - (buf[..32].try_into().unwrap(), buf[32..64].try_into().unwrap()) + let mut K_prime = Secret::<[u8; MLKEM_SS_LEN]>::new(); + K_prime.copy_from_slice(&buf[..32]); + let mut r_prime = Secret::<[u8; 32]>::new(); + r_prime.copy_from_slice(&buf[32..64]); + (K_prime, r_prime) }; // 7: 𝐾_bar ← J(𝑧‖𝑐) @@ -599,13 +612,13 @@ impl< // This is because if the computation is conditional on the Fujisaki-Okamoto check failing, then // it will result in a timing difference between success and failure. - let K_bar: [u8; MLKEM_SS_LEN] = { + let K_bar: Secret<[u8; MLKEM_SS_LEN]> = { let mut j = J::new(); j.do_update(dk.z().as_ref()); j.do_update(c); - let mut buf = [0u8; MLKEM_SS_LEN]; + let mut buf = Secret::<[u8; MLKEM_SS_LEN]>::new(); let mut j = j.into_squeezer(); - let bytes_written = j.do_output_out(&mut buf); + let bytes_written = j.do_output_out(&mut *buf); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); buf @@ -617,14 +630,14 @@ impl< // To allow for pre-computing A_hat for multiple encapsulations, we will either take // A_hat passed in, or compute it fresh. let c_prime = match A_hat { - Some(A_hat) => Self::pke_encrypt(dk.pk(), A_hat, m_prime, &r_prime), - None => Self::pke_encrypt(dk.pk(), &dk.pk().A_hat(), m_prime, &r_prime), + Some(A_hat) => Self::pke_encrypt(dk.pk(), A_hat, &m_prime, &r_prime), + None => Self::pke_encrypt(dk.pk(), &dk.pk().A_hat(), &m_prime, &r_prime), }; // 9: if 𝑐 ≠ 𝑐′ then // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" - let mut K_out = [0u8; MLKEM_SS_LEN]; + let mut K_out = Secret::<[u8; MLKEM_SS_LEN]>::new(); conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(c, &c_prime)); K_out @@ -740,13 +753,13 @@ impl< if rng.security_strength() < P::MAX_SECURITY_STRENGTH { Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } - let mut m = [0u8; 32]; - rng.next_bytes_out(&mut m)?; + let mut m = Secret::<[u8; 32]>::new(); + rng.next_bytes_out(&mut *m)?; // 5: (𝐾, 𝑐) ← ML-KEM.Encaps_internal(ek, 𝑚) - let (K, ct) = Self::encaps_internal(ek, A_hat, m); + let (K, ct) = Self::encaps_internal(ek, A_hat, &m); - Ok((Self::shared_secret_key(K)?, ct)) + Ok((Self::shared_secret_key(&K)?, ct)) } /// Algorithm 21 ML-KEM.Decaps(dk, 𝑐) with the Section 7.3 input checks, over a private key @@ -767,12 +780,12 @@ impl< // 1: 𝐾′ ← ML-KEM.Decaps_internal(dk, 𝑐) let K = Self::decaps_internal(dk, A_hat, ct); - Self::shared_secret_key(K) + Self::shared_secret_key(&K) } /// The shared secret as [`KeyMaterial`]: full-entropy bytes at the parameter set's strength. - fn shared_secret_key(K: [u8; MLKEM_SS_LEN]) -> Result, KEMError> { - let mut key = KeyMaterial::::from_bytes_as_type(&K, KeyType::CryptographicRandom)?; + fn shared_secret_key(K: &[u8; MLKEM_SS_LEN]) -> Result, KEMError> { + let mut key = KeyMaterial::::from_bytes_as_type(K, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut key, |key| { key.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index fd413b2e6..85fd611a4 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -645,9 +645,13 @@ impl< > PartialEq for MLKEMPrivateKey { fn eq(&self, other: &Self) -> bool { - let self_encoded = self.encode(); - let other_encoded = other.encode(); - bouncycastle_utils::ct::ct_eq_bytes(self_encoded.as_ref(), other_encoded.as_ref()) + // The encodings are private-key material; they live in `Secret` buffers for the + // comparison and are scrubbed when it returns. + let mut self_encoded = Secret::<[u8; SK_LEN]>::new(); + let mut other_encoded = Secret::<[u8; SK_LEN]>::new(); + self.encode_out(&mut self_encoded); + other.encode_out(&mut other_encoded); + bouncycastle_utils::ct::ct_eq_bytes(&*self_encoded, &*other_encoded) } } diff --git a/crypto/rng/Cargo.toml b/crypto/rng/Cargo.toml index 48e1a3c6d..a33cf3099 100644 --- a/crypto/rng/Cargo.toml +++ b/crypto/rng/Cargo.toml @@ -13,6 +13,7 @@ getrandom = "0.4.0-rc.1" [dev-dependencies] bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true criterion.workspace = true [[bench]] diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index eaf26fd6e..1bdf8724e 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -1,4 +1,5 @@ -//! Implements Hash_DRBG (Deterministic Random Bit Generator) from NIST SP 800-90Ar1. +//! Implements Hash_DRBG (Deterministic Random Bit Generator) from NIST SP 800-90A Rev. 1, +//! Sections 10.1.1 and 10.3.1, checked against the CAVP `Hash_DRBG.rsp` vectors in `tests/`. // This is here cause HashDRBG80090AParams is private on purpose so that people can't instantiate new parameter sets other than the ones prescribed by NIST. #![allow(private_bounds)] @@ -25,13 +26,19 @@ enum SupportedHash { // ie the structs defined below will be the only allowed ones. trait HashDRBG80090AParams { const HASH: SupportedHash; - // const OUT_LEN: usize; + /// `outlen` in bytes (SP 800-90A Rev. 1 Table 2). + const OUTPUT_LEN: usize; const MAX_SECURITY_STRENGTH: SecurityStrength; - // const SEED_LEN: usize; - const MAX_LENGTH: u64; - const MAX_PERSONALIZATION_STRING_LENGTH: u64; - const MAX_ADDITIONAL_INPUT_LENGTH: u64; - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64; + /// `seedlen` in bytes: Table 2 gives 440 bits for SHA-256 and 888 for SHA-512. `V` and `C` + /// are this long (Section 10.1.1.1), and every `mod 2^seedlen` in the generate algorithm is + /// an addition over this many bytes. + const SEED_LEN: usize; + /// Table 2's limits, in bytes: `max_length` of the entropy input, the personalization string + /// and the additional input are 2^35 bits, `max_number_of_bits_per_request` is 2^19 bits. + const MAX_SEED_LEN: u64; + const MAX_PERSONALIZATION_STRING_LEN: u64; + const MAX_ADDITIONAL_INPUT_LEN: u64; + const MAX_BYTES_PER_REQUEST: u64; const RESEED_INTERVAL: u64; } @@ -41,11 +48,13 @@ pub struct HashDRBG80090AParams_SHA256 {} impl HashDRBG80090AParams for HashDRBG80090AParams_SHA256 { const HASH: SupportedHash = SupportedHash::SHA256; + const OUTPUT_LEN: usize = SHA256::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; - const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits + const SEED_LEN: usize = 440 / 8; + const MAX_SEED_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_PERSONALIZATION_STRING_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_ADDITIONAL_INPUT_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_BYTES_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits const RESEED_INTERVAL: u64 = 1u64 << 48; // 2^48 requests } @@ -54,24 +63,27 @@ impl HashDRBG80090AParams for HashDRBG80090AParams_SHA256 { pub struct HashDRBG80090AParams_SHA512 {} impl HashDRBG80090AParams for HashDRBG80090AParams_SHA512 { const HASH: SupportedHash = SupportedHash::SHA512; + const OUTPUT_LEN: usize = SHA512::OUTPUT_LEN; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; - const MAX_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_PERSONALIZATION_STRING_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_ADDITIONAL_INPUT_LENGTH: u64 = (1u64 << 35) / 8; // 2^35 bits - const MAX_NUMBER_OF_BITS_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits + const SEED_LEN: usize = 888 / 8; + const MAX_SEED_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_PERSONALIZATION_STRING_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_ADDITIONAL_INPUT_LEN: u64 = (1u64 << 35) / 8; // 2^35 bits + const MAX_BYTES_PER_REQUEST: u64 = (1u64 << 19) / 8; // 2^19 bits const RESEED_INTERVAL: u64 = 1u64 << 48; // 2^48 requests } -// TODO: replace / simplify this once the generic_const_exprs feature lands in the stable rust compiler. -const LARGEST_HASHER_OUTPUT_LEN: usize = 64; +/// The backing arrays for `V` and `C` are sized for the largest `seedlen` (SHA-512's 111 bytes) +/// and used as `[..H::SEED_LEN]`, since stable Rust cannot size them by an associated constant. +const LARGEST_SEED_LEN: usize = 888 / 8; +/// The largest `outlen`, for the hash output buffers that are likewise sliced per hash. +const LARGEST_HASHER_OUTPUT_LEN: usize = SHA512::OUTPUT_LEN; #[allow(private_bounds)] /// Implementation of the Hash_DRBG algorithm as specified in NIST SP 800-90Ar1. pub struct HashDRBG80090A { _phantom: core::marker::PhantomData, - // TODO: replace / simplify this once the generic_const_exprs feature lands in the stable rust compiler. - // state: WorkingState, - state: WorkingState, + state: WorkingState, admin_info: AdministrativeInfo, } @@ -145,9 +157,9 @@ impl NewUninitialized for HashDRBG80090A { fn new_uninitialized() -> Self { Self { _phantom: core::marker::PhantomData, - state: WorkingState:: { - v: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), - c: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + state: WorkingState:: { + v: Secret::<[u8; LARGEST_SEED_LEN]>::new(), + c: Secret::<[u8; LARGEST_SEED_LEN]>::new(), reseed_counter: Secret::new(), }, admin_info: AdministrativeInfo { @@ -197,7 +209,7 @@ impl Sp80090ADrbg for HashDRBG80090A { todo!("Prediction resistance is not yet supported by Hash_DRBG80090A.") } - if personalization_string.len() as u64 > H::MAX_PERSONALIZATION_STRING_LENGTH { + if personalization_string.len() as u64 > H::MAX_PERSONALIZATION_STRING_LEN { return Err(RNGError::GenericError( "Personalization string exceeds the maximum length allowed by the DRBG instance", )); @@ -212,13 +224,13 @@ impl Sp80090ADrbg for HashDRBG80090A { "Provided seed must have a length that matches or exceeds the DRBG security strength.", ))?; } - if (seed.key_len() as u64) > H::MAX_LENGTH { + if (seed.key_len() as u64) > H::MAX_SEED_LEN { return Err(KeyMaterialError::SecurityStrength( "Provided seed exceeds the maximum seed length.", ))?; } // On purpose not checking the SecurityStrength field of the seed, - // because we assume it's pure entropy and hasn't been touched by any actual algoritms yet. + // because we assume it's pure entropy and hasn't been touched by any actual algorithms yet. if security_strength > H::MAX_SECURITY_STRENGTH { return Err(KeyMaterialError::SecurityStrength( "Requested security strength exceeds the maximum strength that this DRBG instance can provide.", @@ -228,31 +240,30 @@ impl Sp80090ADrbg for HashDRBG80090A { // 1. seed_material = entropy_input || nonce || personalization_string. // 2. seed = Hash_df (seed_material, seedlen). // 3. V = seed. + let v = &mut (*self.state.v)[..H::SEED_LEN]; match H::HASH { SupportedHash::SHA256 => hash_df::( seed.ref_to_bytes(), nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut *self.state.v, + v, ), SupportedHash::SHA512 => hash_df::( seed.ref_to_bytes(), nonce.ref_to_bytes(), personalization_string, &[0u8; 0], - &mut *self.state.v, + v, ), } // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Precede V with a byte of zeros. + let v = &(*self.state.v)[..H::SEED_LEN]; + let c = &mut (*self.state.c)[..H::SEED_LEN]; match H::HASH { - SupportedHash::SHA256 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) - } - SupportedHash::SHA512 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) - } + SupportedHash::SHA256 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + SupportedHash::SHA512 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), } // 5. reseed_counter = 1. @@ -282,7 +293,7 @@ impl Sp80090ADrbg for HashDRBG80090A { return Err(RNGError::Uninitialized); } - if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LENGTH { + if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LEN { return Err(RNGError::GenericError( "Additional input exceeds the maximum length allowed by the DRBG instance", )); @@ -292,14 +303,14 @@ impl Sp80090ADrbg for HashDRBG80090A { return Err(KeyMaterialError::InvalidKeyType("RNG seed must be KeyType::Seed"))?; } - // On purpose not checking the SecurityStrength field of the seed, because we assume it's pure entropy and hasn't been touched by any actual algoritms yet. + // On purpose not checking the SecurityStrength field of the seed, because we assume it's pure entropy and hasn't been touched by any actual algorithms yet. if (seed.key_len() as u32) < self.admin_info.strength.as_int() / 8 { return Err(KeyMaterialError::SecurityStrength( "Provided seed must have a length that matches or exceeds the DRBG security strength.", ))?; } - if (seed.key_len() as u64) > H::MAX_LENGTH { + if (seed.key_len() as u64) > H::MAX_SEED_LEN { return Err(KeyMaterialError::SecurityStrength( "Provided seed exceeds the maximum seed length.", ))?; @@ -308,33 +319,26 @@ impl Sp80090ADrbg for HashDRBG80090A { // 1. seed_material = 0x01 || V || entropy_input || additional_input. // 2. seed = Hash_df (seed_material, seedlen). // 3. V = seed. - match H::HASH { - SupportedHash::SHA256 => hash_df::( - &[0x01], - &*self.state.v.clone(), - seed.ref_to_bytes(), - additional_input, - &mut *self.state.v, - ), - SupportedHash::SHA512 => hash_df::( - &[0x01], - &*self.state.v.clone(), - seed.ref_to_bytes(), - additional_input, - &mut *self.state.v, - ), - } - - // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Preceed with a byte of all zeros. + let old_v = self.state.v.clone(); + let old_v = &(*old_v)[..H::SEED_LEN]; + let v = &mut (*self.state.v)[..H::SEED_LEN]; match H::HASH { SupportedHash::SHA256 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) + hash_df::(&[0x01], old_v, seed.ref_to_bytes(), additional_input, v) } SupportedHash::SHA512 => { - hash_df::(&[0u8], &*self.state.v, &[0u8; 0], &[0u8; 0], &mut *self.state.c) + hash_df::(&[0x01], old_v, seed.ref_to_bytes(), additional_input, v) } } + // 4. C = Hash_df ((0x00 || V), seedlen). Comment: Preceed with a byte of all zeros. + let v = &(*self.state.v)[..H::SEED_LEN]; + let c = &mut (*self.state.c)[..H::SEED_LEN]; + match H::HASH { + SupportedHash::SHA256 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + SupportedHash::SHA512 => hash_df::(&[0u8], v, &[0u8; 0], &[0u8; 0], c), + } + // 5. reseed_counter = 1. *self.state.reseed_counter = 1; @@ -363,12 +367,12 @@ impl Sp80090ADrbg for HashDRBG80090A { if !self.admin_info.instantiated { return Err(RNGError::Uninitialized); } - if out.len() as u64 > H::MAX_NUMBER_OF_BITS_PER_REQUEST { + if out.len() as u64 > H::MAX_BYTES_PER_REQUEST { return Err(RNGError::GenericError( - "Requested number of bits exceeds the maximum number of bits per request allowed by the DRBG instance", + "Requested number of bytes exceeds the maximum per request allowed by the DRBG instance", )); } - if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LENGTH { + if additional_input.len() as u64 > H::MAX_ADDITIONAL_INPUT_LEN { return Err(RNGError::GenericError( "Additional input exceeds the maximum length allowed by the DRBG instance", )); @@ -384,27 +388,28 @@ impl Sp80090ADrbg for HashDRBG80090A { // 2. If (additional_input ≠ Null), then do // 2.1 w = Hash (0x02 || V || additional_input). // 2.2 V = (V + w) mod 2^seedlen. + let v = &mut (*self.state.v)[..H::SEED_LEN]; if additional_input.len() > 0 { match H::HASH { SupportedHash::SHA256 => { let mut h = SHA256::new(); h.do_update(&[0x02]); - h.do_update(&*self.state.v); + h.do_update(v); h.do_update(additional_input); let mut w = [0u8; SHA256::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut *self.state.v, &w); + add_to_array(v, &w); } SupportedHash::SHA512 => { let mut h = SHA512::new(); h.do_update(&[0x02]); - h.do_update(&*self.state.v); + h.do_update(v); h.do_update(additional_input); let mut w = [0u8; SHA512::OUTPUT_LEN]; h.do_final_out(&mut w); - add_to_array(&mut *self.state.v, &w); + add_to_array(v, &w); } } } @@ -416,36 +421,38 @@ impl Sp80090ADrbg for HashDRBG80090A { // But we do want to continue below to roll the state and increment the request counter. match H::HASH { SupportedHash::SHA256 => { - hashgen::(&*self.state.v, out); + hashgen::(v, out); } SupportedHash::SHA512 => { - hashgen::(&*self.state.v, out); + hashgen::(v, out); } } } - // 4. H = Hash (0x03 || V). - // let mut h = [0u8; H::OUT_LEN]; - let mut h = [0u8; 64]; + // 4. H = Hash (0x03 || V). `h` is sliced to the hash's outlen: H is an outlen-bit + // integer, added at the low end of V in step 5. + let mut h = [0u8; LARGEST_HASHER_OUTPUT_LEN]; + let h = &mut h[..H::OUTPUT_LEN]; match H::HASH { SupportedHash::SHA256 => { let mut sha = SHA256::default(); sha.do_update(&[0x03]); - sha.do_update(&*self.state.v); - sha.do_final_out(&mut h); + sha.do_update(v); + sha.do_final_out(h); } SupportedHash::SHA512 => { let mut sha = SHA512::default(); sha.do_update(&[0x03]); - sha.do_update(&*self.state.v); - sha.do_final_out(&mut h); + sha.do_update(v); + sha.do_final_out(h); } }; - // 5. V = (V + H + C + reseed_counter) mod 2^seedlen. - add_to_array(&mut *self.state.v, &h); - add_to_array(&mut *self.state.v, &*self.state.c); - add_to_array(&mut *self.state.v, &self.state.reseed_counter.to_le_bytes()); + // 5. V = (V + H + C + reseed_counter) mod 2^seedlen. Every operand is a big-endian + // integer aligned at the low end of V; the counter's bytes are its big-endian ones. + add_to_array(v, h); + add_to_array(v, &(*self.state.c)[..H::SEED_LEN]); + add_to_array(v, &self.state.reseed_counter.to_be_bytes()); // 6. reseed_counter = reseed_counter + 1. *self.state.reseed_counter += 1; @@ -526,8 +533,11 @@ impl RNG for HashDRBG80090A { /*** Internal Helper Functions ***/ -/// the hash_df function as defined in SP 800-90Ar1 section 10.3.1. -/// no_of_bits_to_return is the length of the provided output buffer. +/// `Hash_df` as defined in SP 800-90A Rev. 1 Section 10.3.1: `temp = temp || Hash(counter || +/// no_of_bits_to_return || input_string)` for `len = ceil(no_of_bits_to_return / outlen)` blocks, +/// with `counter` an 8-bit value starting at 1 and `no_of_bits_to_return` "represented as a +/// 32-bit integer", i.e. as its four big-endian bytes, which is what the CAVP vectors use. +/// `no_of_bits_to_return` is eight times the length of the provided output buffer. /// Because array concatenation is not available in a no_std / no_alloc build, this takes many input parameters. // To leave a parameter unused, simply provide an empty array &[0u8;0] fn hash_df( @@ -555,8 +565,8 @@ fn hash_df( // out of the loop and by merging i and counter into the same variable. for i in 1..len { let mut h = H::default(); - h.do_update(&counter.to_le_bytes()); - h.do_update(&no_of_bits_to_return.to_le_bytes()); + h.do_update(&[counter]); + h.do_update(&no_of_bits_to_return.to_be_bytes()); h.do_update(in1); h.do_update(in2); h.do_update(in3); @@ -573,15 +583,14 @@ fn hash_df( let remainder = out.len() - bytes_written; if remainder != 0 { let mut h = H::default(); - h.do_update(&counter.to_le_bytes()); - h.do_update(&no_of_bits_to_return.to_le_bytes()); + h.do_update(&[counter]); + h.do_update(&no_of_bits_to_return.to_be_bytes()); h.do_update(in1); h.do_update(in2); h.do_update(in3); h.do_update(in4); - // let mut temp = [0u8; H::OUTPUT_LEN]; - let mut temp = [0u8; 64]; + let mut temp = [0u8; LARGEST_HASHER_OUTPUT_LEN]; h.do_final_out(&mut temp); // Copy only what we need from the last block. @@ -589,26 +598,35 @@ fn hash_df( } } +/// Known answers computed independently in Python from Section 10.3.1 (`hashlib`, `counter` as +/// one byte from 1, `no_of_bits_to_return` as `to_bytes(4, "big")`), not from this function; the +/// previous expected value here was this function's own output and anchored a little-endian +/// length. The CAVP `Hash_DRBG.rsp` suites in `tests/` are the end-to-end check. #[test] fn test_hash_df() { - // success case + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + // The four inputs concatenate to 01 02 03 04 05 06 07 08 09; 100 bytes of output. let mut out = [0u8; 100]; hash_df::(&[0x01, 0x02, 0x03], &[0x04, 0x05], &[0x06, 0x07], &[0x08, 0x09], &mut out); - assert_ne!(out, [0u8; 100]); - // repeatability test - // println!("out: {:?}", out); assert_eq!( - out, - [ - 150u8, 177u8, 87u8, 145u8, 138u8, 4u8, 164u8, 14u8, 162u8, 43u8, 159u8, 152u8, 121u8, - 117u8, 6u8, 18u8, 253u8, 84u8, 41u8, 64u8, 40u8, 209u8, 16u8, 176u8, 106u8, 115u8, - 172u8, 193u8, 246u8, 228u8, 208u8, 79u8, 37u8, 31u8, 134u8, 141u8, 200u8, 7u8, 42u8, - 199u8, 229u8, 236u8, 236u8, 186u8, 28u8, 87u8, 200u8, 14u8, 127u8, 36u8, 132u8, 23u8, - 36u8, 150u8, 23u8, 215u8, 247u8, 121u8, 175u8, 82u8, 99u8, 187u8, 235u8, 25u8, 213u8, - 18u8, 106u8, 22u8, 4u8, 99u8, 1u8, 184u8, 211u8, 160u8, 177u8, 67u8, 78u8, 181u8, 69u8, - 51u8, 117u8, 2u8, 72u8, 36u8, 134u8, 72u8, 2u8, 9u8, 105u8, 149u8, 136u8, 35u8, 81u8, - 114u8, 142u8, 80u8, 94u8, 42u8, 85u8, 155 - ] + out.to_vec(), + hex( + "abeaf302decd192379a95a7a11d248a6e42a631b776d290d4e3003ad6d773f2c3d3943c83dbf789f45629ea0\ + 3c48353bf16fc6d193d61422de4683a2b369a106aa4269a1a40acd90f572ef5206cd4370ac43366f0b8e9d85\ + 314eaac6352274add09c4097" + ) + ); + let mut out = [0u8; 100]; + hash_df::(&[0x01, 0x02, 0x03], &[0x04, 0x05], &[0x06, 0x07], &[0x08, 0x09], &mut out); + assert_eq!( + out.to_vec(), + hex( + "c056d412f5f174c371b71c63ca77eeaaacddca954ce583d09100504eed4f15e15439df83becf02b36967f51c\ + 70dd8fa6bf4263c23635cd3cc512d8442e63ed700ddec045ea860082843abc26291c1fc763117eb274d81581\ + 1a456eac69b0363ac6ecbc81" + ) ); // Test success with out.len() at the maximum allowed for SHA256 (255 * 32 = 8160) @@ -655,9 +673,9 @@ fn hashgen(v: &[u8], out: &mut [u8]) { let m = u32::div_ceil(out.len() as u32, H::OUTPUT_LEN as u32); - // requested_no_of_bits = out.len() - // let mut data= [0u8; H::OUTPUT_LEN]; - let mut data = [0u8; 64]; + // requested_no_of_bits = out.len(); `data` is seedlen bytes, as `v` is. + let mut data = [0u8; LARGEST_SEED_LEN]; + let data = &mut data[..v.len()]; data.copy_from_slice(v); // W = out @@ -667,10 +685,10 @@ fn hashgen(v: &[u8], out: &mut [u8]) { // 4.3 data = (data + 1) mod 2^seedlen. for i in 1..m { H::hash_out( - &data, + data, &mut out[((i - 1) as usize) * H::OUTPUT_LEN..(i as usize) * H::OUTPUT_LEN], ); - add_to_array(&mut data, &[0x01]); + add_to_array(data, &[0x01]); } // Handle the last block separately since not all of it will fit in the output buffer. @@ -679,9 +697,8 @@ fn hashgen(v: &[u8], out: &mut [u8]) { let bytes_written = (m - 1) as usize * H::OUTPUT_LEN; let remainder = out.len() - bytes_written; if remainder != 0 { - // let mut temp = [0u8; H::OUTPUT_LEN]; - let mut temp = [0u8; 64]; - H::hash_out(&data, &mut temp); + let mut temp = [0u8; LARGEST_HASHER_OUTPUT_LEN]; + H::hash_out(data, &mut temp); // Copy only what we need from the last block. out[bytes_written..].copy_from_slice(&temp[..remainder as usize]); diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 8f854a2eb..fb2df49e2 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -71,14 +71,18 @@ pub type Default128BitRNG = HashDRBG_SHA256; /// The library's default RNG at the 256-bit security level. pub type Default256BitRNG = HashDRBG_SHA512; -/// Implements the five functions specified in SP 800-90A section 7.4 are -/// - instantate, -/// - generate, -/// - reseed, -/// - uninstantiate, and -/// - health_test. -/// Note: this function implements Rust's Drop on the sensitive working state in place of the explicit -/// Uninstantiate function listed in SP 800-90Ar1. +/// The DRBG mechanism functions of SP 800-90A Rev. 1 Section 7.4: instantiate, generate and +/// reseed are the methods below; uninstantiate is Rust's `Drop` on the sensitive working state; +/// the health test is not implemented yet. `prediction_resistance` is not supported: only +/// `false` is accepted. A constructor that seeds from the OS (`new`, `new_from_os`, `Default`) +/// panics if the OS entropy source fails, since it cannot return an error. +/// +/// Reseeding is the caller's: after `reseed_interval` (2^48) requests every generating method, +/// the [`bouncycastle_core::traits::RNG`] ones included, returns [`RNGError::ReseedRequired`] until [`Self::reseed`] or +/// [`bouncycastle_core::traits::RNG::add_seed_keymaterial`] has run (Section 9.3.1 steps 7-8, with the "go back" loop left +/// to the application). Both are the full reseed of Section 10.1.1.3, so the seed they take must +/// carry at least the instance's security strength, as at instantiation; there is no automatic +/// reseed from the OS. pub trait Sp80090ADrbg { /// The input KeyMaterial must be of type [`KeyType::Seed`]. /// @@ -99,8 +103,15 @@ pub trait Sp80090ADrbg { /// This function takes ownership of the seed KeyMaterial object, /// to reduce the likelihood of its reuse in a second function call. /// - /// There is no entropy requirement on the nonce, but it is expected as a KeyMaterial so that it - /// benefits from the secure erasure and logging protections in the KeyMaterial object. + /// The nonce is Section 8.6.7's: "either: a. A value with at least (security_strength/2) bits + /// of entropy, or b. A value that is expected to repeat no more often than a + /// (security_strength/2)-bit random string would be expected to repeat", unique to the module + /// and "considered to be a critical security parameter" though it "need not be secret". It is + /// not checked here, which is why it is a KeyMaterial: so that it gets the erasure and logging + /// protections of one. An empty nonce is accepted and the seed then has to carry the whole + /// requirement on its own: `new_from_os` passes one, with a seed of twice the strength, which + /// covers Section 8.6.7's alternative of an entropy input of `(3/2 security_strength)` bits + /// in place of a nonce. Neither is checked here (the deferred R6). fn instantiate( &mut self, prediction_resistance: bool, @@ -126,26 +137,26 @@ pub trait Sp80090ADrbg { /// security strength provided in these returned bits is the minimum of the security strength /// supported by the DRBG and the length of the bit string returned" /// - /// As required by SP 800-90A section 8.4, `len` cannot exceed the initialized [`SecurityStrength`] - /// of this instance, although multiple calls to this function can be made, in which case it is the - /// application's responsibility to track that it is not expecting more entropy than the [`SecurityStrength`] - /// to which this instance was instantiated. For example, extracting two 128-bit values from an instance - /// instantiated to [`SecurityStrength::_128bit`] and then combining tem to form an AES-256 key would likely - /// not pass FIPS certification. + /// Section 8.4 does not limit `len`: "Security_strength_of_output = min(output_length, + /// DRBG_security_strength)", so a longer output is allowed and carries no more strength than + /// the instance has, and neither does a concatenation of outputs. Extracting two 128-bit + /// values from an instance instantiated at [`SecurityStrength::_128bit`] and combining them + /// into an AES-256 key gives a 128-bit key. What is limited is one request: `len` is bytes, + /// and more than `max_number_of_bits_per_request` (2^19 bits, Table 2) is refused as + /// [`RNGError::GenericError`]; so is an `additional_input` over 2^35 bits. /// - /// Throws a [`RNGError::InsufficientSeedEntropy`] if `len` exceeds [`SecurityStrength`]. + /// # Errors + /// [`RNGError::Uninitialized`] before [`Self::instantiate`]; [`RNGError::ReseedRequired`] after + /// `reseed_interval` requests; [`RNGError::GenericError`] for the length limits. fn generate(&mut self, additional_input: &[u8], len: usize) -> Result, RNGError>; - /// As per [`Sp80090ADrbg::generate`], but writes to the provided output slice. - /// The output slice is filled. - /// Throws a [`RNGError::InsufficientSeedEntropy`] if the length of the output slice exceeds [`SecurityStrength`]. - /// Retruns the number of bits output. + /// As per [`Sp80090ADrbg::generate`], but writes to the provided output slice, which is + /// filled (and zeroized first). Returns the number of bytes written, the slice's length. fn generate_out(&mut self, additional_input: &[u8], out: &mut [u8]) -> Result; - /// As per [`Sp80090ADrbg::generate`], but writes to the provided KeyMaterial. - /// The output [`KeyMaterialTrait`] is filled to capacity. - /// Throws a [`RNGError::InsufficientSeedEntropy`] if the capacity of the output KeyMaterial exceeds [`SecurityStrength`]. - /// Retruns the number of bits output. + /// As per [`Sp80090ADrbg::generate`], but writes to the provided KeyMaterial, which is filled + /// to capacity and tagged [`KeyType::CryptographicRandom`] at Section 8.4's + /// `min(output_length, DRBG_security_strength)`. Returns the number of bytes written. fn generate_keymaterial_out( &mut self, additional_input: &[u8], diff --git a/crypto/rng/tests/cavp_hash_drbg.rs b/crypto/rng/tests/cavp_hash_drbg.rs new file mode 100644 index 000000000..eaeb12b4e --- /dev/null +++ b/crypto/rng/tests/cavp_hash_drbg.rs @@ -0,0 +1,136 @@ +//! NIST CAVP Hash_DRBG vectors (`drbgtestvectors.zip`, CAVS 14.3, 2013-04-02), from +//! `bc-test-data/crypto/cavp/drbg/` (the copies there have LF line endings where the zip has +//! CRLF; the content is otherwise identical and the parser trims): `drbgvectors_no_reseed/Hash_DRBG.rsp` (instantiate, two +//! generates) and `drbgvectors_pr_false/Hash_DRBG.rsp` (instantiate, reseed, two generates), the +//! SHA-256 and SHA-512 sections, with and without a personalization string and additional input. +//! The second generate's output is `ReturnedBits`. `drbgvectors_pr_true` is not read: the DRBG +//! does not support prediction resistance. The other hashes' sections are skipped: SHA-256 and +//! SHA-512 are the two the crate implements. +//! +//! A missing `bc-test-data` checkout skips these with the loader's warning, as the other crates' +//! CAVP suites do. + +use bouncycastle_core::key_material::{KeyMaterial512, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex::decode as hex_decode; +use bouncycastle_rng::hazmat::NewUninitialized; +use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512, Sp80090ADrbg}; + +/// One `COUNT` block: the fields in file order, with the two `AdditionalInput` lines kept apart. +#[derive(Default)] +struct Record { + count: String, + entropy: Vec, + nonce: Vec, + personalization: Vec, + entropy_reseed: Option>, + additional_reseed: Vec, + additional: Vec>, + returned_bits: Vec, +} + +/// The records of every `[SHA-256]` and `[SHA-512]` section, tagged with the section's hash. +fn parse(contents: &str) -> Vec<(&'static str, Record)> { + let mut out = Vec::new(); + let mut hash: Option<&'static str> = None; + let mut record: Option = None; + for line in contents.lines().map(str::trim) { + if line.starts_with('#') { + continue; + } + if line.is_empty() { + if let (Some(h), Some(r)) = (hash, record.take()) { + out.push((h, r)); + } + continue; + } + if let Some(section) = line.strip_prefix('[') { + let section = section.trim_end_matches(']'); + if !section.contains('=') { + hash = match section { + "SHA-256" => Some("SHA-256"), + "SHA-512" => Some("SHA-512"), + _ => None, + }; + } + continue; + } + let (key, value) = line.split_once('=').expect("a `key = value` line"); + let (key, value) = (key.trim(), value.trim()); + let bytes = || hex_decode(value).expect("hex"); + if key == "COUNT" { + record = Some(Record { count: value.to_string(), ..Default::default() }); + continue; + } + let r = record.as_mut().expect("a field inside a COUNT block"); + match key { + "EntropyInput" => r.entropy = bytes(), + "Nonce" => r.nonce = bytes(), + "PersonalizationString" => r.personalization = bytes(), + "EntropyInputReseed" => r.entropy_reseed = Some(bytes()), + "AdditionalInputReseed" => r.additional_reseed = bytes(), + "AdditionalInput" => r.additional.push(bytes()), + "ReturnedBits" => r.returned_bits = bytes(), + other => panic!("unexpected field {other}"), + } + } + if let (Some(h), Some(r)) = (hash, record.take()) { + out.push((h, r)); + } + out +} + +fn seed(bytes: &[u8]) -> KeyMaterial512 { + KeyMaterial512::from_bytes_as_type(bytes, KeyType::Seed).expect("a seed") +} + +/// Drives one record as the CAVP procedure does and returns the second generate's output. +fn run(r: &Record, strength: SecurityStrength) -> Vec { + let mut drbg = D::new_uninitialized(); + drbg.instantiate(false, seed(&r.entropy), &seed(&r.nonce), &r.personalization, strength) + .unwrap_or_else(|e| panic!("COUNT {}: instantiate: {e:?}", r.count)); + if let Some(entropy_reseed) = &r.entropy_reseed { + drbg.reseed(&seed(entropy_reseed), &r.additional_reseed) + .unwrap_or_else(|e| panic!("COUNT {}: reseed: {e:?}", r.count)); + } + assert_eq!(r.additional.len(), 2, "COUNT {}: two AdditionalInput lines", r.count); + let n = r.returned_bits.len(); + drbg.generate(&r.additional[0], n) + .unwrap_or_else(|e| panic!("COUNT {}: first generate: {e:?}", r.count)); + drbg.generate(&r.additional[1], n) + .unwrap_or_else(|e| panic!("COUNT {}: second generate: {e:?}", r.count)) +} + +fn check_file(dir: &str) { + let Some(contents) = bc_test_data(&format!("crypto/cavp/drbg/{dir}"), "Hash_DRBG.rsp") else { + return; + }; + let records = parse(&contents); + let (mut sha256, mut sha512) = (0, 0); + for (hash, r) in &records { + let got = match *hash { + "SHA-256" => { + sha256 += 1; + run::(r, SecurityStrength::_128bit) + } + _ => { + sha512 += 1; + run::(r, SecurityStrength::_256bit) + } + }; + assert_eq!(got, r.returned_bits, "{dir} {hash} COUNT {}", r.count); + } + println!("{dir}: {sha256} SHA-256 and {sha512} SHA-512 Hash_DRBG vectors passed"); + assert!(sha256 >= 100 && sha512 >= 100, "{dir}: the SHA-256 and SHA-512 sections were read"); +} + +#[test] +fn cavp_hash_drbg_no_reseed() { + check_file("drbgvectors_no_reseed"); +} + +#[test] +fn cavp_hash_drbg_pr_false() { + check_file("drbgvectors_pr_false"); +} diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index f408fbb8d..0799babdd 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -332,4 +332,82 @@ mod tests { assert_ne!(key.ref_to_bytes(), [0u8; 32]); assert_eq!(key.security_strength(), rng.security_strength()); } + + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + /// The reseed counter is added into V at the end of every generate (Section 10.1.1.4 step 5) + /// and incremented (step 6); the CAVP files stop at the second generate, whose output does not + /// yet depend on the increment, so a third generate is pinned here. Expected values are from a + /// Hash_DRBG written independently in Python from Sections 10.1.1.2-10.1.1.4 and 10.3.1 and + /// checked against the CAVP no-reseed vectors first; the inputs are those vectors' COUNT 0. + #[test] + fn a_third_generate_matches_an_independent_reference() { + fn third( + entropy: &str, + nonce: &str, + strength: SecurityStrength, + len: usize, + ) -> Vec { + let mut drbg = D::new_uninitialized(); + let seed = KeyMaterial::<64>::from_bytes_as_type(&hex(entropy), KeyType::Seed).unwrap(); + let nonce = KeyMaterial::<64>::from_bytes_as_type(&hex(nonce), KeyType::Seed).unwrap(); + drbg.instantiate(false, seed, &nonce, &[], strength).unwrap(); + drbg.generate(&[], len).unwrap(); + drbg.generate(&[], len).unwrap(); + drbg.generate(&[], len).unwrap() + } + assert_eq!( + third::( + "a65ad0f345db4e0effe875c3a2e71f42c7129d620ff5c119a9ef55f05185e0fb", + "8581f9317517276e06e9607ddbcbcc2e", + SecurityStrength::_128bit, + 128 + ), + hex( + "c585885f220a9e616817c24192a6314f2170c6e066b3ebb7be43717a427d3bc36f83390195fe191a990a6926709ec3b59b6a88074dbab83a443ca69061c3e4a645597cd6733e068631b63c731250672134b50898579b17b6a4e9e0f121ec809c46135738e8009fde4f2464a0cd2893c3a1a1c1d96b1b36040271d6e71bb3d430" + ) + ); + assert_eq!( + third::( + "6b50a7d8f8a55d7a3df8bb40bcc3b722d8708de67fda010b03c4c84d72096f8c", + "3ec649cc6256d9fa31db7a2904aaf025", + SecurityStrength::_256bit, + 256 + ), + hex( + "356ad18ba9f67f3a5b5f56d9a20731a05f02f13aa1659eb3906d557e825078208c094ce00e403ec150cfdf98cea7825a54274c23e8194f3f400afe492731987b7e37cbe10eccddb32900b93e859a029ac9032e057428de67af2ad84a877776429ef6d325670be177b0ef1c0a15ad2907bd04cf6b910607abbb179e332b74645dc8d117c4d87da5242c2903216d3ada8ec01402144b61f90ed94b838e298c7a314fe9ebacae1a968dda321de4a9ae1681efa0e714d9f94845e2be898738c1d8b6e288f6599dddf3f4bd049b3512a5046e53b0b5e41276fb94ce63e24371cadc43353bf3dedd9e3b72697f6c10210b261945e052f3f0b1a9c533d4082d5414e61c" + ) + ); + } + + /// Table 2: `max_number_of_bits_per_request` is 2^19 bits, 65536 bytes; one more is refused. + #[test] + fn the_per_request_limit_is_enforced() { + let mut rng = HashDRBG_SHA256::new_from_os(); + assert_eq!(rng.generate(&[], 65536).unwrap().len(), 65536); + assert!(matches!(rng.generate(&[], 65537), Err(RNGError::GenericError(_)))); + let mut out = vec![0u8; 65537]; + assert!(matches!(rng.generate_out(&[], &mut out), Err(RNGError::GenericError(_)))); + } + + /// `add_seed_keymaterial` is the full reseed of Section 10.1.1.3: two instances from one seed + /// agree until one is reseeded, and disagree from then on. + #[test] + fn reseeding_changes_the_stream() { + fn instance() -> HashDRBG_SHA256 { + let mut rng = HashDRBG_SHA256::new_uninitialized(); + let seed = + KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); + rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit) + .unwrap(); + rng + } + let (mut a, mut b) = (instance(), instance()); + assert_eq!(a.next_bytes(32).unwrap(), b.next_bytes(32).unwrap()); + let fresh = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::Seed).unwrap(); + b.add_seed_keymaterial(&fresh).unwrap(); + assert_ne!(a.next_bytes(32).unwrap(), b.next_bytes(32).unwrap()); + } } diff --git a/crypto/sha2/src/hkdf.rs b/crypto/sha2/src/hkdf.rs index f8a2589a0..640e26754 100644 --- a/crypto/sha2/src/hkdf.rs +++ b/crypto/sha2/src/hkdf.rs @@ -205,7 +205,9 @@ //! //! * Resuming a suspended HKDF with a different salt cannot be detected and silently produces a //! different PRK; see the suspend/resume section above. -//! * Entropy is only credited for input supplied via [`HKDF::do_extract_update_key`]. Bytes supplied +//! * Entropy is only credited for input supplied via [`HKDF::do_extract_update_key`], and for a salt +//! typed [`bouncycastle_core::key_material::KeyType::MACKey`] (a secret salt); any other salt is RFC +//! 5869's non-secret value and credits nothing. Bytes supplied //! via [`HKDF::do_extract_update_bytes`] are treated as uncredited context, so a PRK derived only //! from raw bytes will not be tagged as full-entropy key material even if those bytes were in fact //! random. diff --git a/crypto/sha2/src/hmac.rs b/crypto/sha2/src/hmac.rs index 451ab92b8..5bcf834a5 100644 --- a/crypto/sha2/src/hmac.rs +++ b/crypto/sha2/src/hmac.rs @@ -1,5 +1,5 @@ -//! HMAC over the SHA-2 hashes, as specified in RFC 2104, with the truncation rules of NIST SP -//! 800-107 Rev 1. +//! HMAC over the SHA-2 hashes, as specified in RFC 2104, with the truncation rules of FIPS 140-3 +//! IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SHA2 instantiations: [`HMAC_SHA224`], //! [`HMAC_SHA256`], [`HMAC_SHA384`] and [`HMAC_SHA512`]. @@ -234,10 +234,10 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong //! MAC; see the suspend/resume section above. //! * A key longer than the hash's block length is pre-hashed down to the output length (RFC 2104 diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index a44e7f01c..db6003723 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -195,7 +195,8 @@ pub type SHA512 = SHA512Internal; /// initial hash value rather than one produced by the IV Generation Function below. Letting /// `T = 384` through here would derive a second, different 384-bit hash under a name already /// taken, so the standard reserves 384 for SHA-384 instead. Anything else is a compile error -/// naming the rule it broke: +/// naming the rule it broke, raised when the code using it is generated (`cargo build`, not +/// `cargo check`), whether the value is constructed or resumed: /// /// ```compile_fail /// use bouncycastle_sha2::SHA512t; @@ -204,6 +205,14 @@ pub type SHA512 = SHA512Internal; /// ``` /// /// ```compile_fail +/// use bouncycastle_core::traits::Suspendable; +/// use bouncycastle_sha2::{SHA512, SHA512t}; +/// // The same rule on the resume path, which never computes an initial hash value. +/// let state = SHA512::new().suspend(); +/// let _ = SHA512t::<384>::from_suspended(state); +/// ``` +/// +/// ```compile_fail /// use bouncycastle_sha2::SHA512t; /// // Not a whole number of bytes: this crate requires t to be a multiple of 8. /// let _ = SHA512t::<100>::new(); @@ -418,7 +427,10 @@ pub struct SHA512tParams; impl SHA512tParams { /// `"SHA512/t"` with `T` in decimal, NUL-padded; see [`Self::ALG_NAME_STR`]. - const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = sha512::alg_name_bytes(T); + const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = { + sha512::check_t(T); + sha512::alg_name_bytes(T) + }; /// The algorithm name, e.g. `"SHA512/224"`. Built at compile time from `T` because a const /// generic cannot be formatted into a `&'static str` directly. @@ -442,8 +454,12 @@ impl Algorithm for SHA512tParams { } impl HashAlgParams for SHA512tParams { /// FIPS 180-4 s. 6.6 / s. 6.7 exception 2: truncated to the left-most `T` bits. `T` is a - /// multiple of 8 (checked by `check_t` in `sha512.rs`), so this is exact. - const OUTPUT_LEN: usize = T / 8; + /// multiple of 8, so this is exact. `check_t` runs here as well as in `H0`, so that a value + /// resumed from a suspended state, which never reads `H0`, is held to the same rules. + const OUTPUT_LEN: usize = { + sha512::check_t(T); + T / 8 + }; const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits } impl SHA512InitValue for SHA512tParams { diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 7b59d25a9..cfe22dd28 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -424,6 +424,11 @@ impl Suspendable for SHA256 if byte_count % 64 != x_buf_off as u64 { return Err(SuspendableError::InvalidData); } + // FIPS 180-4 s. 5.1.1 encodes the length in a 64-bit block, so a longer message cannot be + // finished correctly. + if byte_count > MAX_MESSAGE_BYTES { + return Err(SuspendableError::InvalidData); + } // Construct the object let state = Sha256State { _params: core::marker::PhantomData, h }; diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index 5e205843a..c08dc0b13 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -541,6 +541,10 @@ impl Suspendable for SHA512 fn from_suspended( serialized_state: [u8; SUSPENDED_SHA512_STATE_LEN], ) -> Result { + // Resuming builds the state without the initial hash value, so evaluate it here: for + // SHA-512/t that is what runs `check_t`, which makes an invalid `t` a compile error on + // this path too. + const { _ = PARAMS::H0 }; // check the version tag // At the moment, we have no not_before version to specify. // infallible: check_lib_ver returns a slice of exactly SUSPENDED_SHA512_STATE_LEN - 3 = 201 bytes. diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 006a95d47..4394a829c 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -306,6 +306,19 @@ mod sha2_tests { _ => panic!("Expected an error"), } + // byte_count may not exceed 2^61 - 1 bytes, the longest message the 64-bit length block + // encodes (x_buf_off kept congruent so only the bound is tested). + let with_byte_count = |byte_count: u64| { + let mut state = serialized_state; + state[3 + 32..3 + 40].copy_from_slice(&byte_count.to_le_bytes()); + state[3 + 104] = (byte_count % 64) as u8; + SHA256::from_suspended(state) + }; + let max: u64 = (1 << 61) - 1; + assert!(with_byte_count(max).is_ok()); + assert!(matches!(with_byte_count(max + 1), Err(SuspendableError::InvalidData))); + assert!(matches!(with_byte_count(u64::MAX - 63), Err(SuspendableError::InvalidData))); + // SHA512 let mut sha512 = SHA512::new(); sha512.do_update(str.as_bytes()); diff --git a/crypto/sha3/benches/sha3_benches.rs b/crypto/sha3/benches/sha3_benches.rs index a12c2e4e9..851ebdc4d 100644 --- a/crypto/sha3/benches/sha3_benches.rs +++ b/crypto/sha3/benches/sha3_benches.rs @@ -256,7 +256,7 @@ fn bench_tuplehash128(c: &mut Criterion) { group.throughput(Throughput::Bytes(big_data.len() as u64)); group.bench_function(format!("{} bytes in 4 elements -- .hash_tuple()", big_data.len()), |b| { b.iter(|| { - black_box(TupleHash128::new(b"", 32).hash_tuple(black_box(&tuple))); + black_box(TupleHash128::new(b"", 32).unwrap().hash_tuple(black_box(&tuple))); }) }); group.finish(); @@ -270,7 +270,9 @@ fn bench_parallelhash128(c: &mut Criterion) { group.throughput(Throughput::Bytes(big_data.len() as u64)); group.bench_function(format!("{} bytes, B = 8192 -- .hash_out()", big_data.len()), |b| { b.iter(|| { - ParallelHash128::new(block_size, b"", 32).hash_out(black_box(&big_data), &mut digest); + ParallelHash128::new(block_size, b"", 32) + .unwrap() + .hash_out(black_box(&big_data), &mut digest); black_box(&digest); }) }); diff --git a/crypto/sha3/src/hmac.rs b/crypto/sha3/src/hmac.rs index 7ec3bdf0f..67eb2f67a 100644 --- a/crypto/sha3/src/hmac.rs +++ b/crypto/sha3/src/hmac.rs @@ -1,5 +1,5 @@ //! HMAC over the SHA-3 hashes, as specified in RFC 2104, taking into account NIST Implementation -//! Guidance in FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! Guidance in FIPS 140-3 IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SHA3 instantiations: [`HMAC_SHA3_224`], //! [`HMAC_SHA3_256`], [`HMAC_SHA3_384`] and [`HMAC_SHA3_512`]. @@ -234,10 +234,10 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * A suspended HMAC state can be inverted to the key and must be stored as securely as the key; //! and resuming with the wrong key cannot be detected and silently produces a wrong MAC. See the //! suspend/resume section above. diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 2a3470424..04ac5f147 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -2,6 +2,7 @@ use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::bounded_usize; /// `RC[ir]` for the 24 rounds of KECCAK-f[1600]: FIPS 202 Algorithm 6 (iota) steps 2-3, /// evaluated with the `rc(t)` LFSR of Algorithm 5 for `ir = 0..24` and stored as lanes. @@ -322,6 +323,19 @@ impl KeccakInternal { if !self.squeezing { self.pad_and_switch_to_squeezing_phase(); } + // The loop below counts in bits, so a slice whose bit length does not fit a `usize` (512 + // MiB on a 32-bit target) would wrap the bound and stop early; squeeze it in pieces that + // fit. On a 64-bit target the piece is larger than any buffer, so this is at most one + // iteration (none for an empty `out`, which has nothing to write). + for piece in out.chunks_mut(usize::MAX >> 3) { + self.squeeze_into(piece); + } + out.len() + } + + /// [`Self::squeeze`]'s loop for a slice of fewer than `usize::MAX / 8` bytes, with the sponge + /// already in the squeezing phase. + fn squeeze_into(&mut self, out: &mut [u8]) { let output_length = out.len() << 3; let mut i = 0; @@ -340,7 +354,6 @@ impl KeccakInternal { self.bits_in_queue -= partial_block; i += partial_block; } - output_length >> 3 } #[inline(always)] @@ -529,9 +542,16 @@ pub(crate) fn deserialize_sha3_family_state( // KeyType and SecurityStrength each own their canonical 1-byte encoding (`as u8` / `TryFrom`). let kdf_key_type = KeyType::try_from(input[1 + KECCAK_SERIALIZED_LEN])?; let kdf_security_strength = SecurityStrength::try_from(input[1 + KECCAK_SERIALIZED_LEN + 1])?; - let kdf_entropy = u64::from_le_bytes( - input[1 + KECCAK_SERIALIZED_LEN + 2..1 + KECCAK_SERIALIZED_LEN + 10].try_into().unwrap(), - ) as usize; + // `usize::MAX` only narrows the width: any count is valid, but a 32-bit target must not + // truncate one that was written on a 64-bit one. + let kdf_entropy = bounded_usize( + u64::from_le_bytes( + input[1 + KECCAK_SERIALIZED_LEN + 2..1 + KECCAK_SERIALIZED_LEN + 10] + .try_into() + .unwrap(), + ), + usize::MAX, + )?; Ok((keccak, kdf_key_type, kdf_security_strength, kdf_entropy)) } diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 5ce50b3b9..4ce533057 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -108,8 +108,9 @@ //! SHA3 offers Key Derivation Functions in the form of KDF, which is accessed through the [`KDF`] trait, //! which is implemented by all SHA3 and SHAKE variants. //! [`KDF`] acts on [`KeyMaterial`] objects as both the input and output values. -//! In the case of SHA3, the [`KDF`] interfaces are simple wrapper functions around the underlying SHA3 or SHAKE -//! primitive that correctly maintains the length and entropy metadata of the key material that it is acting on. +//! In the case of SHA3, the [`KDF`] interfaces hash the concatenated input keys and additional input +//! with the underlying SHA3 or SHAKE primitive, maintaining the length and entropy metadata of the key +//! material; they are not the SP 800-56Cr2 one-step KDF, which puts a counter before the input. //! This is intended to act as a developer aid to prevent some classes of developer mistakes, such as //! deriving a cryptographic key from uninitialized (aka zeroized) input key material, or using low-entropy //! input key material to derive a MAC, symmetric, or asymmetric key. @@ -192,9 +193,8 @@ //! | Object | Size (bytes) | //! |-----------------------------------------------------------------|--------------| //! | `SHA3_224` .. `SHA3_512`, `SHAKE128/256`, `SHAKESqueezer` | 440 | -//! | `CSHAKE128/256`, `TUPLEHASHXOF128/256`, `CSHAKESqueezer` | 448 | -//! | `KMACXOF128/256`, `TUPLEHASH128/256` | 456 | -//! | `KMAC128/256` | 464 | +//! | `CSHAKE128/256`, `KMACXOF128/256`, `TUPLEHASHXOF128/256`, `CSHAKESqueezer` | 448 | +//! | `KMAC128/256`, `KMAC128Key`/`KMAC256Key`, `TUPLEHASH128/256` | 456 | //! | `PARALLELHASHXOF128/256` | 912 | //! | `PARALLELHASH128/256` | 920 | //! @@ -508,3 +508,14 @@ impl AlgorithmOID for SHAKE256 { const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0c]; } + +/// SHAKE as a fixed-length digest, for the algorithms that bind a hash's identity and length +/// into what they sign (HashML-DSA, PKCS#1): id-shake128 is 32 bytes and id-shake256 is 64 +/// (RFC 8702 Section 3.1: "the output length of SHAKE128 or SHAKE256 as the message digest +/// MUST be 32 or 64 bytes, respectively"), which is also FIPS 204 Algorithm 4's +/// SHAKE128(M, 256). The same lengths as [`Hash::output_len`] on these types; the block +/// length is the sponge rate. +impl HashAlgParams for SHAKEInternal { + const OUTPUT_LEN: usize = (PARAMS::SIZE as usize) / 4; + const BLOCK_LEN: usize = PARAMS::SIZE.rate_bits() / 8; +} diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs index c6d42ee59..2f00cb45b 100644 --- a/crypto/sha3/src/parallelhash.rs +++ b/crypto/sha3/src/parallelhash.rs @@ -11,7 +11,7 @@ //! use bouncycastle_sha3::parallelhash::ParallelHash128; //! //! let block_size = NonZeroUsize::new(8192).unwrap(); -//! let output: Vec = ParallelHash128::new(block_size, b"", 32).hash(b"Hello, world!"); +//! let output: Vec = ParallelHash128::new(block_size, b"", 32).unwrap().hash(b"Hello, world!"); //! ``` //! //! # ParallelHashXOF @@ -37,7 +37,7 @@ //! let more: Vec = squeezer.do_output(1024); //! //! let bound: Vec = ParallelHashXOF128::new(block_size, b"").xof(b"Hello, world!", 32); -//! assert_eq!(bound, ParallelHash128::new(block_size, b"", 32).hash(b"Hello, world!")); +//! assert_eq!(bound, ParallelHash128::new(block_size, b"", 32).unwrap().hash(b"Hello, world!")); //! assert_ne!(bound[..16], first[..]); //! ``` @@ -76,6 +76,11 @@ const PARALLEL_STATE_LEN: usize = CSHAKE_COMPONENT_LEN + SHA3_FAMILY_STATE_LEN + /// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; +/// The largest `output_len` [`ParallelHashInternal::new`] accepts, and the bound a resumed state +/// is held to; see [`crate::tuplehash::TUPLEHASH_MAX_OUTPUT_LEN`] for the reasoning, which is the +/// same here (Sec 6.3.1 step 5 encodes `L` in bits). +pub const PARALLELHASH_MAX_OUTPUT_LEN: usize = (u64::MAX / 8) as usize; + /// ParallelHash128: the parallelisable hash of NIST SP 800-185 Sec 6, 128-bit strength. /// /// The block size `B` is part of the function, not a tuning knob: the same message under a @@ -126,7 +131,9 @@ impl ParallelState { let digest = &mut digest[..Self::INNER_LEN]; core::mem::take(&mut self.inner).xof_out(&[], digest); self.cshake.do_update(digest); - self.blocks += 1; + // Saturating: the count is bookkeeping for the final `right_encode(n)`, and a resumed + // state may carry any value, so `+=` could overflow on one a caller corrupted. + self.blocks = self.blocks.saturating_add(1); self.block_fill = 0; } @@ -235,8 +242,20 @@ impl Algorithm for ParallelHashInternal { impl ParallelHashInternal { /// A new ParallelHash over `block_size`-byte blocks, producing `output_len` bytes. - pub fn new(block_size: NonZeroUsize, customization: &[u8], output_len: usize) -> Self { - Self { state: ParallelState::new(block_size, customization), output_len } + /// + /// # Errors + /// [`HashError::InvalidLength`] if `output_len` is above [`PARALLELHASH_MAX_OUTPUT_LEN`]. + pub fn new( + block_size: NonZeroUsize, + customization: &[u8], + output_len: usize, + ) -> Result { + if output_len > PARALLELHASH_MAX_OUTPUT_LEN { + return Err(HashError::InvalidLength( + "ParallelHash output length in bits does not fit a u64", + )); + } + Ok(Self { state: ParallelState::new(block_size, customization), output_len }) } } @@ -256,7 +275,7 @@ impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for ParallelHashInternal< let (parallel, rest) = state.split_at(PARALLEL_STATE_LEN); let state = ParallelState::read_state(parallel, PARAMS::PARALLELHASH_STATE_TAG)?; let mut r = Cursor::new(rest); - let output_len = bounded_usize(r.u64(), usize::MAX)?; + let output_len = bounded_usize(r.u64(), PARALLELHASH_MAX_OUTPUT_LEN)?; debug_assert!(r.is_done()); Ok(Self { state, output_len }) } diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index 977a6aa39..8a9f85773 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -91,7 +91,8 @@ impl SHA3Internal { // track input entropy if key.is_full_entropy() { - self.kdf_entropy += key.key_len(); + // Saturating: the counter is bookkeeping, and a resumed state may carry any value. + self.kdf_entropy = self.kdf_entropy.saturating_add(key.key_len()); self.kdf_security_strength = *max(&self.kdf_security_strength, &key.security_strength()); self.kdf_security_strength = *min( @@ -224,7 +225,9 @@ impl Hash for SHA3Internal { } } -/// SHA3 is allowed to be used as a KDF in the form HASH(X) as per NIST SP 800-56C. +/// A plain hash of the concatenated inputs, `H(key_1 || .. || key_n || additional_input)`, with +/// the entropy bookkeeping of [`KeyMaterial`]. This is not the SP 800-56Cr2 Section 4 one-step +/// KDF, which hashes a 4-byte big-endian counter before `Z || FixedInfo` on every block. impl KDF for SHA3Internal { /// Returns a [`KeyMaterial`]. /// For the KDF to be considered "fully-seeded" and be capable of outputting full-entropy KeyMaterials, @@ -307,6 +310,13 @@ impl Suspendable for SHA3Internal< let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + // A SHA3 is only ever suspended while absorbing: finalisation consumes it. A state in + // the squeezing phase is not one this type produced, and every finalisation of it would + // fail on absorb_bits' phase check, so refuse it here as SHAKE does. + if keccak.squeezing { + return Err(SuspendableError::InvalidData); + } + Ok(SHA3Internal { _params: core::marker::PhantomData, keccak, diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index bafe7a783..ff82eab2e 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -35,7 +35,7 @@ impl Algorithm for SHAKEInternal { } impl SHAKEInternal { - /// Get a new SHA3 instance, ready for use. + /// Get a new SHAKE instance, ready for use. pub fn new() -> Self { Self { _phantomdata: core::marker::PhantomData, @@ -131,7 +131,8 @@ impl SHAKEInternal { // track input entropy if key.is_full_entropy() { - self.kdf_entropy += key.key_len(); + // Saturating: the counter is bookkeeping, and a resumed state may carry any value. + self.kdf_entropy = self.kdf_entropy.saturating_add(key.key_len()); self.kdf_security_strength = *max(&self.kdf_security_strength, &key.security_strength()); self.kdf_security_strength = *min( diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs index 3c94dde80..8d693cc4b 100644 --- a/crypto/sha3/src/tuplehash.rs +++ b/crypto/sha3/src/tuplehash.rs @@ -14,10 +14,10 @@ //! //! // .hash_tuple() takes tuples as an array of arrays //! let tuple: [&[u8]; 2] = [b"user id", b"session"]; -//! let output: Vec = TupleHash128::new(b"", 32).hash_tuple(&tuple); +//! let output: Vec = TupleHash128::new(b"", 32).unwrap().hash_tuple(&tuple); //! //! // The same computation, one element per .do_update() -//! let mut th = TupleHash128::new(b"", 32); +//! let mut th = TupleHash128::new(b"", 32).unwrap(); //! th.do_update(b"user id"); //! th.do_update(b"session"); //! assert_eq!(th.do_final(), output); @@ -44,7 +44,7 @@ //! let more: Vec = squeezer.do_output(1024); //! //! let bound: Vec = TupleHashXOF128::new(b"").xof(b"Hello, world!", 32); -//! assert_eq!(bound, TupleHash128::new(b"", 32).hash(b"Hello, world!")); +//! assert_eq!(bound, TupleHash128::new(b"", 32).unwrap().hash(b"Hello, world!")); //! assert_ne!(bound[..16], first[..]); //! ``` @@ -77,6 +77,13 @@ pub const SUSPENDED_TUPLEHASHXOF_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COM /// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; +/// The largest `output_len` [`TupleHashInternal::new`] accepts, and the bound a resumed state is +/// held to: the largest `L` in bits that fits the `u64` the final `right_encode(L)` (Sec 5.3.1 +/// step 3) and the suspended state carry. SP 800-185 Sec 5.3.1 allows any `L < 2^2040`, and Sec +/// 7.2 lets an implementation limit the lengths it produces; past this one the bit count would +/// wrap and the fixed-length function would silently become the XOF. +pub const TUPLEHASH_MAX_OUTPUT_LEN: usize = (u64::MAX / 8) as usize; + /// TupleHash128: the unambiguous tuple hash of NIST SP 800-185 Sec 5, 128-bit strength. /// /// Each [`Hash::do_update`] call appends one *tuple @@ -127,8 +134,16 @@ impl TupleHashInternal { /// /// `output_len` is `L` and is bound into the computation (Sec 5.3 step 4), so a different /// length is a different function rather than a longer or shorter view of the same one. - pub fn new(customization: &[u8], output_len: usize) -> Self { - Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization), output_len } + /// + /// # Errors + /// [`HashError::InvalidLength`] if `output_len` is above [`TUPLEHASH_MAX_OUTPUT_LEN`]. + pub fn new(customization: &[u8], output_len: usize) -> Result { + if output_len > TUPLEHASH_MAX_OUTPUT_LEN { + return Err(HashError::InvalidLength( + "TupleHash output length in bits does not fit a u64", + )); + } + Ok(Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization), output_len }) } /// Hashes a whole tuple in one call, the shape the specification is written in. @@ -156,7 +171,7 @@ impl<'k, PARAMS: SHAKEParams> SuspendableComponent<'k> for TupleHashInternal ParallelHash128::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), - 256 => ParallelHash256::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), + 128 => ParallelHash128::new(v.block_size, v.s.as_bytes(), want).unwrap().hash(&v.msg), + 256 => ParallelHash256::new(v.block_size, v.s.as_bytes(), want).unwrap().hash(&v.msg), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -144,7 +144,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { match f.strength { 128 => check_do_final_binds_length( || ParallelHashXOF128::new(b, s), - |n| ParallelHash128::new(b, s, n).hash(&f.msg), + |n| ParallelHash128::new(b, s, n).unwrap().hash(&f.msg), &f.msg, &f.output, &x.output, @@ -152,7 +152,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { ), 256 => check_do_final_binds_length( || ParallelHashXOF256::new(b, s), - |n| ParallelHash256::new(b, s, n).hash(&f.msg), + |n| ParallelHash256::new(b, s, n).unwrap().hash(&f.msg), &f.msg, &f.output, &x.output, @@ -349,15 +349,15 @@ fn hash_trait_view_agrees_with_the_sample_values() { let ctx = format!("COUNT {i}: ParallelHash{} B={b}", v.strength); match v.strength { 128 => check_fixed_view( - || ParallelHash128::new(b, s, n), - |m, o| ParallelHash128::new(b, s, n).hash_out(m, o), + || ParallelHash128::new(b, s, n).unwrap(), + |m, o| ParallelHash128::new(b, s, n).unwrap().hash_out(m, o), &v.msg, &v.output, &ctx, ), 256 => check_fixed_view( - || ParallelHash256::new(b, s, n), - |m, o| ParallelHash256::new(b, s, n).hash_out(m, o), + || ParallelHash256::new(b, s, n).unwrap(), + |m, o| ParallelHash256::new(b, s, n).unwrap().hash_out(m, o), &v.msg, &v.output, &ctx, diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs index 137b597ea..f7fb86531 100644 --- a/crypto/sha3/tests/parallelhash_tests.rs +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -5,7 +5,8 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_sha3::parallelhash::{ - ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, + PARALLELHASH_MAX_OUTPUT_LEN, ParallelHash128, ParallelHash256, ParallelHashXOF128, + ParallelHashXOF256, }; use core::num::NonZeroUsize; @@ -20,10 +21,10 @@ fn nz(n: usize) -> NonZeroUsize { #[test] fn chunking_does_not_change_the_result() { let msg: Vec = (0..=200u8).collect(); - let one = ParallelHash128::new(nz(8), b"S", 32).hash(&msg); + let one = ParallelHash128::new(nz(8), b"S", 32).unwrap().hash(&msg); for chunk in [1usize, 3, 7, 8, 9, 16, 64, 201] { - let mut p = ParallelHash128::new(nz(8), b"S", 32); + let mut p = ParallelHash128::new(nz(8), b"S", 32).unwrap(); for piece in msg.chunks(chunk) { p.do_update(piece); } @@ -36,9 +37,9 @@ fn chunking_does_not_change_the_result() { #[test] fn the_block_size_is_part_of_the_hash() { let msg: Vec = (0..=100u8).collect(); - let b8 = ParallelHash128::new(nz(8), b"", 32).hash(&msg); - let b12 = ParallelHash128::new(nz(12), b"", 32).hash(&msg); - let b16 = ParallelHash128::new(nz(16), b"", 32).hash(&msg); + let b8 = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&msg); + let b12 = ParallelHash128::new(nz(12), b"", 32).unwrap().hash(&msg); + let b16 = ParallelHash128::new(nz(16), b"", 32).unwrap().hash(&msg); assert_ne!(b8, b12); assert_ne!(b8, b16); assert_ne!(b12, b16); @@ -54,16 +55,16 @@ fn the_block_size_is_part_of_the_hash() { #[test] fn block_boundary_cases() { // exactly one full block, versus one full block plus one byte - let full = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 8]); - let plus = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 9]); + let full = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 8]); + let plus = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 9]); assert_ne!(full, plus); // two full blocks versus one short block: different block counts, so different output - let two = ParallelHash128::new(nz(8), b"", 32).hash(&[0xAAu8; 16]); + let two = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(&[0xAAu8; 16]); assert_ne!(two, full); // an empty message is zero blocks, and must still produce a hash - let empty = ParallelHash128::new(nz(8), b"", 32).hash(b""); + let empty = ParallelHash128::new(nz(8), b"", 32).unwrap().hash(b""); assert_eq!(empty.len(), 32); assert_ne!(empty, full); } @@ -73,8 +74,8 @@ fn block_boundary_cases() { #[test] fn length_binding_differs_between_the_two() { let msg = b"parallel"; - let short = ParallelHash128::new(nz(4), b"", 16).hash(msg); - let long = ParallelHash128::new(nz(4), b"", 32).hash(msg); + let short = ParallelHash128::new(nz(4), b"", 16).unwrap().hash(msg); + let long = ParallelHash128::new(nz(4), b"", 32).unwrap().hash(msg); assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); let squeeze = |n| { @@ -90,7 +91,7 @@ fn length_binding_differs_between_the_two() { /// A partial final byte cannot be expressed: the block count and length encodings must follow. #[test] fn partial_final_byte_is_refused() { - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(b"abc"); assert!(matches!(p.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); @@ -111,13 +112,25 @@ fn algorithm_names() { /// length of the fixed forms. The generic checks elsewhere only require these to be positive. #[test] fn metadata() { - assert_eq!(ParallelHash128::new(nz(8), b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); - assert_eq!(ParallelHash256::new(nz(8), b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!( + ParallelHash128::new(nz(8), b"", 32).unwrap().block_bitlen(), + 1344, + "cSHAKE128 rate" + ); + assert_eq!( + ParallelHash256::new(nz(8), b"", 64).unwrap().block_bitlen(), + 1088, + "cSHAKE256 rate" + ); assert_eq!(ParallelHashXOF128::new(nz(8), b"").block_bitlen(), 1344); assert_eq!(ParallelHashXOF256::new(nz(8), b"").block_bitlen(), 1088); - assert_eq!(ParallelHash128::new(nz(8), b"", 17).output_len(), 17, "whatever was asked for"); - assert_eq!(ParallelHash256::new(nz(8), b"", 100).output_len(), 100); + assert_eq!( + ParallelHash128::new(nz(8), b"", 17).unwrap().output_len(), + 17, + "whatever was asked for" + ); + assert_eq!(ParallelHash256::new(nz(8), b"", 100).unwrap().output_len(), 100); assert_eq!(ParallelHashXOF128::new(nz(8), b"").output_len(), 32, "the nominal length"); assert_eq!(ParallelHashXOF256::new(nz(8), b"").output_len(), 64); } @@ -131,11 +144,29 @@ fn output_buffers_of_every_length() { let framework = TestFrameworkHash::new(); let input = b"the quick brown fox jumps over the lazy dog"; - framework.test_hash_output_buffers(|| ParallelHash128::new(nz(8), b"", 32), input); - framework.test_hash_output_buffers(|| ParallelHash256::new(nz(8), b"", 64), input); + framework.test_hash_output_buffers(|| ParallelHash128::new(nz(8), b"", 32).unwrap(), input); + framework.test_hash_output_buffers(|| ParallelHash256::new(nz(8), b"", 64).unwrap(), input); // A block size that does not divide the input, a customization string, odd output lengths. - framework - .test_hash_output_buffers(|| ParallelHash128::new(nz(12), b"Parallel Data", 17), input); - framework.test_hash_output_buffers(|| ParallelHash256::new(nz(5), b"Parallel Data", 5), input); + framework.test_hash_output_buffers( + || ParallelHash128::new(nz(12), b"Parallel Data", 17).unwrap(), + input, + ); + framework.test_hash_output_buffers( + || ParallelHash256::new(nz(5), b"Parallel Data", 5).unwrap(), + input, + ); +} + +/// `L` is bound into the computation as a bit count in a `u64` (Sec 6.3.1 step 5): a byte length +/// whose bit count does not fit is refused by `new` rather than wrapped into a different +/// function. +#[test] +fn an_output_length_past_the_cap_is_refused() { + let b = NonZeroUsize::new(8).unwrap(); + assert!(matches!( + ParallelHash128::new(b, b"", PARALLELHASH_MAX_OUTPUT_LEN + 1), + Err(HashError::InvalidLength(_)) + )); + assert!(ParallelHash256::new(b, b"", PARALLELHASH_MAX_OUTPUT_LEN).is_ok(), "the cap is valid"); } diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index 0c6e63f79..6248938aa 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -419,7 +419,10 @@ mod sha3_tests { let str = "Colorless green ideas sleep furiously"; // A helper that exercises the full round-trip for one SHA3 variant. - fn round_trip + Clone>(mut hash: H, input: &[u8]) { + fn round_trip + Clone>( + mut hash: H, + input: &[u8], + ) { hash.do_update(input); // do the default trait-conformance tests @@ -444,6 +447,27 @@ mod sha3_tests { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error for a corrupt squeezing byte"), } + + // A well-formed phase byte that says "squeezing" is not a state a SHA3 produces + // either: it is only ever suspended while absorbing, and every finalisation of a + // squeezing one would fail. Refused on resume, as SHAKE refuses it. + let mut squeezing = serialized_state; + squeezing[3 + 1 + 400] = 1; + assert!( + matches!(H::from_suspended(squeezing), Err(SuspendableError::InvalidData)), + "a squeezing phase byte must be rejected" + ); + + // The KDF entropy counter (8 bytes after the key-type and strength bytes) may carry + // any value: the largest resumes, and mixing a further key saturates rather than + // overflowing. + let mut maxed = serialized_state; + let entropy = 3 + 1 + 401 + 2; + maxed[entropy..entropy + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + let resumed = H::from_suspended(maxed).expect("any entropy count resumes"); + let key = KeyMaterial256::from_bytes_as_type(&[7u8; 32], KeyType::CryptographicRandom) + .unwrap(); + resumed.derive_key(&key, &[]).expect("a saturated counter still derives"); } round_trip(SHA3_224::new(), str.as_bytes()); diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index 3bdf84d95..46c275e65 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -7,8 +7,11 @@ mod shake_tests { KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; use bouncycastle_core::security_strength::SecurityStrength; - use bouncycastle_core::traits::{AlgorithmInfo, Hash, HashOneShot, KDF, XOF, XOFSqueezer}; + use bouncycastle_core::traits::{ + AlgorithmInfo, Hash, HashAlgParams, HashOneShot, KDF, XOF, XOFSqueezer, + }; use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_sha3::{SHA3_256, SHAKE128, SHAKE256}; @@ -18,6 +21,24 @@ mod shake_tests { /// Vector: NIST CAVP SHA3VS SHAKE128ShortMsg (bit-oriented), Len = 4, Msg = 08 (FIPS 202 B.1 /// packing: message bits 0001 in the low nibble, first bit in the LSB), i.e. 0x10 in the API's /// MSB-first order. + /// The low `8 - num_bits` bits of the partial byte are the BIT STRING's unused bits (X.690 + /// s. 8.6.2.1) and must not reach the sponge: all ones there and all zeros there give the + /// same output. + #[test] + fn the_unused_bits_of_a_partial_byte_are_ignored() { + for num_bits in 1..=7usize { + let mut all_ones = SHAKE128::new(); + all_ones.do_update(b"abc"); + let mut all_zeros = SHAKE128::new(); + all_zeros.do_update(b"abc"); + let used_mask = 0xffu8 << (8 - num_bits); + let a = all_ones.into_squeezer_partial_bits(0xff, num_bits).unwrap().do_output(32); + let b = + all_zeros.into_squeezer_partial_bits(used_mask, num_bits).unwrap().do_output(32); + assert_eq!(a, b, "num_bits = {num_bits}"); + } + } + #[test] fn into_squeezer_partial_bits_four_bits() { let shake = SHAKE128::new(); @@ -371,4 +392,18 @@ mod shake_tests { _ => panic!("Expected an error when loading a SHAKE256 state into SHA3-256"), } } + + /// RFC 8702 Section 3.1's fixed digests, which `HashAlgParams` carries: 32 bytes for SHAKE128 + /// and 64 for SHAKE256, the sponge rate as the block length (FIPS 202 Table 3), and the same + /// bytes as the XOF at that length, through the framework's hash suite. + #[test] + fn shake_as_a_fixed_length_digest() { + assert_eq!(SHAKE128::OUTPUT_LEN, 32); + assert_eq!(SHAKE256::OUTPUT_LEN, 64); + assert_eq!(SHAKE128::BLOCK_LEN, 168); + assert_eq!(SHAKE256::BLOCK_LEN, 136); + let tf = TestFrameworkHash::new(); + tf.test_hash::(&DUMMY_SEED[..512], &SHAKE128::new().xof(&DUMMY_SEED[..512], 32)); + tf.test_hash::(&DUMMY_SEED[..512], &SHAKE256::new().xof(&DUMMY_SEED[..512], 64)); + } } diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs index f7a480c3b..f71b52429 100644 --- a/crypto/sha3/tests/sp800_185_suspend_tests.rs +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -115,8 +115,8 @@ fn kmacxof_round_trips() { #[test] fn tuplehash_round_trips() { - hash_round_trip(|| TupleHash128::new(b"", 32)); - hash_round_trip(|| TupleHash256::new(b"My Tuple App", 48)); + hash_round_trip(|| TupleHash128::new(b"", 32).unwrap()); + hash_round_trip(|| TupleHash256::new(b"My Tuple App", 48).unwrap()); hash_round_trip(|| TupleHashXOF128::new(b"")); hash_round_trip(|| TupleHashXOF256::new(b"My Tuple App")); squeezer_round_trip(|| TupleHashXOF128::new(b"")); @@ -128,8 +128,8 @@ fn parallelhash_round_trips() { // PART1 is 21 bytes: a block size of 8 suspends five bytes into a block, 7 suspends exactly // on a block boundary, and 64 suspends before the first block completes. for block_size in [nz(8), nz(7), nz(64)] { - hash_round_trip(|| ParallelHash128::new(block_size, b"", 32)); - hash_round_trip(|| ParallelHash256::new(block_size, b"Parallel Data", 64)); + hash_round_trip(|| ParallelHash128::new(block_size, b"", 32).unwrap()); + hash_round_trip(|| ParallelHash256::new(block_size, b"Parallel Data", 64).unwrap()); hash_round_trip(|| ParallelHashXOF128::new(block_size, b"")); hash_round_trip(|| ParallelHashXOF256::new(block_size, b"Parallel Data")); squeezer_round_trip(|| ParallelHashXOF128::new(block_size, b"")); @@ -168,11 +168,11 @@ fn each_type_rejects_the_others() { k.do_update(PART1); rejected::<_, TupleHash128>(k.clone().suspend(), "a KMAC128 state in TupleHash128"); rejected::<_, KMAC256>(k.suspend(), "a KMAC128 state in KMAC256"); - let mut t = TupleHash128::new(b"", 32); + let mut t = TupleHash128::new(b"", 32).unwrap(); t.do_update(PART1); rejected::<_, KMAC128>(t.suspend(), "a TupleHash128 state in KMAC128"); - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(PART1); rejected::<_, ParallelHash256>(p.suspend(), "a ParallelHash128 state in ParallelHash256"); let mut p = ParallelHashXOF128::new(nz(8), b""); @@ -235,7 +235,7 @@ fn corrupt_fields_are_rejected() { const INNER_SQUEEZING_FLAG: usize = 416 + 1 + 400; const BLOCK_SIZE: usize = 416 + 412; const BLOCK_FILL: usize = BLOCK_SIZE + 8; - let mut p = ParallelHash128::new(nz(8), b"", 32); + let mut p = ParallelHash128::new(nz(8), b"", 32).unwrap(); p.do_update(PART1); let good = p.suspend(); assert_eq!(&good[BLOCK_SIZE..BLOCK_SIZE + 8], &8u64.to_le_bytes(), "layout check"); @@ -252,6 +252,43 @@ fn corrupt_fields_are_rejected() { let mut bad = good; bad[CUSTOMIZED] = 0; rejected::<_, ParallelHash128>(bad, "a ParallelHash claiming an empty function name"); + // The output length is the trailing u64, bounded as `new` bounds it. + let output_len = good.len() - 8; + let mut bad = good; + bad[output_len..].copy_from_slice(&u64::MAX.to_le_bytes()); + rejected::<_, ParallelHash128>( + bad, + "a ParallelHash whose output length in bits overflows a u64", + ); + let mut bad = good; + bad[output_len..].copy_from_slice(&(PARALLELHASH_MAX_OUTPUT_LEN as u64 + 1).to_le_bytes()); + rejected::<_, ParallelHash128>(bad, "a ParallelHash output length one above the cap"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(PARALLELHASH_MAX_OUTPUT_LEN as u64).to_le_bytes()); + assert!(ParallelHash128::from_suspended(edge).is_ok(), "the cap itself resumes"); + // The block count may carry any value: the largest resumes, and a further block saturates + // the count rather than overflowing it. + const BLOCKS: usize = BLOCK_FILL + 8; + let mut maxed = good; + maxed[BLOCKS..BLOCKS + 8].copy_from_slice(&u64::MAX.to_le_bytes()); + let mut p = ParallelHash128::from_suspended(maxed).expect("any block count resumes"); + p.do_update(&[0u8; 16]); + assert_eq!(p.do_final().len(), 32); + + // TupleHash: cSHAKE 3..416, then output_len, bounded as `new` bounds it. + let mut t = TupleHash128::new(b"", 32).unwrap(); + t.do_update(PART1); + let good = t.suspend(); + let output_len = good.len() - 8; + let mut bad = good; + bad[output_len..].copy_from_slice(&u64::MAX.to_le_bytes()); + rejected::<_, TupleHash128>(bad, "a TupleHash whose output length in bits overflows a u64"); + let mut bad = good; + bad[output_len..].copy_from_slice(&(TUPLEHASH_MAX_OUTPUT_LEN as u64 + 1).to_le_bytes()); + rejected::<_, TupleHash128>(bad, "a TupleHash output length one above the cap"); + let mut edge = good; + edge[output_len..].copy_from_slice(&(TUPLEHASH_MAX_OUTPUT_LEN as u64).to_le_bytes()); + assert!(TupleHash128::from_suspended(edge).is_ok(), "the cap itself resumes"); } /// Every type in the crate writes a different variant tag, so no state can be misread as @@ -271,17 +308,17 @@ fn state_tags_are_distinct() { tag(CSHAKE128::new(b"", b"S")), tag(KMAC128Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF128::new(&key(), b"", false).unwrap()), - tag(TupleHash128::new(b"", 32)), + tag(TupleHash128::new(b"", 32).unwrap()), tag(TupleHashXOF128::new(b"")), - tag(ParallelHash128::new(nz(8), b"", 32)), + tag(ParallelHash128::new(nz(8), b"", 32).unwrap()), tag(ParallelHashXOF128::new(nz(8), b"")), tag(TupleHashXOF128::new(b"").into_squeezer()), tag(CSHAKE256::new(b"", b"S")), tag(KMAC256Key::from_key_material(&key()).unwrap().create_generator()), tag(KMACXOF256::new(&key(), b"", false).unwrap()), - tag(TupleHash256::new(b"", 32)), + tag(TupleHash256::new(b"", 32).unwrap()), tag(TupleHashXOF256::new(b"")), - tag(ParallelHash256::new(nz(8), b"", 32)), + tag(ParallelHash256::new(nz(8), b"", 32).unwrap()), tag(ParallelHashXOF256::new(nz(8), b"")), tag(TupleHashXOF256::new(b"").into_squeezer()), ]; diff --git a/crypto/sha3/tests/tuplehash_bc-test-data.rs b/crypto/sha3/tests/tuplehash_bc-test-data.rs index dc4f72f09..c7733c789 100644 --- a/crypto/sha3/tests/tuplehash_bc-test-data.rs +++ b/crypto/sha3/tests/tuplehash_bc-test-data.rs @@ -77,8 +77,8 @@ fn nist_sp800_185_tuplehash_sample_values() { let want = v.output_len / 8; let t = as_slices(&v.tuple); let got = match v.strength { - 128 => TupleHash128::new(v.s.as_bytes(), want).hash_tuple(&t), - 256 => TupleHash256::new(v.s.as_bytes(), want).hash_tuple(&t), + 128 => TupleHash128::new(v.s.as_bytes(), want).unwrap().hash_tuple(&t), + 256 => TupleHash256::new(v.s.as_bytes(), want).unwrap().hash_tuple(&t), other => panic!("COUNT {i}: unexpected strength {other}"), }; assert_eq!( @@ -139,7 +139,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { match f.strength { 128 => check_do_final_binds_length( || TupleHashXOF128::new(s), - |n| TupleHash128::new(s, n).hash_tuple(&t), + |n| TupleHash128::new(s, n).unwrap().hash_tuple(&t), &t, &f.output, &x.output, @@ -147,7 +147,7 @@ fn do_final_binds_the_length_when_nothing_has_been_read() { ), 256 => check_do_final_binds_length( || TupleHashXOF256::new(s), - |n| TupleHash256::new(s, n).hash_tuple(&t), + |n| TupleHash256::new(s, n).unwrap().hash_tuple(&t), &t, &f.output, &x.output, @@ -380,8 +380,18 @@ fn hash_trait_view_agrees_with_the_sample_values() { let t = as_slices(&v.tuple); let ctx = format!("COUNT {i}: TupleHash{}", v.strength); match v.strength { - 128 => check_fixed_view(|| TupleHash128::new(v.s.as_bytes(), n), &t, &v.output, &ctx), - 256 => check_fixed_view(|| TupleHash256::new(v.s.as_bytes(), n), &t, &v.output, &ctx), + 128 => check_fixed_view( + || TupleHash128::new(v.s.as_bytes(), n).unwrap(), + &t, + &v.output, + &ctx, + ), + 256 => check_fixed_view( + || TupleHash256::new(v.s.as_bytes(), n).unwrap(), + &t, + &v.output, + &ctx, + ), other => panic!("COUNT {i}: unexpected strength {other}"), } } diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs index b497e8f30..14f7d91ad 100644 --- a/crypto/sha3/tests/tuplehash_tests.rs +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -4,23 +4,26 @@ use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; -use bouncycastle_sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; +use bouncycastle_sha3::CSHAKE128; +use bouncycastle_sha3::tuplehash::{ + TUPLEHASH_MAX_OUTPUT_LEN, TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256, +}; /// Sec 5.1, the reason TupleHash exists: the boundaries between elements are part of the hash, so /// re-splitting the same bytes gives an unrelated result. Every other hash in this library has the /// opposite property, which is why it is worth pinning explicitly. #[test] fn the_tuple_boundaries_are_part_of_the_hash() { - let a = TupleHash128::new(b"", 32).hash_tuple(&[b"abc", b"d"]); - let b = TupleHash128::new(b"", 32).hash_tuple(&[b"ab", b"cd"]); - let c = TupleHash128::new(b"", 32).hash_tuple(&[b"abcd"]); + let a = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"abc", b"d"]); + let b = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"ab", b"cd"]); + let c = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"abcd"]); assert_ne!(a, b, "the same bytes split differently must hash differently"); assert_ne!(a, c, "... and differently again from a single element"); assert_ne!(b, c); // An empty element is an element: dropping it changes the answer. - let with = TupleHash128::new(b"", 32).hash_tuple(&[b"a", b"", b"b"]); - let without = TupleHash128::new(b"", 32).hash_tuple(&[b"a", b"b"]); + let with = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"a", b"", b"b"]); + let without = TupleHash128::new(b"", 32).unwrap().hash_tuple(&[b"a", b"b"]); assert_ne!(with, without, "an empty tuple element must still count"); } @@ -28,9 +31,9 @@ fn the_tuple_boundaries_are_part_of_the_hash() { #[test] fn hash_tuple_matches_successive_updates() { let tuple: [&[u8]; 3] = [b"first", b"second", b"third"]; - let one = TupleHash128::new(b"S", 32).hash_tuple(&tuple); + let one = TupleHash128::new(b"S", 32).unwrap().hash_tuple(&tuple); - let mut t = TupleHash128::new(b"S", 32); + let mut t = TupleHash128::new(b"S", 32).unwrap(); for element in tuple { t.do_update(element); } @@ -43,8 +46,8 @@ fn hash_tuple_matches_successive_updates() { fn length_binding_differs_between_the_two() { let t: [&[u8]; 2] = [b"x", b"y"]; - let short = TupleHash128::new(b"", 16).hash_tuple(&t); - let long = TupleHash128::new(b"", 32).hash_tuple(&t); + let short = TupleHash128::new(b"", 16).unwrap().hash_tuple(&t); + let long = TupleHash128::new(b"", 32).unwrap().hash_tuple(&t); assert_ne!(&long[..16], &short[..], "TupleHash: a different length is a different function"); let short = TupleHashXOF128::new(b"").output_for(&t).do_output(16); @@ -57,15 +60,15 @@ fn length_binding_differs_between_the_two() { fn customization_separates_the_functions() { let t: [&[u8]; 2] = [b"x", b"y"]; assert_ne!( - TupleHash128::new(b"", 32).hash_tuple(&t), - TupleHash128::new(b"My Application", 32).hash_tuple(&t), + TupleHash128::new(b"", 32).unwrap().hash_tuple(&t), + TupleHash128::new(b"My Application", 32).unwrap().hash_tuple(&t), ); } /// A partial final byte cannot be expressed: the length encoding has to follow the tuple. #[test] fn partial_final_byte_is_refused() { - let mut t = TupleHash128::new(b"", 32); + let mut t = TupleHash128::new(b"", 32).unwrap(); t.do_update(b"abc"); assert!(matches!(t.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); @@ -86,13 +89,13 @@ fn algorithm_names() { /// length of the fixed forms. The generic checks elsewhere only require these to be positive. #[test] fn metadata() { - assert_eq!(TupleHash128::new(b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); - assert_eq!(TupleHash256::new(b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!(TupleHash128::new(b"", 32).unwrap().block_bitlen(), 1344, "cSHAKE128 rate"); + assert_eq!(TupleHash256::new(b"", 64).unwrap().block_bitlen(), 1088, "cSHAKE256 rate"); assert_eq!(TupleHashXOF128::new(b"").block_bitlen(), 1344); assert_eq!(TupleHashXOF256::new(b"").block_bitlen(), 1088); - assert_eq!(TupleHash128::new(b"", 17).output_len(), 17, "whatever was asked for"); - assert_eq!(TupleHash256::new(b"", 100).output_len(), 100); + assert_eq!(TupleHash128::new(b"", 17).unwrap().output_len(), 17, "whatever was asked for"); + assert_eq!(TupleHash256::new(b"", 100).unwrap().output_len(), 100); assert_eq!(TupleHashXOF128::new(b"").output_len(), 32, "the nominal length"); assert_eq!(TupleHashXOF256::new(b"").output_len(), 64); } @@ -107,10 +110,62 @@ fn output_buffers_of_every_length() { let framework = TestFrameworkHash::new(); let input = b"the quick brown fox"; - framework.test_hash_output_buffers(|| TupleHash128::new(b"", 32), input); - framework.test_hash_output_buffers(|| TupleHash256::new(b"", 64), input); + framework.test_hash_output_buffers(|| TupleHash128::new(b"", 32).unwrap(), input); + framework.test_hash_output_buffers(|| TupleHash256::new(b"", 64).unwrap(), input); // Non-default lengths, and a customization string. - framework.test_hash_output_buffers(|| TupleHash128::new(b"My Tuple App", 17), input); - framework.test_hash_output_buffers(|| TupleHash256::new(b"My Tuple App", 5), input); + framework.test_hash_output_buffers(|| TupleHash128::new(b"My Tuple App", 17).unwrap(), input); + framework.test_hash_output_buffers(|| TupleHash256::new(b"My Tuple App", 5).unwrap(), input); +} + +/// `L` is bound into the computation as a bit count in a `u64` (Sec 5.3.1 step 3): a byte length +/// whose bit count does not fit is refused by `new` rather than wrapped into a different +/// function. +#[test] +fn an_output_length_past_the_cap_is_refused() { + assert!(matches!( + TupleHash128::new(b"", TUPLEHASH_MAX_OUTPUT_LEN + 1), + Err(HashError::InvalidLength(_)) + )); + assert!(TupleHash256::new(b"", TUPLEHASH_MAX_OUTPUT_LEN).is_ok(), "the cap itself is valid"); +} + +/// Sec 5.3 written out by hand over the public cSHAKE: `TupleHash128(X, L, S) = +/// cSHAKE128(encode_string(X[1]) || ... || encode_string(X[n]) || right_encode(L), L, +/// "TupleHash", S)`, with `left_encode` / `right_encode` / `encode_string` from Sec 2.3.1-2.3.3. +/// `L = 264` bits is chosen so that `right_encode(L)` is `01 08 02`, a two-byte value whose low +/// byte is non-zero; the SP 800-185 sample values only ever encode 256 and 512, whose low byte +/// is zero, and so cannot tell the digits' order apart. +#[test] +fn matches_the_cshake_definition_at_a_length_with_a_non_zero_low_encoding_byte() { + fn encode_string(s: &[u8]) -> Vec { + // left_encode(len(s) in bits): the digit count, then the big-endian digits. + let bits = (s.len() * 8) as u64; + let digits: Vec = bits.to_be_bytes().iter().copied().skip_while(|&b| b == 0).collect(); + let digits = if digits.is_empty() { vec![0] } else { digits }; + let mut out = vec![digits.len() as u8]; + out.extend_from_slice(&digits); + out.extend_from_slice(s); + out + } + fn right_encode(x: u64) -> Vec { + let mut digits: Vec = x.to_be_bytes().iter().copied().skip_while(|&b| b == 0).collect(); + if digits.is_empty() { + digits.push(0); + } + let n = digits.len() as u8; + digits.push(n); + digits + } + let (x1, x2, s, l_bytes) = (b"abc".as_slice(), b"de".as_slice(), b"S".as_slice(), 33usize); + + let mut z = encode_string(x1); + z.extend_from_slice(&encode_string(x2)); + z.extend_from_slice(&right_encode((l_bytes * 8) as u64)); + assert_eq!(&z[z.len() - 3..], &[0x01, 0x08, 0x02], "right_encode(264)"); + let mut c = CSHAKE128::new(b"TupleHash", s); + c.do_update(&z); + let by_definition = c.into_squeezer().do_output(l_bytes); + + assert_eq!(TupleHash128::new(s, l_bytes).unwrap().hash_tuple(&[x1, x2]), by_definition); } diff --git a/crypto/sm3/Cargo.toml b/crypto/sm3/Cargo.toml index 924fdf7a8..8f6415af4 100644 --- a/crypto/sm3/Cargo.toml +++ b/crypto/sm3/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true [dependencies] bouncycastle-core.workspace = true +bouncycastle-hkdf.workspace = true bouncycastle-hmac.workspace = true bouncycastle-utils.workspace = true @@ -21,3 +22,7 @@ harness = false [[bench]] name = "hmac_sm3_benches" harness = false + +[[bench]] +name = "hkdf_sm3_benches" +harness = false diff --git a/crypto/sm3/benches/hkdf_sm3_benches.rs b/crypto/sm3/benches/hkdf_sm3_benches.rs new file mode 100644 index 000000000..c9d506ec6 --- /dev/null +++ b/crypto/sm3/benches/hkdf_sm3_benches.rs @@ -0,0 +1,75 @@ +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, +}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use bouncycastle_sm3::hkdf::HKDF_SM3; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_hkdf_sm3(c: &mut Criterion) { + let mut data_block = [0_u8; 1024]; + let mut output = KeyMaterial512::new(); + rng::DefaultRNG::default().next_bytes_out(&mut data_block).unwrap(); + + let key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + + let mut big_data: Vec = vec![]; + for _ in 0..100 { + big_data.extend_from_slice(&data_block); + } + + let mut group = c.benchmark_group("HKDF_SM3::do_extract_update_key"); + group.throughput(Throughput::Bytes(key.key_len() as u64)); + group.bench_function(format!("{} bytes of IKM per call", key.key_len() as u64), |b| { + let mut hkdf = HKDF_SM3::default(); + hkdf.do_extract_init(&key).unwrap(); + b.iter(|| { + let bytes_written = hkdf.do_extract_update_key(&key).unwrap(); + black_box(&bytes_written); + }); + hkdf.do_extract_final_out(&mut output).unwrap(); + black_box(&output); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::do_extract_update_bytes"); + group.throughput(Throughput::Bytes(big_data.len() as u64)); + group.bench_function(format!("{} bytes of IKM data", big_data.len() as u64), |b| { + let mut hkdf = HKDF_SM3::default(); + hkdf.do_extract_init(&key).unwrap(); + b.iter(|| { + let bytes_written = hkdf.do_extract_update_bytes(black_box(&big_data)).unwrap(); + black_box(&bytes_written); + }); + hkdf.do_extract_final_out(&mut output).unwrap(); + black_box(&output); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::expand_out large info"); + group.throughput(Throughput::Bytes(big_data.len() as u64)); + group.bench_function(format!("{} bytes of additional data", big_data.len() as u64), |b| { + let mut output_key = KeyMaterial512::new(); + b.iter(|| { + HKDF_SM3::expand_out(&key, &big_data, 64, &mut output_key).unwrap(); + black_box(&output_key); + }); + }); + group.finish(); + + let mut group = c.benchmark_group("HKDF_SM3::expand_out max output size (255*HashLen)"); + group.throughput(Throughput::Bytes(255 * 32u64)); + group.bench_function(format!("{} bytes of output key material", 255 * 32u64), |b| { + let mut output_key = KeyMaterial::<8160>::new(); + b.iter(|| { + HKDF_SM3::extract_and_expand_out(&key, &key, &data_block, 255 * 32, &mut output_key) + .unwrap(); + black_box(&output_key); + }); + }); + group.finish(); +} + +criterion_group!(benches, bench_hkdf_sm3); +criterion_main!(benches); diff --git a/crypto/sm3/src/hkdf.rs b/crypto/sm3/src/hkdf.rs new file mode 100644 index 000000000..ac3583084 --- /dev/null +++ b/crypto/sm3/src/hkdf.rs @@ -0,0 +1,51 @@ +//! HMAC-based Extract-and-Expand Key Derivation Function (HKDF) over SM3, as per RFC 5869. +//! +//! Uses [`bouncycastle_hkdf`] to provide the HKDF-SM3 instantiation: [`HKDF_SM3`]. HKDF itself is +//! implemented generically in [`bouncycastle_hkdf`]; this module pins its const parameters to SM3 +//! and publishes the resulting type alias. See [`bouncycastle_hkdf`] for the extract and expand +//! phases, entropy tracking and suspend/resume, all of which apply here unchanged. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyType}; +//! use bouncycastle_sm3::hkdf::HKDF_SM3; +//! +//! let salt = KeyMaterial256::from_bytes_as_type(&[0x5a; 32], KeyType::MACKey).unwrap(); +//! let ikm = KeyMaterial256::from_bytes_as_type(&[0xa5; 32], KeyType::MACKey).unwrap(); +//! +//! let mut okm = KeyMaterial::<64>::new(); +//! HKDF_SM3::extract_and_expand_out(&salt, &ikm, b"context info", 64, &mut okm).unwrap(); +//! ``` +//! +//! # Memory Usage +//! +//! | Object | Size (bytes) | +//! |----------------------------|--------------| +//! | `HKDF_SM3` | 368 | +//! | Suspended `HKDF_SM3` state | 122 | +//! +//! The suspended state is the inner HMAC's (which is SM3's) plus 14 bytes; the salt is not part of +//! it and must be re-supplied on resume. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * Resuming a suspended HKDF with a different salt cannot be detected and silently produces a +//! different PRK. +//! * The output key inherits the security strength of the inputs, at most SM3's 128 bits; HKDF +//! stretches key material but does not create entropy. + +use crate::hmac::SUSPENDED_HMAC_SM3_STATE_LEN; +use crate::{SM3, SUSPENDED_SM3_STATE_LEN}; +use bouncycastle_hkdf::HKDF; + +/// Algorithm name string for HKDF-SM3, as used by the factories and CLI. +pub const HKDF_SM3_NAME: &str = "HKDF-SM3"; + +/// Length in bytes of the serialized state of [`HKDF_SM3`]: the inner HMAC's, plus the 14 bytes +/// of HKDF's own bookkeeping (see the `SuspendableKeyed` impl in `bouncycastle-hkdf`). +pub const SUSPENDED_HKDF_SM3_STATE_LEN: usize = SUSPENDED_HMAC_SM3_STATE_LEN + 14; + +/// Public type for HKDF using SM3. +#[allow(non_camel_case_types)] +pub type HKDF_SM3 = HKDF; diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs index 0d928fd72..6b323c20b 100644 --- a/crypto/sm3/src/hmac.rs +++ b/crypto/sm3/src/hmac.rs @@ -1,5 +1,5 @@ //! HMAC over SM3, as specified in RFC 2104, taking into account NIST Implementation Guidance in -//! FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! FIPS 140-3 IG C.L. //! //! Uses [`bouncycastle_hmac`] to provide the HMAC-SM3 instantiation: [`HMAC_SM3`]. //! @@ -32,15 +32,16 @@ //! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself //! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how //! many leading bytes matched. -//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 -//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 -//! Section 5 recommends that the output length "be not less than half the length of the hash -//! output ... and not less than 80 bits". +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-3 +//! IG C.L Resolution C.4 (formerly NIST SP 800-107r1 Section 5.3.3). That is a floor, not a +//! recommendation -- RFC 2104 Section 5 recommends that the output length "be not less than half +//! the length of the hash output ... and not less than 80 bits". //! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong //! MAC. //! * SM3 is a Merkle-Damgard construction and so is subject to length extension; `SM3(k || m)` is //! not a secure MAC and HMAC-SM3 is the right construction for keyed hashing over SM3. +use crate::hkdf::HKDF_SM3_NAME; use crate::{SM3, SUSPENDED_SM3_STATE_LEN}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; @@ -59,8 +60,6 @@ use bouncycastle_hmac::{HmacKey, MIN_FIPS_DIGEST_LEN}; /*** String constants ***/ /// Algorithm name string for HMAC-SM3, as used by the factories and CLI. pub const HMAC_SM3_NAME: &str = "HMAC-SM3"; -/// The name of HKDF over SM3, for `HMACParams::HKDF_ALG_NAME`. -pub const HKDF_SM3_NAME: &str = "HKDF-SM3"; /*** Type aliases ***/ /// Public type for HMAC using SM3. diff --git a/crypto/sm3/src/lib.rs b/crypto/sm3/src/lib.rs index 6e8d1f92f..8c9af863d 100644 --- a/crypto/sm3/src/lib.rs +++ b/crypto/sm3/src/lib.rs @@ -1,10 +1,10 @@ //! Implements the SM3 cryptographic hash function as per GB/T 32905-2016 (also ISO/IEC 10118-3:2018 -//! and IETF draft-shen-sm3-hash-01). +//! and IETF draft-sca-cfrg-sm3-02). //! //! SM3 is a 256-bit Merkle–Damgård hash with a 512-bit block, structurally similar to SHA-256 but //! with different message expansion, round compression functions and constants. //! -//! # Examples +//! # Usage Examples //! ## Hash //! Hash functionality is accessed via the [`Hash`] trait, which is implemented by [`SM3`]. //! @@ -51,6 +51,9 @@ //! ## HMAC //! See [hmac]. //! +//! ## HKDF +//! See [hkdf]. +//! //! # Memory Usage //! //! No heap memory is used by the algorithm itself; the `Vec`-returning convenience methods @@ -61,20 +64,33 @@ //! | `SM3` | 112 | //! | Suspended state | 108 | //! -//! The object holds the 8-word chaining value plus one 64-byte block of buffered input. The -//! compression function additionally uses a 68-word message schedule (272 bytes) on the stack for -//! the duration of a call. +//! The object holds the 8-word chaining value plus one 64-byte block of buffered input. +//! +//! Per-call stack usage does not depend on the data length. Measured as the deepest frame chain +//! below each entry point in the release build (x86-64 Linux, rustc 1.97.0-nightly, return +//! addresses included; see `mem_usage_benches/src/bench_sm3_mem_usage.rs`): +//! +//! | Entry point | Stack | +//! |-------------------------------------------------|-------------| +//! | `SM3::do_update` | 320 B | +//! | `SM3::do_final_out` | 336 B | +//! | `SM3::do_final_partial_bits_out` | 464 B | +//! | `SM3::hash_out` (one-shot) | 592 B | +//! | `SM3::suspend` / `SM3::from_suspended` | 8 / 80 B | +//! | `HmacKey::::from_key_material` / `mac_out` | 992 / 928 B | +//! | `HKDF_SM3::extract_and_expand_out` | 2560 B | //! //! # 🚨 Security Considerations 🚨 //! //! * SM3 offers 128 bits of collision resistance and 256 bits of preimage resistance. //! * SM3 is a Merkle–Damgård construction and is therefore subject to length-extension: -//! `H(k || m)` is not a secure MAC. Use HMAC ([`sm3::hmac`](crate::hmac) for keyed hashing. +//! `H(k || m)` is not a secure MAC. Use HMAC ([`sm3::hmac`](crate::hmac)) for keyed hashing. //! * The chaining value and input buffer are held in [`bouncycastle_utils::secret::Secret`] and //! zeroized on drop. Transient copies (working variables and message schedule) in registers/stack //! locals during compression are not zeroized. //! * The implementation contains no data-dependent branches or table lookups. -//! * Messages up to 2^64 bytes are supported (the specification allows 2^64 bits). +//! * Message length: the specification permits messages shorter than 2^64 bits, so this +//! implementation supports 2^61 - 1 bytes. //! //! # Suspending and resuming execution //! @@ -109,6 +125,7 @@ mod sm3; +pub mod hkdf; pub mod hmac; pub use self::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; diff --git a/crypto/sm3/src/sm3.rs b/crypto/sm3/src/sm3.rs index b351b13fb..cab5c79f5 100644 --- a/crypto/sm3/src/sm3.rs +++ b/crypto/sm3/src/sm3.rs @@ -70,7 +70,7 @@ fn p1(x: u32) -> u32 { pub struct SM3 { /// Chaining value V^(i), 8 big-endian words. v: Secret<[u32; 8]>, - /// Total number of message bytes absorbed so far. Supports messages up to 2^64 bytes. + /// Total number of message bytes absorbed so far, at most `MAX_MESSAGE_BYTES`. byte_count: u64, /// Buffered input that has not yet formed a whole block. x_buf: Secret<[u8; 64]>, @@ -189,8 +189,11 @@ impl SM3 { self.x_buf[self.x_buf_off] = pad_byte; self.x_buf_off += 1; - // ... then k zero bits so that l + 1 + k = 448 mod 512. If the 64-bit length field no longer - // fits in this block, zero-fill and compress, then start a fresh block. + // ... then k zero bits, "where k is the smallest non-negative number that satisfies + // l + 1 + k = 448 (mod 512)". If the 64-bit length field no longer fits in this block, + // zero-fill and compress, then start a fresh block. At l = 447 (mod 512) this gives k = 0; + // the draft's closed form k = 512 - (((l mod 512) + 1 + 64) mod 512) would give k = 512 and + // a different digest. The prose rule is followed. if self.x_buf_off > 56 { self.x_buf[self.x_buf_off..].fill(0x00); Self::compress(&mut self.v, slice::from_ref(&self.x_buf)); @@ -357,6 +360,15 @@ impl Suspendable for SM3 { if x_buf_off >= 64 { return Err(SuspendableError::InvalidData); } + // do_update keeps byte_count congruent to x_buf_off modulo the block length, so a state in + // which they disagree describes no message and must not resume. + if byte_count % 64 != x_buf_off as u64 { + return Err(SuspendableError::InvalidData); + } + // A longer message cannot be encoded in the 64-bit length field (s. 5.1, l < 2^64 bits). + if byte_count > MAX_MESSAGE_BYTES { + return Err(SuspendableError::InvalidData); + } Ok(SM3 { v, byte_count, x_buf, x_buf_off }) } diff --git a/crypto/sm3/tests/hkdf_sm3_tests.rs b/crypto/sm3/tests/hkdf_sm3_tests.rs new file mode 100644 index 000000000..9cfd5818a --- /dev/null +++ b/crypto/sm3/tests/hkdf_sm3_tests.rs @@ -0,0 +1,136 @@ +//! HKDF-SM3 tests. There are no published HKDF-SM3 vectors (RFC 5869 covers SHA-1 and SHA-256, +//! Wycheproof has no SM3 HKDF file), so the known answers are RFC 5869 Appendix A's three +//! SHA-256 test-case inputs run through OpenSSL 3.0.13 +//! (`openssl kdf -kdfopt digest:SM3 ... HKDF`, `mode:EXTRACT_ONLY` for the PRK), whose SHA-256 +//! output for the same inputs reproduces the RFC's values. The 2026-11-04 verification review +//! reproduced all six PRK and OKM values independently with OpenSSL 3.6.3 and bc-java 1.86. + +#[cfg(test)] +mod hkdf_sm3_tests { + use bouncycastle_core::hazmat::do_hazardous_operations; + use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Algorithm, AlgorithmInfo}; + use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; + use bouncycastle_hex as hex; + use bouncycastle_sm3::hkdf::{HKDF_SM3, HKDF_SM3_NAME, SUSPENDED_HKDF_SM3_STATE_LEN}; + + /// Key material of any length and content, including empty or all-zero, as the RFC 5869 + /// vectors need. + fn key(hex_str: &str, key_type: KeyType) -> KeyMaterial<100> { + let mut k = KeyMaterial::<100>::new(); + do_hazardous_operations(&mut k, |k| { + k.set_bytes_as_type(&hex::decode(hex_str).unwrap(), key_type) + }) + .unwrap(); + k + } + + #[test] + fn algorithm() { + assert_eq!(HKDF_SM3::ALG_NAME, HKDF_SM3_NAME); + assert_eq!(HKDF_SM3::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HKDF_SM3::new().alg_name(), "HKDF-SM3"); + assert_eq!(SUSPENDED_HKDF_SM3_STATE_LEN, 122); + } + + /// `(ikm, salt, info, prk, okm)`, from RFC 5869 Appendix A.1, A.2 and A.3's inputs. + const KATS: [(&str, &str, &str, &str, &str); 3] = [ + ( + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "000102030405060708090a0b0c", + "f0f1f2f3f4f5f6f7f8f9", + "e0d6f7b0bd056327b7659f1f39ad850561fbcf4fb10fb58e88eafa55cf7cd01e", + "c69fe91b7aaee2dd5718d72dcaee0cce93f1b8e41f792da51261b6a517e68b36ed2c595572b01dfa359b", + ), + ( + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f", + "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "1a43a7fedb2d111eb33babd0d256c272aa3262cdb12e6b43d4321ae8888485d5", + "c1226236bbdefa7921f9febe27b864f33e449201b436d8844ea53f58170dd6426defbd22ed1f3c5960f35523e62e3b6c0d657f2c61893436f539013199bfaef25aafd1e7726ede927623a9f5cbb8885c7e5d", + ), + ( + "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", + "", + "", + "004fc37143377d072d74e82ff480e8d7937ec607411bc1ec65dd34401871ff9c", + "c8c91a38ae2fb3b023a7c38ce9f0748f28230d59b6b950ba3ba949bf0d713a5774815778801741cb2034", + ), + ]; + + #[test] + fn known_answers() { + for (ikm, salt, info, prk, okm) in KATS { + let ikm = key(ikm, KeyType::CryptographicRandom); + let salt = key(salt, KeyType::MACKey); + let info = hex::decode(info).unwrap(); + let okm = hex::decode(okm).unwrap(); + + // Extract, then expand. + let mut prk_key = HKDF_SM3::extract(&salt, &ikm).unwrap(); + assert_eq!(prk_key.ref_to_bytes(), hex::decode(prk).unwrap()); + // Some inputs are too short to meet the seeding rules; the PRK is used as a key anyway. + do_hazardous_operations(&mut prk_key, |p| p.set_key_type(KeyType::MACKey)).unwrap(); + let mut out = KeyMaterial::<100>::new(); + HKDF_SM3::expand_out(&prk_key, &info, okm.len(), &mut out).unwrap(); + assert_eq!(out.ref_to_bytes(), &okm[..]); + + // The streaming extract gives the same PRK. + let mut hkdf = HKDF_SM3::new(); + hkdf.do_extract_init(&salt).unwrap(); + for chunk in ikm.ref_to_bytes().chunks(5) { + hkdf.do_extract_update_bytes(chunk).unwrap(); + } + assert_eq!(hkdf.do_extract_final().unwrap().ref_to_bytes(), hex::decode(prk).unwrap()); + } + } + + #[test] + fn as_kdf() { + // derive_key uses an all-zero salt and a hash-length output, which is the extract and + // expand of the same inputs truncated to 32 bytes. + let ikm = key(&hex::encode(&DUMMY_SEED[..64]), KeyType::MACKey); + let zero_salt = key(&"00".repeat(32), KeyType::MACKey); + let info = &DUMMY_SEED[64..80]; + + let mut expected = KeyMaterial::<64>::new(); + HKDF_SM3::extract_and_expand_out(&zero_salt, &ikm, info, 64, &mut expected).unwrap(); + let mut expected_truncated = expected.clone(); + expected_truncated.set_key_len(32).unwrap(); + + let framework = TestFrameworkKDF::new(); + framework.test_kdf_single_key::(&ikm, info, &expected_truncated); + framework.test_kdf_multiple_key::(&[&zero_salt, &ikm], info, &mut expected); + } + + #[test] + fn suspendable_keyed_state() { + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + + let salt = key("000102030405060708090a0b0c", KeyType::MACKey); + let (part1, part2) = DUMMY_SEED[..48].split_at(20); + + let mut hkdf = HKDF_SM3::new(); + hkdf.do_extract_init(&salt).unwrap(); + hkdf.do_extract_update_bytes(part1).unwrap(); + TestFrameworkSuspendableKeyedState::new().test(&hkdf, &salt); + + let mut uninterrupted = hkdf.clone(); + uninterrupted.do_extract_update_bytes(part2).unwrap(); + let expected = uninterrupted.do_extract_final().unwrap(); + + let state = hkdf.suspend(); + assert_eq!(state.len(), SUSPENDED_HKDF_SM3_STATE_LEN); + let mut resumed = HKDF_SM3::from_suspended(state, &salt).unwrap(); + resumed.do_extract_update_bytes(part2).unwrap(); + assert_eq!(resumed.do_extract_final().unwrap().ref_to_bytes(), expected.ref_to_bytes()); + } + + #[test] + fn struct_size() { + assert_eq!(size_of::(), 368); + } +} diff --git a/crypto/sm3/tests/sm3_tests.rs b/crypto/sm3/tests/sm3_tests.rs index 47097ff46..c56d71598 100644 --- a/crypto/sm3/tests/sm3_tests.rs +++ b/crypto/sm3/tests/sm3_tests.rs @@ -169,7 +169,7 @@ mod sm3_tests { /// `partial_byte` (ASN.1 BIT STRING order). #[test] fn partial_bits_known_answers() { - let cases: [(&[u8], u8, usize, &str); 6] = [ + let cases: [(&[u8], u8, usize, &str); 7] = [ (b"", 0x80, 1, "985ffe9568be96328729b1c16631e9328d356432413d7556a646b9eefe479b9e"), (b"", 0xA8, 5, "469dd7b688a7b98d6362a8e2488a148cb4231bc196b796eee9652cb9044f3dcd"), (b"abc", 0xfe, 7, "5ad9f5745671e4a49f6704fdadff8cc2ff8a9683d1c7c0810a5dd7db367e9d74"), @@ -179,6 +179,14 @@ mod sm3_tests { 2, "65985be43230ee70a939d38e34a88198e0d63bb307081459d8d75541d54a382e", ), + // l = 447 (mod 512): s. 5.2's prose gives k = 0 and its closed-form k = 512; this is the + // prose digest. + ( + &[0x5a; 55], + 0xFE, + 7, + "e50c8288dfc79d82120e1f33765898380ea6fe6a685a145df0f836ab0e6dac76", + ), ( &[0x5a; 111], 0xA0, @@ -237,5 +245,24 @@ mod sm3_tests { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error"), } + + // byte_count must agree with x_buf_off modulo the block length, and stay within the + // longest message the 64-bit length field can encode, 2^61 - 1 bytes. + let with_counts = |byte_count: u64, x_buf_off: u8| { + let mut state = serialized_state; + state[3 + 32..3 + 40].copy_from_slice(&byte_count.to_le_bytes()); + state[3 + 104] = x_buf_off; + SM3::from_suspended(state) + }; + let off = serialized_state[3 + 104]; + assert!(with_counts(str.len() as u64 + 64, off).is_ok()); + assert!(matches!( + with_counts(str.len() as u64 + 1, off), + Err(SuspendableError::InvalidData) + )); + let max: u64 = (1 << 61) - 1; + assert!(with_counts(max, 63).is_ok()); + assert!(matches!(with_counts(max + 1, 0), Err(SuspendableError::InvalidData))); + assert!(matches!(with_counts(u64::MAX, 63), Err(SuspendableError::InvalidData))); } } diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index ab8ee9b97..027894491 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -54,10 +54,16 @@ where // offer any guarantees for cryptographic or security purposes". // --------------------------------------------------------------------------------------------- -/// Returns `value` unchanged, via a volatile store to a stack slot and a volatile load back. The -/// section comment above says what that does and does not guarantee. +/// Returns `value` unchanged, via a volatile store to a stack slot and a volatile load back, so +/// the optimiser carries no facts about `value` across it: not its range, not which bits are +/// zero. Every constant-time construction in this module is built on it. Public for a caller +/// whose own arithmetic the optimiser would otherwise rewrite into something that is not +/// constant-time on every target; the AES crate widens a round-key word by shifts and ORs, which +/// LLVM turns into a multiply (a software routine on AVR and RISC-V without M) unless the word's +/// known-zero high bits are hidden first. Best effort, not a guarantee: see the section comment +/// above. #[inline(always)] -fn value_barrier(value: T) -> T { +pub fn value_barrier(value: T) -> T { let mut slot = value; // SAFETY: // * `&mut slot` must be a reference to an initialised, aligned `T` local on this stack frame, so @@ -112,10 +118,15 @@ impl Condition { pub const fn from_lsb(value: i64) -> Self { Self(-(value & 1)) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= i64::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: i64, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned /// counterpart of this mask is `from_msb`, where the top bit carries a @@ -185,26 +196,8 @@ impl Condition { *dst = self.select(src, *dst); } - /// Conditionally negate the value. - /// - /// negate(-1) gives -3 - /// - /// `value` is `-1` (i.e., all bits are `1`, `...1111`) - /// - /// Condition `self.0` is 1 (`...0001`) (assuming `TRUE`) - /// - /// XOR operation was executed as `value ^ self.0` - /// - /// Then `...1111 XOR ...0001 = ...1110` (i.e., `-2`) - /// - /// Subtraction operation is `wrapping_sub(self.0)` - /// - /// Then `-2 - 1 = -3` - /// - /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. - /// - /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, - /// the test also runs normally. + /// Conditionally negate the value: returns `-value` (wrapping, so `MIN` stays `MIN`) if the + /// condition is true, otherwise `value`. pub fn negate(self, value: i64) -> i64 { let mask = self.barrier().0; (value ^ mask).wrapping_sub(mask) @@ -251,10 +244,15 @@ impl Condition { pub const fn from_lsb(value: i32) -> Self { Self(-(value & 1)) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= i32::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: i32, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value < 0`, i.e. the sign (top) bit is set. The unsigned /// counterpart of this mask is `from_msb`, where the top bit carries a @@ -324,26 +322,8 @@ impl Condition { *dst = self.select(src, *dst); } - /// Conditionally negate the value. - /// - /// negate(-1) gives -3 - /// - /// `value` is `-1` (i.e., all bits are `1`, `...1111`) - /// - /// Condition `self.0` is 1 (`...0001`) (assuming `TRUE`) - /// - /// XOR operation was executed as `value ^ self.0` - /// - /// Then `...1111 XOR ...0001 = ...1110` (i.e., `-2`) - /// - /// Subtraction operation is `wrapping_sub(self.0)` - /// - /// Then `-2 - 1 = -3` - /// - /// As a result, `1`, which is the negation of `-1`, should be returned, but `-3` is output. - /// - /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, - /// the test also runs normally. + /// Conditionally negate the value: returns `-value` (wrapping, so `MIN` stays `MIN`) if the + /// condition is true, otherwise `value`. pub fn negate(self, value: i32) -> i32 { let mask = self.barrier().0; (value ^ mask).wrapping_sub(mask) @@ -413,10 +393,15 @@ impl Condition { pub const fn from_msb(value: u64) -> Self { Self(0u64.wrapping_sub(value >> (u64::BITS - 1))) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= u64::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: u64, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value != 0`. /// @@ -493,10 +478,15 @@ impl Condition { pub const fn from_msb(value: u32) -> Self { Self(0u32.wrapping_sub(value >> (u32::BITS - 1))) } - /// TRUE iff bit `bit` of `value` is set. The bit index must be public data - /// (the shift amount is timing-visible on some targets). + /// TRUE iff bit `bit` of `value` is set; FALSE for `bit >= u32::BITS`. The bit index must be + /// public data (the shift amount is timing-visible on some targets). pub const fn is_bit_set(value: u32, bit: u32) -> Self { - Self::from_lsb(value >> bit) + // Branches on the public index only: a plain `>>` panics in debug and masks the shift + // amount in release for an out-of-range index. + match value.checked_shr(bit) { + Some(shifted) => Self::from_lsb(shifted), + None => Self::FALSE, + } } /// TRUE iff `value != 0`. /// @@ -678,7 +668,8 @@ pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { ct_eq_bytes_mask(a, b).to_bool() } -/// Constant-time check that every byte of `a` is zero. +/// Constant-time check that every byte of `a` is zero, as a `bool`, for callers that go on to +/// branch on the result at a public decision point. /// /// The runtime depends on the length of `a`, which is treated as public, but not on its contents /// or on the position of the first non-zero byte. Same construction as [`ct_eq_bytes_mask`] with diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs index e86cec849..8d0d4bacd 100644 --- a/crypto/utils/src/secret.rs +++ b/crypto/utils/src/secret.rs @@ -1,5 +1,5 @@ //! A transparent wrapper type, [`Secret`], which is a wrapper that holds a secret value and -//! guarantees it is securely zeroized on drop, and protected from accidintal logging by implementing +//! guarantees it is securely zeroized on drop, and protected from accidental logging by implementing //! redacting `fmt::Debug` and `fmt::Display`. //! //! # Why write_volatile @@ -89,6 +89,45 @@ impl ZeroizablePrimitive for [T; N] { const ZEROED: Self = [T::ZEROED; N]; } +/// A type with no padding bytes, so that every byte of every value is initialized. +/// +/// [`Secret`]'s equality compares the two values' `size_of::()` bytes, so it is defined only +/// for a `T` that implements this trait. It is implemented for the primitive integers, `bool`, +/// `char` and arrays of them. A type with padding can be held in a `Secret` but not compared: +/// +/// ```compile_fail +/// use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; +/// +/// #[derive(Clone, Copy)] +/// struct Padded { a: u8, b: u32 } +/// +/// impl ZeroizablePrimitive for Padded { +/// const ZEROED: Self = Padded { a: 0, b: 0 }; +/// } +/// +/// let x = Secret::::new(); +/// let y = Secret::::new(); +/// let _ = x == y; +/// ``` +/// +/// # Safety +/// +/// An implementor must guarantee that every byte of every value of `Self` is initialized: no +/// padding bytes, and no `MaybeUninit` or union field that may hold uninitialized bytes. +pub unsafe trait NoPadding {} + +macro_rules! impl_no_padding { + ($($t:ty),+ $(,)?) => {$( + // SAFETY: a primitive scalar has no padding. + unsafe impl NoPadding for $t {} + )+}; +} + +impl_no_padding!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, bool, char); + +// SAFETY: an array has no padding between or around its elements, and `T` has none of its own. +unsafe impl NoPadding for [T; N] {} + /// A wrapper that holds a secret value of any primitive or fixed-size array type /// and guarantees it is securely zeroized on drop, and is /// protected from accidental logging by implementing redacting `fmt::Debug` and `fmt::Display`. @@ -159,7 +198,8 @@ impl ZeroizablePrimitive for [T; N] { /// /// Since [`ZeroizablePrimitive`] is a public trait, you can implement it on your own types and then /// trivially be able to wrap them in a `Secret`. The only requirement is that there is a -/// well-defined "ZEROED" value for the type. +/// well-defined "ZEROED" value for the type. Comparing two such secrets with `==` additionally +/// needs the `unsafe` trait [`NoPadding`]. /// /// Toy Example: /// @@ -204,7 +244,7 @@ impl ZeroizablePrimitive for [T; N] { /// ``` /// /// This will also work for custom types; let's say the `User` struct from the previous example had -/// impl'd Display and Debug; mhen you wrap them in `Secret` then `Secret`'s Display and Debug +/// impl'd Display and Debug; when you wrap them in `Secret` then `Secret`'s Display and Debug /// are invoked instead of `User`'s and you get the `""` output. /// /// # Memory Usage @@ -265,17 +305,11 @@ impl Secret { pub fn zeroize(&mut self) { // SAFETY: `&mut self.0` is a valid, properly aligned, mutable reference to an initialized // `T`, which is exactly the contract `write_volatile` requires. - // `T::ZEROED` is defined above for each supported primitive and primitive-array as the - // is the all-zero value of `T`, which is a valid and correctly-sized bit pattern - // for the primitive scalar/array being zeroized. + // `T::ZEROED` is a valid value of `T`, so it is a valid value to write. //`write_volatile` (rather than a plain store) is what forbids the compiler from eliding // this scrub as a dead write as per its contract: // https://doc.rust-lang.org/std/ptr/fn.write_volatile.html - // Just to make sure -- this should trigger on any unit tests for any instantiation of - // Secret that causes this assumption to be violated. - debug_assert_eq!(size_of::(), size_of_val(&T::ZEROED)); - unsafe { ptr::write_volatile(&mut self.0, T::ZEROED); } @@ -337,17 +371,18 @@ impl fmt::Display for Secret { /// Both operands are the same type `T`, so both views are exactly `size_of::()` bytes and the /// comparison never short-circuits: it always inspects every byte, avoiding a timing side channel /// that would otherwise leak how many leading bytes of two secrets match. -impl PartialEq for Secret { +impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { let len = size_of::(); // SAFETY: `self.0` / `other.0` are live, initialized `T` values, so the `len` bytes starting // at each address lie within that single object. `u8` has alignment 1, so every byte address // is well aligned, and the slices are read-only and used only within this call. `T: Copy` // (via `ZeroizablePrimitive`) means there is no interior mutability or drop glue to worry - // about. + // about. `T: NoPadding` means `T` has no padding, so all `len` bytes are initialized, as + // `from_raw_parts` requires. let a = unsafe { core::slice::from_raw_parts((&self.0 as *const T).cast::(), len) }; let b = unsafe { core::slice::from_raw_parts((&other.0 as *const T).cast::(), len) }; ct::ct_eq_bytes(a, b) } } -impl Eq for Secret {} +impl Eq for Secret {} diff --git a/crypto/utils/src/suspendable_state.rs b/crypto/utils/src/suspendable_state.rs index 9e676bd24..f6cbbf10b 100644 --- a/crypto/utils/src/suspendable_state.rs +++ b/crypto/utils/src/suspendable_state.rs @@ -98,13 +98,20 @@ impl From for [u8; 3] { /// Parse a decimal ASCII string (a Cargo version component) into a u8 at compile time. const fn parse_version_component(s: &str) -> u8 { let bytes = s.as_bytes(); + assert!(!bytes.is_empty(), "version component must not be empty"); let mut result: u8 = 0; let mut i = 0; while i < bytes.len() { let d = bytes[i]; assert!(d >= b'0' && d <= b'9', "version component must be numeric"); - // A component > 255 overflows u8 and fails the build (SemVer fields are u8 by design). - result = result * 10 + (d - b'0'); + // SemVer fields are u8 by design, so a component > 255 must fail the build. Plain `*` and + // `+` would wrap instead when overflow checks are off (const evaluation follows + // -C overflow-checks), so the overflow is checked explicitly. + let Some(shifted) = result.checked_mul(10) else { panic!("version component exceeds 255") }; + let Some(next) = shifted.checked_add(d - b'0') else { + panic!("version component exceeds 255") + }; + result = next; i += 1; } result @@ -130,34 +137,46 @@ pub const LIB_VERSION_LEN: usize = 3; /// Puts the library version into the first three bytes of the state array. /// -/// Hands back a slice to the same array, starting after the version tag. +/// Hands back a slice to the same array, starting after the version tag. `SERIALIZED_LEN` must be +/// at least [`LIB_VERSION_LEN`], checked at compile time: +/// +/// ```compile_fail +/// use bouncycastle_utils::suspendable_state::add_lib_ver; +/// +/// let mut state = [0u8; 2]; +/// add_lib_ver(&mut state); +/// ``` pub fn add_lib_ver(state: &mut [u8; SERIALIZED_LEN]) -> &mut [u8] { + const { + assert!( + SERIALIZED_LEN >= LIB_VERSION_LEN, + "the state must have room for the version header" + ) + }; state[..LIB_VERSION_LEN].copy_from_slice(&<[u8; 3]>::from(LIB_VERSION)); &mut state[LIB_VERSION_LEN..] } -/// A helper for deserializing an object's state -/// -/// The state_out array must have length at least SERIALIZED_LEN - 3. +/// A helper for deserializing an object's state: checks the version tag in its first three bytes. /// -/// Returns the number of bytes written to state_out, or a [`SuspendableError::IncompatibleVersion`] if -/// the version of the serialized state is earlier than the specified `not_before` version, or -/// is a future MAJOR or MINOR version (but future PATCH versions are ok). +/// Hands back a slice to the same array, starting after the version tag, or a +/// [`SuspendableError::IncompatibleVersion`] if the version of the serialized state is earlier than +/// the specified `not_before` version, or is a future MAJOR or MINOR version (but future PATCH +/// versions are ok). `SERIALIZED_LEN` must be at least [`LIB_VERSION_LEN`], checked at compile time. /// /// Note that for testability, this will always reject if the serialized state contains a version tag /// of `[0,0,0]`. -/// -/// Hands back a slice to the same array, starting after the version tag. pub fn check_lib_ver( state: &[u8; SERIALIZED_LEN], not_before: Option<[u8; 3]>, ) -> Result<&[u8], SuspendableError> { - // the .unwrap is infallible after the guard check - if state.len() < LIB_VERSION_LEN { - return Err(SuspendableError::InvalidData); - } - let ver_bytes: [u8; 3] = state[..LIB_VERSION_LEN].try_into().unwrap(); - let ver = SemVer::from(ver_bytes); + const { + assert!( + SERIALIZED_LEN >= LIB_VERSION_LEN, + "the state must have room for the version header" + ) + }; + let ver = SemVer::from([state[0], state[1], state[2]]); let not_before = SemVer::from(not_before.unwrap_or([0, 0, 0])); @@ -319,6 +338,8 @@ impl<'a> CursorMut<'a> { } /// A `usize` field read back from its `u64` encoding, refused if it is above `max`. +/// +/// `max` must be a real bound on the field: `usize::MAX` only narrows the width. pub fn bounded_usize(v: u64, max: usize) -> Result { if v > max as u64 { Err(SuspendableError::InvalidData) } else { Ok(v as usize) } } @@ -342,4 +363,31 @@ mod tests { assert_eq!(cmp([1, 1, 2], [1, 1, 1]), Ordering::Greater); assert_eq!(cmp([1, 1, 1], [1, 1, 1]), Ordering::Equal); } + + #[test] + fn test_parse_version_component() { + assert_eq!(parse_version_component("0"), 0); + assert_eq!(parse_version_component("3"), 3); + assert_eq!(parse_version_component("25"), 25); + assert_eq!(parse_version_component("255"), 255); + } + + #[test] + #[should_panic(expected = "version component exceeds 255")] + fn test_parse_version_component_256() { + parse_version_component("256"); + } + + #[test] + #[should_panic(expected = "version component exceeds 255")] + fn test_parse_version_component_300() { + // 30 * 10 overflows a u8 (in the multiply, not the add). + parse_version_component("300"); + } + + #[test] + #[should_panic(expected = "version component must not be empty")] + fn test_parse_version_component_empty() { + parse_version_component(""); + } } diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index 81a76d335..fe85e1385 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -183,6 +183,12 @@ mod unsigned_u64_tests { Condition::::from_msb(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [u64::BITS, u64::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -332,6 +338,12 @@ mod unsigned_u32_tests { Condition::::from_msb(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [u32::BITS, u32::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -464,6 +476,12 @@ mod signed_i64_tests { Condition::::is_negative(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [i64::BITS, i64::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] @@ -665,6 +683,12 @@ mod signed_i32_tests { Condition::::is_negative(v).to_bool() ); } + // an index past the width gives FALSE, never a masked shift or a panic + for v in BOUNDARY { + for bit in [i32::BITS, i32::BITS + 1, u32::MAX] { + assert_canonical(Condition::::is_bit_set(v, bit), false); + } + } } #[test] diff --git a/dev_scripts/aes_ct_harness/Cargo.toml b/dev_scripts/aes_ct_harness/Cargo.toml new file mode 100644 index 000000000..bbe398f69 --- /dev/null +++ b/dev_scripts/aes_ct_harness/Cargo.toml @@ -0,0 +1,13 @@ +# The AES round-key widening, out of line, for `../check_aes_ct_asm.sh` to build on the targets +# whose multiply is not fixed-latency and grep. Its own workspace, so the root `cargo build +# --workspace` never builds it for the host. +[package] +name = "aes_ct_harness" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +bouncycastle-utils = { path = "../../crypto/utils" } + +[workspace] diff --git a/dev_scripts/aes_ct_harness/src/lib.rs b/dev_scripts/aes_ct_harness/src/lib.rs new file mode 100644 index 000000000..8f1670d47 --- /dev/null +++ b/dev_scripts/aes_ct_harness/src/lib.rs @@ -0,0 +1,38 @@ +//! The two bodies of `PlaneWord::splat` from `crypto/aes/src/bitslice.rs`, with the barrier the +//! crate applies off x86-64 and aarch64 and without it, each out of line so that it lands in the +//! assembly as one symbol. `../check_aes_ct_asm.sh` builds this for the Cortex-M0 and RISC-V 32I +//! targets and checks that the barriered bodies hold no multiply, call or branch, and that the +//! plain ones do show the multiply LLVM folds the shifts and ORs into, which is what the check +//! exists to catch. Keep the bodies the same as the crate's. +#![no_std] +#![forbid(unsafe_code)] + +use bouncycastle_utils::ct; + +/// `u32::splat` as the crate compiles it off the fixed-latency targets. +#[inline(never)] +pub fn splat32_barrier(pattern: u16) -> u32 { + let x = ct::value_barrier(u32::from(pattern)); + x | (x << 16) +} + +/// `u64::splat` as the crate compiles it off the fixed-latency targets. +#[inline(never)] +pub fn splat64_barrier(pattern: u16) -> u64 { + let x = ct::value_barrier(u64::from(pattern)); + x | (x << 16) | (x << 32) | (x << 48) +} + +/// The same without the barrier: the positive control, which LLVM may fold into a multiply. +#[inline(never)] +pub fn splat32_plain(pattern: u16) -> u32 { + let x = u32::from(pattern); + x | (x << 16) +} + +/// The same without the barrier: the positive control, which LLVM may fold into a multiply. +#[inline(never)] +pub fn splat64_plain(pattern: u16) -> u64 { + let x = u64::from(pattern); + x | (x << 16) | (x << 32) | (x << 48) +} diff --git a/dev_scripts/check_aes_ct_asm.sh b/dev_scripts/check_aes_ct_asm.sh new file mode 100755 index 000000000..4804d6c86 --- /dev/null +++ b/dev_scripts/check_aes_ct_asm.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Checks that the AES round-key widening (`PlaneWord::splat` in crypto/aes/src/bitslice.rs) +# compiles to no multiply, call or branch on the targets whose multiply is not fixed-latency. +# Builds dev_scripts/aes_ct_harness with the nightly toolchain and `-Zbuild-std=core` (the +# targets have no shipped `core`), emits the assembly and greps the four `splat*` bodies: the +# barriered pair must be clean, and the plain pair must show the multiply the barrier prevents, +# so that a grep that matches nothing is noticed. Targets may be given as arguments. +# +# rustup toolchain install nightly --profile minimal --component rust-src +# ./dev_scripts/check_aes_ct_asm.sh # thumbv6m-none-eabi riscv32i-unknown-none-elf +# ./dev_scripts/check_aes_ct_asm.sh thumbv6m-none-eabi +set -euo pipefail + +cd "$(dirname "$0")/aes_ct_harness" +TARGETS=${*:-"thumbv6m-none-eabi riscv32i-unknown-none-elf"} +status=0 + +# Prints the instruction lines of the function whose mangled symbol contains $2 in $1. +body() { + awk -v f="$2" '$0 ~ f && /:$/ { on = 1 } on { print } on && /^\.Lfunc_end/ { exit }' "$1" | + grep -vE '^\s*(\.|@|#|//)' | grep -vE ':\s*$' || true +} + +for t in $TARGETS; do + case "$t" in + thumb*|arm*) + bad='\b(mul|muls|smull|umull|mla|bl|blx|b(eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le)|cbz|cbnz|it|ite|itt)\b|__aeabi_' + mul='\b(mul|muls|smull|umull)\b|__aeabi_lmul|__aeabi_.*mul' ;; + riscv*) + bad='\b(mul|mulh|mulhu|mulhsu|call|jal|jalr|tail|b(eq|ne|lt|ge|ltu|geu|nez|eqz|lez|gez|ltz|gtz))\b|__mul' + mul='\bmul|__mul' ;; + avr*) + bad='\b(mul|muls|mulsu|fmul|call|rcall|icall|br(eq|ne|cs|cc|sh|lo|mi|pl|ge|lt|hs|vs|vc|ie|id|ts|tc)|sbrc|sbrs|cpse)\b|__mul' + mul='\bmul|__mul' ;; + *) echo "no mnemonic table for $t"; exit 2 ;; + esac + + rm -rf target + # `--emit asm` writes beside the intermediate artifacts, which newer cargo keeps in a build + # directory separate from the target directory; pinning both to `target` keeps the file + # findable on every cargo, and `find` does not depend on the exact layout within it. + cargo +nightly rustc --release --target "$t" -Zbuild-std=core \ + --config 'build.build-dir="target"' -- --emit asm + asm=$(find target -name 'aes_ct_harness-*.s' -path "*$t*" | head -n 1) + if [ -z "$asm" ]; then + echo "FAIL $t: no aes_ct_harness-*.s under $(pwd)/target" + find target -name '*.s' | head + exit 2 + fi + + for f in splat32_barrier splat64_barrier; do + hits=$(body "$asm" "$f" | grep -cE "$bad" || true) + n=$(body "$asm" "$f" | wc -l) + if [ "$hits" -ne 0 ]; then + echo "FAIL $t $f: $hits multiply/call/branch line(s) in $n instructions" + body "$asm" "$f" | grep -E "$bad" + status=1 + else + echo "ok $t $f: no multiply, call or branch in $n instructions" + fi + done + control=$(for f in splat32_plain splat64_plain; do body "$asm" "$f"; done | grep -cE "$mul" || true) + if [ "$control" -eq 0 ]; then + echo "FAIL $t: the unbarriered bodies show no multiply, so the grep proves nothing" + status=1 + else + echo "ok $t control: the unbarriered bodies show the multiply ($control line(s))" + fi +done +exit $status diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index 804cbe57a..1e06763dc 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -34,3 +34,7 @@ path = "src/bench_ccm_mem_usage.rs" [[bin]] name = "bench_ascon_mem_usage" path = "src/bench_ascon_mem_usage.rs" + +[[bin]] +name = "bench_sm3_mem_usage" +path = "src/bench_sm3_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_ascon_mem_usage.rs b/mem_usage_benches/src/bench_ascon_mem_usage.rs index 1bdfe8000..387e2511b 100644 --- a/mem_usage_benches/src/bench_ascon_mem_usage.rs +++ b/mem_usage_benches/src/bench_ascon_mem_usage.rs @@ -56,14 +56,14 @@ use core::hint::black_box; +use bouncycastle::ascon::{ + ASCON_AEAD128_KEY_LEN, ASCON_AEAD128_NONCE_LEN, ASCON_AEAD128_TAG_LEN, + SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, +}; use bouncycastle::ascon::{Ascon_AEAD128, AsconAEAD128Key}; use bouncycastle::ascon::{AsconCXof128, AsconCXof128Squeezer, SUSPENDED_ASCON_CXOF128_STATE_LEN}; use bouncycastle::ascon::{AsconHash256, SUSPENDED_ASCON_HASH256_STATE_LEN}; use bouncycastle::ascon::{AsconXof128, AsconXof128Squeezer, SUSPENDED_ASCON_XOF128_STATE_LEN}; -use bouncycastle::ascon::{ - KEY_LEN, NONCE_LEN, SUSPENDED_ASCON_AEAD128_DECRYPTOR_STATE_LEN, - SUSPENDED_ASCON_AEAD128_ENCRYPTOR_STATE_LEN, TAG_LEN, -}; use bouncycastle::cipher::{Decrypting, Encrypting}; use bouncycastle::core::errors::{HashError, RNGError, SymmetricCipherError}; use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; @@ -83,12 +83,12 @@ const MSG_LEN: usize = 40; /// One full AEAD block and a partial one. const AAD_LEN: usize = 20; /// The inline layout: ciphertext || tag. -const SEALED_LEN: usize = MSG_LEN + TAG_LEN; +const SEALED_LEN: usize = MSG_LEN + ASCON_AEAD128_TAG_LEN; -const KEY: [u8; KEY_LEN] = [ +const KEY: [u8; ASCON_AEAD128_KEY_LEN] = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, ]; -const NONCE: [u8; NONCE_LEN] = [ +const NONCE: [u8; ASCON_AEAD128_NONCE_LEN] = [ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, ]; @@ -115,7 +115,7 @@ impl RNG for FixedNonce { fn next_bytes_out(&mut self, out: &mut [u8]) -> Result { for (i, b) in out.iter_mut().enumerate() { - *b = NONCE[i % NONCE_LEN]; + *b = NONCE[i % ASCON_AEAD128_NONCE_LEN]; } Ok(out.len()) } @@ -178,8 +178,9 @@ fn measure(f: impl FnOnce()) { /// Wraps the hard-coded key. `#[inline(never)]` so the wrapping is a sibling frame of whatever /// uses the key, not part of it. #[inline(never)] -fn key() -> KeyMaterial { - KeyMaterial::::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey).unwrap() +fn key() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&KEY, KeyType::SymmetricCipherKey) + .unwrap() } /// The key object the workers are made from, likewise in its own frame. @@ -290,8 +291,8 @@ fn bench_aead_from_key_material() { let k = key(); measure(|| { let op = black_box( - >::from_key_material - as fn(&KeyMaterial) -> Result, + >::from_key_material + as fn(&KeyMaterial) -> Result, ); let key = op(&k).unwrap(); black_box(&key); @@ -305,11 +306,16 @@ fn bench_aead_encrypt_init() { let k = aead_key(); measure(|| { let op = black_box( - >::create_encryptor + >::create_encryptor as fn( &AsconAEAD128Key, &mut dyn RNG, - ) -> Result<(Enc, [u8; NONCE_LEN]), CipherErr>, + ) -> Result<(Enc, [u8; ASCON_AEAD128_NONCE_LEN]), CipherErr>, ); let (enc, nonce) = op(&k, &mut FixedNonce).unwrap(); black_box(&enc); @@ -325,8 +331,11 @@ fn bench_aead_encrypt_update_aad() { measure(move || { let mut enc = enc; let op = black_box( - >::do_update_aad - as fn(&mut Enc, &[u8]) -> Result<(), CipherErr>, + >::do_update_aad as fn(&mut Enc, &[u8]) -> Result<(), CipherErr>, ); op(&mut enc, &aad).unwrap(); black_box(&enc); @@ -342,7 +351,7 @@ fn bench_aead_encrypt_out() { let mut enc = enc; let mut ct = [0u8; MSG_LEN]; let op = black_box( - >::do_encrypt_out + >::do_encrypt_out as fn(&mut Enc, &[u8], &mut [u8]) -> Result, ); let n = op(&mut enc, &pt, &mut ct).unwrap(); @@ -357,8 +366,8 @@ fn bench_aead_encrypt_final() { let enc = black_box(encryptor_after_data()); measure(move || { let op = black_box( - >::do_encrypt_final - as fn(Enc) -> Result<([u8; TAG_LEN], usize), CipherErr>, + >::do_encrypt_final + as fn(Enc) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), CipherErr>, ); let (tag, n) = op(enc).unwrap(); print!("{:x?}", &tag[..n]); @@ -370,10 +379,17 @@ fn bench_aead_encrypt_final_detached() { eprintln!("Ascon_AEAD128::do_encrypt_final_detached_out"); let enc = black_box(encryptor_after_data()); measure(move || { - let mut last = [0u8; TAG_LEN]; + let mut last = [0u8; ASCON_AEAD128_TAG_LEN]; let op = black_box( - >::do_encrypt_final_detached_out - as fn(Enc, &mut [u8; TAG_LEN]) -> Result<(usize, [u8; TAG_LEN]), CipherErr>, + >::do_encrypt_final_detached_out + as fn( + Enc, + &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result<(usize, [u8; ASCON_AEAD128_TAG_LEN]), CipherErr>, ); let (_, tag) = op(enc, &mut last).unwrap(); print!("{tag:x?}"); @@ -390,14 +406,19 @@ fn bench_aead_encrypt_oneshot() { measure(move || { let mut ct = [0u8; SEALED_LEN]; let op = black_box( - >::encrypt_with_aad_out + >::encrypt_with_aad_out as fn( &AsconAEAD128Key, &mut dyn RNG, &[u8], &[u8], &mut [u8], - ) -> Result<(usize, [u8; NONCE_LEN]), CipherErr>, + ) -> Result<(usize, [u8; ASCON_AEAD128_NONCE_LEN]), CipherErr>, ); let (n, _) = op(&k, &mut FixedNonce, &aad, &pt, &mut ct).unwrap(); print!("{:x?}", &ct[..n]); @@ -412,8 +433,13 @@ fn bench_aead_decrypt_init() { let k = aead_key(); measure(|| { let op = black_box( - >::create_decryptor - as fn(&AsconAEAD128Key, &[u8; NONCE_LEN]) -> Dec, + >::create_decryptor + as fn(&AsconAEAD128Key, &[u8; ASCON_AEAD128_NONCE_LEN]) -> Dec, ); let dec = op(&k, &NONCE); black_box(&dec); @@ -428,8 +454,11 @@ fn bench_aead_decrypt_update_aad() { measure(move || { let mut dec = dec; let op = black_box( - >::do_update_aad - as fn(&mut Dec, &[u8]) -> Result<(), CipherErr>, + >::do_update_aad as fn(&mut Dec, &[u8]) -> Result<(), CipherErr>, ); op(&mut dec, &aad).unwrap(); black_box(&dec); @@ -447,7 +476,7 @@ fn bench_aead_decrypt_out() { let mut dec = dec; let mut pt = [0u8; SEALED_LEN]; let op = black_box( - >::do_decrypt_out + >::do_decrypt_out as fn(&mut Dec, &[u8], &mut [u8]) -> Result, ); let n = op(&mut dec, &ct, &mut pt).unwrap(); @@ -462,8 +491,8 @@ fn bench_aead_decrypt_final() { let dec = black_box(decryptor_holding_tag()); measure(move || { let op = black_box( - >::do_decrypt_final - as fn(Dec) -> Result<([u8; TAG_LEN], usize), CipherErr>, + >::do_decrypt_final + as fn(Dec) -> Result<([u8; ASCON_AEAD128_TAG_LEN], usize), CipherErr>, ); let (_, n) = op(dec).unwrap(); print!("{n}"); @@ -475,12 +504,20 @@ fn bench_aead_decrypt_final() { fn bench_aead_decrypt_final_detached() { eprintln!("Ascon_AEAD128::do_decrypt_final_detached_out"); let dec = black_box(decryptor_holding_ciphertext()); - let tag: [u8; TAG_LEN] = black_box(sealed()[MSG_LEN..].try_into().unwrap()); + let tag: [u8; ASCON_AEAD128_TAG_LEN] = black_box(sealed()[MSG_LEN..].try_into().unwrap()); measure(move || { - let mut last = [0u8; TAG_LEN]; + let mut last = [0u8; ASCON_AEAD128_TAG_LEN]; let op = black_box( - >::do_decrypt_final_detached_out - as fn(Dec, &[u8; TAG_LEN], &mut [u8; TAG_LEN]) -> Result, + >::do_decrypt_final_detached_out + as fn( + Dec, + &[u8; ASCON_AEAD128_TAG_LEN], + &mut [u8; ASCON_AEAD128_TAG_LEN], + ) -> Result, ); let n = op(dec, &tag, &mut last).unwrap(); print!("{:x?}", &last[..n]); @@ -497,10 +534,15 @@ fn bench_aead_decrypt_oneshot() { measure(move || { let mut pt = [0u8; MSG_LEN]; let op = black_box( - >::decrypt_with_aad_out + >::decrypt_with_aad_out as fn( &AsconAEAD128Key, - &[u8; NONCE_LEN], + &[u8; ASCON_AEAD128_NONCE_LEN], &[u8], &[u8], &mut [u8], diff --git a/mem_usage_benches/src/bench_sm3_mem_usage.rs b/mem_usage_benches/src/bench_sm3_mem_usage.rs new file mode 100644 index 000000000..5b906e4ad --- /dev/null +++ b/mem_usage_benches/src/bench_sm3_mem_usage.rs @@ -0,0 +1,242 @@ +//! Stack usage of the SM3 entry points, and the in-memory sizes printed by `print_struct_sizes`, +//! the source of the "Memory Usage" tables in the bouncycastle-sm3 crate docs. +//! +//! As with Ascon, a call needs a few hundred bytes, below the floor of the process's own start-up +//! that `bench_aes_mem_usage` describes, so massif cannot see it. The figures in the crate docs +//! are read from the compiler the way `bench_ascon_mem_usage` describes: `-C remark=prologepilog` +//! gives each function's frame and `--emit=asm` its calls, after LTO, and a figure is the deepest +//! chain of frames below the entry point plus 8 bytes of return address per call, not following +//! the panic paths. With every bench called from `main`: +//! +//! ```text +//! cargo rustc --release -p mem_usage_benches --bin bench_sm3_mem_usage -- -C remark=prologepilog --emit=asm +//! ``` +//! +//! Each entry point is called through a `black_box`ed function pointer, which gives it an +//! out-of-line body of its own, so the frame being read is the entry point's and not the +//! harness closure's. The inputs are built in `#[inline(never)]` helpers, so their frames are +//! siblings of the operation's. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use core::hint::black_box; + +use bouncycastle::core::errors::{KDFError, MACError}; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle::core::symmetric_key::{FromAnyKeyMaterial, MacGenerationKey}; +use bouncycastle::core::traits::{Hash, HashOneShot, MAC, Suspendable}; +use bouncycastle::hmac::HmacKey; +use bouncycastle::sm3::hkdf::{HKDF_SM3, SUSPENDED_HKDF_SM3_STATE_LEN}; +use bouncycastle::sm3::hmac::{HMAC_SM3, SUSPENDED_HMAC_SM3_STATE_LEN}; +use bouncycastle::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; + +/// Two full 64-byte blocks and a partial one. +const MSG_LEN: usize = 150; + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +#[inline(never)] +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each value a caller holds between calls, and the size of its +/// suspended state. Neither depends on how much data has been processed. +#[inline(never)] +fn print_struct_sizes() { + use core::mem::size_of; + + println!("size_of: {}", size_of::()); + println!("SUSPENDED_SM3_STATE_LEN: {}", SUSPENDED_SM3_STATE_LEN); + println!("size_of: {}", size_of::>()); + println!("size_of>: {}", size_of::>()); + println!("SUSPENDED_HMAC_SM3_STATE_LEN: {}", SUSPENDED_HMAC_SM3_STATE_LEN); + println!("size_of: {}", size_of::()); + println!("SUSPENDED_HKDF_SM3_STATE_LEN: {}", SUSPENDED_HKDF_SM3_STATE_LEN); +} + +/// Runs the operation in its own frame. Returns nothing, so no result crosses the boundary. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// A message filled at run time, so that its contents are not a constant the compiler could fold +/// the whole computation over. +#[inline(never)] +fn message(fill: u8) -> [u8; N] { + let mut m = [0u8; N]; + m.fill(black_box(fill)); + m +} + +/// An SM3 that has taken the message, so the final pads a partial block. +#[inline(never)] +fn sm3_after_data() -> SM3 { + let mut h = SM3::new(); + h.do_update(&message::(0xa5)); + h +} + +#[inline(never)] +fn key() -> KeyMaterial256 { + KeyMaterial256::from_bytes_as_type(&message::<32>(0x0b), KeyType::MACKey).unwrap() +} + +// ---- SM3 ---------------------------------------------------------------------------------- + +#[inline(never)] +fn bench_sm3_update() { + eprintln!("SM3::do_update, {MSG_LEN} B"); + let h = black_box(SM3::new()); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut h = h; + let op = black_box(::do_update as fn(&mut SM3, &[u8])); + op(&mut h, &msg); + black_box(&h); + }); +} + +#[inline(never)] +fn bench_sm3_final() { + eprintln!("SM3::do_final_out"); + let h = black_box(sm3_after_data()); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box(::do_final_out as fn(SM3, &mut [u8]) -> usize); + let n = op(h, &mut out); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_final_partial_bits() { + eprintln!("SM3::do_final_partial_bits_out"); + let h = black_box(sm3_after_data()); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box( + ::do_final_partial_bits_out + as fn(SM3, u8, usize, &mut [u8]) -> Result, + ); + let n = op(h, 0xa0, 3, &mut out).unwrap(); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_oneshot() { + eprintln!("SM3::hash_out, {MSG_LEN} B"); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box(::hash_out as fn(&[u8], &mut [u8]) -> usize); + let n = op(&msg, &mut out); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_sm3_suspend() { + eprintln!("SM3::suspend"); + let h = black_box(sm3_after_data()); + measure(move || { + let op = black_box( + >::suspend + as fn(SM3) -> [u8; SUSPENDED_SM3_STATE_LEN], + ); + let state = op(h); + print!("{:x?}", &state[..]); + }); +} + +#[inline(never)] +fn bench_sm3_from_suspended() { + eprintln!("SM3::from_suspended"); + let state = black_box(sm3_after_data().suspend()); + measure(move || { + let op = black_box( + >::from_suspended + as fn([u8; SUSPENDED_SM3_STATE_LEN]) -> Result, + ); + let h = op(state).unwrap(); + black_box(&h); + }); +} + +// ---- HMAC-SM3 and HKDF-SM3 ---------------------------------------------------------------- + +#[inline(never)] +fn hmac_sm3_key() -> HmacKey { + HmacKey::::from_key_material(&key()).unwrap() +} + +#[inline(never)] +fn bench_hmac_sm3_key() { + eprintln!("HmacKey::::from_key_material"); + let key = black_box(key()); + measure(move || { + let op = black_box( + as FromAnyKeyMaterial>::from_key_material + as fn(&dyn KeyMaterialTrait) -> Result, MACError>, + ); + let k = op(&key).unwrap(); + black_box(&k); + }); +} + +#[inline(never)] +fn bench_hmac_sm3_mac_out() { + eprintln!("HmacKey::::mac_out, {MSG_LEN} B"); + let k = black_box(hmac_sm3_key()); + let msg = black_box(message::(0xa5)); + measure(move || { + let mut out = [0u8; 32]; + let op = black_box( + as MacGenerationKey>::mac_out + as fn(&HmacKey, &[u8], &mut [u8]) -> Result, + ); + let n = op(&k, &msg, &mut out).unwrap(); + print!("{:x?}", &out[..n]); + }); +} + +#[inline(never)] +fn bench_hkdf_sm3_oneshot() { + eprintln!("HKDF_SM3::extract_and_expand_out, 64 B out"); + let salt = black_box(key()); + let ikm = black_box(key()); + let info = black_box(message::<16>(0x5a)); + measure(move || { + let mut okm = KeyMaterial::<64>::new(); + let op = black_box( + HKDF_SM3::extract_and_expand_out + as fn( + &KeyMaterial256, + &KeyMaterial256, + &[u8], + usize, + &mut KeyMaterial<64>, + ) -> Result, + ); + let n = op(&salt, &ikm, &info, 64, &mut okm).unwrap(); + black_box((n, &okm)); + }); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_sm3_update() + // bench_sm3_final() + // bench_sm3_final_partial_bits() + // bench_sm3_oneshot() + // bench_sm3_suspend() + // bench_sm3_from_suspended() + // bench_hmac_sm3_key() + // bench_hmac_sm3_mac_out() + // bench_hkdf_sm3_oneshot() +} diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 8e86dc3c5..231a1b730 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -5,3 +5,4 @@ mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha2_mem_usage; mod bench_sha3_mem_usage; +mod bench_sm3_mem_usage;