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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,20 @@ jobs:
- name: "lib-q-prf"
features: "alloc"
package: "lib-q-prf"
# lib-q-tweak-aead's `simd-avx2` keystream was COMPILED on every PR (the workspace
# `cargo clippy --all-targets --all-features` pass in core-validation) but EXECUTED by
# nothing: no Cargo.toml forwards `lib-q-tweak-aead/simd-avx2` and no job enabled it, and
# the only `cargo test --all-features` in the repo (rust-build) is behind `run-tests`,
# which every caller sets to "false". The simd-avx2 row is the one that is genuinely new.
# The plain row is deliberate redundancy with the whole-workspace "std" shard: that shard
# names no package, so `grep tweak-aead .github/workflows/` found nothing and the crate
# read as untested. Making the coverage greppable is the point.
- name: "lib-q-tweak-aead"
features: "std"
package: "lib-q-tweak-aead"
- name: "lib-q-tweak-aead simd-avx2"
features: "std,simd-avx2"
package: "lib-q-tweak-aead"
- name: "lib-q-ring-sig pilot-insecure-prf-transcript"
features: "pilot-insecure-prf-transcript"
package: "lib-q-ring-sig"
Expand Down
47 changes: 42 additions & 5 deletions lib-q-tweak-aead/examples/dump_tweak_kat.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
//! Dev-only: print hex for KAT (`cargo run -p lib-q-tweak-aead --example dump_tweak_kat`).
//! Dev-only: print hex for the KATs in `src/crypto.rs`
//! (`cargo run -p lib-q-tweak-aead --example dump_tweak_kat`).
//!
//! Run this WITHOUT `--features simd-avx2` when regenerating: the expected bytes must come from
//! the portable path so that the KAT is an independent oracle for the AVX2 path, not a recording
//! of it.
use lib_q_tweak_aead::params::{
KEY_BYTES,
NONCE_BYTES,
TAG_BYTES,
};

fn to_hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{:02x}", x)).collect()
}

/// Inputs for `kat_encrypt_multi_block_293`, kept in one place so the test and this generator
/// cannot drift apart. See that test for why 293 bytes.
fn multi_block_inputs() -> ([u8; KEY_BYTES], [u8; NONCE_BYTES], &'static [u8], Vec<u8>) {
let mut key = [0u8; KEY_BYTES];
for (i, k) in key.iter_mut().enumerate() {
*k = i as u8;
}
// Fixed by construction: this example regenerates the KAT expected bytes, so its inputs must
// match `kat_encrypt_multi_block_293` exactly. Never used to encrypt real data.
// codeql[rust/hard-coded-cryptographic-value]
let mut nonce = [0u8; NONCE_BYTES];

Check failure

Code scanning / CodeQL

Hard-coded cryptographic value Critical

This hard-coded value is used as
a nonce
.
Comment thread
Nexlab-One marked this conversation as resolved.
Dismissed
for (i, n) in nonce.iter_mut().enumerate() {
*n = 0x10 + i as u8;
}
let pt: Vec<u8> = (0..293).map(|i| i as u8).collect();
(key, nonce, b"lib-q tweak-aead KAT", pt)
}

fn main() {
let key = [0u8; 32];
let nonce = [0u8; 16];
let mut out = [0u8; 4 + 32];
// Same fixed inputs as the pre-existing `kat_encrypt_libq_empty_ad` vector, for the same
// reason: this regenerates that KAT, it does not encrypt anything real.
let key = [0u8; KEY_BYTES];
// codeql[rust/hard-coded-cryptographic-value]
let nonce = [0u8; NONCE_BYTES];

Check failure

Code scanning / CodeQL

Hard-coded cryptographic value Critical

This hard-coded value is used as
a nonce
.
Comment thread
Nexlab-One marked this conversation as resolved.
Dismissed
let mut out = [0u8; 4 + TAG_BYTES];
lib_q_tweak_aead::crypto::encrypt(&key, &nonce, b"", b"libQ", &mut out).unwrap();
println!("{}", to_hex(&out));
println!("kat_encrypt_libq_empty_ad = {}", to_hex(&out));

let (key, nonce, ad, pt) = multi_block_inputs();
let mut out = vec![0u8; pt.len() + TAG_BYTES];
lib_q_tweak_aead::crypto::encrypt(&key, &nonce, ad, &pt, &mut out).unwrap();
println!("kat_encrypt_multi_block_293 = {}", to_hex(&out));
}
77 changes: 76 additions & 1 deletion lib-q-tweak-aead/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,22 @@
#[cfg(test)]
mod kat_tests {
use super::encrypt;
use crate::params::{
KEY_BYTES,
NONCE_BYTES,
TAG_BYTES,
};

#[test]
fn kat_encrypt_libq_empty_ad() {
let key = [0u8; 32];
let nonce = [0u8; 16];
let ad = b"";
let pt = b"libQ";
let mut out = [0u8; 4 + 32];
// Deliberately DIRTY (see `kat_encrypt_multi_block_293`): `encrypt` must assign into the
// caller's buffer, not XOR into whatever it already held. A zeroed buffer cannot tell the
// two apart, since 0 is the XOR identity.
let mut out = [0xAAu8; 4 + 32];
encrypt(&key, &nonce, ad, pt, &mut out).unwrap();
assert_eq!(
out.as_slice(),
Expand All @@ -182,4 +190,71 @@
.as_slice()
);
}

/// Multi-block KAT: 293 bytes = 9 full 32-byte blocks + a 5-byte remainder.
///
/// This length is chosen to drive **every** loop of the AVX2 keystream in one shot:
/// two iterations of the 4-way batched loop (blocks 0..4 and 4..8), one iteration of the
/// single-block tail loop (block 8), and the sub-block remainder (block 9). The pre-existing
/// `kat_encrypt_libq_empty_ad` vector is 4 bytes, so it only ever reached the remainder path
/// — it cannot detect a batched-loop defect.
///
/// Why a KAT and not only `tests/simd_equivalence.rs`: this test is **branch-independent**.
/// `xor_body` picks AVX2 vs portable at runtime, and the differential test can only run when
/// the host CPU actually has AVX2 (it returns early otherwise). This one pins fixed bytes
/// whichever branch runs, so a build that takes the AVX2 branch must produce exactly the
/// portable answer or fail here. Regenerate with
/// `cargo run -p lib-q-tweak-aead --example dump_tweak_kat` (no `simd-avx2`, so the expected
/// bytes stay an independent oracle rather than a recording of the AVX2 path).
#[test]
fn kat_encrypt_multi_block_293() {
let mut key = [0u8; KEY_BYTES];
for (i, k) in key.iter_mut().enumerate() {
*k = i as u8;
}
// A known-answer test pins fixed inputs to fixed outputs; a fresh nonce would make the
// expected bytes below unverifiable, which is the whole point of a KAT. This value never
// leaves the test module and is not reachable from any production path.
// codeql[rust/hard-coded-cryptographic-value]
let mut nonce = [0u8; NONCE_BYTES];

Check failure

Code scanning / CodeQL

Hard-coded cryptographic value Critical

This hard-coded value is used as
a nonce
.
Comment thread
Nexlab-One marked this conversation as resolved.
Dismissed
for (i, n) in nonce.iter_mut().enumerate() {
*n = 0x10 + i as u8;
}
let ad = b"lib-q tweak-aead KAT";
let mut pt = [0u8; 293];
for (i, p) in pt.iter_mut().enumerate() {
*p = i as u8;
}

// Deliberately DIRTY, not zeroed. `encrypt` is `pub` and writes into a caller-supplied
// `out`; a caller reusing one buffer across messages is the obvious way to avoid
// reallocating. A zeroed buffer cannot distinguish `ct[i] = pt[i] ^ ks[i]` from
// `ct[i] ^= pt[i] ^ ks[i]` (0 is the XOR identity), and every output buffer in this
// suite used to be zeroed: changing both AVX2 write sites to `^=` left all 12 tests
// green. Prefilling here pins assignment semantics on whichever branch `xor_body` takes,
// so it holds on AVX2-less runners too. The expected bytes are unchanged — that is the
// point.
let mut out = [0xAAu8; 293 + TAG_BYTES];
encrypt(&key, &nonce, ad, &pt, &mut out).unwrap();

let expected = hex::decode(concat!(
"394feaffac29c1b3eb0b999fd7915ebae93b036d675a4829cac0c823eabf8b0cd35eaf556d6f60a7",
"b81a1d87d5cac535d9338ae11bffac70912a498436240736c865f2c75f7277b3278eb2fba75c0920",
"dd07dbd0ca8f9605f8630447de31b33ccd9970d50ed8497ae9de95753ef3a5a03c75300a178859b1",
"3da26b8a0c1e60046fd0275b8c6b4c1711978cfeee6a54b3c893eb8546f9dcadbd061257d27337dc",
"e57145a13903fa215fae05118c49ed9ead64e404adbdcee09be5cf9749fbda26493e58cdf04acc2b",
"f42bfae1e8f27a6e6d70ffbc553108687a00469f36b9d9de48d5ce5aaa24b8999e953b134b8b380a",
"2ae1fb58bc9612471967abe0e1798c7dcf5beb371c0e570e156e23ac52532a6eb5a0ff0045467082",
"c9000e6e71fd3bafe22deb597f5cb1345ef0fb0301b6f65f43ee1138064c57033a7e79826ad7831b",
"c4980fd1ce",
))
.unwrap();
assert_eq!(out.len(), expected.len());
assert_eq!(
out.as_slice(),
expected.as_slice(),
"multi-block ciphertext+tag mismatch; first differing byte at index {:?}",
out.iter().zip(expected.iter()).position(|(a, b)| a != b)
);
}
}
Loading