Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b1a7731
core, sha3: XOF extends Hash, so SHAKE128 and SHAKE256 are hashes; sq…
dghgit Sep 7, 2026
61a798a
sha3: pin the SHAKE block_bitlen and output_len values, which three m…
dghgit Sep 7, 2026
11b645a
core: XofOutput gains do_final and do_final_out, matching BC Java's d…
dghgit Sep 7, 2026
52d88a7
sha3: add cSHAKE128 and cSHAKE256 (SP 800-185 Sec 3) with the Sec 2.3…
dghgit Sep 7, 2026
dfbd3f9
docs: record the cargo mutants scoping flags, the bc-test-data conven…
dghgit Sep 7, 2026
68933c2
sha3: add KMAC128 and KMAC256 (SP 800-185 Sec 4) with KMACXOF, MACFac…
dghgit Sep 7, 2026
386b43d
core: drop the Default supertrait from Hash, so keyed constructions c…
dghgit Sep 7, 2026
3c25e45
sha3: KMACXOF128 and KMACXOF256 as keyed XOFs, now that Hash no longe…
dghgit Sep 7, 2026
e5b707b
core-test-framework: the XOF suite takes a constructor closure, so ke…
dghgit Sep 7, 2026
d2a103c
sha3: add TupleHash and TupleHashXOF (SP 800-185 Sec 5), where each u…
dghgit Sep 7, 2026
59c360f
sha3: add ParallelHash and ParallelHashXOF (SP 800-185 Sec 6), comple…
dghgit Sep 7, 2026
4eb5eed
cli: add tuplehash and parallelhash subcommands, completing SP 800-18…
dghgit Sep 7, 2026
7df50c4
sha3: pin the Hash and XOF trait views of TupleHash, ParallelHash and…
dghgit Sep 7, 2026
5c45ae8
factory: replace the todo stub in xof_factory_tests with a differenti…
dghgit Sep 7, 2026
cb06141
core: Hash gains Clone as a supertrait, so a hash mid-stream can be f…
dghgit Sep 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,33 @@ Rules when working from the downloaded copy:
- **Quote exactly, and locate precisely.** Comments and commit messages should name the document with its revision (e.g. "FIPS 203, Algorithm 13 (ML-KEM.Encaps_internal), step 2", "RFC 5869 §2.2"), and quote the spec verbatim where a quote is clearer than a paraphrase. Verify every section/algorithm/step number against the file you just downloaded — including numbers already present in the code, which may predate a spec revision.
- **The specification is the source of truth for correct behaviour** — not the C/Java/Go implementation you have seen, not the BC Java or BC C# port, and not another crate. When an existing implementation appears to disagree with the spec, re-read the spec, and if the disagreement is real, follow the spec and note the discrepancy in the PR description rather than silently copying the other implementation.
- **Optimizations are allowed, provided externally-visible behaviour is identical.** Restructuring loops, fusing steps, precomputing tables, constant-time rewrites, and in-place buffer reuse are all fine — the spec constrains observable outputs (and, for this library, timing behaviour on secret data), not the shape of the code. Any such deviation from the spec's literal steps gets a comment saying which spec steps it implements and why it is equivalent.
- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices), downloaded the same way. Never hand-write an "expected" value from recall.
- **Test vectors come from the spec or its official companion files** (NIST CAVP / ACVP vectors, RFC test-vector appendices, the NIST "Examples with Intermediate Values" sample files). Never hand-write an "expected" value from recall.

### Test vector data

Vectors live in the **`bc-test-data`** repo, cloned alongside this one at `../bc-test-data`; suites read from it by relative path and print a warning and pass vacuously if it is absent (see `crypto/sha3/tests/cavp_tests.rs` for the pattern). Symlink it to `/tmp/bc-test-data` before running `cargo mutants`, whose build directories are elsewhere.

- Commit the vectors there, not here, and not as PDFs — that repo holds `.rsp`, `.txt` and `.json`, and has no PDFs at all. Extract what a harness needs into the CAVP-style `.rsp` shape already used by `crypto/sha3/`.
- Every new directory gets a `README.md` giving provenance: upstream URL, licence or copyright status, retrieval date, and the SHA-256 of each source document so a refresh can be checked. `crypto/wycheproof/` and `crypto/sp800-185/` are the examples.
- **Validate an extraction against declared lengths, not just that it parses.** NIST sample-value PDFs split hex blocks across page boundaries, and the continuation line then begins with a form feed rather than spaces, so an "indented hex lines" pattern stops at the break and silently truncates. The result is still well-formed hex. Check each value against the length the file states (`Outputlen`, `Length of data is`, `Length of Key is`), and cross-check against BC Java's expected values where an equivalent test exists.

## Notes on testing

- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`).
- Scoping a mutation run: **`--file` is silently ignored** by the installed cargo-mutants — it accepts the flag, filters nothing, and runs the whole package, so a run reported as covering one file may have covered the crate. Use **`-F <regex>`**, which matches the mutant names `--list` prints, and confirm the scope with `--list` first. `--test-workspace` needs an explicit value (`--test-workspace=true`), and is required whenever the mutated code is a `core` trait used by other crates.
- `--in-diff` finds nothing for a change that is mostly trait declarations, renamed call sites and documentation, because the executable code in impl bodies is unchanged. File-scoped runs are the useful gate for that shape of change; do not read "no mutants to filter" as "nothing to test".
- Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate.
- For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests — don't duplicate them per-implementation.
- The per-width `impl Condition<W>` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group.

## Commit messages

One-line subject only: no body, no "Squashed commits" list, and **no `Co-Authored-By` trailer**. This overrides the usual default of adding one. It applies on the release branches and on feature branches alike, so `git commit -m "<subject>"` is the whole of it — put in the subject what the body would have said.

Subjects are `<crate>: <what changed>`, and a change spanning several crates is normally split into one commit per crate, including that crate's factory and CLI wiring. Split only where each commit still builds: a trait change that every implementor must follow cannot be split that way and belongs in one commit.

Do not strip `Co-Authored-By` from commits written in earlier sessions when rewording them during a rebase — that removes someone else's attribution.

## CI

The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test` is the gate.
52 changes: 44 additions & 8 deletions cli/src/mac_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use bouncycastle::core::key_material::{
use bouncycastle::core::traits::MAC;
use bouncycastle::hex;
use bouncycastle::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256, HMAC_SM3};
use bouncycastle::sha3::{KMAC128, KMAC256};

#[allow(non_camel_case_types)]
pub(crate) enum HMACVariant {
Expand All @@ -18,14 +19,8 @@ pub(crate) enum HMACVariant {
SM3,
}

pub(crate) fn mac_cmd(
hmac_variant: HMACVariant,
key: &Option<String>,
key_file: &Option<String>,
verify_val: &Option<String>,
output_hex: bool,
) {
// load the key
/// Loads a MAC key from `--key` (hex) or `--key-file` (raw), tagged as a MAC key.
fn load_mac_key(key: &Option<String>, key_file: &Option<String>) -> KeyMaterial512 {
let key_bytes: Vec<u8> = if key.is_some() {
hex::decode(key.as_ref().unwrap()).unwrap()
} else if key_file.is_some() {
Expand All @@ -41,6 +36,17 @@ pub(crate) fn mac_cmd(
}
let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap();
do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap();
key
}

pub(crate) fn mac_cmd(
hmac_variant: HMACVariant,
key: &Option<String>,
key_file: &Option<String>,
verify_val: &Option<String>,
output_hex: bool,
) {
let key = load_mac_key(key, key_file);

// instantiate the MAC object and call do_mac()
match hmac_variant {
Expand All @@ -67,6 +73,36 @@ pub(crate) fn mac_cmd(
}
}

/// 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,
length: usize,
customization: &Option<String>,
key: &Option<String>,
key_file: &Option<String>,
verify_val: &Option<String>,
output_hex: bool,
) {
let key = load_mac_key(key, key_file);
let s = customization.as_deref().unwrap_or("").as_bytes();
// new_allow_weak_key, as the HMAC commands do: 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 => do_mac(
KMAC128::new_with_params(&key, s, length, true).expect("a valid MAC key"),
verify_val,
output_hex,
),
256 => do_mac(
KMAC256::new_with_params(&key, s, length, true).expect("a valid MAC key"),
verify_val,
output_hex,
),
_ => panic!("Unsupported algorithm: KMAC-{bit_len}"),
}
}

fn do_mac(mut mac: impl MAC, verify_val: &Option<String>, output_hex: bool) {
// read the content to be MAC'd from stdin
let mut buf: [u8; 1024] = [0u8; 1024];
Expand Down
198 changes: 198 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,180 @@ enum Subcommands {
x: bool,
},

/// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given
/// by repeated --element flags, each in hex; with none, stdin is hashed as a single element.
/// The boundaries between elements are part of the hash.
TUPLEHASH128 {
/// 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<String>,

#[arg(short = 's', long)]
/// Customization string.
customization: Option<String>,

#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},

/// Perform TupleHash256 (NIST SP 800-185 Sec 5). See tuplehash128.
TUPLEHASH256 {
/// 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<String>,

#[arg(short = 's', long)]
/// Customization string.
customization: Option<String>,

#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},

/// Perform ParallelHash128 (NIST SP 800-185 Sec 6) of the content provided on stdin.
/// The block size is part of the function: the same input under a different block size gives
/// an unrelated hash, so both sides must use the same value.
/// Supports streaming update for low memory footprint.
PARALLELHASH128 {
/// 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.
customization: Option<String>,

#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},

/// Perform ParallelHash256 (NIST SP 800-185 Sec 6). See parallelhash128.
PARALLELHASH256 {
/// 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.
customization: Option<String>,

#[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.
KMAC128 {
/// Length of the tag in bytes.
length: usize,

#[arg(short = 's', long)]
/// Customization string, domain-separating this use of KMAC from another.
customization: Option<String>,

#[arg(short, long)]
/// The key, in hex.
key: Option<String>,

#[arg(long)]
/// File containing the key, as raw bytes.
key_file: Option<String>,

#[arg(short, long)]
/// Verify against this tag (hex) instead of computing one.
verify: Option<String>,

#[arg(short)]
/// Output the tag in hex format.
x: bool,
},

/// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin.
/// See kmac128.
KMAC256 {
/// Length of the tag in bytes.
length: usize,

#[arg(short = 's', long)]
/// Customization string, domain-separating this use of KMAC from another.
customization: Option<String>,

#[arg(short, long)]
/// The key, in hex.
key: Option<String>,

#[arg(long)]
/// File containing the key, as raw bytes.
key_file: Option<String>,

#[arg(short, long)]
/// Verify against this tag (hex) instead of computing one.
verify: Option<String>,

#[arg(short)]
/// Output the tag in hex format.
x: bool,
},

/// Perform cSHAKE128 (NIST SP 800-185) of the content provided on stdin. Requires the output
/// length in bytes. With no customization string this is exactly SHAKE128.
/// Supports streaming update for low memory footprint.
CSHAKE128 {
/// Length of the output in bytes.
length: usize,

#[arg(short = 's', long)]
/// Customization string. Two cSHAKEs with different customization strings produce
/// unrelated output, so this domain-separates one use of the function from another.
customization: Option<String>,

#[arg(short = 'n', long)]
/// Function-name string. Reserved by NIST for functions it defines (SP 800-185 Sec 3.4);
/// use --customization for your own domain separation.
function_name: Option<String>,

#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},

/// Perform cSHAKE256 (NIST SP 800-185) of the content provided on stdin. Requires the output
/// length in bytes. With no customization string this is exactly SHAKE256.
/// Supports streaming update for low memory footprint.
CSHAKE256 {
/// Length of the output in bytes.
length: usize,

#[arg(short = 's', long)]
/// Customization string. See cshake128.
customization: Option<String>,

#[arg(short = 'n', long)]
/// Function-name string, reserved by NIST. See cshake128.
function_name: Option<String>,

#[arg(short)]
/// Output the hashes in hex format.
x: bool,
},

/// 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
Expand Down Expand Up @@ -1051,6 +1225,30 @@ fn main() {
Some(Subcommands::SHAKE256 { length, x }) => {
sha3_cmd::shake_cmd(256, *length, *x);
}
Some(Subcommands::CSHAKE128 { length, customization, function_name, x }) => {
sha3_cmd::cshake_cmd(128, *length, function_name, customization, *x);
}
Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => {
sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x);
}
Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => {
sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x);
}
Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => {
sha3_cmd::parallelhash_cmd(128, *length, *block_size, customization, *x);
}
Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => {
sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *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::KMAC256 { length, customization, key, key_file, verify, x }) => {
mac_cmd::kmac_cmd(256, *length, customization, key, key_file, verify, *x)
}
Some(Subcommands::CSHAKE256 { length, customization, function_name, x }) => {
sha3_cmd::cshake_cmd(256, *length, function_name, customization, *x);
}
Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => {
mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x)
}
Expand Down
Loading
Loading