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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"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}')",
"Bash(sed -i -E 's/^\\(\\\\s*x\\\\.absorb\\\\\\(&msg\\\\\\)\\);/\\\\1.unwrap\\(\\);/; s/^\\(\\\\s*xc\\\\.absorb\\\\\\(piece\\\\\\)\\);/\\\\1.unwrap\\(\\);/' xof128_tests.rs)",
"Bash(sed -i -E 's/^\\(\\\\s*x\\\\.absorb\\\\\\(&msg\\\\\\)\\);/\\\\1.unwrap\\(\\);/; s/^\\(\\\\s*xc\\\\.absorb\\\\\\(piece\\\\\\)\\);/\\\\1.unwrap\\(\\);/; s/^\\(\\\\s*c\\\\.absorb\\\\\\(&msg\\\\\\)\\);/\\\\1.unwrap\\(\\);/' cxof128_tests.rs)",
"Bash(git checkout *)",
"Bash(rm -f crypto/core-test-framework/src/aead.rs)",
"Bash(sed -n '/\\\\[dependencies\\\\]/,/\\\\[dev-dependencies\\\\]/p' crypto/mlkem/Cargo.toml)",
"Bash(sed -n '/\\\\[dependencies\\\\]/,/\\\\[dev-dependencies\\\\]/p' crypto/mldsa/Cargo.toml)",
"Bash(cargo doc *)"
]
}
}
18 changes: 10 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ edition = "2024"
[workspace.dependencies]

# *** Internal Dependencies ***
bouncycastle = { path = "./", version = "0.1.2" }
bouncycastle-base64 = { path = "./crypto/base64", version = "0.1.2" }
bouncycastle-core = { path = "crypto/core", version = "0.1.2" }
bouncycastle-core-test-framework = { path = "./crypto/core-test-framework", version = "0.1.2" }
bouncycastle-factory = { path = "./crypto/factory", version = "0.1.2" }
bouncycastle-hex = { path = "./crypto/hex", version = "0.1.2" }
bouncycastle-hkdf = { path = "./crypto/hkdf", version = "0.1.2" }
bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.2" }
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" }
bouncycastle-factory = { path = "./crypto/factory", version = "0.1.1" }
bouncycastle-hex = { path = "./crypto/hex", version = "0.1.1" }
bouncycastle-hkdf = { path = "./crypto/hkdf", version = "0.1.1" }
bouncycastle-hmac = { path = "./crypto/hmac", version = "0.1.1" }
bouncycastle-mlkem = { path = "./crypto/mlkem", version = "0.1.2" }
bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory", version = "0.1.2" }
bouncycastle-mldsa = { path = "./crypto/mldsa", version = "0.1.2" }
Expand All @@ -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
Expand Down
152 changes: 152 additions & 0 deletions cli/src/ascon_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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<u8> {
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<String>, value_file: &Option<String>, label: &str) -> Vec<u8> {
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<u8>, label: &str) -> [u8; 16] {
bytes.try_into().unwrap_or_else(|_: Vec<u8>| {
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 {
// Absorb cannot fail here: we only absorb before any squeeze.
x.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible");
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<String>, 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 {
// Absorb cannot fail here: we only absorb before any squeeze.
x.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible");
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<String>,
key_file: &Option<String>,
nonce: &Option<String>,
nonce_file: &Option<String>,
ad: &Option<String>,
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);
}
}
83 changes: 83 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod ascon_cmd;
mod encoders_cmd;
mod helpers;
mod hkdf_cmd;
Expand Down Expand Up @@ -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<String>,

#[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<String>,

/// A file containing the 128-bit key in binary.
#[arg(long)]
key_file: Option<String>,

/// The 128-bit nonce in hex. Must be unique per encryption under a given key.
#[arg(long)]
nonce: Option<String>,

/// A file containing the 128-bit nonce in binary.
#[arg(long)]
nonce_file: Option<String>,

/// Associated data in hex (authenticated but not encrypted).
#[arg(long)]
ad: Option<String>,

/// 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
Expand Down Expand Up @@ -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)
}
Expand Down
27 changes: 27 additions & 0 deletions crypto/ascon/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "bouncycastle-ascon"
version = "0.1.2"
edition.workspace = true

[features]
# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot cipher APIs, mirroring the
# `std` feature of `bouncycastle-core`. On by default; a future `--no-default-features` build is
# what will let the crate move toward `#![no_std]`.
default = ["std"]
std = ["bouncycastle-core/std"]

[dependencies]
bouncycastle-core.workspace = true
bouncycastle-rng.workspace = true
bouncycastle-utils.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
Loading
Loading