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:
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]