Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
87c1ae7
sm3: from_suspended rejects a byte_count that disagrees with x_buf_of…
dghgit Nov 3, 2026
ce18b96
utils: Secret's byte-view equality now needs the new unsafe trait NoP…
dghgit Nov 3, 2026
5af9c4f
ascon: AsconHash256::do_final_partial_bits and _out follow the Hash c…
dghgit Nov 3, 2026
f131301
sm3, factory, cli: HKDF-SM3 (the HKDF of the RFC 8998 TLS 1.3 SM suit…
dghgit Nov 3, 2026
17681b1
sm3, mem_usage_benches: bench_sm3_mem_usage, in bench_ascon_mem_usage…
dghgit Nov 3, 2026
74249cf
ascon: KEY_LEN, NONCE_LEN and TAG_LEN become ASCON_AEAD128_KEY_LEN, A…
dghgit Nov 3, 2026
e0781ee
hmac, sha2, sha3, sm3: the 32-bit HMAC truncation floor (MIN_FIPS_DIG…
dghgit Nov 3, 2026
d921046
ascon: into_squeezer ends the absorb phase there, through a new Spong…
dghgit Nov 3, 2026
fda953d
core: KeyMaterial::from_rng sets its metadata through the setters ins…
dghgit Nov 3, 2026
967ae57
hmac: a test that HmacKey::<SHA224>::generate works with the default …
dghgit Nov 3, 2026
bdb5f04
base64: Base64Decoder::do_final refuses input that ends one symbol in…
dghgit Nov 3, 2026
ade7b31
sha2: SHA512t<T>'s check_t now also runs in OUTPUT_LEN and ALG_NAME, …
dghgit Nov 3, 2026
996da8d
factory: MACFactory's 256-bit default (DEFAULT_256BIT_MAC_NAME, defau…
dghgit Nov 3, 2026
09cc458
utils: ct::value_barrier is public, for a caller whose own arithmetic…
dghgit Nov 3, 2026
7773e92
aes: the 2026-11-03 crate review (REVIEW-2026-11-03-aes-correctness-m…
dghgit Nov 3, 2026
b029043
sha3, cli: the 2026-11-03 crate review's hardening items S2-S6 (REVIE…
dghgit Nov 3, 2026
607ad4a
rng: Hash_DRBG now matches SP 800-90A Rev. 1 and passes all 960 CAVP …
dghgit Nov 3, 2026
555a9f7
hkdf, sha2: the 2026-11-03 hmac/hkdf review's HKDF hardening items K2…
dghgit Nov 3, 2026
d8a04fc
sha3: the 2026-11-03 review's doc notes S-N2 and S-N3: the KDF impl i…
dghgit Nov 3, 2026
c5a7dae
hmac: the 2026-11-03 review's doc notes H5 (b)-(d): the crate doc's i…
dghgit Nov 3, 2026
777401b
hkdf: the 2026-11-03 review's doc note K8: credited entropy is report…
dghgit Nov 3, 2026
be9522c
rng: the 2026-11-03 review's doc notes R9, R10 and R16: the Sp80090AD…
dghgit Nov 3, 2026
cfa2782
cipher: the 2026-11-03 cipher review's items still open after PR #172…
dghgit Nov 3, 2026
d0c2481
core-test-framework: the AEAD suite checks that an empty data call is…
dghgit Nov 3, 2026
83bcbd3
ascon: an empty data call leaves the AAD phase open, the family rule …
dghgit Nov 3, 2026
474749e
rng: tests for the three gaps the scoped mutants run exposed: a third…
dghgit Nov 3, 2026
11405b4
sha3: the unused-bits test for a partial byte on the customized (cSHA…
dghgit Nov 3, 2026
3b2f570
mlkem-common: barrett_reduce is valid for every i16 (the ML-KEM revie…
dghgit Nov 3, 2026
9f97213
mlkem: the 2026-11-03 ML-KEM review's hardening items M1 and M3 and t…
dghgit Nov 3, 2026
494893a
mlkem-lowmemory: the ML-KEM review's M1, M3 and M5 for the low-memory…
dghgit Nov 3, 2026
7122745
core: SignatureError gains RejectionLimitExceeded and PreHashTooWeak,…
dghgit Nov 3, 2026
c312729
mldsa-common: check_norm and make_hint are branch-free (the ML-DSA re…
dghgit Nov 3, 2026
a2bc479
mldsa: the ML-DSA review's M2 (rho' in key generation and rho'' in si…
dghgit Nov 3, 2026
e72aee4
mldsa-lowmemory: the ML-DSA review's M1 (the iteration-cap return cle…
dghgit Nov 3, 2026
dda6bb3
sha3: SHAKE128 and SHAKE256 implement HashAlgParams as the fixed-leng…
dghgit Nov 3, 2026
cb587c0
mldsa: SHAKE128 and SHAKE256 as HashML-DSA pre-hashes (the ML-DSA rev…
dghgit Nov 3, 2026
20613a8
mldsa-lowmemory: SHAKE128 and SHAKE256 as HashML-DSA pre-hashes (the …
dghgit Nov 3, 2026
4be9fda
cli: mlkem1024 keygen generates an ML-KEM-1024 key (it called the ML-…
dghgit Nov 3, 2026
d125822
cli: one body for the three ML-KEM and the three ML-DSA parameter set…
dghgit Nov 3, 2026
36524ba
cli: the CLI review's hardening and consistency items. One input rule…
dghgit Nov 3, 2026
bf463f7
cli: the subcommands the library had and the CLI did not (the CLI rev…
dghgit Nov 3, 2026
32b09ad
cli/tests: the CLI review's eight known-answer scripts (sha2, sha3, s…
dghgit Nov 3, 2026
ea159fa
ascon: do_decrypt_inplace ends the AAD phase on its first non-empty c…
dghgit Nov 4, 2026
169e499
utils: the NoPadding safety contract says every byte is initialized, …
dghgit Nov 4, 2026
9d13f4f
core: the do_update_aad docs carry the empty-call rule the AEAD suite…
dghgit Nov 4, 2026
5c97f57
sha3: the squeeze loop comment allows for an empty output (P190-sha3-2)
dghgit Nov 4, 2026
62897b0
sm3: the HKDF-SM3 test header records the verification review's indep…
dghgit Nov 4, 2026
3ad6540
hkdf: the resume check's comment records that the credited label is b…
dghgit Nov 4, 2026
7a25f21
base64: the impossible padding arm of do_final is unreachable! rather…
dghgit Nov 4, 2026
d7b64d2
rng: the nonce doc says new_from_os seeds with twice the strength, SP…
dghgit Nov 4, 2026
a86b3b7
aes: the round-key widening's cross-target assembly check is in the t…
dghgit Nov 4, 2026
def27da
cipher: GCM's resumed-counter rule reads the counter through CtrKeySt…
dghgit Nov 4, 2026
0f8c9e6
mlkem-common: poly_reduce's doc gives the closed range [0, q] that ba…
dghgit Nov 4, 2026
b277420
mlkem: a unit test that dot_product returns reduced coefficients, whi…
dghgit Nov 4, 2026
4013dde
mldsa-common: the rounding tests say they pin the value, not the bran…
dghgit Nov 4, 2026
1108f34
cli: comments on the process-wide --hex-input flag and on why the bas…
dghgit Nov 4, 2026
c3c5e9d
aes: the assembly check pins cargo's build directory to target and fi…
dghgit Nov 4, 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
26 changes: 26 additions & 0 deletions .github/workflows/aes-ct-asm.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ custom_mutants_output/
!.claude/agents/
.claude/settings.local.json
.claude 2/
dev_scripts/aes_ct_harness/Cargo.lock
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <subcommand> --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:

```
Expand Down
46 changes: 17 additions & 29 deletions cli/src/aes_ccm_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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};
Expand Down Expand Up @@ -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<String>,
Expand All @@ -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<String>,
Expand All @@ -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<String>,
Expand Down Expand Up @@ -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
Expand All @@ -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<String>, nonce_file: &Option<String>) -> Vec<u8> {
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."
Expand All @@ -191,10 +192,7 @@ fn load_nonce(nonce: &Option<String>, nonce_file: &Option<String>) -> Vec<u8> {
}
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.");
Expand All @@ -215,31 +213,20 @@ fn load_nonce(nonce: &Option<String>, nonce_file: &Option<String>) -> Vec<u8> {
/// 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<String>, aad_file: &Option<String>) -> Vec<u8> {
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<u8> {
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<F: CcmKeys<KEY_LEN>, const KEY_LEN: usize>(
action: &CipherDirection,
key: &KeyMaterial<KEY_LEN>,
Expand All @@ -262,7 +249,8 @@ fn run<F: CcmKeys<KEY_LEN>, 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 {
Expand Down
3 changes: 3 additions & 0 deletions cli/src/aes_gcm_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
Expand All @@ -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<String>,
Expand All @@ -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<String>,
Expand Down
Loading
Loading