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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/add-aes-siv-cmac-256.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rscrypto" = "minor"
---

Add a misuse-resistant AES-SIV-CMAC-256 nonce-based AEAD profile with allocation-free in-place seal/open operations, typed keys, non-empty borrowed nonces, opaque failed-open errors, and complete rejected-plaintext cleanup.
5 changes: 5 additions & 0 deletions .config/benchmark-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"sha2",
"aes-gcm",
"aes-gcm-siv",
"aes-siv",
"chacha20poly1305",
"xchacha20poly1305",
"aegis256",
Expand All @@ -137,6 +138,7 @@
"parallel",
"aes-gcm",
"aes-gcm-siv",
"aes-siv",
"chacha20poly1305",
"xchacha20poly1305",
"aegis256",
Expand Down Expand Up @@ -212,6 +214,7 @@
"aes-128-gcm-siv": { "crate": "aead", "bench": "aead", "filter": "aes-128-gcm-siv" },
"aes-256-gcm": { "crate": "aead", "bench": "aead", "filter": "^aes-256-gcm/" },
"aes-128-gcm": { "crate": "aead", "bench": "aead", "filter": "^aes-128-gcm/" },
"aes-siv-cmac-256": { "crate": "aead", "bench": "aead", "filter": "aes-siv-cmac-256" },
"aegis-256": { "crate": "aead", "bench": "aead", "filter": "aegis-256" },
"ascon-aead128": { "crate": "aead", "bench": "aead", "filter": "ascon-aead128" },
"header-protection": { "crate": "aead", "bench": "aead", "filter": "header-protection" },
Expand Down Expand Up @@ -272,6 +275,7 @@
"aes-128-gcm-siv",
"aes-256-gcm",
"aes-128-gcm",
"aes-siv-cmac-256",
"aegis-256",
"ascon-aead128",
"header-protection"
Expand Down Expand Up @@ -306,6 +310,7 @@
"aes-128-gcm-siv",
"aes-256-gcm",
"aes-128-gcm",
"aes-siv-cmac-256",
"aegis-256",
"ascon-aead128",
"header-protection"
Expand Down
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ scrypt = ["pbkdf2", "alloc"]

aes-gcm = []
aes-gcm-siv = []
aes-siv = []
chacha20poly1305 = []
xchacha20poly1305 = []
aegis256 = []
Expand All @@ -200,7 +201,7 @@ password-hashing = ["argon2", "scrypt", "phc-strings"]
signatures = ["ecdsa", "ed25519", "rsa"]
key-exchange = ["x25519", "ml-kem"]
auth = ["macs", "kdfs", "password-hashing", "signatures", "key-exchange"]
aead = ["aes-gcm", "aes-gcm-siv", "chacha20poly1305", "xchacha20poly1305", "aegis256", "ascon-aead"]
aead = ["aes-gcm", "aes-gcm-siv", "aes-siv", "chacha20poly1305", "xchacha20poly1305", "aegis256", "ascon-aead"]
full = ["checksums", "hashes", "auth", "aead"]
getrandom = ["dep:getrandom"]
serde = ["dep:serde"]
Expand Down Expand Up @@ -228,6 +229,7 @@ proptest = "^1.11.0"

# Oracles
aes = "0.9.2"
aes-siv = { version = "0.8.0", default-features = false, features = ["alloc"] }
cshake = { version = "0.2.1", default-features = false }
crc = "3.4.0"
crc-fast = { version = "1.10.0", default-features = false, features = ["std"] }
Expand Down Expand Up @@ -342,6 +344,7 @@ harness = false
required-features = [
"aes-gcm",
"aes-gcm-siv",
"aes-siv",
"chacha20poly1305",
"xchacha20poly1305",
"aegis256",
Expand Down
8 changes: 7 additions & 1 deletion THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Review the `ct_intended` candidate core before the rest of the repository:
4. RSA private sign/decrypt leaves.
5. ML-KEM secret-noise key generation, encapsulation coins, decapsulation
secret-key material, and implicit rejection.
6. AEAD authentication and failed-open cleanup.
6. AEAD authentication, including AES-SIV synthetic-IV derivation, and failed-open cleanup.
7. Header-protection mask generation.
8. MAC/tag verification, fixed-size owner comparison/declassification, and selected
password-verification comparisons.
Expand Down Expand Up @@ -95,6 +95,12 @@ claims remain limited to the release-evidenced configurations.
parameters. The API uses typed keys and nonces, `#[must_use]` verification
results, `NonceCounter` invocation budgets, opaque errors, and explicit drop
cleanup for the named secret owners.

AES-SIV-CMAC-256 preserves authenticity when a nonce repeats, but it reveals
equality when the complete key/nonce/AAD/plaintext tuple repeats. The nonce-based
profile therefore still treats nonce uniqueness as the normal caller contract;
misuse resistance is a containment property, not permission to omit nonce
management.
4. **Supply-chain attacker.** Targets the path between this repository and the
artifact a downstream build consumes.

Expand Down
129 changes: 129 additions & 0 deletions benches/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,134 @@ fn header_protection(c: &mut Criterion) {
mask.finish();
}

// AES-SIV-CMAC-256 (RFC 5297 nonce-based profile)

fn aes_siv_cmac256(c: &mut Criterion) {
use aes_siv::{KeyInit as _, siv::Aes128Siv};
use rscrypto::{AesSivCmac256, AesSivCmac256Key, AesSivCmac256Nonce};

let nonce_rs = AesSivCmac256Nonce::try_from(NONCE_16.as_slice()).expect("benchmark nonce is non-empty");
let key_rs = AesSivCmac256Key::from_bytes(KEY_32);
let key_rc: aes_siv::Key<Aes128Siv> = KEY_32.into();

let mut construction = c.benchmark_group("aes-siv-cmac-256/construct");
construction.bench_function("rscrypto", |b| {
b.iter(|| black_box(AesSivCmac256::new(black_box(&key_rs))))
});
construction.bench_function("rustcrypto", |b| {
b.iter(|| black_box(Aes128Siv::new(black_box(&key_rc))))
});
construction.finish();

// NTS packet shapes: empty authenticator payload, short field, extension-field,
// cookie-shaped, and near-MTU protected bodies.
let inputs = [0usize, 16, 64, 256, 1232]
.into_iter()
.map(|len| (len, common::random_bytes(len)))
.collect::<Vec<_>>();
let cipher_rs = AesSivCmac256::new(&key_rs);
let mut cipher_rc = Aes128Siv::new(&key_rc);
let headers: [&[u8]; 2] = [AAD, &NONCE_16];

// Construction prepares different amounts of reusable work in the two libraries. Measure the
// complete one-context/one-seal lifecycle separately so setup deferral cannot skew the result.
let mut construct_and_seal = c.benchmark_group("aes-siv-cmac-256/construct-and-seal");
for (len, data) in &inputs {
common::set_throughput(&mut construct_and_seal, *len);
let mut buffer_rs = data.clone();
construct_and_seal.bench_with_input(BenchmarkId::new("rscrypto", len), data, |b, input| {
b.iter(|| {
buffer_rs.copy_from_slice(input);
let cipher = AesSivCmac256::new(black_box(&key_rs));
black_box(cipher.seal_in_place(black_box(nonce_rs), black_box(AAD), black_box(&mut buffer_rs)))
})
});

let mut buffer_rc = data.clone();
construct_and_seal.bench_with_input(BenchmarkId::new("rustcrypto", len), data, |b, input| {
b.iter(|| {
buffer_rc.copy_from_slice(input);
let mut cipher = Aes128Siv::new(black_box(&key_rc));
black_box(
cipher
.encrypt_inout_detached(black_box(headers), black_box(buffer_rc.as_mut_slice().into()))
.expect("valid benchmark input must seal"),
)
})
});
}
construct_and_seal.finish();

let mut seal = c.benchmark_group("aes-siv-cmac-256/seal");

for (len, data) in &inputs {
common::set_throughput(&mut seal, *len);
let mut buffer_rs = data.clone();
seal.bench_with_input(BenchmarkId::new("rscrypto", len), data, |b, input| {
b.iter(|| {
buffer_rs.copy_from_slice(input);
black_box(cipher_rs.seal_in_place(black_box(nonce_rs), black_box(AAD), black_box(&mut buffer_rs)))
})
});

let mut buffer_rc = data.clone();
seal.bench_with_input(BenchmarkId::new("rustcrypto", len), data, |b, input| {
b.iter(|| {
buffer_rc.copy_from_slice(input);
black_box(
cipher_rc
.encrypt_inout_detached(black_box(headers), black_box(buffer_rc.as_mut_slice().into()))
.expect("valid benchmark input must seal"),
)
})
});
}
seal.finish();

let mut open = c.benchmark_group("aes-siv-cmac-256/open");
for (len, data) in &inputs {
common::set_throughput(&mut open, *len);

let mut ciphertext_rs = data.clone();
let tag_rs = cipher_rs.seal_in_place(nonce_rs, AAD, &mut ciphertext_rs);
let mut buffer_rs = ciphertext_rs.clone();
open.bench_with_input(BenchmarkId::new("rscrypto", len), &ciphertext_rs, |b, ciphertext| {
b.iter(|| {
buffer_rs.copy_from_slice(ciphertext);
cipher_rs
.open_in_place(
black_box(nonce_rs),
black_box(AAD),
black_box(&mut buffer_rs),
black_box(&tag_rs),
)
.expect("valid benchmark ciphertext must open");
black_box(&buffer_rs);
})
});

let mut ciphertext_rc = data.clone();
let tag_rc = cipher_rc
.encrypt_inout_detached(headers, ciphertext_rc.as_mut_slice().into())
.expect("valid benchmark input must seal");
let mut buffer_rc = ciphertext_rc.clone();
open.bench_with_input(BenchmarkId::new("rustcrypto", len), &ciphertext_rc, |b, ciphertext| {
b.iter(|| {
buffer_rc.copy_from_slice(ciphertext);
cipher_rc
.decrypt_inout_detached(
black_box(headers),
black_box(buffer_rc.as_mut_slice().into()),
black_box(&tag_rc),
)
.expect("valid benchmark ciphertext must open");
black_box(&buffer_rc);
})
});
}
open.finish();
}

// Criterion harness

criterion_group!(
Expand All @@ -1488,5 +1616,6 @@ criterion_group!(
ascon_aead128_encrypt,
ascon_aead128_decrypt,
header_protection,
aes_siv_cmac256,
);
criterion_main!(benches);
Loading