Add fuzzing support for chacha20_poly1305 via cfg flag
What changed, and why it matters
This commit adds a special build-time flag named `chacha20_poly1305_fuzz` that swaps out real ChaCha20/Poly1305 cryptography with fake, deterministic no-op versions intended only for fuzz testing. The fake versions do not encrypt data and do not authenticate it; they are meant to be used only when explicitly compiling with the fuzz flag. The change itself is a test/infrastructure addition, not a normal production code path. The main risk is accidental misuse: if someone builds or ships production code with `chacha20_poly1305_fuzz` enabled, encryption and authentication would be silently disabled. The commit does not claim to fix a vulnerability and does not appear to introduce one in normal builds.
Treat `chacha20_poly1305_fuzz` as a debug/test-only configuration. Ensure CI, release builds, and published crates never enable it. Consider adding a compile-time or documentation warning that the cfg disables all cryptographic security. Review any downstream packaging that might pass `--cfg chacha20_poly1305_fuzz` unintentionally.
Security signals we found
Conditional compilation replaces cryptographic primitives with no-op/deterministic stubs under `chacha20_poly1305_fuzz`
AEAD encryption path becomes identity transform and authentication tag becomes first 16 bytes of key when fuzz cfg is active
Key material is read directly from `Key.0` in fuzz path, requiring visibility change to `pub(super)`
Workspace lints updated to allow the new cfg, reducing build friction for fuzz targets
No production code path is modified when the cfg is absent
Evidence from the diff
The patch gates three functions behind #[cfg(not(chacha20_poly1305_fuzz))] and adds #[cfg(chacha20_poly1305_fuzz)] alternatives: ChaCha20::keystream_at_block returns zeroed 64-byte blocks, ChaCha20::apply_keystream becomes a no-op, Poly1305::new stores the first 16 key bytes as the tag while ignoring input, Poly1305::input becomes a no-op, and Poly1305::tag returns that stored value. The AEAD layer in lib.rs bypasses the ChaCha20-derived Poly1305 key and uses the raw 32-byte key directly when the cfg is set. The cfg is registered in workspace lints so unexpected_cfgs does not reject it. Key.0 visibility is relaxed from private to pub(super) so the AEAD code can read it in the fuzz path. These changes are compile-time conditional and do not affect default builds.
Changed components
chacha20_poly1305/src/chacha20.rschacha20_poly1305/src/poly1305.rschacha20_poly1305/src/lib.rsCargo.toml workspace lintsInspect captured patch +55 / −9
diff --git a/Cargo.toml b/Cargo.toml
index 7131ed24..49dca5b5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ exclude = ["benches"]
resolver = "2"
[workspace.lints.rust]
-unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(fuzzing)', 'cfg(hashes_fuzz)', 'cfg(kani)'] }
+unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(chacha20_poly1305_fuzz)', 'cfg(fuzzing)', 'cfg(hashes_fuzz)', 'cfg(kani)'] }
[workspace.lints.clippy]
# Exclude lints we don't think are valuable.
diff --git a/chacha20_poly1305/src/chacha20.rs b/chacha20_poly1305/src/chacha20.rs
index 4ffbe60e..1a87ec3b 100644
--- a/chacha20_poly1305/src/chacha20.rs
+++ b/chacha20_poly1305/src/chacha20.rs
@@ -15,7 +15,7 @@ const CHACHA_BLOCKSIZE: usize = 64;
/// A 256-bit secret key shared by the parties communicating.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct Key([u8; 32]);
+pub struct Key(pub(super) [u8; 32]);
impl Key {
/// Constructs a new key.
@@ -278,6 +278,7 @@ impl ChaCha20 {
}
/// Gets the keystream for a specific block.
+ #[cfg(not(chacha20_poly1305_fuzz))]
#[inline(always)]
fn keystream_at_block(&self, block: u32) -> [u8; 64] {
let mut state = State::new(self.key, self.nonce, block);
@@ -285,7 +286,12 @@ impl ChaCha20 {
state.keystream()
}
+ /// Gets the keystream for a specific block.
+ #[cfg(chacha20_poly1305_fuzz)]
+ fn keystream_at_block(&self, _block: u32) -> [u8; 64] { [0u8; 64] }
+
/// Apply the keystream to a buffer updating the cipher block state as necessary.
+ #[cfg(not(chacha20_poly1305_fuzz))]
pub fn apply_keystream(&mut self, buffer: &mut [u8]) {
// If we have an initial offset, handle the first partial block to get back to alignment.
let remaining_buffer = if self.seek_offset_bytes != 0 {
@@ -332,6 +338,10 @@ impl ChaCha20 {
}
}
+ /// Apply the keystream to a buffer updating the cipher block state as necessary.
+ #[cfg(chacha20_poly1305_fuzz)]
+ pub fn apply_keystream(&mut self, _buffer: &mut [u8]) {}
+
/// Gets the keystream for specified block.
pub fn get_keystream(&self, block: u32) -> [u8; 64] { self.keystream_at_block(block) }
diff --git a/chacha20_poly1305/src/lib.rs b/chacha20_poly1305/src/lib.rs
index 5da1a662..8d181029 100644
--- a/chacha20_poly1305/src/lib.rs
+++ b/chacha20_poly1305/src/lib.rs
@@ -12,6 +12,7 @@
#![doc(test(attr(warn(unused))))]
// Exclude lints we don't think are valuable.
#![allow(clippy::inline_always)] // Not sure yet if we should give up the inline always, possible that the LLVM knows better.
+#![cfg_attr(chacha20_poly1305_fuzz, allow(dead_code, unused_imports))]
#[cfg(feature = "alloc")]
extern crate alloc;
@@ -79,9 +80,15 @@ impl ChaCha20Poly1305 {
pub fn encrypt(self, content: &mut [u8], aad: Option<&[u8]>) -> [u8; 16] {
let mut chacha = ChaCha20::new_from_block(self.key, self.nonce, 1);
chacha.apply_keystream(content);
- let keystream = chacha.get_keystream(0);
- let mut poly_key = [0u8; 32];
- poly_key.copy_from_slice(&keystream[..32]);
+ #[cfg(not(chacha20_poly1305_fuzz))]
+ let poly_key = {
+ let keystream = chacha.get_keystream(0);
+ let mut k = [0u8; 32];
+ k.copy_from_slice(&keystream[..32]);
+ k
+ };
+ #[cfg(chacha20_poly1305_fuzz)]
+ let poly_key = self.key.0;
let mut poly = Poly1305::new(poly_key);
let aad = aad.unwrap_or(&[]);
// AAD and ciphertext are padded if not 16-byte aligned.
@@ -120,10 +127,16 @@ impl ChaCha20Poly1305 {
tag: [u8; 16],
aad: Option<&[u8]>,
) -> Result<(), Error> {
- let chacha = ChaCha20::new_from_block(self.key, self.nonce, 0);
- let keystream = chacha.get_keystream(0);
- let mut poly_key = [0u8; 32];
- poly_key.copy_from_slice(&keystream[..32]);
+ #[cfg(not(chacha20_poly1305_fuzz))]
+ let poly_key = {
+ let chacha = ChaCha20::new_from_block(self.key, self.nonce, 0);
+ let keystream = chacha.get_keystream(0);
+ let mut k = [0u8; 32];
+ k.copy_from_slice(&keystream[..32]);
+ k
+ };
+ #[cfg(chacha20_poly1305_fuzz)]
+ let poly_key = self.key.0;
let mut poly = Poly1305::new(poly_key);
let aad = aad.unwrap_or(&[]);
poly.input(aad);
diff --git a/chacha20_poly1305/src/poly1305.rs b/chacha20_poly1305/src/poly1305.rs
index a18b05d0..4e67e95e 100644
--- a/chacha20_poly1305/src/poly1305.rs
+++ b/chacha20_poly1305/src/poly1305.rs
@@ -29,6 +29,7 @@ pub struct Poly1305 {
impl Poly1305 {
/// Initializes authenticator with a 32-byte one-time secret key.
+ #[cfg(not(chacha20_poly1305_fuzz))]
pub const fn new(key: [u8; 32]) -> Self {
// Taken from Donna. Assigns r to a 26-bit 5-limb number while simultaneously 'clamping' r.
let r0 = u32::from_le_bytes([key[0], key[1], key[2], key[3]]) & 0x03ff_ffff;
@@ -51,7 +52,20 @@ impl Poly1305 {
}
}
+ /// Initializes authenticator with a 32-byte one-time secret key.
+ #[cfg(chacha20_poly1305_fuzz)]
+ pub const fn new(key: [u8; 32]) -> Self {
+ let mut tag = [0u8; 16];
+ let mut i = 0;
+ while i < 16 {
+ tag[i] = key[i];
+ i += 1;
+ }
+ Self { r: [0; 5], s: [0; 4], acc: [0; 5], leftovers: tag, leftovers_len: 0 }
+ }
+
/// Adds message to be authenticated, can be called multiple times before creating tag.
+ #[cfg(not(chacha20_poly1305_fuzz))]
pub fn input(&mut self, message: &[u8]) {
// Process previous leftovers if the message is long enough to fill the leftovers buffer. If
// the message is too short then it will just be added to the leftovers at the end. Now if there
@@ -96,7 +110,12 @@ impl Poly1305 {
}
}
+ /// Adds message to be authenticated, can be called multiple times before creating tag.
+ #[cfg(chacha20_poly1305_fuzz)]
+ pub fn input(&mut self, _message: &[u8]) {}
+
/// Generates authentication tag.
+ #[cfg(not(chacha20_poly1305_fuzz))]
pub fn tag(mut self) -> [u8; 16] {
// Add any remaining leftovers to accumulator.
if self.leftovers_len > 0 {
@@ -160,6 +179,10 @@ impl Poly1305 {
ret
}
+ /// Generates authentication tag.
+ #[cfg(chacha20_poly1305_fuzz)]
+ pub fn tag(self) -> [u8; 16] { self.leftovers }
+
fn r_times_a(&mut self) {
// Multiply and reduce.
// While this looks complicated, it is a variation of schoolbook multiplication,
Why this scored 24/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.