lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit d6e3b284de9d6cd131ae5791041ed230fb846b43
parent e3badfb7b9891fbd249c789a38b68f5f42a0fce2
Author: triesap <tyson@radroots.org>
Date:   Wed,  5 Aug 2026 19:48:24 +0000

test(secrets): qualify context-bound envelope security

Diffstat:
Mcontracts/releases/fuzz_matrix.toml | 1+
Mcrates/secrets/src/envelope.rs | 20+++++++++++++-------
Mcrates/secrets/tests/security_contract.rs | 16++++++++++------
Mcrates/storage_sqlite/src/private_artifact/mod.rs | 17++++++++++-------
Mfuzz/event_codec/Cargo.lock | 15+++++++++++----
Mfuzz/event_codec/Cargo.toml | 8++++++++
Afuzz/event_codec/fuzz_targets/secrets_envelope_and_artifact.rs | 13+++++++++++++
Mtools/xtask/src/fuzz_qualification.rs | 4++--
8 files changed, 68 insertions(+), 26 deletions(-)

diff --git a/contracts/releases/fuzz_matrix.toml b/contracts/releases/fuzz_matrix.toml @@ -12,4 +12,5 @@ targets = [ "nip46_messages_and_uris", "protocol_identifiers", "restore_plan", + "secrets_envelope_and_artifact", ] diff --git a/crates/secrets/src/envelope.rs b/crates/secrets/src/envelope.rs @@ -14,7 +14,7 @@ use crate::{SecretId, SecretRef}; use alloc::string::String; use alloc::vec::Vec; use chacha20poly1305::aead::{Aead, KeyInit, Payload}; -use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; +use chacha20poly1305::{XChaCha20Poly1305, XNonce}; use core::fmt; use sha2::{Digest, Sha256}; use subtle::ConstantTimeEq; @@ -254,10 +254,12 @@ impl EncryptedEnvelope { )?; let ciphertext = material.data_key.expose_secret(|data_key| { plaintext.expose_secret(|plaintext| { - let cipher = XChaCha20Poly1305::new(Key::from_slice(data_key)); + let cipher = XChaCha20Poly1305::new_from_slice(data_key) + .map_err(|_| Error::EncryptFailed)?; + let nonce = XNonce::from(*material.nonce.as_bytes()); cipher .encrypt( - XNonce::from_slice(material.nonce.as_bytes()), + &nonce, Payload { msg: plaintext, aad: aad.as_slice(), @@ -304,10 +306,12 @@ impl EncryptedEnvelope { validate_data_key(&data_key)?; let aad = self.encoded_header()?; let plaintext = data_key.expose_secret(|data_key| { - let cipher = XChaCha20Poly1305::new(Key::from_slice(data_key)); + let cipher = + XChaCha20Poly1305::new_from_slice(data_key).map_err(|_| Error::DecryptFailed)?; + let nonce = XNonce::from(*self.nonce.as_bytes()); cipher .decrypt( - XNonce::from_slice(self.nonce.as_bytes()), + &nonce, Payload { msg: self.ciphertext.as_slice(), aad: aad.as_slice(), @@ -533,10 +537,12 @@ impl EncryptedEnvelope { validate_data_key(&data_key)?; let aad = self.encoded_header()?; let plaintext = data_key.expose_secret(|data_key| { - let cipher = XChaCha20Poly1305::new(Key::from_slice(data_key)); + let cipher = + XChaCha20Poly1305::new_from_slice(data_key).map_err(|_| Error::DecryptFailed)?; + let nonce = XNonce::from(*self.nonce.as_bytes()); cipher .decrypt( - XNonce::from_slice(self.nonce.as_bytes()), + &nonce, Payload { msg: self.ciphertext.as_slice(), aad: aad.as_slice(), diff --git a/crates/secrets/tests/security_contract.rs b/crates/secrets/tests/security_contract.rs @@ -146,19 +146,23 @@ fn diagnostics_snapshot_is_redacted_and_plaintext_free() { } #[test] -fn production_sources_have_no_plaintext_logging_surface() { - let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); +fn envelope_and_private_artifact_sources_have_no_plaintext_logging_surface() { + let crates_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("secrets crate has a crates directory parent"); let mut paths = Vec::new(); - collect_rust_sources(&source_root, &mut paths); - assert!(!paths.is_empty()); + for crate_name in ["secrets", "storage", "storage_sqlite"] { + collect_rust_sources(&crates_root.join(crate_name).join("src"), &mut paths); + } + assert!(!paths.is_empty(), "audited production sources must exist"); for path in paths { - let source = fs::read_to_string(&path).expect("read secret source"); + let source = fs::read_to_string(&path).expect("read audited source"); let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); for forbidden in ["tracing::", "log::", "println!(", "eprintln!(", "dbg!("] { assert!( !production.contains(forbidden), - "secret production source contains logging surface `{forbidden}`: {}", + "envelope or private-artifact source contains logging surface `{forbidden}`: {}", path.display() ); } diff --git a/crates/storage_sqlite/src/private_artifact/mod.rs b/crates/storage_sqlite/src/private_artifact/mod.rs @@ -1415,14 +1415,17 @@ mod tests { 200, ) .expect("reseal request"); - let committed = store - .commit_private_artifact_reseal(request.clone(), resealed.envelope()) - .await - .expect("atomic commit"); - assert_eq!( - committed.disposition(), - PrivateArtifactResealDisposition::Committed + let contender = store.clone(); + let (left, right) = tokio::join!( + store.commit_private_artifact_reseal(request.clone(), resealed.envelope()), + contender.commit_private_artifact_reseal(request.clone(), resealed.envelope()), ); + let dispositions = [ + left.expect("first concurrent outcome").disposition(), + right.expect("second concurrent outcome").disposition(), + ]; + assert!(dispositions.contains(&PrivateArtifactResealDisposition::Committed)); + assert!(dispositions.contains(&PrivateArtifactResealDisposition::Replayed)); let replayed = store .commit_private_artifact_reseal(request.clone(), resealed.envelope()) .await diff --git a/fuzz/event_codec/Cargo.lock b/fuzz/event_codec/Cargo.lock @@ -845,6 +845,7 @@ dependencies = [ "radroots_event_codec", "radroots_nostr_connect", "radroots_protocol", + "radroots_secrets", "radroots_storage", "serde_json", ] @@ -857,6 +858,16 @@ dependencies = [ ] [[package]] +name = "radroots_secrets" +version = "0.1.0-alpha" +dependencies = [ + "chacha20poly1305", + "sha2", + "subtle", + "zeroize", +] + +[[package]] name = "radroots_storage" version = "0.1.0-alpha" dependencies = [ @@ -1436,10 +1447,6 @@ name = "radroots_geonames" version = "0.1.0-alpha" [[patch.unused]] -name = "radroots_secrets" -version = "0.1.0-alpha" - -[[patch.unused]] name = "radroots_signing" version = "0.1.0-alpha" diff --git a/fuzz/event_codec/Cargo.toml b/fuzz/event_codec/Cargo.toml @@ -14,6 +14,7 @@ radroots_blossom = { path = "../../crates/blossom", default-features = false, fe radroots_event_codec = { path = "../../crates/event_codec", default-features = false, features = ["manifests", "std"] } radroots_nostr_connect = { path = "../../crates/nostr_connect" } radroots_protocol = { path = "../../crates/protocol", default-features = false, features = ["std"] } +radroots_secrets = { path = "../../crates/secrets", default-features = false } radroots_storage = { path = "../../crates/storage", default-features = false, features = ["serde"] } serde_json = "1" @@ -66,5 +67,12 @@ test = false doc = false bench = false +[[bin]] +name = "secrets_envelope_and_artifact" +path = "fuzz_targets/secrets_envelope_and_artifact.rs" +test = false +doc = false +bench = false + [workspace] members = ["."] diff --git a/fuzz/event_codec/fuzz_targets/secrets_envelope_and_artifact.rs b/fuzz/event_codec/fuzz_targets/secrets_envelope_and_artifact.rs @@ -0,0 +1,13 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use radroots_secrets::EncryptedEnvelope; +use radroots_storage::private_artifact::PrivateArtifactMetadata; + +fuzz_target!(|data: &[u8]| { + if let Ok(envelope) = EncryptedEnvelope::decode(data) { + let encoded = envelope.encode().expect("decoded envelope must re-encode"); + assert_eq!(encoded, data); + } + let _ = serde_json::from_slice::<PrivateArtifactMetadata>(data); +}); diff --git a/tools/xtask/src/fuzz_qualification.rs b/tools/xtask/src/fuzz_qualification.rs @@ -60,8 +60,8 @@ fn validate(root: &Path, contract: &Contract) -> Result<(), String> { return Err("invalid fuzz qualification contract".to_owned()); } let expected = contract.targets.iter().collect::<BTreeSet<_>>(); - if expected.len() != 7 || expected.len() != contract.targets.len() { - return Err("fuzz matrix requires exactly seven unique parser targets".to_owned()); + if expected.len() != 8 || expected.len() != contract.targets.len() { + return Err("fuzz matrix requires exactly eight unique parser targets".to_owned()); } let path = root.join(&contract.harness).join("Cargo.toml"); let raw = fs::read_to_string(&path)