lib

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

commit 2dd2a8dc1e18f6f1b112be63f8519f529229669f
parent 4aa834670f77f6559322ffcb5592e1d77965ad5d
Author: triesap <tyson@radroots.org>
Date:   Sat,  1 Aug 2026 10:39:38 +0000

secrets: complete package conformance coverage

- Add one shared capability and exact-selection contract for every built-in adapter.
- Enforce exact dependency sets, public traits, exports, and implementation isolation.
- Prove no-default, default, individual-feature, bundle, all-feature, docs, and wasm lanes.
- Make no_std tests import allocation types explicitly instead of relying on std.

Diffstat:
Mcrates/secrets/src/wrapping.rs | 1+
Acrates/secrets/tests/adapter_conformance.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/secrets/tests/package_boundary.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/secrets/tests/security_contract.rs | 26++++++++++++++++++++++++++
4 files changed, 160 insertions(+), 1 deletion(-)

diff --git a/crates/secrets/src/wrapping.rs b/crates/secrets/src/wrapping.rs @@ -183,6 +183,7 @@ pub trait KeyWrapping: Send + Sync { #[cfg(test)] mod tests { use super::SecretMaterial; + use alloc::vec::Vec; use zeroize::Zeroize; #[test] diff --git a/crates/secrets/tests/adapter_conformance.rs b/crates/secrets/tests/adapter_conformance.rs @@ -0,0 +1,70 @@ +#![cfg(any(feature = "memory", feature = "file", feature = "keyring"))] + +use radroots_secrets::id::BackendKind; +use radroots_secrets::provider::{ + AccessPolicy, CapabilitySupport, ResidencySupport, SelectionPolicy, +}; +use radroots_secrets::{Error, SecretProvider}; + +fn assert_provider_contract( + provider: &dyn SecretProvider, + backend: BackendKind, + residency: ResidencySupport, +) { + assert_eq!(provider.backend_kind(), backend); + let capabilities = provider.capabilities(); + assert!(capabilities.is_available()); + assert_eq!(capabilities.residency(), residency); + assert_eq!(capabilities.user_presence(), CapabilitySupport::Unavailable); + assert_eq!( + capabilities.hardware_backed(), + CapabilitySupport::Unavailable + ); + + let candidates = [provider]; + let selected = SelectionPolicy::new(backend, AccessPolicy::standard()) + .select(&candidates) + .expect("exact provider selection"); + assert_eq!(selected.backend_kind(), backend); + assert!(matches!( + SelectionPolicy::new(BackendKind::External, AccessPolicy::standard()).select(&candidates), + Err(Error::BackendUnavailable { + backend: BackendKind::External + }) + )); +} + +#[cfg(feature = "memory")] +#[test] +fn memory_provider_satisfies_shared_capability_contract() { + let provider = radroots_secrets::memory::MemoryProvider::new(); + assert_provider_contract(&provider, BackendKind::Memory, ResidencySupport::Volatile); +} + +#[cfg(feature = "file")] +#[test] +fn file_provider_satisfies_shared_capability_contract() { + use radroots_secrets::file::{FileOpenMode, FileProvider}; + use radroots_secrets::wrapping::SecretMaterial; + + let temporary = tempfile::tempdir().expect("temporary directory"); + let provider = FileProvider::open( + temporary.path().join("secrets"), + FileOpenMode::CreateNew, + SecretMaterial::from_slice(&[0x5a; 32]).expect("master key"), + ) + .expect("file provider"); + assert_provider_contract(&provider, BackendKind::File, ResidencySupport::UserProfile); +} + +#[cfg(feature = "keyring")] +#[test] +fn keyring_provider_satisfies_shared_capability_contract_without_access() { + let provider = radroots_secrets::keyring::KeyringProvider::new("org.radroots.conformance") + .expect("keyring provider"); + assert_provider_contract( + &provider, + BackendKind::Keyring, + ResidencySupport::UserProfile, + ); +} diff --git a/crates/secrets/tests/package_boundary.rs b/crates/secrets/tests/package_boundary.rs @@ -1,4 +1,6 @@ use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; const MANIFEST: &str = include_str!("../Cargo.toml"); const ROOT: &str = include_str!("../src/lib.rs"); @@ -32,6 +34,50 @@ fn manifest_has_final_identity_features_and_no_radroots_dependencies() { .all(|dependency| !dependency.starts_with("radroots_")), "security SPI must not depend on another Radroots package" ); + assert_eq!( + table_keys(MANIFEST, "[dependencies]"), + BTreeSet::from([ + "chacha20poly1305", + "keyring", + "serde", + "tempfile", + "zeroize" + ]) + ); + assert_eq!( + table_keys(MANIFEST, "[dev-dependencies]"), + BTreeSet::from(["futures-executor", "hex", "serde_json"]) + ); +} + +#[test] +fn production_sources_publish_only_the_approved_traits() { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut paths = Vec::new(); + collect_rust_sources(&source_root, &mut paths); + let mut public_traits = BTreeSet::new(); + + for path in paths { + let source = fs::read_to_string(&path).expect("read secrets source"); + let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); + for line in production.lines() { + if let Some(name) = line + .trim_start() + .strip_prefix("pub trait ") + .and_then(|rest| rest.split([':', '<', ' ']).next()) + { + public_traits.insert(name.to_owned()); + } + } + } + + assert_eq!( + public_traits, + ["KeyWrapping", "SecretProvider"] + .into_iter() + .map(str::to_owned) + .collect() + ); } #[test] @@ -66,7 +112,12 @@ fn table_keys<'a>(source: &'a str, table: &str) -> BTreeSet<&'a str> { .skip(1) .take_while(|line| !line.starts_with('[')) .filter_map(|line| line.split_once('=').map(|(key, _)| key.trim())) - .filter(|key| !key.is_empty()) + .filter(|key| { + !key.is_empty() + && key.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '_' | '-') + }) + }) .collect() } @@ -78,3 +129,14 @@ fn declarations<'a>(source: &'a str, prefix: &str) -> BTreeSet<&'a str> { .filter_map(|line| line.strip_suffix(';')) .collect() } + +fn collect_rust_sources(root: &Path, paths: &mut Vec<PathBuf>) { + for entry in fs::read_dir(root).expect("read source directory") { + let path = entry.expect("source entry").path(); + if path.is_dir() { + collect_rust_sources(&path, paths); + } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { + paths.push(path); + } + } +} diff --git a/crates/secrets/tests/security_contract.rs b/crates/secrets/tests/security_contract.rs @@ -54,6 +54,32 @@ fn reviewed_api_forbids_secret_bearing_clone_serialize_and_byte_access() { "reviewed API exposes forbidden plaintext or duplication surface `{forbidden}`" ); } + + for forbidden_dependency in [ + "chacha20poly1305", + "futures_executor", + "keyring", + "serde_json", + "tempfile", + "zeroize", + ] { + assert!( + !exposes_crate_path(PUBLIC_API, forbidden_dependency), + "reviewed API leaks implementation dependency `{forbidden_dependency}`" + ); + } +} + +fn exposes_crate_path(public_api: &str, crate_name: &str) -> bool { + public_api + .split(|character: char| { + !(character.is_ascii_alphanumeric() || matches!(character, '_' | ':')) + }) + .any(|token| { + token + .strip_prefix(crate_name) + .is_some_and(|remainder| remainder.starts_with("::")) + }) } #[test]