diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..276fefe --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "Bash(cargo install *)", + "Bash(cargo mutants *)", + "Bash(cat custom_mutants_output/mutants.out/timeout.txt)", + "Bash(python -c \"print\\(f'{801/\\(801+48\\)*100:.1f}% of viable mutants caught \\(801/{801+48}\\)'\\)\")", + "Read(//c/Users/fmendoza/Work/bc-test-data/crypto/ascon/**)", + "Bash(awk '/^Count = \\(1|2|5|33|68|69|153\\)$/{p=1} p&&/^\\(Count|Key|Nonce|PT|AD|CT\\) /{print} /^$/{p=0}' asconaead128/LWC_AEAD_KAT_128_128.txt)", + "Bash(awk '/^Count = \\(1|2|9|17|33\\)$/{p=1} p&&/^\\(Count|Msg|MD\\) /{print} /^$/{p=0}' asconhash256/LWC_HASH_KAT_256.txt)", + "Bash(awk '/^Count = \\(1|2|9|17|33\\)$/{p=1} p&&/^\\(Count|Msg|MD\\) /{print} /^$/{p=0}' asconxof128/LWC_XOF_KAT_128_512.txt)", + "Bash(awk '/^Count = \\(1|2|3\\)$/{p=1} p&&/^\\(Count|Msg|Z|MD\\) /{print} /^$/{p=0}' asconcxof128/LWC_CXOF_KAT_128_512.txt)", + "Bash(awk 'BEGIN{RS=\"\";FS=\"\\\\n\"} /Msg = [0-9A-F]/ && /Z = [0-9A-F]/ {print; c++} c>=2{exit}' asconcxof128/LWC_CXOF_KAT_128_512.txt)", + "Bash(awk 'BEGIN{RS=\"\";FS=\"\\\\n\"} {pt=\"\"} {for\\(i=1;i<=NF;i++\\) if\\($i ~ /^PT = /\\){pt=substr\\($i,6\\)}} length\\(pt\\)==64 {print; exit}' asconaead128/LWC_AEAD_KAT_128_128.txt)", + "Bash(rm -f tests/test_vector.rs tests/behavior.rs)", + "Bash(rm -rf tests/data)", + "Bash(awk '{p+=$4; f+=$6} END{print \"ascon passed:\",p,\" failed:\",f}')" + ] + } +} diff --git a/Cargo.toml b/Cargo.toml index 4301f8f..b81e51d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2024" # *** Internal Dependencies *** bouncycastle = { path = "./", version = "0.1.1" } +bouncycastle-ascon = { path = "./crypto/ascon", version = "0.1.2" } bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.1" } bouncycastle-core = { path = "crypto/core", version = "0.1.1" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.1" } @@ -40,6 +41,7 @@ version = "0.1.2" edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs new file mode 100644 index 0000000..7755ce2 --- /dev/null +++ b/cli/src/ascon_cmd.rs @@ -0,0 +1,150 @@ +use std::io::{Read, Write}; +use std::process::exit; +use std::{fs, io}; + +use bouncycastle::ascon::ascon_aead128::AsconAead128; +use bouncycastle::ascon::ascon_cxof128::AsconCXof128; +use bouncycastle::ascon::ascon_hash256::AsconHash256; +use bouncycastle::ascon::ascon_xof128::AsconXof128; +use bouncycastle::core::traits::{Hash, XOF}; +use bouncycastle::hex; + +/// Write `data` to stdout, either as hex or raw binary, followed by a newline. +fn emit(data: &[u8], output_hex: bool) { + if output_hex { + for b in data.iter() { + print!("{b:02x}"); + } + } else { + io::stdout().write_all(data).unwrap(); + } + println!(); +} + +/// Read all of stdin into a Vec. +fn read_stdin() -> Vec { + let mut data = Vec::new(); + io::stdin().read_to_end(&mut data).expect("Failed to read from stdin"); + data +} + +/// Load a hex string or a binary 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 { + fs::read(file).unwrap_or_else(|e| { + eprintln!("Error: failed to read {label} file: {e}"); + exit(-1) + }) + } 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 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) + }) +} + +/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. +pub(crate) fn hash256_cmd(output_hex: bool) { + let mut h = AsconHash256::new(); + let mut buf = [0u8; 1024]; + let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + while bytes_read != 0 { + h.do_update(&buf[..bytes_read]); + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + let out = h.do_final(); + emit(&out, output_hex); +} + +/// Ascon-XOF128 of stdin, producing `output_len` bytes. Streaming absorb. +pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) { + let mut x = AsconXof128::new(); + let mut buf = [0u8; 1024]; + let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + while bytes_read != 0 { + x.absorb(&buf[..bytes_read]); + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + let out = x.squeeze(output_len); + emit(&out, 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 mut x = AsconCXof128::with_customization(&z); + let mut buf = [0u8; 1024]; + let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + while bytes_read != 0 { + x.absorb(&buf[..bytes_read]); + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + let out = x.squeeze(output_len); + emit(&out, output_hex); +} + +/// Ascon-AEAD128 of stdin. Encrypts (stdin = plaintext, output = ciphertext||tag) or, with +/// `decrypt`, decrypts (stdin = ciphertext||tag, output = plaintext). Decryption exits with a +/// non-zero status if the authentication tag does not verify. +#[allow(clippy::too_many_arguments)] +pub(crate) fn aead128_cmd( + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + ad: &Option, + decrypt: bool, + output_hex: bool, +) { + let key = require_16(load_bytes(key, key_file, "key"), "key"); + let nonce = require_16(load_bytes(nonce, nonce_file, "nonce"), "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 ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) }; + + let input = read_stdin(); + + if decrypt { + if input.len() < 16 { + eprintln!("Error: ciphertext is shorter than the 16-byte tag."); + exit(-1); + } + let mut out = vec![0u8; input.len() - 16]; + match AsconAead128::decrypt(&key, &nonce, ad_opt, &input, &mut out) { + Ok(n) => { + out.truncate(n); + emit(&out, output_hex); + } + Err(_) => { + eprintln!("Error: Ascon-AEAD128 authentication failed."); + exit(-1); + } + } + } else { + let mut out = vec![0u8; input.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &input, &mut out); + out.truncate(n); + emit(&out, output_hex); + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 45205bf..83a9df8 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,4 @@ +mod ascon_cmd; mod encoders_cmd; mod helpers; mod hkdf_cmd; @@ -124,6 +125,76 @@ enum Subcommands { x: bool, }, + /// Perform Ascon-Hash256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + AsconHash256 { + #[arg(short)] + /// Output the digest in hex format. + x: bool, + }, + + /// Perform Ascon-XOF128 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + AsconXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform Ascon-CXOF128 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + AsconCXOF128 { + /// Length of the output in bytes. + length: usize, + + /// Customization string in hex (optional). + #[arg(long)] + customization: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin. + /// Encrypts by default (stdin = plaintext, output = ciphertext||tag); with --decrypt the + /// reverse. Decryption fails with a non-zero exit status if the tag does not verify. + /// 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. + AsconAEAD128 { + /// The 128-bit 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 128-bit key in binary. + #[arg(long)] + key_file: Option, + + /// The 128-bit nonce in hex. Must be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the 128-bit nonce in binary. + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex (authenticated but not encrypted). + #[arg(long)] + ad: Option, + + /// Decrypt instead of encrypt. + #[arg(short, long)] + decrypt: bool, + + #[arg(short)] + /// Output 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 @@ -531,6 +602,18 @@ fn main() { Some(Subcommands::SHAKE256 { length, x }) => { sha3_cmd::shake_cmd(256, *length, *x); } + Some(Subcommands::AsconHash256 { x }) => { + ascon_cmd::hash256_cmd(*x); + } + Some(Subcommands::AsconXOF128 { length, x }) => { + ascon_cmd::xof128_cmd(*length, *x); + } + Some(Subcommands::AsconCXOF128 { length, customization, x }) => { + ascon_cmd::cxof128_cmd(customization, *length, *x); + } + Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, decrypt, x }) => { + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, *decrypt, *x); + } Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) } diff --git a/crypto/ascon/Cargo.toml b/crypto/ascon/Cargo.toml new file mode 100644 index 0000000..0b8d3a5 --- /dev/null +++ b/crypto/ascon/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bouncycastle-ascon" +version = "0.1.2" +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "ascon_benches" +harness = false diff --git a/crypto/ascon/benches/ascon_benches.rs b/crypto/ascon/benches/ascon_benches.rs new file mode 100644 index 0000000..3e53360 --- /dev/null +++ b/crypto/ascon/benches/ascon_benches.rs @@ -0,0 +1,90 @@ +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ascon::ascon_aead128::AsconAead128; +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::traits::{Hash, RNG, XOF}; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_aead128_encrypt(c: &mut Criterion) { + let key = [0x42u8; 16]; + let nonce = [0x24u8; 16]; + let data = random_data(DATA_LEN); + let mut out = vec![0u8; DATA_LEN + 16]; + + let mut group = c.benchmark_group("ascon::AsconAead128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::encrypt()"), |b| { + b.iter(|| { + AsconAead128::encrypt(&key, &nonce, None, black_box(&data), &mut out); + black_box(&out); + }) + }); + group.finish(); +} + +fn bench_hash256(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut digest = [0u8; 32]; + + let mut group = c.benchmark_group("ascon::AsconHash256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + group.bench_function(format!("{DATA_LEN} bytes -- ::hash_out()"), |b| { + b.iter(|| { + AsconHash256::new().hash_out(black_box(&data), &mut digest); + black_box(&digest); + }) + }); + group.finish(); +} + +fn bench_xof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconXof128::new().hash_xof_out(black_box(&data), &mut out); + black_box(&out); + }) + }, + ); + group.finish(); +} + +fn bench_cxof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let customization = b"bench-customization"; + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconCXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconCXof128::with_customization(customization) + .hash_xof_out(black_box(&data), &mut out); + black_box(&out); + }) + }, + ); + group.finish(); +} + +criterion_group!(benches, bench_aead128_encrypt, bench_hash256, bench_xof128, bench_cxof128); +criterion_main!(benches); diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs new file mode 100644 index 0000000..47d5492 --- /dev/null +++ b/crypto/ascon/src/ascon_aead128.rs @@ -0,0 +1,662 @@ +//! Ascon-AEAD128 authenticated encryption, as specified in NIST SP 800-232 §4. +//! +//! Rate = 128 bits, capacity = 192 bits, 128-bit key/nonce/tag. Initialization and finalization use +//! `Ascon-p[12]`; associated-data and plaintext/ciphertext blocks use `Ascon-p[8]`. + +use core::fmt::{self, Debug, Display, Formatter}; + +use bouncycastle_core::errors::AeadError; +use bouncycastle_core::traits::{AeadCipher, Algorithm, Secret, SecurityStrength}; + +use crate::util::{load_u64_le, store_u64_le}; + +const CRYPTO_KEYBYTES: usize = 16; +const CRYPTO_ABYTES: usize = 16; +const RATE: usize = 16; +const BUF_SIZE_DECRYPT: usize = RATE + CRYPTO_ABYTES; // 16 + 16 = 32 + +/// Ascon-AEAD128 initial value (SP 800-232 Table 14). +const ASCON_IV: u64 = 0x00001000808C0001; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum State { + EncInit, + EncAad, + EncData, + EncFinal, + DecInit, + DecAad, + DecData, + DecFinal, +} + +/// An implementation of the Ascon-AEAD128 algorithm (NIST SP 800-232). +/// +/// A single instance performs one operation (encryption or decryption) under one (key, nonce) pair. +/// See [`AsconAead128::new`] for the streaming workflow and [`AsconAead128::encrypt`] / +/// [`AsconAead128::decrypt`] for the one-shot APIs. +pub struct AsconAead128 { + // Secret key and nonce (both 128 bits). + k0: u64, + k1: u64, + n0: u64, + n1: u64, + // 320-bit internal state (five 64-bit words). + s0: u64, + s1: u64, + s2: u64, + s3: u64, + s4: u64, + // Buffer used for processing AAD or (de)ciphertext. + // For decryption the buffer size is RATE + CRYPTO_ABYTES = 32 bytes. + buf: [u8; BUF_SIZE_DECRYPT], + buf_pos: usize, + // The computed authentication tag (after encryption finalization). + mac: Option<[u8; CRYPTO_ABYTES]>, + // State machine for enforcing the call order. + state: State, + // true for encryption mode; false for decryption. + for_encryption: bool, + finished: bool, +} + +impl AsconAead128 { + /// Create a new instance. + /// * `key` is the 128-bit secret key. + /// * `nonce` is the 128-bit nonce. It **must** be unique per encryption under a given key. + /// * `ad` is optional associated data (authenticated, not encrypted); processed immediately. + /// * `for_encryption` is true for encryption, false for decryption. + pub fn new( + key: &[u8; CRYPTO_KEYBYTES], + nonce: &[u8; CRYPTO_KEYBYTES], + ad: Option<&[u8]>, + for_encryption: bool, + ) -> Self { + let k0 = load_u64_le(key, 0); + let k1 = load_u64_le(key, 8); + let n0 = load_u64_le(nonce, 0); + let n1 = load_u64_le(nonce, 8); + let state = if for_encryption { State::EncInit } else { State::DecInit }; + let mut aead = AsconAead128 { + k0, + k1, + n0, + n1, + s0: 0, + s1: 0, + s2: 0, + s3: 0, + s4: 0, + buf: [0u8; BUF_SIZE_DECRYPT], + buf_pos: 0, + mac: None, + state, + for_encryption, + finished: false, + }; + aead.init_state(); + if let Some(ad_bytes) = ad + && !ad_bytes.is_empty() + { + aead.process_aad_bytes(ad_bytes); + } + aead + } + + /// One-shot authenticated encryption (SP 800-232 Algorithm 3). + /// Writes ciphertext followed by the 128-bit tag into `out`, which must be at least + /// `plaintext.len() + 16` bytes. Returns the number of bytes written. + pub fn encrypt( + key: &[u8; CRYPTO_KEYBYTES], + nonce: &[u8; CRYPTO_KEYBYTES], + ad: Option<&[u8]>, + plaintext: &[u8], + out: &mut [u8], + ) -> usize { + let mut cipher = Self::new(key, nonce, ad, true); + let n = cipher.encrypt_update(plaintext, out); + n + cipher.encrypt_finalize(&mut out[n..]) + } + + /// One-shot authenticated decryption (SP 800-232 Algorithm 4). + /// `ciphertext` is the ciphertext followed by the 128-bit tag. Writes the recovered plaintext + /// into `out`, which must be at least `ciphertext.len() - 16` bytes. Returns the number of bytes + /// written, or [`AeadError::AuthenticationFailed`] if the tag does not verify. + pub fn decrypt( + key: &[u8; CRYPTO_KEYBYTES], + nonce: &[u8; CRYPTO_KEYBYTES], + ad: Option<&[u8]>, + ciphertext: &[u8], + out: &mut [u8], + ) -> Result { + let mut cipher = Self::new(key, nonce, ad, false); + let n = cipher.decrypt_update(ciphertext, out); + Ok(n + cipher.decrypt_finalize(&mut out[n..])?) + } + + // Initialization (SP 800-232 §4.1.1 step 1): S = IV || K || N, then Ascon-p[12], then XOR K into + // the last 128 bits. No caching, since the key and/or nonce change for every operation. + fn init_state(&mut self) { + self.s0 = ASCON_IV; + self.s1 = self.k0; + self.s2 = self.k1; + self.s3 = self.n0; + self.s4 = self.n1; + self.p12(); + self.s3 ^= self.k0; + self.s4 ^= self.k1; + } + + fn p12(&mut self) { + // Round constants c_i for Ascon-p[12] (SP 800-232 Table 5, indices 4..=15). + let round_consts: [u64; 12] = + [0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B]; + for &c in round_consts.iter() { + self.round(c); + } + } + + fn p8(&mut self) { + // Round constants c_i for Ascon-p[8] (SP 800-232 Table 5, indices 8..=15). + let round_consts: [u64; 8] = [0xB4, 0xA5, 0x96, 0x87, 0x78, 0x69, 0x5A, 0x4B]; + for &c in round_consts.iter() { + self.round(c); + } + } + + // One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4). The S-box (§3.3 Eq. 7) and the per-word + // linear diffusion Σ0..Σ4 (§3.4 Eq. 8–12) are fused here in their bitsliced form. + #[inline(always)] + fn round(&mut self, c: u64) { + let sx = self.s2 ^ c; + let t0 = self.s0 ^ self.s1 ^ sx ^ self.s3 ^ (self.s1 & (self.s0 ^ sx ^ self.s4)); + let t1 = self.s0 ^ sx ^ self.s3 ^ self.s4 ^ ((self.s1 ^ sx) & (self.s1 ^ self.s3)); + let t2 = self.s1 ^ sx ^ self.s4 ^ (self.s3 & self.s4); + let t3 = self.s0 ^ self.s1 ^ sx ^ ((!self.s0) & (self.s3 ^ self.s4)); + let t4 = self.s1 ^ self.s3 ^ self.s4 ^ ((self.s0 ^ self.s4) & self.s1); + self.s0 = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + self.s1 = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + self.s2 = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + self.s3 = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + self.s4 = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); + } + + /// Returns a 64-bit value with a single "1" at bit position (i*8): the integer form of the + /// padding rule for a partial block (SP 800-232 Appendix A.2). + fn pad(i: usize) -> u64 { + debug_assert!(i < 8); + 0x01u64 << (i * 8) + } + + fn check_aad(&mut self) { + match self.state { + State::DecInit => self.state = State::DecAad, + State::EncInit => self.state = State::EncAad, + State::DecAad | State::EncAad => {} + State::EncFinal => panic!("Ascon-AEAD128 cannot be reused for encryption"), + _ => panic!("Ascon-AEAD128 needs to be initialized"), + } + } + + fn finish_aad(&mut self, next_state: State) { + if matches!(self.state, State::DecAad | State::EncAad) { + debug_assert!(self.buf_pos < RATE); + + self.buf[self.buf_pos] = 0x01; + + let block0 = load_u64_le(&self.buf, 0); + if self.buf_pos >= 8 { + self.s0 ^= block0; + + let block1 = load_u64_le(&self.buf, 8); + self.s1 ^= block1 & (u64::MAX >> (56 - ((self.buf_pos - 8) * 8))); + } else { + self.s0 ^= block0 & (u64::MAX >> (56 - (self.buf_pos * 8))); + } + self.p8(); + } + // Domain separation (SP 800-232 §4.1.1 step 2: S ← S ⊕ (0^319 || 1)). + self.s4 ^= 0x8000000000000000; + self.buf_pos = 0; + self.state = next_state; + } + + fn finish_data(&mut self, next_state: State) { + // Finalization (SP 800-232 §4.1.1 step 4 / §4.1.2 step 4). + self.s2 ^= self.k0; + self.s3 ^= self.k1; + self.p12(); + self.s3 ^= self.k0; + self.s4 ^= self.k1; + + self.state = next_state; + } + + fn check_data(&mut self) -> bool { + match self.state { + State::DecInit | State::DecAad => { + self.finish_aad(State::DecData); + false + } + State::EncInit | State::EncAad => { + self.finish_aad(State::EncData); + true + } + State::DecData => false, + State::EncData => true, + State::EncFinal => panic!("Ascon-AEAD128 cannot be reused for encryption"), + _ => panic!("Ascon-AEAD128 needs to be initialized"), + } + } + + /// Process associated data (AAD) bytes. May be called multiple times, but only before any + /// plaintext/ciphertext is processed. + pub fn process_aad_bytes(&mut self, input: &[u8]) { + if input.is_empty() { + return; + } + + self.check_aad(); + + let mut input = input; + + if self.buf_pos > 0 { + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } + + self.buf[self.buf_pos..RATE].copy_from_slice(&input[..available]); + input = &input[available..]; + + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_buffer_aad(&tmp); + } + + while input.len() >= RATE { + self.process_buffer_aad(&input[..RATE]); + input = &input[RATE..]; + } + + self.buf[..input.len()].copy_from_slice(input); + self.buf_pos = input.len(); + } + + fn process_buffer_aad(&mut self, block: &[u8]) { + debug_assert!(block.len() >= RATE); + + self.s0 ^= load_u64_le(block, 0); + self.s1 ^= load_u64_le(block, 8); + + self.p8(); + } + + fn process_buffer_encrypt(&mut self, block: &[u8], output: &mut [u8]) { + debug_assert!(block.len() >= RATE); + debug_assert!(output.len() >= RATE); + + self.s0 ^= load_u64_le(block, 0); + store_u64_le(output, 0, self.s0); + + self.s1 ^= load_u64_le(block, 8); + store_u64_le(output, 8, self.s1); + + self.p8(); + } + + fn process_buffer_decrypt(&mut self, block: &[u8], output: &mut [u8]) { + debug_assert!(block.len() >= RATE); + debug_assert!(output.len() >= RATE); + + let t0 = load_u64_le(block, 0); + store_u64_le(output, 0, self.s0 ^ t0); + self.s0 = t0; + + let t1 = load_u64_le(block, 8); + store_u64_le(output, 8, self.s1 ^ t1); + self.s1 = t1; + + self.p8(); + } + + fn process_final_encrypt_64(input: &[u8], output: &mut [u8], s: &mut u64) { + debug_assert!((1..8).contains(&input.len())); + debug_assert!(output.len() >= input.len()); + let mut t = 0u64; + for (i, &b) in input.iter().enumerate() { + t |= (b as u64) << (i * 8); + } + *s ^= t; + let s_bytes = s.to_le_bytes(); + output[..input.len()].copy_from_slice(&s_bytes[..input.len()]); + } + + fn process_final_encrypt(&mut self, input: &[u8], output: &mut [u8]) { + debug_assert!(input.len() < RATE); + + if input.len() >= 8 { + self.s0 ^= load_u64_le(input, 0); + store_u64_le(output, 0, self.s0); + + let input = &input[8..]; + if !input.is_empty() { + Self::process_final_encrypt_64(input, &mut output[8..], &mut self.s1); + } + + self.s1 ^= Self::pad(input.len()); + } else { + if !input.is_empty() { + Self::process_final_encrypt_64(input, output, &mut self.s0); + } + + self.s0 ^= Self::pad(input.len()); + } + self.finish_data(State::EncFinal); + } + + fn process_final_decrypt_64(input: &[u8], output: &mut [u8], s: &mut u64) { + debug_assert!((1..8).contains(&input.len())); + debug_assert!(output.len() >= input.len()); + let mut t = 0u64; + for (i, &b) in input.iter().enumerate() { + t |= (b as u64) << (i * 8); + } + let res = *s ^ t; + let res_bytes = res.to_le_bytes(); + output[..input.len()].copy_from_slice(&res_bytes[..input.len()]); + *s = (*s & (u64::MAX << (input.len() * 8))) ^ t; + } + + fn process_final_decrypt(&mut self, input: &[u8], output: &mut [u8]) { + debug_assert!(input.len() < RATE); + + if input.len() >= 8 { + let t0 = load_u64_le(input, 0); + store_u64_le(output, 0, self.s0 ^ t0); + self.s0 = t0; + + let input = &input[8..]; + if !input.is_empty() { + Self::process_final_decrypt_64(input, &mut output[8..], &mut self.s1); + } + + self.s1 ^= Self::pad(input.len()); + } else { + if !input.is_empty() { + Self::process_final_decrypt_64(input, output, &mut self.s0); + } + + self.s0 ^= Self::pad(input.len()); + } + self.finish_data(State::DecFinal); + } + + /// Process plaintext bytes (encryption update). + /// Returns the number of output (ciphertext) bytes produced. + pub fn encrypt_update(&mut self, plaintext: &[u8], output: &mut [u8]) -> usize { + if self.finished { + panic!("Ascon-AEAD128 cannot be reused after finish"); + } + if !self.for_encryption { + panic!("Not initialized for encryption"); + } + if !matches!(self.state, State::EncData) { + self.check_data(); + } + + let mut in_off = 0; + let mut len = plaintext.len(); + let mut out_off = 0; + if self.buf_pos > 0 { + let available = RATE - self.buf_pos; + if len < available { + self.buf[self.buf_pos..self.buf_pos + len].copy_from_slice(plaintext); + self.buf_pos += len; + return 0; + } + self.buf[self.buf_pos..RATE].copy_from_slice(&plaintext[..available]); + in_off += available; + len -= available; + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_buffer_encrypt(&tmp, &mut output[out_off..out_off + RATE]); + out_off += RATE; + self.buf_pos = 0; + } + while len >= RATE { + self.process_buffer_encrypt( + &plaintext[in_off..in_off + RATE], + &mut output[out_off..out_off + RATE], + ); + in_off += RATE; + len -= RATE; + out_off += RATE; + } + if len > 0 { + self.buf[0..len].copy_from_slice(&plaintext[in_off..in_off + len]); + self.buf_pos = len; + } + out_off + } + + /// Finalize encryption and output the last (possibly partial) ciphertext block followed by the + /// 128-bit tag. Returns the number of bytes written. + pub fn encrypt_finalize(&mut self, output: &mut [u8]) -> usize { + if self.finished { + panic!("Ascon-AEAD128 cannot be reused after finish"); + } + if !self.for_encryption { + panic!("Not initialized for encryption"); + } + if !matches!(self.state, State::EncData) { + self.check_data(); + } + let in_len = self.buf_pos; + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_final_encrypt(&tmp[..in_len], output); + + let mut tag = [0u8; CRYPTO_ABYTES]; + store_u64_le(&mut tag, 0, self.s3); + store_u64_le(&mut tag, 8, self.s4); + + output[in_len..in_len + CRYPTO_ABYTES].copy_from_slice(&tag); + self.mac = Some(tag); + self.finished = true; + in_len + CRYPTO_ABYTES + } + + /// Process ciphertext bytes (decryption update). Returns the number of plaintext bytes produced. + #[allow(clippy::assertions_on_constants)] + pub fn decrypt_update(&mut self, ciphertext: &[u8], output: &mut [u8]) -> usize { + if self.finished { + panic!("Ascon-AEAD128 cannot be reused after finish"); + } + if self.for_encryption { + panic!("Not initialized for decryption"); + } + if !matches!(self.state, State::DecData) { + self.check_data(); + } + + let mut len = ciphertext.len(); + let mut out_off = 0; + let available = BUF_SIZE_DECRYPT - self.buf_pos; + if len < available { + self.buf[self.buf_pos..self.buf_pos + len].copy_from_slice(ciphertext); + self.buf_pos += len; + return 0; + } + + debug_assert!(RATE >= CRYPTO_ABYTES); + if self.buf_pos >= RATE { + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_buffer_decrypt(&tmp, &mut output[..RATE]); + out_off += RATE; + + self.buf_pos -= RATE; + let (head, tail) = self.buf.split_at_mut(RATE); + head[..self.buf_pos].copy_from_slice(&tail[..self.buf_pos]); + + let available = BUF_SIZE_DECRYPT - self.buf_pos; + if len < available { + self.buf[self.buf_pos..self.buf_pos + len].copy_from_slice(ciphertext); + self.buf_pos += len; + return out_off; + } + } + + let fill = RATE - self.buf_pos; + self.buf[self.buf_pos..RATE].copy_from_slice(&ciphertext[..fill]); + let mut in_off = fill; + len -= fill; + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_buffer_decrypt(&tmp, &mut output[out_off..out_off + RATE]); + out_off += RATE; + + while len >= BUF_SIZE_DECRYPT { + self.process_buffer_decrypt( + &ciphertext[in_off..in_off + RATE], + &mut output[out_off..out_off + RATE], + ); + in_off += RATE; + len -= RATE; + out_off += RATE; + } + + self.buf[..len].copy_from_slice(&ciphertext[in_off..in_off + len]); + self.buf_pos = len; + out_off + } + + /// Finalize decryption, verifying the tag. + /// Returns the number of plaintext bytes produced, or [`AeadError::AuthenticationFailed`] if the + /// tag does not verify. + pub fn decrypt_finalize(&mut self, output: &mut [u8]) -> Result { + if self.finished { + return Err(AeadError::InvalidState("Ascon-AEAD128 already finalized")); + } + if self.for_encryption { + return Err(AeadError::InvalidState("Not initialized for decryption")); + } + if self.buf_pos < CRYPTO_ABYTES { + return Err(AeadError::InvalidLength("Ascon-AEAD128 ciphertext shorter than tag")); + } + + let data_len = self.buf_pos - CRYPTO_ABYTES; + let mut tmp = [0u8; RATE]; + tmp.copy_from_slice(&self.buf[..RATE]); + self.process_final_decrypt(&tmp[..data_len], output); + + // Recompute the tag in the state and compare against the supplied tag. XORing the supplied + // tag into the final state words yields zero iff they match; folding both words and testing + // against zero is branch-free (constant time w.r.t. the tag value). + self.s3 ^= load_u64_le(&self.buf, data_len); + self.s4 ^= load_u64_le(&self.buf, data_len + 8); + if (self.s3 | self.s4) != 0 { + return Err(AeadError::AuthenticationFailed); + } + self.finished = true; + Ok(data_len) + } +} + +impl Algorithm for AsconAead128 { + const ALG_NAME: &'static str = "Ascon-AEAD128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl AeadCipher for AsconAead128 { + fn process_aad_byte(&mut self, input: u8) { + self.process_aad_bytes(&[input]); + } + + fn process_aad_bytes(&mut self, in_bytes: &[u8]) { + self.process_aad_bytes(in_bytes); + } + + fn process_byte(&mut self, input: u8, out_bytes: &mut [u8]) -> usize { + if self.for_encryption { + self.encrypt_update(&[input], out_bytes) + } else { + self.decrypt_update(&[input], out_bytes) + } + } + + fn process_bytes(&mut self, in_bytes: &[u8], out_bytes: &mut [u8]) -> usize { + if self.for_encryption { + self.encrypt_update(in_bytes, out_bytes) + } else { + self.decrypt_update(in_bytes, out_bytes) + } + } + + fn do_final(mut self, out_bytes: &mut [u8]) -> Result { + if self.for_encryption { + Ok(self.encrypt_finalize(out_bytes)) + } else { + self.decrypt_finalize(out_bytes) + } + } + + fn get_mac(&self) -> [u8; CRYPTO_ABYTES] { + self.mac.unwrap_or([0u8; CRYPTO_ABYTES]) + } + + fn get_update_output_size(&self, len: usize) -> usize { + match self.state { + State::EncFinal | State::DecFinal => 0, + State::EncInit | State::EncAad | State::EncData => ((self.buf_pos + len) / RATE) * RATE, + State::DecInit | State::DecAad | State::DecData => { + let total = self.buf_pos + len; + if total >= BUF_SIZE_DECRYPT { ((total - CRYPTO_ABYTES) / RATE) * RATE } else { 0 } + } + } + } + + fn get_output_size(&self, len: usize) -> usize { + match self.state { + State::EncFinal | State::DecFinal => 0, + State::EncInit | State::EncAad | State::EncData => self.buf_pos + len + CRYPTO_ABYTES, + State::DecInit | State::DecAad | State::DecData => { + (self.buf_pos + len).saturating_sub(CRYPTO_ABYTES) + } + } + } +} + +impl Debug for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +impl Display for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +// Zeroize the key, nonce, working state, and buffer before returning the memory to the OS. +impl Drop for AsconAead128 { + fn drop(&mut self) { + self.k0 = 0; + self.k1 = 0; + self.n0 = 0; + self.n1 = 0; + self.s0 = 0; + self.s1 = 0; + self.s2 = 0; + self.s3 = 0; + self.s4 = 0; + self.buf.fill(0); + self.mac = None; + } +} + +impl Secret for AsconAead128 {} diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs new file mode 100644 index 0000000..f8c4115 --- /dev/null +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -0,0 +1,301 @@ +//! Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +//! +//! A variant of Ascon-XOF128 that first absorbs a user-supplied customization string `Z` +//! (length-prefixed per SP 800-232 Alg. 7) to provide domain separation. Same sponge parameters as +//! Ascon-XOF128 (rate = 64 bits, capacity = 256 bits, `Ascon-p[12]`). + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, XOF}; + +use crate::util::{load_u64_le, store_u64_le}; + +const RATE: usize = 8; + +/// Maximum customization-string length in bytes (2048 bits, per SP 800-232 §5.3). +const MAX_CUSTOMIZATION_BYTES: usize = 256; + +/// Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +pub struct AsconCXof128 { + s0: u64, + s1: u64, + s2: u64, + s3: u64, + s4: u64, + + buf: [u8; RATE], + buf_pos: usize, + squeezing: bool, +} + +impl AsconCXof128 { + /// Create a new Ascon-CXOF128 instance with no customization string. + pub fn new() -> Self { + Self::with_customization(&[]) + } + + /// Create a new Ascon-CXOF128 instance with the given customization string `z`. + /// `z` must be at most 256 bytes (2048 bits). + pub fn with_customization(z: &[u8]) -> Self { + if z.len() > MAX_CUSTOMIZATION_BYTES { + panic!("Ascon-CXOF128 customization string exceeds 256 bytes"); + } + + let mut st = if z.is_empty() { + // Precomputed state after initialization + absorbing an empty (length-0) customization + // string and its padding block. + AsconCXof128 { + s0: 0x500CCCC894E3C9E8, + s1: 0x5BED06F28F71248D, + s2: 0x3B03A0F930AFD512, + s3: 0x112EF093AA5C698B, + s4: 0x00C8356340A347F0, + buf: [0u8; RATE], + buf_pos: 0, + squeezing: false, + } + } else { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + let mut st = AsconCXof128 { + s0: 0x675527C2A0E8DE03, + s1: 0x43D12D7DC0377BBC, + s2: 0xE9901DEC426E81B5, + s3: 0x2AB14907720780B6, + s4: 0x8F3F1D02D432BC46, + buf: [0u8; RATE], + buf_pos: 0, + squeezing: false, + }; + + // Z0 = int64(|Z|) in bits, then absorb the parsed/padded customization blocks + // (SP 800-232 Alg. 7, "Customization" loop). + let bit_length = (z.len() as u64) << 3; + st.s0 ^= bit_length; + st.p12(); + st.update(z); + st.pad_and_absorb(); + st.p12(); + st + }; + + // Customization is complete; reset the buffer to begin the message-absorb phase. + st.buf.fill(0); + st.buf_pos = 0; + st + } + + /// Absorb message input. Cannot be called once squeezing has begun. + pub fn update(&mut self, input: &[u8]) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } + + let mut input = input; + + if self.buf_pos > 0 { + self.buf[self.buf_pos..].copy_from_slice(&input[..available]); + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + input = &input[available..]; + } + + while input.len() >= RATE { + self.s0 ^= load_u64_le(input, 0); + self.p12(); + input = &input[RATE..]; + } + + self.buf[..input.len()].copy_from_slice(input); + self.buf_pos = input.len(); + } + + /// Absorb a single message byte. Cannot be called once squeezing has begun. + pub fn update_byte(&mut self, input: u8) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + self.buf[self.buf_pos] = input; + self.buf_pos += 1; + if self.buf_pos == RATE { + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + self.buf_pos = 0; + } + } + + /// Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + /// absorb phase by padding and absorbing the final block. Returns the number of bytes written. + pub fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let result = output.len(); + let mut output = output; + + if !self.squeezing { + self.pad_and_absorb(); + self.squeezing = true; + self.buf_pos = RATE; + } else if self.buf_pos < RATE { + let available = RATE - self.buf_pos; + if output.len() <= available { + let end_pos = self.buf_pos + output.len(); + output.copy_from_slice(&self.buf[self.buf_pos..end_pos]); + self.buf_pos = end_pos; + return result; + } + + output[..available].copy_from_slice(&self.buf[self.buf_pos..]); + output = &mut output[available..]; + self.buf_pos = RATE; + } + + while output.len() >= RATE { + self.p12(); + store_u64_le(output, 0, self.s0); + output = &mut output[RATE..]; + } + + if !output.is_empty() { + self.p12(); + self.buf = self.s0.to_le_bytes(); + output.copy_from_slice(&self.buf[..output.len()]); + self.buf_pos = output.len(); + } + + result + } + + // Pad the final absorbed block (SP 800-232 §5.2 step 3 / Alg. 7). + fn pad_and_absorb(&mut self) { + let final_bits = (self.buf_pos << 3) as u32; + let mask: u64 = if final_bits == 0 { + 0x00FFFFFFFFFFFFFF + } else { + 0x00FFFFFFFFFFFFFF >> (56 - final_bits) + }; + + let mut block_val = 0u64; + if self.buf_pos > 0 { + let mut temp = [0u8; RATE]; + for (i, t) in temp.iter_mut().enumerate() { + *t = if i < self.buf_pos { self.buf[i] } else { 0 }; + } + block_val = u64::from_le_bytes(temp); + } + + self.s0 ^= block_val & mask; + self.s0 ^= 0x01u64 << final_bits; + } + + fn p12(&mut self) { + self.round(0xF0); + self.round(0xE1); + self.round(0xD2); + self.round(0xC3); + self.round(0xB4); + self.round(0xA5); + self.round(0x96); + self.round(0x87); + self.round(0x78); + self.round(0x69); + self.round(0x5A); + self.round(0x4B); + } + + // One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4). + #[inline(always)] + fn round(&mut self, c: u64) { + let sx = self.s2 ^ c; + + let t0 = self.s0 ^ self.s1 ^ sx ^ self.s3 ^ (self.s1 & (self.s0 ^ sx ^ self.s4)); + let t1 = self.s0 ^ sx ^ self.s3 ^ self.s4 ^ ((self.s1 ^ sx) & (self.s1 ^ self.s3)); + let t2 = self.s1 ^ sx ^ self.s4 ^ (self.s3 & self.s4); + let t3 = self.s0 ^ self.s1 ^ sx ^ ((!self.s0) & (self.s3 ^ self.s4)); + let t4 = self.s1 ^ self.s3 ^ self.s4 ^ ((self.s0 ^ self.s4) & self.s1); + + self.s0 = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + self.s1 = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + self.s2 = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + self.s3 = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + self.s4 = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); + } +} + +impl Default for AsconCXof128 { + fn default() -> Self { + Self::new() + } +} + +// Zeroize the working state and buffer before returning the memory to the OS. +impl Drop for AsconCXof128 { + fn drop(&mut self) { + self.buf.fill(0); + self.s0 = 0; + self.s1 = 0; + self.s2 = 0; + self.s3 = 0; + self.s4 = 0; + } +} + +impl Algorithm for AsconCXof128 { + const ALG_NAME: &'static str = "Ascon-CXOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconCXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.update(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.update(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) { + self.update(data); + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs new file mode 100644 index 0000000..f87a3e4 --- /dev/null +++ b/crypto/ascon/src/ascon_hash256.rs @@ -0,0 +1,245 @@ +//! Ascon-Hash256 cryptographic hash (NIST SP 800-232 §5.1), producing a 256-bit digest. +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength}; + +use crate::util::load_u64_le; + +const RATE: usize = 8; +const DIGEST_BYTES: usize = 32; + +/// Ascon-Hash256 hash function (NIST SP 800-232 §5.1), producing a 256-bit digest. +#[derive(Clone)] +pub struct AsconHash256 { + buf: [u8; RATE], + buf_pos: usize, + s0: u64, + s1: u64, + s2: u64, + s3: u64, + s4: u64, +} + +impl AsconHash256 { + /// Creates a new AsconHash256 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + buf: [0u8; RATE], + buf_pos: 0, + s0: 0x9B1E_5494_E934_D681, + s1: 0x4BC3_A01E_3337_51D2, + s2: 0xAE65_396C_6B34_B81A, + s3: 0x3C7F_D4A4_D56A_4DB3, + s4: 0x1A5C_4649_06C5_976D, + } + } + + /// Returns the digest size in bytes (32). + pub fn digest_size() -> usize { + DIGEST_BYTES + } + + /// One-shot hash of `data`, returning the 32-byte digest. + pub fn digest(data: &[u8]) -> [u8; DIGEST_BYTES] { + let mut hasher = Self::new(); + hasher.update_bytes(data); + let mut out = [0u8; DIGEST_BYTES]; + hasher.squeeze_into(&mut out); + out + } + + /// Updates the hasher with a single byte. + pub fn update(&mut self, input: u8) { + self.buf[self.buf_pos] = input; + self.buf_pos += 1; + if self.buf_pos == RATE { + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + self.buf_pos = 0; + } + } + + /// Updates the hasher with the given input data (streaming absorb). + pub fn update_bytes(&mut self, input: &[u8]) { + if input.is_empty() { + return; + } + + let mut in_pos = 0; + + if self.buf_pos > 0 { + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } else { + self.buf[self.buf_pos..].copy_from_slice(&input[..available]); + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + self.buf_pos = 0; + in_pos += available; + } + } + + while input.len() - in_pos >= RATE { + self.s0 ^= load_u64_le(input, in_pos); + self.p12(); + in_pos += RATE; + } + + let remaining = input.len() - in_pos; + self.buf[..remaining].copy_from_slice(&input[in_pos..]); + self.buf_pos = remaining; + } + + /// Finalizes the hasher, consuming it and writing the 32-byte digest into `output`. + pub fn do_final_into(mut self, output: &mut [u8; DIGEST_BYTES]) { + self.squeeze_into(output); + } + + // Pad, absorb the final block, and squeeze the four 64-bit digest blocks (SP 800-232 Alg. 5). + fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { + self.pad_and_absorb(); + + output[0..8].copy_from_slice(&self.s0.to_le_bytes()); + for i in 1..4 { + self.p12(); + output[i * 8..(i + 1) * 8].copy_from_slice(&self.s0.to_le_bytes()); + } + } + + fn pad_and_absorb(&mut self) { + let final_bits = self.buf_pos << 3; + let x = u64::from_le_bytes(self.buf); + let mask = + if final_bits == 0 { 0u64 } else { 0x00FF_FFFF_FFFF_FFFF_u64 >> (56 - final_bits) }; + self.s0 ^= x & mask; + self.s0 ^= 0x01u64 << final_bits; + + self.p12(); + } + + fn p12(&mut self) { + self.round(0xF0); + self.round(0xE1); + self.round(0xD2); + self.round(0xC3); + self.round(0xB4); + self.round(0xA5); + self.round(0x96); + self.round(0x87); + self.round(0x78); + self.round(0x69); + self.round(0x5A); + self.round(0x4B); + } + + // One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4). + #[inline(always)] + fn round(&mut self, c: u64) { + let sx = self.s2 ^ c; + let t0 = self.s0 ^ self.s1 ^ sx ^ self.s3 ^ (self.s1 & (self.s0 ^ sx ^ self.s4)); + let t1 = self.s0 ^ sx ^ self.s3 ^ self.s4 ^ ((self.s1 ^ sx) & (self.s1 ^ self.s3)); + let t2 = self.s1 ^ sx ^ self.s4 ^ (self.s3 & self.s4); + let t3 = self.s0 ^ self.s1 ^ sx ^ (!self.s0 & (self.s3 ^ self.s4)); + let t4 = self.s1 ^ self.s3 ^ self.s4 ^ ((self.s0 ^ self.s4) & self.s1); + + self.s0 = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + self.s1 = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + self.s2 = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + self.s3 = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + self.s4 = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); + } +} + +impl Default for AsconHash256 { + fn default() -> Self { + Self::new() + } +} + +// Zeroize the working state and buffer before returning the memory to the OS. +impl Drop for AsconHash256 { + fn drop(&mut self) { + self.buf.fill(0); + self.s0 = 0; + self.s1 = 0; + self.s2 = 0; + self.s3 = 0; + self.s4 = 0; + } +} + +impl Algorithm for AsconHash256 { + const ALG_NAME: &'static str = "Ascon-Hash256"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Hash for AsconHash256 { + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + fn output_len(&self) -> usize { + DIGEST_BYTES + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.update_bytes(data); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.update_bytes(data); + output[..DIGEST_BYTES].fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + output[..DIGEST_BYTES].copy_from_slice(&out); + DIGEST_BYTES + } + + fn do_update(&mut self, data: &[u8]) { + self.update_bytes(data); + } + + fn do_final(mut self) -> Vec { + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + output[..DIGEST_BYTES].fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + output[..DIGEST_BYTES].copy_from_slice(&out); + DIGEST_BYTES + } + + fn do_final_partial_bits( + self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result, HashError> { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + _num_partial_bits: usize, + _output: &mut [u8], + ) -> Result { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs new file mode 100644 index 0000000..387f8d2 --- /dev/null +++ b/crypto/ascon/src/ascon_xof128.rs @@ -0,0 +1,256 @@ +//! Ascon-XOF128 extendable-output function (NIST SP 800-232 §5.2). +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. Supports the streaming +//! absorb/squeeze API of SP 800-232 §5.4 (squeeze may be called repeatedly). + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, XOF}; + +use crate::util::{load_u64_le, store_u64_le}; + +const RATE: usize = 8; + +/// Ascon-XOF128 as specified in NIST SP 800-232. +pub struct AsconXof128 { + s0: u64, + s1: u64, + s2: u64, + s3: u64, + s4: u64, + buf: [u8; RATE], + buf_pos: usize, + squeezing: bool, +} + +impl AsconXof128 { + /// Creates a new Ascon-XOF128 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + s0: 0xDA82CE768D9447EB, + s1: 0xCC7CE6C75F1EF969, + s2: 0xE7508FD780085631, + s3: 0x0EE0EA53416B58CC, + s4: 0xE0547524DB6F0BDE, + buf: [0u8; RATE], + buf_pos: 0, + squeezing: false, + } + } + + /// Absorb input data. Cannot be called once squeezing has begun. + pub fn update(&mut self, input: &[u8]) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } + + let mut input = input; + + if self.buf_pos > 0 { + self.buf[self.buf_pos..].copy_from_slice(&input[..available]); + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + input = &input[available..]; + } + + while input.len() >= RATE { + self.s0 ^= load_u64_le(input, 0); + self.p12(); + input = &input[RATE..]; + } + + self.buf[..input.len()].copy_from_slice(input); + self.buf_pos = input.len(); + } + + /// Absorb a single byte. Cannot be called once squeezing has begun. + pub fn update_byte(&mut self, input: u8) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + self.buf[self.buf_pos] = input; + self.buf_pos += 1; + if self.buf_pos == RATE { + self.s0 ^= u64::from_le_bytes(self.buf); + self.p12(); + self.buf_pos = 0; + } + } + + /// Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + /// absorb phase by padding and absorbing the final block. Returns the number of bytes written. + pub fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let result = output.len(); + let mut output = output; + + if !self.squeezing { + self.pad_and_absorb(); + self.squeezing = true; + self.buf_pos = RATE; + } else if self.buf_pos < RATE { + let available = RATE - self.buf_pos; + if output.len() <= available { + let end_pos = self.buf_pos + output.len(); + output.copy_from_slice(&self.buf[self.buf_pos..end_pos]); + self.buf_pos = end_pos; + return result; + } + + output[..available].copy_from_slice(&self.buf[self.buf_pos..]); + output = &mut output[available..]; + self.buf_pos = RATE; + } + + while output.len() >= RATE { + self.p12(); + store_u64_le(output, 0, self.s0); + output = &mut output[RATE..]; + } + + if !output.is_empty() { + self.p12(); + self.buf = self.s0.to_le_bytes(); + output.copy_from_slice(&self.buf[..output.len()]); + self.buf_pos = output.len(); + } + + result + } + + // Pad the final absorbed block (SP 800-232 §5.2 step 3 / Alg. 6). + fn pad_and_absorb(&mut self) { + let final_bits = (self.buf_pos << 3) as u32; + let mask: u64 = if final_bits == 0 { + 0x00FFFFFFFFFFFFFF + } else { + 0x00FFFFFFFFFFFFFF >> (56 - final_bits) + }; + + let mut block_val = 0u64; + if self.buf_pos > 0 { + let mut temp = [0u8; RATE]; + for (i, t) in temp.iter_mut().enumerate() { + *t = if i < self.buf_pos { self.buf[i] } else { 0 }; + } + block_val = u64::from_le_bytes(temp); + } + + self.s0 ^= block_val & mask; + self.s0 ^= 0x01u64 << final_bits; + } + + fn p12(&mut self) { + self.round(0xF0); + self.round(0xE1); + self.round(0xD2); + self.round(0xC3); + self.round(0xB4); + self.round(0xA5); + self.round(0x96); + self.round(0x87); + self.round(0x78); + self.round(0x69); + self.round(0x5A); + self.round(0x4B); + } + + // One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4). + #[inline(always)] + fn round(&mut self, c: u64) { + let sx = self.s2 ^ c; + + let t0 = self.s0 ^ self.s1 ^ sx ^ self.s3 ^ (self.s1 & (self.s0 ^ sx ^ self.s4)); + let t1 = self.s0 ^ sx ^ self.s3 ^ self.s4 ^ ((self.s1 ^ sx) & (self.s1 ^ self.s3)); + let t2 = self.s1 ^ sx ^ self.s4 ^ (self.s3 & self.s4); + let t3 = self.s0 ^ self.s1 ^ sx ^ ((!self.s0) & (self.s3 ^ self.s4)); + let t4 = self.s1 ^ self.s3 ^ self.s4 ^ ((self.s0 ^ self.s4) & self.s1); + + self.s0 = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + self.s1 = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + self.s2 = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + self.s3 = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + self.s4 = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); + } +} + +impl Default for AsconXof128 { + fn default() -> Self { + Self::new() + } +} + +// Zeroize the working state and buffer before returning the memory to the OS. +impl Drop for AsconXof128 { + fn drop(&mut self) { + self.buf.fill(0); + self.s0 = 0; + self.s1 = 0; + self.s2 = 0; + self.s3 = 0; + self.s4 = 0; + } +} + +impl Algorithm for AsconXof128 { + const ALG_NAME: &'static str = "Ascon-XOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.update(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.update(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) { + self.update(data); + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs new file mode 100644 index 0000000..b6868bd --- /dev/null +++ b/crypto/ascon/src/lib.rs @@ -0,0 +1,100 @@ +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +//! Ascon-based lightweight cryptography (NIST SP 800-232). +//! +//! This crate implements the four Ascon functions standardized in NIST SP 800-232 (August 2025): +//! +//! - [`ascon_aead128::AsconAead128`] — Ascon-AEAD128 authenticated encryption (128-bit +//! key/nonce/tag, 128-bit single-key security). +//! - [`ascon_hash256::AsconHash256`] — Ascon-Hash256 hash function (256-bit digest, 128-bit +//! security). +//! - [`ascon_xof128::AsconXof128`] — Ascon-XOF128 extendable-output function. +//! - [`ascon_cxof128::AsconCXof128`] — Ascon-CXOF128 customized extendable-output function. +//! +//! All four share the same `Ascon-p` permutation. The implementation follows the little-endian +//! convention of the standard and uses the precomputed initialization states from SP 800-232 +//! Table 12. +//! +//! # Usage Examples +//! +//! Hashing (one-shot and streaming): +//! ``` +//! use bouncycastle_ascon::ascon_hash256::AsconHash256; +//! +//! // One-shot: +//! let digest = AsconHash256::digest(b"hello world"); +//! assert_eq!(digest.len(), 32); +//! +//! // Streaming: +//! let mut h = AsconHash256::new(); +//! h.update_bytes(b"hello "); +//! h.update_bytes(b"world"); +//! let mut out = [0u8; 32]; +//! h.do_final_into(&mut out); +//! assert_eq!(out, digest); +//! ``` +//! +//! Authenticated encryption (one-shot): +//! ``` +//! use bouncycastle_ascon::ascon_aead128::AsconAead128; +//! +//! let key = [0u8; 16]; +//! let nonce = [1u8; 16]; // MUST be unique per encryption under a given key +//! let ad = b"associated data"; +//! let plaintext = b"secret message"; +//! +//! let mut ct = vec![0u8; plaintext.len() + 16]; // ciphertext || 16-byte tag +//! let n = AsconAead128::encrypt(&key, &nonce, Some(ad), plaintext, &mut ct); +//! ct.truncate(n); +//! +//! let mut pt = vec![0u8; ct.len() - 16]; +//! let m = AsconAead128::decrypt(&key, &nonce, Some(ad), &ct, &mut pt).unwrap(); +//! pt.truncate(m); +//! assert_eq!(&pt, plaintext); +//! ``` +//! +//! Extendable output: +//! ``` +//! use bouncycastle_ascon::ascon_xof128::AsconXof128; +//! use bouncycastle_core::traits::XOF; +//! +//! let out = AsconXof128::new().hash_xof(b"input", 64); +//! assert_eq!(out.len(), 64); +//! ``` +//! +//! # Memory Usage +//! +//! Ascon is a lightweight, permutation-based design intended for constrained devices. The internal +//! permutation state is 320 bits (40 bytes), held as five `u64` words. Each function additionally +//! keeps a small fixed input buffer (8 bytes for the hash/XOFs, 32 bytes for AEAD decryption). There +//! are no heap allocations in the streaming/`*_out` APIs, and stack usage is small and constant; +//! consequently this crate has no dedicated `mem_usage_benches` harness. +//! +//! # Security Considerations +//! +//! - **Nonce uniqueness (SP 800-232 R3):** a (key, nonce) pair must never be reused for two +//! different Ascon-AEAD128 encryptions. Nonce reuse breaks confidentiality. +//! - **Tag length:** this crate always produces and verifies the full 128-bit tag. Truncated tags +//! (SP 800-232 §4.2.1) are not exposed. +//! - **Key handling:** [`ascon_aead128::AsconAead128`] zeroizes its key, nonce, and working state on +//! drop and implements [`bouncycastle_core::traits::Secret`]; the hash/XOF states are likewise +//! zeroized on drop. +//! - **Decryption release:** never release decrypted plaintext until finalization returns `Ok`; an +//! `Err(AuthenticationFailed)` means the ciphertext or tag was tampered with. + +mod util; + +pub mod ascon_aead128; +pub mod ascon_cxof128; +pub mod ascon_hash256; +pub mod ascon_xof128; + +/// Algorithm name for Ascon-AEAD128. +pub const ASCON_AEAD128_NAME: &str = "Ascon-AEAD128"; +/// Algorithm name for Ascon-Hash256. +pub const ASCON_HASH256_NAME: &str = "Ascon-Hash256"; +/// Algorithm name for Ascon-XOF128. +pub const ASCON_XOF128_NAME: &str = "Ascon-XOF128"; +/// Algorithm name for Ascon-CXOF128. +pub const ASCON_CXOF128_NAME: &str = "Ascon-CXOF128"; diff --git a/crypto/ascon/src/util.rs b/crypto/ascon/src/util.rs new file mode 100644 index 0000000..87a1500 --- /dev/null +++ b/crypto/ascon/src/util.rs @@ -0,0 +1,20 @@ +//! Internal little-endian load/store helpers. +//! +//! These replace the external `arrayref` crate so that this crate carries no third-party runtime +//! dependencies (per the project's QUALITY_AND_STYLE rules). All callers pass slices that are at +//! least 8 bytes long at the given offset, so `copy_from_slice` is infallible by construction and +//! no fallible conversion is involved. + +/// Load the 8 bytes at `src[off..off + 8]` as a little-endian `u64`. +#[inline(always)] +pub(crate) fn load_u64_le(src: &[u8], off: usize) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&src[off..off + 8]); + u64::from_le_bytes(b) +} + +/// Store `val` as little-endian into `dst[off..off + 8]`. +#[inline(always)] +pub(crate) fn store_u64_le(dst: &mut [u8], off: usize, val: u64) { + dst[off..off + 8].copy_from_slice(&val.to_le_bytes()); +} diff --git a/crypto/ascon/summary.md b/crypto/ascon/summary.md new file mode 100644 index 0000000..c107807 --- /dev/null +++ b/crypto/ascon/summary.md @@ -0,0 +1,262 @@ +# ASCON Implementation & Testing — Work Summary + +NIST SP 800-232 (August 2025) Ascon family for the `bc-rust` workspace. +Branch: `feature/officialfrancismendoza/15-ASCON`. **Nothing is committed** — all +changes are in the working tree. + +--- + +## 1. Overview + +Starting point: `crypto/ascon/` contained a drafted (but non-compiling) +implementation of all four Ascon functions. The work delivered a fully-fledged, +house-style-compliant primitive crate and a comprehensive test suite. + +Functions implemented (all share the `Ascon-p` permutation, little-endian, with +precomputed init states from SP 800-232 Table 12): + +- **Ascon-AEAD128** — authenticated encryption (128-bit key/nonce/tag). +- **Ascon-Hash256** — 256-bit hash. +- **Ascon-XOF128** — extendable-output function. +- **Ascon-CXOF128** — customized XOF. + +--- + +## 2. Core crate changes (`crypto/core`) + +### New `AeadError` enum — `crypto/core/src/errors.rs` +``` +AuthenticationFailed | InvalidLength(&'static str) | +InvalidState(&'static str) | GenericError(&'static str) | +KeyMaterialError(KeyMaterialError) +``` +Plus `impl From for AeadError`. + +### New `AeadCipher` trait — `crypto/core/src/traits.rs` +Fully documented (the `core` crate is `#![forbid(missing_docs)]`). Final shape: +```rust +pub trait AeadCipher { + fn process_aad_byte(&mut self, input: u8); + fn process_aad_bytes(&mut self, in_bytes: &[u8]); + fn process_byte(&mut self, input: u8, out_bytes: &mut [u8]) -> usize; + fn process_bytes(&mut self, in_bytes: &[u8], out_bytes: &mut [u8]) -> usize; + fn do_final(self, out_bytes: &mut [u8]) -> Result; + fn get_mac(&self) -> [u8; 16]; + fn get_update_output_size(&self, len: usize) -> usize; + fn get_output_size(&self, len: usize) -> usize; +} +``` +Decisions vs. the originally-proposed signature: +- `get_mac` returns `[u8; 16]` (not `Vec`) — per the Vec→array directive; Ascon + tags are always 128-bit. +- `do_final` returns `Result` (not `()`/panic) so a failed tag + check on attacker-controlled input is a recoverable error, never a DoS panic. +- Streaming `process_*` outputs remain `&mut [u8]` (output length is input-dependent). + +--- + +## 3. ASCON crate changes (`crypto/ascon`) + +### Files +- `src/lib.rs` — `#![forbid(unsafe_code)]` + `#![forbid(missing_docs)]`; crate docs + with the mandated **Usage Examples / Memory Usage / Security Considerations** + sections; `ASCON_*_NAME` constants; private `mod util`. +- `src/util.rs` — NEW. Dependency-free little-endian `load_u64_le`/`store_u64_le` + (using `copy_from_slice`), replacing the external `arrayref` crate. **Zero + `unwrap()`/`Result`** in these helpers. +- `src/ascon_aead128.rs`, `src/ascon_hash256.rs`, `src/ascon_xof128.rs`, + `src/ascon_cxof128.rs` — brought to house style (see below). + +### Behavioral / API changes +- **Removed the external `arrayref` runtime dependency** (QUALITY rule: no + non-internal runtime deps). `Cargo.toml` now depends only on + `bouncycastle-core`; version bumped `0.0.0 → 0.1.2`; added the criterion bench. +- **AEAD secrecy hardening:** `AsconAead128` now takes `&[u8; 16]` for key/nonce + (compile-time length), implements a zeroizing `Drop` (clears key/nonce/state/ + buffer), implements `core::Secret`, and has redacted `Debug`/`Display` + (`"AsconAead128 (key/state masked)"`). Hash/XOF/CXOF states are also zeroized + on drop. +- **AEAD one-shot statics:** `AsconAead128::encrypt(...) -> usize` and + `decrypt(...) -> Result`. +- **`decrypt_finalize`** now returns `Result` (was + `Result<_, &'static str>`); tag check is the branch-free `(s3 | s4) != 0`. +- **House-style API:** removed `reset()` (forbidden by QUALITY) and the + reset-on-finalize behavior. Hash uses consume-self `do_final_into(self, &mut + [u8; 32])` + one-shot `digest(&[u8]) -> [u8; 32]`. XOF/CXOF keep the + absorb/`squeeze_into` model (matches the `XOF` trait + SP 800-232 §5.4); the + CXOF post-customization state cache and the unused `Uninitialized` AEAD state + were removed as dead code. +- The shared `core::Hash` / `core::XOF` trait impls **keep `Vec`** returns — + those traits are shared with sha2/sha3/factory and are not ours to change; the + Vec→array conversion applies only to ASCON-specific inherent methods. +- Spec-referencing comments added throughout (round function §3.2–3.4, IV, + padding, domain separation, finalization). + +--- + +## 4. Wiring / registration + +- Root `Cargo.toml`: `bouncycastle-ascon` added to `[workspace.dependencies]` + and to the umbrella `[dependencies]`. +- `src/lib.rs` (umbrella `bouncycastle` crate): `pub use bouncycastle_ascon as ascon;`. +- Factory (`crypto/factory`): + - `HashFactory` ← `Ascon-Hash256` (new variant + name arm + all 10 trait-method + delegations). + - `XOFFactory` ← `Ascon-XOF128` (new variant + name arm + all 9 delegations). + - `Ascon-CXOF128` and `Ascon-AEAD128` are intentionally NOT in the factories: + CXOF needs a customization string and AEAD needs key/nonce, neither of which + fits the `new(name)` factory signature (AEAD has no factory at all yet). + - `crypto/factory/Cargo.toml` gained the `bouncycastle-ascon` dep. +- CLI (`cli/`): new `src/ascon_cmd.rs` + four streaming subcommands registered in + `src/main.rs` (clap kebab-case): `ascon-hash256`, `ascon-xof128 `, + `ascon-cxof128 [--customization ]`, and + `ascon-aead128 --key/--key-file --nonce/--nonce-file [--ad] [--decrypt] [-x]`. + +--- + +## 5. Test suite (mirrors `crypto/mldsa/tests` — no in-crate `data/` folder) + +The crate no longer ships `tests/data/` (the 2.8 MB of `LWC_*.txt` was removed). +Large vector sweeps are read at test time from the externally-cloned +`bc-test-data` repo (graceful skip when absent); always-on correctness is held by +a small embedded vector set in each per-primitive file. The six test files are +each a self-contained integration-test crate: + +### Per-primitive files (always-on, no external repo) +`aead128_tests.rs`, `hash256_tests.rs`, `xof128_tests.rs`, `cxof128_tests.rs` +— each embeds ~5–8 NIST LWC known-answer vectors (`const` hex arrays copied from +bc-test-data, spanning empty / sub-block / exact-block / multi-block, plus AD or +customization variants) and the behavior/contract tests for that primitive: +- AEAD: embedded KAT, round-trips, streaming chunk-boundary equivalence (enc+dec), + chunked AAD (inherent + trait path), auth failures (wrong key/nonce/AD, flipped + tag/body, short→`InvalidLength`), determinism + nonce sensitivity, output-size + predictors (both directions), `get_mac`, masked `Debug`/`Display`, trait-method + AAD, and the `TestFrameworkAead` conformance run. +- Hash256: embedded KAT, streaming/byte-at-a-time equivalence, trait wrappers, + metadata accessors, unsupported-partial-op `Err`. +- XOF128 / CXOF128: embedded KAT, prefix property, chunked + byte-at-a-time absorb, + trait wrappers, unsupported-partial-op `Err`, absorb-after-squeeze panic guard; + CXOF128 also covers domain separation. + +40 ASCON tests total, all passing without any external repo. + +### `bc_test_data.rs` (full sweep) +Mirrors mldsa's `Once` + two-path resolution +(`../../../bc-test-data/crypto/ascon`, fallback `../bc-test-data/crypto/ascon`). +Reads the per-variant NIST files and runs the **full 4228-case sweep** +(`asconaead128/` 1089, `asconhash256/` 1025, `asconxof128/` 1025, +`asconcxof128/` 1089). Prints a warning and skips when bc-test-data is absent. + +### `wycheproof.rs` (skeleton; skips legacy) +Mirrors mldsa's path resolution + `serde_json` AEAD runner. wycheproof only ships +the **pre-NIST CAESAR** Ascon vectors (`ascon128/128a/80pq`), which are a different +algorithm from SP 800-232 `Ascon-AEAD128` and are intentionally NOT run. The test +requests a NIST-named file (`ascon_aead128_test.json`) that does not exist today, +so it skips with a clear message — and will run automatically if C2SP later +publishes NIST-compatible vectors under that name. + +### Trait conformance framework +`crypto/core-test-framework/src/aead.rs` provides the generic `TestFrameworkAead` +(encrypt→decrypt round-trip, byte-at-a-time vs one-shot equivalence, +tamper-the-tag → `Err(AuthenticationFailed)`), driven from `aead128_tests.rs`. + +### Factory tests +`crypto/factory/tests/hash_factory_tests.rs` and `xof_factory_tests.rs`: +`Ascon-Hash256`/`Ascon-XOF128` via factory match the direct impl (by literal name +and by name constant); unknown names → `FactoryError::UnsupportedAlgorithm`. + +### Doctests +3 crate-level doc examples (hash, AEAD one-shot, XOF) compile and pass. + +--- + +## 6. Verification results + +| Check | Result | +|-------|--------| +| `cargo build --workspace` | OK (no `arrayref`) | +| `cargo test -p bouncycastle-ascon` | KAT 4228 + 22 behavior + 5 (smoke/framework) + 3 doctests, all pass | +| `cargo test -p bouncycastle-factory` | OK incl. Ascon round-trips | +| `cargo test --workspace` | ~351 tests, no regressions | +| `cargo clippy -p bouncycastle-ascon --all-targets` | 0 warnings in ASCON code (fixed one collapsible-`if`) | +| `quality_stats.sh ./crypto/ascon` (fallibility) | 0 real `unwrap()`s in impl; 13 justified `Err()`s | +| `cargo bench -p bouncycastle-ascon` | runs (~117 MiB/s Hash256 on dev hardware) | +| CLI smoke | Hash256/XOF128/CXOF128/AEAD empty vectors match KAT byte-for-byte; round-trip OK; tamper → non-zero exit | + +--- + +## 7. Mutation testing (`cargo mutants --package bouncycastle-ascon`) + +882 mutants generated. Progression as tests were added: + +| Run | caught | missed | unviable | timeout | +|-----|--------|--------|----------|---------| +| 1 (KAT + initial behavior + framework) | 723 | 128 | 29 | 2 | +| 2 (+ API-surface tests) | 792 | 58 | 30 | 2 | +| 3 (+ trait-AAD & bidirectional size-predictor tests) | **801** | **48** | 31 | 2 | + +**Final: ~94% of viable mutants killed.** Every high-value mutant is killed +(trait one-shot wrappers, metadata accessors, unsupported-op `Err` stubs, +`get_mac`, `update_byte`, size predictors). + +The **48 surviving mutants are all in the accepted class** (CLAUDE.md: "not all +need to die — e.g. XOR/OR equivalences in crypto code are acceptable"): +- **4** zeroizing `Drop → ()` — untestable in safe Rust (same as `sha3/keccak.rs`). +- **4** `match`-arm deletions — dead arms guarded by a preceding + `matches!(...)`/`finished` check (unreachable ⇒ equivalent). +- **~5** XOR/OR algebraic equivalences (`^`↔`|`/`&` on provably-zero bits; the + `s3 | s4` tag fold). +- **~35** comparison/arithmetic boundary mutants (`>`↔`>=`, `<`↔`<=`, `-`↔`+`) in + buffering/padding code that are equivalent for reachable buffer-fill states. + +The **2 "timeouts"** are mutations that cause hangs and are effectively detected, +not real survivors: +- `with_customization -> Default::default()` (infinite recursion: `Default`→`new`→`with_customization`). +- `+= → *=` on a loop counter in `update_bytes`. + +Full mutant lists are in `custom_mutants_output/mutants.out/` +(`caught.txt`, `missed.txt`, `timeout.txt`, `outcomes.json`). + +--- + +## 8. Unresolved issues, caveats & warnings + +- **`get_mac` is largely vestigial.** Because `AeadCipher::do_final` consumes + `self`, you cannot observe the tag via the trait after finalization (the tag is + already written to the output buffer). It is reachable only through the inherent + `encrypt_finalize` (`&mut self`) path. This matches the original author comment + that "tag re-exposure encourages misuse." Consider removing `get_mac` from the + trait in a future revision. +- **`AsconCXof128::with_customization` panics** on customization strings > 256 + bytes (SP 800-232 limit). This is a documented precondition, but it is a panic + on caller input; a future revision could make construction fallible + (`Result`) — deferred to avoid rippling `Result` through `new`/`Default`/factory. +- **`quality_stats.sh` line counts need `cloc` and `bc`**, which are not installed + on this machine (the fallibility metrics — the important part — do work). Run on + a host with those tools (or `choco install cloc`) for the LOC/docstring/test + ratios. +- **Tooling installed for this work:** `cargo-mutants` (27.1.0) was installed via + `cargo install`. It was not previously present. +- **Pre-existing clippy warnings** exist in *other* crates (`utils`, `hex`, + `core`, `core-test-framework`) — not introduced here and out of scope. +- **Truncated tags** (SP 800-232 §4.2.1) and **nonce masking** (§4.2.2) are not + implemented; this crate always uses the full 128-bit tag. +- **Nothing is committed.** All changes live in the working tree on the feature + branch. + +--- + +## 9. Reproduce + +```sh +cargo test -p bouncycastle-ascon # KAT + behavior + framework + doctests +cargo test -p bouncycastle-factory # Ascon factory round-trips +cargo test --workspace # full regression +cargo clippy -p bouncycastle-ascon --all-targets +cargo bench -p bouncycastle-ascon +cargo mutants --package bouncycastle-ascon -j 4 # ~13 min; output in custom_mutants_output/ + +# CLI (after `cargo build -p cli`); clap uses kebab-case: +printf '' | ./target/debug/bc-rust ascon-hash256 -x +# -> 0b3be5850f2f6b98caf29f8fdea89b64a1fa70aa249b8f839bd53baa304d92b2 +``` diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs new file mode 100644 index 0000000..27855dd --- /dev/null +++ b/crypto/ascon/tests/aead128_tests.rs @@ -0,0 +1,382 @@ +//! Ascon-AEAD128 tests (NIST SP 800-232). +//! +//! - A small embedded set of NIST LWC known-answer vectors (always-on correctness, no external +//! repo required). The full sweep lives in `bc_test_data.rs`. +//! - Behavioral / contract tests (round-trips, streaming chunk-boundary equivalence, authentication +//! failures, determinism, output-size predictors), and the shared `AeadCipher` conformance +//! framework. + +use bouncycastle_ascon::ascon_aead128::AsconAead128; +use bouncycastle_core::errors::AeadError; +use bouncycastle_core::traits::AeadCipher; +use bouncycastle_core_test_framework::aead::TestFrameworkAead; +use bouncycastle_hex as hex; + +// All embedded vectors use this fixed key/nonce (the NIST LWC KAT convention). +const KEY: [u8; 16] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +]; +const NONCE: [u8; 16] = [ + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, +]; + +const PT_SIZES: [usize; 10] = [0, 1, 15, 16, 17, 31, 32, 33, 64, 100]; +const CHUNK_SIZES: [usize; 6] = [1, 3, 7, 13, 16, 17]; + +/// Embedded NIST LWC Ascon-AEAD128 vectors `(plaintext, associated_data, ciphertext||tag)` in hex. +/// Key = Nonce = 000102…0F. Spans empty input, AD-only (incl. a full 32-byte AD block), partial PT +/// with AD, and a multi-block plaintext. (Counts 1, 2, 5, 33, 68, 69, 153, 1057 of +/// LWC_AEAD_KAT_128_128.txt.) +const AEAD_KAT: &[(&str, &str, &str)] = &[ + ("", "", "4427D64B8E1E1451FC445960F0839BB0"), + ("", "00", "103AB79D913A0321287715A979BB8585"), + ("", "00010203", "C6FF3CF70575B144B955820D9BC7685E"), + ( + "", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "22133A313FBF0B38029A45870AADC542", + ), + ("0001", "00", "25FB41D2732019820A0F8BAB4248B35E7B0B"), + ("0001", "0001", "49E57017A30E8073D1FA284AC8346110F89F"), + ( + "00010203", + "000102030405060708090A0B0C0D0E0F10111213", + "C305EB0E9A9A7833C5F6FB36BD82F1C78C322678", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "", + "E770D289D2A44AEE7CD0A48ECE5274E381BAD7E163DCC4970F7873610DEBBEB1A28657F6E82FE53D08B09EFF9330BD2B", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn ad_opt(ad: &[u8]) -> Option<&[u8]> { + if ad.is_empty() { None } else { Some(ad) } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { + let mut out = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(key, nonce, ad_opt(ad), pt, &mut out); + out.truncate(n); + out +} + +fn dec_oneshot( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], +) -> Result, AeadError> { + let mut out = vec![0u8; ct.len()]; + let n = AsconAead128::decrypt(key, nonce, ad_opt(ad), ct, &mut out)?; + out.truncate(n); + Ok(out) +} + +fn enc_chunked(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8], chunk: usize) -> Vec { + let mut cipher = AsconAead128::new(key, nonce, ad_opt(ad), true); + let mut out = vec![0u8; pt.len() + 16]; + let mut off = 0; + for ch in pt.chunks(chunk.max(1)) { + off += cipher.encrypt_update(ch, &mut out[off..]); + } + off += cipher.encrypt_finalize(&mut out[off..]); + out.truncate(off); + out +} + +fn dec_chunked( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], + chunk: usize, +) -> Result, AeadError> { + let mut cipher = AsconAead128::new(key, nonce, ad_opt(ad), false); + let mut out = vec![0u8; ct.len()]; + let mut off = 0; + for ch in ct.chunks(chunk.max(1)) { + off += cipher.decrypt_update(ch, &mut out[off..]); + } + off += cipher.decrypt_finalize(&mut out[off..])?; + out.truncate(off); + Ok(out) +} + +/* -------------------------------------------------------------------------- */ +/* Embedded known-answer vectors */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_embedded_kat() { + // The NIST LWC AEAD KAT convention uses Key == Nonce == 000102…0F (i.e. KEY for both). + let kat_nonce = KEY; + for (pt_hex, ad_hex, ct_hex) in AEAD_KAT { + let pt = dh(pt_hex); + let ad = dh(ad_hex); + let expected_ct = dh(ct_hex); + + let got_ct = enc_oneshot(&KEY, &kat_nonce, &ad, &pt); + assert_eq!(got_ct, expected_ct, "encrypt mismatch for PT={pt_hex} AD={ad_hex}"); + + let got_pt = + dec_oneshot(&KEY, &kat_nonce, &ad, &expected_ct).expect("decrypt should succeed"); + assert_eq!(got_pt, pt, "decrypt mismatch for CT={ct_hex}"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Round-trips and AAD handling */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_round_trip_sizes_and_ad() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + for ad in [Vec::new(), b"associated-data".to_vec(), pattern(40)] { + let ct = enc_oneshot(&KEY, &NONCE, &ad, &pt); + assert_eq!(ct.len(), pt_len + 16, "ciphertext = plaintext || 16-byte tag"); + let recovered = dec_oneshot(&KEY, &NONCE, &ad, &ct).expect("decrypt should succeed"); + assert_eq!(recovered, pt, "round-trip mismatch (pt_len={pt_len}, ad_len={})", ad.len()); + } + } +} + +#[test] +fn aead_aad_only_round_trip() { + // Empty plaintext, non-empty AD: ciphertext is just the 16-byte tag. + let ad = b"only-associated-data"; + let ct = enc_oneshot(&KEY, &NONCE, ad, b""); + assert_eq!(ct.len(), 16); + let recovered = dec_oneshot(&KEY, &NONCE, ad, &ct).expect("decrypt should succeed"); + assert!(recovered.is_empty()); +} + +/* -------------------------------------------------------------------------- */ +/* Streaming chunk-boundary equivalence */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_streaming_matches_one_shot() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + let ad = pattern(20); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + + for &chunk in CHUNK_SIZES.iter() { + let ct = enc_chunked(&KEY, &NONCE, &ad, &pt, chunk); + assert_eq!(ct, ct_ref, "chunked encrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + + let pt_back = dec_chunked(&KEY, &NONCE, &ad, &ct_ref, chunk) + .expect("chunked decrypt should pass"); + assert_eq!(pt_back, pt, "chunked decrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + } + } +} + +#[test] +fn aead_chunked_aad_matches_one_shot() { + let pt = pattern(30); + let ad = pattern(40); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + + for &chunk in CHUNK_SIZES.iter() { + let mut e = AsconAead128::new(&KEY, &NONCE, None, true); + for piece in ad.chunks(chunk) { + e.process_aad_bytes(piece); + } + let mut out = vec![0u8; pt.len() + 16]; + let n = e.encrypt_update(&pt, &mut out); + let m = e.encrypt_finalize(&mut out[n..]); + out.truncate(n + m); + assert_eq!(out, ct_ref, "chunked AAD mismatch (chunk={chunk})"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Authentication failures */ +/* -------------------------------------------------------------------------- */ + +fn assert_auth_failed(result: Result, AeadError>, ctx: &str) { + match result { + Err(AeadError::AuthenticationFailed) => {} + other => panic!("{ctx}: expected AuthenticationFailed, got {other:?}"), + } +} + +#[test] +fn aead_rejects_tampering() { + let pt = pattern(50); + let ad = b"the-aad"; + let ct = enc_oneshot(&KEY, &NONCE, ad, &pt); + + // Wrong key. + let mut bad_key = KEY; + bad_key[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&bad_key, &NONCE, ad, &ct), "wrong key"); + + // Wrong nonce. + let mut bad_nonce = NONCE; + bad_nonce[3] ^= 0x80; + assert_auth_failed(dec_oneshot(&KEY, &bad_nonce, ad, &ct), "wrong nonce"); + + // Modified associated data. + assert_auth_failed(dec_oneshot(&KEY, &NONCE, b"the-AAD", &ct), "modified ad"); + + // Flipped tag byte (last byte). + let mut tag_flip = ct.clone(); + let last = tag_flip.len() - 1; + tag_flip[last] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &tag_flip), "flipped tag"); + + // Flipped ciphertext body byte. + let mut body_flip = ct.clone(); + body_flip[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &body_flip), "flipped body"); +} + +#[test] +fn aead_short_ciphertext_is_length_error() { + let short = [0u8; 8]; // shorter than the 16-byte tag + let mut out = [0u8; 16]; + match AsconAead128::decrypt(&KEY, &NONCE, None, &short, &mut out) { + Err(AeadError::InvalidLength(_)) => {} + other => panic!("expected InvalidLength, got {other:?}"), + } +} + +/* -------------------------------------------------------------------------- */ +/* Determinism / nonce sensitivity / size predictors / get_mac / Debug mask */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_is_deterministic_and_nonce_sensitive() { + let pt = pattern(40); + let ad = b"ctx"; + let a = enc_oneshot(&KEY, &NONCE, ad, &pt); + let b = enc_oneshot(&KEY, &NONCE, ad, &pt); + assert_eq!(a, b, "same (key,nonce,ad,pt) must yield identical (ct,tag)"); + + let mut other_nonce = NONCE; + other_nonce[0] ^= 0x01; + let c = enc_oneshot(&KEY, &other_nonce, ad, &pt); + assert_ne!(a, c, "changing the nonce must change the ciphertext (SP 800-232 R3)"); +} + +#[test] +fn aead_output_size_predictors() { + for &pt_len in PT_SIZES.iter() { + let enc = AsconAead128::new(&KEY, &NONCE, None, true); + assert_eq!(enc.get_output_size(pt_len), pt_len + 16); + assert_eq!(enc.get_update_output_size(pt_len), (pt_len / 16) * 16); + + let dec = AsconAead128::new(&KEY, &NONCE, None, false); + assert_eq!(dec.get_output_size(pt_len + 16), pt_len); + } +} + +#[test] +fn aead_update_output_size_both_directions() { + // Encryption branch: whole 16-byte blocks only, accounting for buffered bytes. + let mut e = AsconAead128::new(&KEY, &NONCE, None, true); + assert_eq!(e.get_update_output_size(0), 0); + assert_eq!(e.get_update_output_size(16), 16); + assert_eq!(e.get_update_output_size(17), 16); + assert_eq!(e.get_update_output_size(40), 32); + // After buffering 5 bytes (no full block emitted yet), buf_pos == 5. + let mut out = [0u8; 64]; + assert_eq!(e.process_bytes(&[0u8; 5], &mut out), 0); + assert_eq!(e.get_update_output_size(11), 16); // 5 + 11 = 16 -> one block + assert_eq!(e.get_update_output_size(10), 0); // 5 + 10 = 15 -> none + + // Decryption branch: (buffered + len - tag) whole blocks, once the total reaches 32. + let d = AsconAead128::new(&KEY, &NONCE, None, false); + assert_eq!(d.get_update_output_size(0), 0); + assert_eq!(d.get_update_output_size(31), 0); // < 32 -> nothing emitted yet + assert_eq!(d.get_update_output_size(32), 16); // (32 - 16)/16 * 16 + assert_eq!(d.get_update_output_size(48), 32); // (48 - 16)/16 * 16 +} + +#[test] +fn aead_get_mac_returns_tag() { + let pt = pattern(20); + let mut e = AsconAead128::new(&KEY, &NONCE, None, true); + let mut out = vec![0u8; pt.len() + 16]; + let n = e.encrypt_update(&pt, &mut out); + let m = e.encrypt_finalize(&mut out[n..]); + let total = n + m; + let tag = AeadCipher::get_mac(&e); + assert_ne!(tag, [0u8; 16]); + assert_eq!(&tag[..], &out[total - 16..total], "get_mac must equal the appended tag"); +} + +#[test] +fn aead_debug_display_are_masked() { + let e = AsconAead128::new(&KEY, &NONCE, None, true); + assert!(format!("{e:?}").contains("masked")); + assert!(format!("{e}").contains("masked")); +} + +#[test] +fn aead_aad_via_trait_methods() { + // Drive AAD through the AeadCipher *trait* methods (the inherent process_aad_bytes otherwise + // shadows them), covering the process_aad_byte / process_aad_bytes delegators. + let pt = pattern(24); + let ad = pattern(20); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + + // Whole AAD via the trait method. + let mut e = AsconAead128::new(&KEY, &NONCE, None, true); + AeadCipher::process_aad_bytes(&mut e, &ad); + let mut out = vec![0u8; pt.len() + 16]; + let n = AeadCipher::process_bytes(&mut e, &pt, &mut out); + let m = AeadCipher::do_final(e, &mut out[n..]).unwrap(); + out.truncate(n + m); + assert_eq!(out, ct_ref, "AAD via trait process_aad_bytes"); + + // Byte-at-a-time AAD via the trait method (crosses the 16-byte AAD block boundary). + let mut e = AsconAead128::new(&KEY, &NONCE, None, true); + for &b in &ad { + AeadCipher::process_aad_byte(&mut e, b); + } + let mut out = vec![0u8; pt.len() + 16]; + let n = AeadCipher::process_bytes(&mut e, &pt, &mut out); + let m = AeadCipher::do_final(e, &mut out[n..]).unwrap(); + out.truncate(n + m); + assert_eq!(out, ct_ref, "AAD via trait process_aad_byte"); +} + +/* -------------------------------------------------------------------------- */ +/* AeadCipher trait conformance (shared core-test-framework) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_trait_framework() { + let ad = b"framework-associated-data"; + let fw = TestFrameworkAead::new(); + + for &pt_len in PT_SIZES.iter() { + let pt: Vec = (0..pt_len).map(|i| i as u8).collect(); + + fw.test_aead( + || AsconAead128::new(&KEY, &NONCE, Some(ad), true), + || AsconAead128::new(&KEY, &NONCE, Some(ad), false), + &pt, + ); + + fw.test_aead( + || AsconAead128::new(&KEY, &NONCE, None, true), + || AsconAead128::new(&KEY, &NONCE, None, false), + &pt, + ); + } +} diff --git a/crypto/ascon/tests/bc_test_data.rs b/crypto/ascon/tests/bc_test_data.rs new file mode 100644 index 0000000..2b4f22c --- /dev/null +++ b/crypto/ascon/tests/bc_test_data.rs @@ -0,0 +1,198 @@ +//! Test against the bc-test-data repo. +//! Requires that the bc-test-data repository is cloned and available for testing at +//! "../bc-test-data" relative to the root of this git project (or "../../../bc-test-data" relative +//! to this crate). When the repo is absent these tests print a warning and are skipped. +//! +//! The NIST SP 800-232 ASCON known-answer test (KAT) vectors live under +//! `bc-test-data/crypto/ascon//`. These full sweeps (1025–1089 cases each) complement the +//! small embedded vector sets in the per-primitive test files. + +#![allow(dead_code)] + +#[cfg(test)] +mod bc_test_data { + use bouncycastle_ascon::ascon_aead128::AsconAead128; + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::traits::XOF; + use bouncycastle_hex as hex; + use std::collections::BTreeMap; + use std::fs; + use std::path::Path; + use std::sync::Once; + + const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/ascon"; + const TEST_DATA_PATH: &str = "../bc-test-data/crypto/ascon"; + + static TEST_DATA_CHECK: Once = Once::new(); + + fn get_test_data(filename: &str) -> Result { + let found: u8; + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + found = 1; + } else if Path::new(TEST_DATA_PATH).exists() { + found = 2; + } else { + found = 3; + }; + + // just print once + TEST_DATA_CHECK.call_once(|| match found { + 1 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH_RELATIVE), + 2 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH), + _ => println!("WARNING: bc-test-data directory not found; tests will be skipped"), + }); + + let contents = if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + fs::read_to_string(TEST_DATA_PATH_RELATIVE.to_string() + "/" + filename).unwrap() + } else if Path::new(TEST_DATA_PATH).exists() { + fs::read_to_string(TEST_DATA_PATH.to_string() + "/" + filename).unwrap() + } else { + return Err(()); + }; + + Ok(contents) + } + + fn decode_hex(value: &str) -> Vec { + let clean = value.trim(); + if clean.is_empty() { Vec::new() } else { hex::decode(clean).expect("valid hex") } + } + + /// Parse a NIST LWC KAT file: blank-line-delimited `Tag = Value` cases. + fn parse_kat(contents: &str) -> Vec> { + let mut cases = Vec::new(); + let mut current = BTreeMap::new(); + + for raw in contents.lines() { + let line = raw.trim(); + if line.is_empty() { + if !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + continue; + } + if line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + let key = key.trim().to_string(); + let value = value.trim().to_string(); + if key == "Count" && !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + current.insert(key, value); + } + } + if !current.is_empty() { + cases.push(current); + } + cases + } + + fn field<'a>(case: &'a BTreeMap, names: &[&str]) -> &'a str { + for name in names { + if let Some(v) = case.get(*name) { + return v.as_str(); + } + } + panic!("missing field {names:?}; case had {:?}", case.keys().collect::>()); + } + + fn to_16(bytes: &[u8], what: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_| panic!("{what} must be 16 bytes, got {}", bytes.len())) + } + + #[test] + fn ascon_aead128_kat() { + let contents = match get_test_data("asconaead128/LWC_AEAD_KAT_128_128.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no AEAD cases parsed"); + + for case in &cases { + let key = to_16(&decode_hex(field(case, &["Key", "K"])), "key"); + let nonce = to_16(&decode_hex(field(case, &["Nonce", "N"])), "nonce"); + let ad = decode_hex(field(case, &["AD", "A"])); + let pt = decode_hex(field(case, &["PT", "P"])); + let expected_ct = decode_hex(field(case, &["CT", "C"])); + let ad_opt = if ad.is_empty() { None } else { Some(ad.as_slice()) }; + + // Encrypt. + let mut ct = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &pt, &mut ct); + ct.truncate(n); + assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); + + // Decrypt round-trip. + let mut pt_out = vec![0u8; expected_ct.len()]; + let m = AsconAead128::decrypt(&key, &nonce, ad_opt, &expected_ct, &mut pt_out) + .expect("decrypt should authenticate"); + pt_out.truncate(m); + assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-AEAD128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_hash256_kat() { + let contents = match get_test_data("asconhash256/LWC_HASH_KAT_256.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no Hash256 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD"])); + assert_eq!( + AsconHash256::digest(&msg).as_slice(), + expected.as_slice(), + "Hash256 mismatch (Count {})", + field(case, &["Count"]) + ); + } + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_xof128_kat() { + let contents = match get_test_data("asconxof128/LWC_XOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no XOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconXof128::new().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "XOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-XOF128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_cxof128_kat() { + let contents = match get_test_data("asconcxof128/LWC_CXOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no CXOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let z = decode_hex(field(case, &["Z", "Customization"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconCXof128::with_customization(&z).hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "CXOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-CXOF128: {} KAT cases passed", cases.len()); + } +} diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs new file mode 100644 index 0000000..8ea177a --- /dev/null +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -0,0 +1,155 @@ +//! Ascon-CXOF128 tests (NIST SP 800-232 §5.3). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::traits::XOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-CXOF128 vectors `(message, customization Z, 512-bit output)` in hex, +/// spanning empty/non-empty customization and message. (Counts 1, 2, 3, 35, 36 of +/// LWC_CXOF_KAT_128_512.txt; each output is 64 bytes.) +const CXOF_KAT: &[(&str, &str, &str)] = &[ + ( + "", + "", + "4F50159EF70BB3DAD8807E034EAEBD44C4FA2CBBC8CF1F05511AB66CDCC529905CA12083FC186AD899B270B1473DC5F7EC88D1052082DCDFE69FB75D269E7B74", + ), + ( + "", + "10", + "0C93A483E7D574D49FE52CCE03EE646117977D57A8AA57704AB4DAF44B501430FF6AC11A5D1FD6F2154B5C65728268270C8BB578508487B8965718ADA6272FD6", + ), + ( + "", + "1011", + "D1106C7622E79FE955BD9D79E03B918E770FE0E0CDDDE28BEB924B02C5FC936B33ACCA299C89ECA5D71886CBBFA4D54A21C55FDE2B679F5E2488063A1719DC32", + ), + ( + "00", + "10", + "63FA8BA86382F2D544580F51322D080424B42C556EB74503CD73CF052BB993BD6F5210984C71C9C445F43CCC5B158226E509BD339CD634414377F79411AA8D5C", + ), + ( + "00", + "1011", + "DF7909DD1F371E54ABBABB50DDEE195720D7EF1BB2CF2271C36A76C19908178BA3255E5A3D31D994C1D217A67AE4D13681AC1ABC4FAA2ECDD1681520BC7D7347", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn cxof128_embedded_kat() { + for (msg_hex, z_hex, md_hex) in CXOF_KAT { + let msg = dh(msg_hex); + let z = dh(z_hex); + let expected = dh(md_hex); + let got = AsconCXof128::with_customization(&z).hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex} z={z_hex}"); + } +} + +#[test] +fn cxof128_domain_separation() { + let msg = pattern(48); + + let out_z1 = AsconCXof128::with_customization(b"context-1").hash_xof(&msg, 64); + let out_z2 = AsconCXof128::with_customization(b"context-2").hash_xof(&msg, 64); + assert_ne!(out_z1, out_z2, "different customization strings must give different output"); + + // Empty-customization CXOF128 must differ from XOF128 (different IV). + let cxof_empty = AsconCXof128::new().hash_xof(&msg, 64); + let xof = AsconXof128::new().hash_xof(&msg, 64); + assert_ne!(cxof_empty, xof, "CXOF128 (empty Z) must differ from XOF128"); +} + +#[test] +fn cxof128_prefix_property_and_streaming() { + let z = b"cust"; + let msg = pattern(70); + let full = AsconCXof128::with_customization(z).hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconCXof128::with_customization(z); + x.absorb(&msg); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_into(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconCXof128::with_customization(z); + for piece in msg.chunks(chunk) { + xc.absorb(piece); + } + let mut got = vec![0u8; 100]; + xc.squeeze_into(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn cxof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so update_byte triggers full-block absorption + let cref = AsconCXof128::with_customization(b"zz").hash_xof(&msg, 48); + let mut c = AsconCXof128::with_customization(b"zz"); + for &b in &msg { + c.update_byte(b); + } + let mut o = [0u8; 48]; + c.squeeze_into(&mut o); + assert_eq!(o.to_vec(), cref, "CXOF128 update_byte mismatch"); +} + +#[test] +fn cxof128_trait_wrappers_match_inherent() { + let msg = pattern(50); + let cref = AsconCXof128::with_customization(b"z").hash_xof(&msg, 40); + + let mut c = AsconCXof128::with_customization(b"z"); + c.absorb(&msg); + assert_eq!(c.squeeze(40), cref); + + let mut c = AsconCXof128::with_customization(b"z"); + c.absorb(&msg); + let mut o = [0u8; 40]; + assert_eq!(c.squeeze_out(&mut o), 40); + assert_eq!(o.to_vec(), cref); + + let mut o = [0u8; 40]; + assert_eq!(AsconCXof128::with_customization(b"z").hash_xof_out(&msg, &mut o), 40); + assert_eq!(o.to_vec(), cref); +} + +#[test] +fn cxof128_unsupported_partial_ops_return_err() { + let mut c = AsconCXof128::new(); + assert!(c.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconCXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconCXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +#[should_panic] +fn cxof128_absorb_after_squeeze_panics() { + let mut x = AsconCXof128::with_customization(b"z"); + x.absorb(b"data"); + let mut out = [0u8; 8]; + x.squeeze_into(&mut out); + x.absorb(b"more"); +} diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs new file mode 100644 index 0000000..a478172 --- /dev/null +++ b/crypto/ascon/tests/hash256_tests.rs @@ -0,0 +1,113 @@ +//! Ascon-Hash256 tests (NIST SP 800-232 §5.1). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. + +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_core::traits::Hash; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-Hash256 vectors `(message, digest)` in hex, spanning empty, sub-block, +/// exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of LWC_HASH_KAT_256.txt.) +const HASH_KAT: &[(&str, &str)] = &[ + ("", "0B3BE5850F2F6B98CAF29F8FDEA89B64A1FA70AA249B8F839BD53BAA304D92B2"), + ("00", "0728621035AF3ED2BCA03BF6FDE900F9456F5330E4B5EE23E7F6A1E70291BC80"), + ("0001020304050607", "B88E497AE8E6FB641B87EF622EB8F2FCA0ED95383F7FFEBE167ACF1099BA764F"), + ( + "000102030405060708090A0B0C0D0E0F", + "3158C1940A2FBADBD68AB661777859B94A689E4EFC375911467ADDD641835C38", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "BD9D3D60A66B53868EAB2A5C74539A518A1F60F01EB176C60E43DEE81680B33E", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn hash256_embedded_kat() { + for (msg_hex, md_hex) in HASH_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + assert_eq!(AsconHash256::digest(&msg).as_slice(), expected.as_slice(), "msg={msg_hex}"); + } +} + +#[test] +fn hash256_streaming_matches_one_shot() { + let msg = pattern(100); + let expected = AsconHash256::digest(&msg); + + // One-shot APIs agree. + assert_eq!(AsconHash256::new().hash(&msg), expected.to_vec()); + let mut buf = [0u8; 32]; + let mut h = AsconHash256::new(); + h.update_bytes(&msg); + h.do_final_into(&mut buf); + assert_eq!(buf, expected); + + // Chunked update_bytes agrees for a range of chunk sizes. + for chunk in [1usize, 7, 8, 9, 16, 33] { + let mut hasher = AsconHash256::new(); + for piece in msg.chunks(chunk) { + hasher.update_bytes(piece); + } + let mut got = [0u8; 32]; + hasher.do_final_into(&mut got); + assert_eq!(got, expected, "chunked hash mismatch (chunk={chunk})"); + } + + // Byte-at-a-time update() agrees. + let mut hasher = AsconHash256::new(); + for &b in &msg { + hasher.update(b); + } + let mut got = [0u8; 32]; + hasher.do_final_into(&mut got); + assert_eq!(got, expected, "byte-at-a-time hash mismatch"); +} + +#[test] +fn hash256_metadata_accessors() { + assert_eq!(AsconHash256::digest_size(), 32); + let h = AsconHash256::new(); + assert_eq!(h.output_len(), 32); + assert_eq!(h.block_bitlen(), 64); +} + +#[test] +fn hash256_trait_wrappers_match_inherent() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + assert_eq!(AsconHash256::new().hash(&msg), expected.to_vec()); + + let mut h = AsconHash256::new(); + h.do_update(&msg); + assert_eq!(h.do_final(), expected.to_vec()); + + let mut h = AsconHash256::new(); + h.do_update(&msg); + let mut o = [0u8; 32]; + assert_eq!(h.do_final_out(&mut o), 32); + assert_eq!(o, expected); + + let mut o2 = [0u8; 32]; + assert_eq!(AsconHash256::new().hash_out(&msg, &mut o2), 32); + assert_eq!(o2, expected); +} + +#[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()); +} diff --git a/crypto/ascon/tests/wycheproof.rs b/crypto/ascon/tests/wycheproof.rs new file mode 100644 index 0000000..aa27332 --- /dev/null +++ b/crypto/ascon/tests/wycheproof.rs @@ -0,0 +1,143 @@ +//! Test against the project wycheproof repo available at: +//! https://github.com/C2SP/wycheproof +//! Requires that the wycheproof repository is cloned and available for testing at "../wycheproof" +//! relative to the root of this git project. When absent, these tests print a warning and skip. +//! +//! IMPORTANT — algorithm compatibility: +//! As of writing, wycheproof only ships the *pre-standardization CAESAR* Ascon AEAD vectors +//! (`ascon128_test.json`, `ascon128a_test.json`, `ascon80pq_test.json`). NIST SP 800-232 +//! `Ascon-AEAD128` is a DIFFERENT algorithm: §1 of the standard switched the endianness from +//! big-endian to little-endian and changed the initial-value format. The CAESAR vectors therefore +//! do NOT validate this crate's `Ascon-AEAD128` and are intentionally not run here. +//! +//! This file is a ready-to-activate harness: it looks for a NIST-compatible Ascon-AEAD128 wycheproof +//! file (`ascon_aead128_test.json`). That file does not exist in wycheproof today, so the test +//! skips. If/when C2SP publishes NIST SP 800-232 vectors under that name, this test runs them with +//! no further changes. + +#![allow(dead_code)] + +#[cfg(test)] +mod wycheproof { + use bouncycastle_ascon::ascon_aead128::AsconAead128; + use bouncycastle_hex as hex; + use std::fs; + use std::path::Path; + use std::sync::Once; + + const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; + const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; + + static TEST_DATA_CHECK: Once = Once::new(); + + fn get_test_data(filename: &str) -> Result { + let found: u8; + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + found = 1; + } else if Path::new(TEST_DATA_PATH).exists() { + found = 2; + } else { + found = 3; + }; + + // just print once + TEST_DATA_CHECK.call_once(|| match found { + 1 => println!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), + 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), + _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), + }); + + // The requested file (a NIST-compatible Ascon-AEAD128 set) may not exist even when the repo + // is present; treat that as a skip rather than a failure. + let base = if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + TEST_DATA_PATH_RELATIVE + } else if Path::new(TEST_DATA_PATH).exists() { + TEST_DATA_PATH + } else { + return Err(()); + }; + + match fs::read_to_string(base.to_string() + "/" + filename) { + Ok(contents) => Ok(contents), + Err(_) => { + println!( + "WARNING: {filename} not found in wycheproof; test skipped \ + (no NIST-compatible Ascon-AEAD128 vectors available)" + ); + Err(()) + } + } + } + + fn dh(s: &str) -> Vec { + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } + } + + fn to_16(bytes: &[u8], what: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_| panic!("{what} must be 16 bytes, got {}", bytes.len())) + } + + /// Run an Aead wycheproof file (schema `aead_test_schema_v1`) against Ascon-AEAD128. + /// Fields per test: `key`, `iv` (nonce), `aad`, `msg`, `ct`, `tag`, `result` (valid|invalid). + /// For our API the authenticated ciphertext is `ct || tag`. + fn run_aead_file(contents: &str) -> usize { + let json: serde_json::Value = + serde_json::from_str(contents).expect("test data is not valid JSON"); + let groups = json["testGroups"].as_array().expect("testGroups is not an array"); + + let mut executed = 0usize; + for group in groups { + let tests = group["tests"].as_array().expect("tests is not an array"); + for test in tests { + let tc_id = test["tcId"].as_u64().unwrap_or(0); + let key = to_16(&dh(test["key"].as_str().unwrap_or("")), "key"); + let nonce = to_16(&dh(test["iv"].as_str().unwrap_or("")), "iv"); + let aad = dh(test["aad"].as_str().unwrap_or("")); + let msg = dh(test["msg"].as_str().unwrap_or("")); + let ct = dh(test["ct"].as_str().unwrap_or("")); + let tag = dh(test["tag"].as_str().unwrap_or("")); + let result = test["result"].as_str().unwrap_or(""); + let aad_opt = if aad.is_empty() { None } else { Some(aad.as_slice()) }; + + let mut authenticated = ct.clone(); + authenticated.extend_from_slice(&tag); + + let mut pt_out = vec![0u8; authenticated.len()]; + let dec = AsconAead128::decrypt(&key, &nonce, aad_opt, &authenticated, &mut pt_out); + + match result { + "valid" => { + let m = dec.unwrap_or_else(|e| { + panic!("tcId {tc_id}: valid vector failed to authenticate: {e:?}") + }); + pt_out.truncate(m); + assert_eq!(pt_out, msg, "tcId {tc_id}: decrypted plaintext mismatch"); + + // Encryption direction must reproduce ct || tag. + let mut enc_out = vec![0u8; msg.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, aad_opt, &msg, &mut enc_out); + enc_out.truncate(n); + assert_eq!(enc_out, authenticated, "tcId {tc_id}: ciphertext mismatch"); + } + "invalid" => { + assert!(dec.is_err(), "tcId {tc_id}: invalid vector authenticated"); + } + other => panic!("tcId {tc_id}: unexpected result {other:?}"), + } + executed += 1; + } + } + executed + } + + #[test] + fn ascon_aead128_wycheproof() { + // NIST-compatible Ascon-AEAD128 vectors (not present in wycheproof today -> skip). + let contents = match get_test_data("ascon_aead128_test.json") { + Ok(c) => c, + Err(()) => return, + }; + let n = run_aead_file(&contents); + println!("Ascon-AEAD128 wycheproof: {n} cases passed"); + } +} diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs new file mode 100644 index 0000000..46dfc64 --- /dev/null +++ b/crypto/ascon/tests/xof128_tests.rs @@ -0,0 +1,134 @@ +//! Ascon-XOF128 tests (NIST SP 800-232 §5.2). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus the +//! prefix property, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::traits::XOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-XOF128 vectors `(message, 512-bit output)` in hex, spanning empty, +/// sub-block, exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of +/// LWC_XOF_KAT_128_512.txt; each output is 64 bytes.) +const XOF_KAT: &[(&str, &str)] = &[ + ( + "", + "473D5E6164F58B39DFD84AACDB8AE42EC2D91FED33388EE0D960D9B3993295C6AD77855A5D3B13FE6AD9E6098988373AF7D0956D05A8F1665D2C67D1A3AD10FF", + ), + ( + "00", + "51430E0438ECDF642B393630D977625F5F337656BA58AB1E960784AC32A16E0D446405551F5469384F8EA283CF12E64FA72C426BFEBAEA3AA1529E2C4AB23A2F", + ), + ( + "0001020304050607", + "8D1886F5D3EC4AF8D15B44BC62B74DA6EA91BC28FB82F9C34079B5ED6E38B6C951803D7DFB3C5E512A0EF5E4060062A6FD067F9C73EF9BEE527411BDA67FC896", + ), + ( + "000102030405060708090A0B0C0D0E0F", + "10BFEDC5F6442D3E1D8C324878CE1DDF73B01CAFC365589283AC4CBB98E48DE3CEDA8A41BB0983D539E4D90F6458C5C781724FAD641ED3CDB4779931097440B3", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "2E5F3403F4171471CC7934B51982CECE8D6628435DB70E89880F3BE4E0B7B05232DFE63C44A836D771337C9C5A2688D1B71ECABE0D5C2006FEF36EF3186138AD", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn xof128_embedded_kat() { + for (msg_hex, md_hex) in XOF_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + let got = AsconXof128::new().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex}"); + } +} + +#[test] +fn xof128_prefix_property_and_streaming() { + let msg = pattern(70); + let full = AsconXof128::new().hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconXof128::new(); + x.absorb(&msg); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_into(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconXof128::new(); + for piece in msg.chunks(chunk) { + xc.absorb(piece); + } + let mut got = vec![0u8; 100]; + xc.squeeze_into(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn xof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so update_byte triggers full-block absorption + let xref = AsconXof128::new().hash_xof(&msg, 48); + let mut x = AsconXof128::new(); + for &b in &msg { + x.update_byte(b); + } + let mut o = [0u8; 48]; + x.squeeze_into(&mut o); + assert_eq!(o.to_vec(), xref, "XOF128 update_byte mismatch"); +} + +#[test] +fn xof128_trait_wrappers_match_inherent() { + let msg = pattern(50); + let xref = AsconXof128::new().hash_xof(&msg, 40); + + let mut x = AsconXof128::new(); + x.absorb(&msg); + assert_eq!(x.squeeze(40), xref); + + let mut x = AsconXof128::new(); + x.absorb(&msg); + let mut o = [0u8; 40]; + assert_eq!(x.squeeze_out(&mut o), 40); + assert_eq!(o.to_vec(), xref); + + let mut o = [0u8; 40]; + assert_eq!(AsconXof128::new().hash_xof_out(&msg, &mut o), 40); + assert_eq!(o.to_vec(), xref); +} + +#[test] +fn xof128_unsupported_partial_ops_return_err() { + let mut x = AsconXof128::new(); + assert!(x.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +#[should_panic] +fn xof128_absorb_after_squeeze_panics() { + let mut x = AsconXof128::new(); + x.absorb(b"data"); + let mut out = [0u8; 8]; + x.squeeze_into(&mut out); + // Absorbing after squeezing has begun is a usage error. + x.absorb(b"more"); +} diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs new file mode 100644 index 0000000..209cc80 --- /dev/null +++ b/crypto/core-test-framework/src/aead.rs @@ -0,0 +1,79 @@ +use bouncycastle_core::errors::AeadError; +use bouncycastle_core::traits::AeadCipher; + +/// Shared conformance tests for implementations of the [AeadCipher] trait. +/// +/// Because [AeadCipher] does not define a constructor (construction is implementation-specific, and +/// depends on a key/nonce/associated-data context), callers supply two factory closures that each +/// build a fresh cipher: one configured for encryption and one for decryption, both over the *same* +/// key/nonce/associated-data. +pub struct TestFrameworkAead { + // Put any config options here. +} + +impl TestFrameworkAead { + pub fn new() -> Self { + Self {} + } + + /// Exercise the core behaviours of an [AeadCipher] implementation: + /// encrypt→decrypt round-trip, ciphertext length, byte-at-a-time vs one-shot equivalence, and + /// rejection of a tampered tag. `new_enc`/`new_dec` must each build a fresh cipher over the same + /// key/nonce/associated-data. + pub fn test_aead(&self, new_enc: FE, new_dec: FD, plaintext: &[u8]) + where + C: AeadCipher, + FE: Fn() -> C, + FD: Fn() -> C, + { + const TAG_LEN: usize = 16; + + // A fresh encryptor has not computed a tag yet. + assert_eq!(new_enc().get_mac(), [0u8; TAG_LEN]); + + // --- Encrypt (one-shot) --- + let mut enc = new_enc(); + let mut ct = vec![0u8; enc.get_output_size(plaintext.len())]; + let n1 = enc.process_bytes(plaintext, &mut ct); + let n2 = enc.do_final(&mut ct[n1..]).expect("encryption do_final should succeed"); + ct.truncate(n1 + n2); + assert_eq!(ct.len(), plaintext.len() + TAG_LEN, "ciphertext = plaintext || 16-byte tag"); + + // --- Decrypt round-trip --- + let mut dec = new_dec(); + let mut pt = vec![0u8; dec.get_output_size(ct.len())]; + let m1 = dec.process_bytes(&ct, &mut pt); + let m2 = dec.do_final(&mut pt[m1..]).expect("decryption do_final should succeed"); + pt.truncate(m1 + m2); + assert_eq!(pt, plaintext, "round-trip plaintext must match"); + + // --- Byte-at-a-time encryption must equal one-shot encryption --- + let mut enc_chunked = new_enc(); + let mut ct2 = vec![0u8; enc_chunked.get_output_size(plaintext.len())]; + let mut wrote = 0; + for &b in plaintext { + wrote += enc_chunked.process_byte(b, &mut ct2[wrote..]); + } + wrote += enc_chunked.do_final(&mut ct2[wrote..]).expect("chunked encryption do_final"); + ct2.truncate(wrote); + assert_eq!(ct2, ct, "byte-at-a-time encryption must match one-shot"); + + // --- Tampering with the tag must fail authentication --- + let mut tampered = ct.clone(); + let last = tampered.len() - 1; + tampered[last] ^= 0x01; + let mut dec_bad = new_dec(); + let mut pt_bad = vec![0u8; tampered.len()]; + let k1 = dec_bad.process_bytes(&tampered, &mut pt_bad); + match dec_bad.do_final(&mut pt_bad[k1..]) { + Err(AeadError::AuthenticationFailed) => { /* expected */ } + other => panic!("tampered ciphertext should fail authentication, got {other:?}"), + } + } +} + +impl Default for TestFrameworkAead { + fn default() -> Self { + Self::new() + } +} diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 92f7215..8d61d6c 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -9,6 +9,7 @@ //! //! Should only ever be a dev-dependency. +pub mod aead; pub mod hash; pub mod kdf; pub mod kem; diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index b2eaf0c..aaa59a8 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -1,3 +1,17 @@ +#[derive(Debug)] +pub enum AeadError { + /// The authentication tag did not verify during decryption finalization. + /// The decrypted plaintext (if any) must not be released to the caller. + AuthenticationFailed, + /// An input or output buffer had an invalid or insufficient length. + InvalidLength(&'static str), + /// The cipher was used out of order (e.g. reused after finalization, or + /// associated data was supplied after plaintext/ciphertext processing began). + InvalidState(&'static str), + GenericError(&'static str), + KeyMaterialError(KeyMaterialError), +} + #[derive(Debug)] pub enum HashError { GenericError(&'static str), @@ -81,6 +95,12 @@ pub enum SignatureError { } /*** Promotion functions ***/ +impl From for AeadError { + fn from(e: KeyMaterialError) -> AeadError { + Self::KeyMaterialError(e) + } +} + impl From for HashError { fn from(e: KeyMaterialError) -> HashError { Self::KeyMaterialError(e) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 089e282..be26a71 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1,6 +1,6 @@ //! Provides simplified abstracted APIs over classes of cryptigraphic primitives, such as Hash, KDF, etc. -use crate::errors::{HashError, KDFError, KEMError, MACError, RNGError, SignatureError}; +use crate::errors::{AeadError, HashError, KDFError, KEMError, MACError, RNGError, SignatureError}; use crate::key_material::KeyMaterialTrait; use core::fmt::{Debug, Display}; use core::marker::Sized; @@ -17,6 +17,72 @@ pub trait Algorithm { const MAX_SECURITY_STRENGTH: SecurityStrength; } +/// An Authenticated Encryption with Associated Data (AEAD) cipher. +/// +/// An AEAD cipher simultaneously provides confidentiality for plaintext and integrity/authenticity +/// for both the plaintext and some additional "associated data" (AAD) that is authenticated but not +/// encrypted. Encryption produces a ciphertext (of equal length to the plaintext) plus an +/// authentication tag; decryption recovers the plaintext only if the tag verifies. +/// +/// # Lifecycle +/// An implementation is constructed for a single operation (either encryption or decryption) and a +/// single (key, nonce) pair. The expected call order is: +/// 1. Zero or more calls to [`AeadCipher::process_aad_byte`] / [`AeadCipher::process_aad_bytes`] to +/// absorb associated data. All AAD must be supplied before any plaintext/ciphertext. +/// 2. Zero or more calls to [`AeadCipher::process_byte`] / [`AeadCipher::process_bytes`] to encrypt +/// plaintext or decrypt ciphertext, writing output to the provided buffer. +/// 3. Exactly one call to [`AeadCipher::do_final`], which consumes the cipher, flushes any buffered +/// final block, and (for encryption) appends the tag or (for decryption) verifies it. +/// +/// Because these primitives buffer internally to assemble full blocks, a given `process_*` call may +/// write fewer (or more) bytes than it was handed. Use [`AeadCipher::get_update_output_size`] and +/// [`AeadCipher::get_output_size`] to size output buffers conservatively. +/// +/// # Nonce uniqueness +/// As with all nonce-based AEAD schemes, a (key, nonce) pair must never be reused for two different +/// encryptions. Reuse breaks confidentiality. See the implementation's documentation for details. +pub trait AeadCipher { + /// Absorb a single byte of associated data. Must be called before any plaintext/ciphertext. + fn process_aad_byte(&mut self, input: u8); + + /// Absorb a slice of associated data. May be called multiple times, but must be called before + /// any plaintext/ciphertext is processed. + fn process_aad_bytes(&mut self, in_bytes: &[u8]); + + /// Process a single byte of plaintext (encryption) or ciphertext (decryption), writing any + /// output that becomes available into `out_bytes`. Returns the number of bytes written. + fn process_byte(&mut self, input: u8, out_bytes: &mut [u8]) -> usize; + + /// Process a slice of plaintext (encryption) or ciphertext (decryption), writing any output that + /// becomes available into `out_bytes`. Returns the number of bytes written. `out_bytes` must be + /// at least [`AeadCipher::get_update_output_size`] bytes long. + fn process_bytes(&mut self, in_bytes: &[u8], out_bytes: &mut [u8]) -> usize; + + /// Finalize the operation, consuming the cipher. + /// + /// For encryption: flushes the final (possibly partial) ciphertext block followed by the + /// authentication tag into `out_bytes`. + /// + /// For decryption: flushes the final plaintext block and verifies the tag. + /// + /// Returns the number of bytes written to `out_bytes`, or + /// [`AeadError::AuthenticationFailed`] if (during decryption) the tag does not verify — in which + /// case any bytes already written must be treated as invalid and discarded by the caller. + fn do_final(self, out_bytes: &mut [u8]) -> Result; + + /// The 128-bit authentication tag. Returns all-zero bytes if finalization has not yet produced a + /// tag (e.g. before [`AeadCipher::do_final`] on the encryption path). + fn get_mac(&self) -> [u8; 16]; + + /// The maximum number of output bytes a `process_*` call could write given `len` input bytes, + /// accounting for currently-buffered data. Does not include the tag. + fn get_update_output_size(&self, len: usize) -> usize; + + /// The maximum number of output bytes that processing `len` more input bytes and then finalizing + /// could produce, accounting for currently-buffered data and (for encryption) the tag. + fn get_output_size(&self, len: usize) -> usize; +} + pub trait Hash: Default { /// The size of the internal block in bits -- needed by functions such as HMAC to compute security parameters. fn block_bitlen(&self) -> usize; diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index b869561..630229c 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.1" edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true bouncycastle-hkdf.workspace = true bouncycastle-hmac.workspace = true diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index 9d12117..4263f1e 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -28,6 +28,9 @@ use crate::{AlgorithmFactory, FactoryError}; use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT}; +use bouncycastle_ascon as ascon; +use bouncycastle_ascon::ASCON_HASH256_NAME; +use bouncycastle_ascon::ascon_hash256::AsconHash256; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{Hash, SecurityStrength}; use bouncycastle_sha2 as sha2; @@ -51,6 +54,7 @@ pub enum HashFactory { SHA3_256(sha3::SHA3_256), SHA3_384(sha3::SHA3_384), SHA3_512(sha3::SHA3_512), + AsconHash256(ascon::ascon_hash256::AsconHash256), } impl Default for HashFactory { @@ -80,6 +84,7 @@ impl AlgorithmFactory for HashFactory { SHA3_256_NAME => Ok(Self::SHA3_256(sha3::SHA3_256::new())), SHA3_384_NAME => Ok(Self::SHA3_384(sha3::SHA3_384::new())), SHA3_512_NAME => Ok(Self::SHA3_512(sha3::SHA3_512::new())), + ASCON_HASH256_NAME => Ok(Self::AsconHash256(AsconHash256::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known Hash", alg_name @@ -99,6 +104,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.block_bitlen(), Self::SHA3_384(h) => h.block_bitlen(), Self::SHA3_512(h) => h.block_bitlen(), + Self::AsconHash256(h) => h.block_bitlen(), } } @@ -112,6 +118,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.output_len(), Self::SHA3_384(h) => h.output_len(), Self::SHA3_512(h) => h.output_len(), + Self::AsconHash256(h) => h.output_len(), } } @@ -125,6 +132,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.hash(data), Self::SHA3_384(h) => h.hash(data), Self::SHA3_512(h) => h.hash(data), + Self::AsconHash256(h) => h.hash(data), } } @@ -140,6 +148,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.hash_out(data, output), Self::SHA3_384(h) => h.hash_out(data, output), Self::SHA3_512(h) => h.hash_out(data, output), + Self::AsconHash256(h) => h.hash_out(data, output), } } @@ -153,6 +162,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.do_update(data), Self::SHA3_384(h) => h.do_update(data), Self::SHA3_512(h) => h.do_update(data), + Self::AsconHash256(h) => h.do_update(data), } } @@ -166,6 +176,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.do_final(), Self::SHA3_384(h) => h.do_final(), Self::SHA3_512(h) => h.do_final(), + Self::AsconHash256(h) => h.do_final(), } } @@ -181,6 +192,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.do_final_out(output), Self::SHA3_384(h) => h.do_final_out(output), Self::SHA3_512(h) => h.do_final_out(output), + Self::AsconHash256(h) => h.do_final_out(output), } } @@ -198,6 +210,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_384(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_512(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), + Self::AsconHash256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), } } @@ -224,6 +237,9 @@ impl Hash for HashFactory { Self::SHA3_512(h) => { h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) } + Self::AsconHash256(h) => { + h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) + } } } @@ -237,6 +253,7 @@ impl Hash for HashFactory { Self::SHA3_256(h) => h.max_security_strength(), Self::SHA3_384(h) => h.max_security_strength(), Self::SHA3_512(h) => h.max_security_strength(), + Self::AsconHash256(h) => h.max_security_strength(), } } } diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index e35e86e..b1d293e 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -35,6 +35,8 @@ //! ``` use crate::{AlgorithmFactory, FactoryError}; +use bouncycastle_ascon::ASCON_XOF128_NAME; +use bouncycastle_ascon::ascon_xof128::AsconXof128; use bouncycastle_core::errors::HashError; use bouncycastle_core::traits::{KDF, SecurityStrength, XOF}; use bouncycastle_sha3 as sha3; @@ -49,6 +51,7 @@ pub const DEFAULT_256BIT_XOF_NAME: &str = SHAKE256_NAME; pub enum XOFFactory { SHAKE128(sha3::SHAKE128), SHAKE256(sha3::SHAKE256), + AsconXof128(AsconXof128), } impl Default for XOFFactory { @@ -70,6 +73,7 @@ impl AlgorithmFactory for XOFFactory { match alg_name { SHAKE128_NAME => Ok(Self::SHAKE128(sha3::SHAKE128::new())), SHAKE256_NAME => Ok(Self::SHAKE256(sha3::SHAKE256::new())), + ASCON_XOF128_NAME => Ok(Self::AsconXof128(AsconXof128::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known XOF", alg_name @@ -82,6 +86,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.hash_xof(data, result_len), Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::AsconXof128(h) => h.hash_xof(data, result_len), } } @@ -91,6 +96,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.hash_xof_out(data, output), Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::AsconXof128(h) => h.hash_xof_out(data, output), } } @@ -98,6 +104,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.absorb(data), Self::SHAKE256(h) => h.absorb(data), + Self::AsconXof128(h) => h.absorb(data), } } @@ -109,6 +116,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), Self::SHAKE256(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), + Self::AsconXof128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), } } @@ -116,6 +124,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze(num_bytes), Self::SHAKE256(h) => h.squeeze(num_bytes), + Self::AsconXof128(h) => h.squeeze(num_bytes), } } @@ -125,6 +134,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze_out(output), Self::SHAKE256(h) => h.squeeze_out(output), + Self::AsconXof128(h) => h.squeeze_out(output), } } @@ -132,6 +142,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze_partial_byte_final(num_bits), Self::SHAKE256(h) => h.squeeze_partial_byte_final(num_bits), + Self::AsconXof128(h) => h.squeeze_partial_byte_final(num_bits), } } @@ -145,6 +156,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze_partial_byte_final_out(num_bits, output), Self::SHAKE256(h) => h.squeeze_partial_byte_final_out(num_bits, output), + Self::AsconXof128(h) => h.squeeze_partial_byte_final_out(num_bits, output), } } @@ -152,6 +164,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => KDF::max_security_strength(h), Self::SHAKE256(h) => XOF::max_security_strength(h), + Self::AsconXof128(h) => XOF::max_security_strength(h), } } } diff --git a/crypto/factory/tests/hash_factory_tests.rs b/crypto/factory/tests/hash_factory_tests.rs index 6be8756..02163f0 100644 --- a/crypto/factory/tests/hash_factory_tests.rs +++ b/crypto/factory/tests/hash_factory_tests.rs @@ -101,6 +101,30 @@ mod hash_factory_tests { assert_eq!(XOFFactory::new("SHAKE256").unwrap().hash_xof(DUMMY_SEED_512, 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); } + #[test] + fn ascon_hash_tests() { + use bouncycastle_ascon::ASCON_HASH256_NAME; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_factory::FactoryError; + + let direct = AsconHash256::new().hash(DUMMY_SEED_512); + + // Construct by literal name and by the crate's name constant; both must match the + // direct implementation. + let by_name = HashFactory::new("Ascon-Hash256").unwrap(); + assert_eq!(by_name.output_len(), 32); + assert_eq!(by_name.hash(DUMMY_SEED_512), direct); + + let by_const = HashFactory::new(ASCON_HASH256_NAME).unwrap(); + assert_eq!(by_const.hash(DUMMY_SEED_512), direct); + + // Unknown algorithm names are still rejected. + assert!(matches!( + HashFactory::new("Ascon-Hash999"), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); + } + #[test] fn test_defaults() { // All the ways to get "default" diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index 7e414f9..593a73d 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -1,4 +1,31 @@ #[cfg(test)] mod tests { - // todo + use bouncycastle_ascon::ASCON_XOF128_NAME; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::traits::XOF; + use bouncycastle_core_test_framework::DUMMY_SEED_512; + use bouncycastle_factory::AlgorithmFactory; + use bouncycastle_factory::FactoryError; + use bouncycastle_factory::xof_factory::XOFFactory; + + #[test] + fn ascon_xof_round_trip() { + let direct = AsconXof128::new().hash_xof(DUMMY_SEED_512, 64); + + // Construct by literal name and by the crate's name constant; both must match the direct + // implementation. + let by_name = XOFFactory::new("Ascon-XOF128").unwrap(); + assert_eq!(by_name.hash_xof(DUMMY_SEED_512, 64), direct); + + let by_const = XOFFactory::new(ASCON_XOF128_NAME).unwrap(); + assert_eq!(by_const.hash_xof(DUMMY_SEED_512, 64), direct); + } + + #[test] + fn unknown_xof_name_is_rejected() { + assert!(matches!( + XOFFactory::new("Ascon-XOF999"), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); + } } diff --git a/src/lib.rs b/src/lib.rs index b46df8c..66fb09d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory;