lib

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

commit b58a27624804401074fa0afb966981bc2bea307e
parent 400d923acbeaedc48646f88bb0c1dd5e27a9cc04
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Jul 2026 12:22:33 +0000

signing: complete package conformance coverage

- enforce exact feature dependency module export and trait boundaries
- cover public error deadline cancellation and redaction contracts
- prove std serde empty default and all-feature test matrices
- qualify docs doctests native WASM and architecture gates

Diffstat:
Mcrates/signing/src/receipt.rs | 18+++++++++++++++++-
Mcrates/signing/src/request.rs | 13++++++++++++-
Mcrates/signing/src/status.rs | 6++++++
Acrates/signing/tests/conformance.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/signing/tests/package_boundary.rs | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 296 insertions(+), 2 deletions(-)

diff --git a/crates/signing/src/receipt.rs b/crates/signing/src/receipt.rs @@ -7,6 +7,16 @@ use radroots_protocol::runtime::v1::OperationId; use crate::{Error, SignRequest, error::Kind}; /// Successful signer output with portable operation provenance. +/// +/// Native receipts cannot be deserialized without the originating request; +/// adapters must use [`SignReceipt::from_signed_event`] so exact-draft +/// verification cannot be bypassed. +/// +/// ```compile_fail +/// use radroots_signing::SignReceipt; +/// +/// let _: SignReceipt = serde_json::from_str("{}").unwrap(); +/// ``` #[non_exhaustive] #[cfg_attr(feature = "serde", derive(serde::Serialize))] #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] @@ -87,7 +97,13 @@ mod tests { use radroots_identity::PublicKey; #[cfg(not(feature = "std"))] - use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; + use alloc::{ + borrow::ToOwned, + format, + string::{String, ToString}, + vec, + vec::Vec, + }; #[cfg(feature = "std")] use std::{borrow::ToOwned, string::String, vec, vec::Vec}; diff --git a/crates/signing/src/request.rs b/crates/signing/src/request.rs @@ -69,6 +69,17 @@ pub trait ProgressObserver: Send + Sync { } /// One authorized actor, frozen draft, and bounded signer invocation. +/// +/// Runtime-local observers intentionally prevent native requests from becoming +/// passive wire DTOs. Versioned protocol types own serialized boundaries. +/// +/// ```compile_fail +/// use radroots_signing::SignRequest; +/// +/// fn serialize(request: &SignRequest) { +/// let _ = serde_json::to_string(request).unwrap(); +/// } +/// ``` #[derive(Clone)] pub struct SignRequest { operation_id: OperationId, @@ -199,7 +210,7 @@ mod tests { use radroots_identity::PublicKey; #[cfg(not(feature = "std"))] - use alloc::{string::String, sync::Arc, vec, vec::Vec}; + use alloc::{borrow::ToOwned, string::String, sync::Arc, vec, vec::Vec}; #[cfg(feature = "std")] use std::{string::String, sync::Arc, vec, vec::Vec}; diff --git a/crates/signing/src/status.rs b/crates/signing/src/status.rs @@ -252,8 +252,14 @@ impl SignerStatus { #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "serde")] use crate::capability::{CancellationSupport, SignerKind}; + #[cfg(not(feature = "std"))] + use alloc::format; + #[cfg(all(not(feature = "std"), feature = "serde"))] + use alloc::vec; + #[test] fn challenge_validation_and_debug_redaction_are_explicit() { let challenge = diff --git a/crates/signing/tests/conformance.rs b/crates/signing/tests/conformance.rs @@ -0,0 +1,111 @@ +use radroots_signing::{ + Error, Signer, + capability::{CancellationSupport, SignerCapability, SignerKind}, + error::{CATALOG, Kind}, + request::{CancellationPolicy, SignPolicy}, +}; + +#[test] +fn public_error_catalog_is_unique_consistent_and_protocol_redacted() { + let mut codes = std::collections::BTreeSet::new(); + for descriptor in CATALOG.iter().copied() { + assert!(codes.insert(descriptor.code())); + let error = Error::new(descriptor.kind()); + assert_eq!(error.kind(), descriptor.kind()); + assert_eq!(error.code(), descriptor.code()); + assert_eq!(error.class(), descriptor.class()); + assert_eq!(error.retryable(), descriptor.retryable()); + assert_eq!(error.recovery_actions(), descriptor.recovery_actions()); + let report = error.to_report(None); + assert_eq!(report.code().as_str(), descriptor.code()); + assert_eq!(report.message().as_str(), "[redacted]"); + } + assert_eq!(codes.len(), Kind::ALL.len()); +} + +#[test] +fn deadline_and_cancellation_contracts_are_explicit() { + let error = SignPolicy::new(0, CancellationPolicy::LocalCooperative) + .expect_err("zero deadline must fail"); + assert_eq!(error.kind(), Kind::InvalidArgument); + + let local = SignPolicy::new(42, CancellationPolicy::LocalCooperative).expect("local policy"); + let remote = + SignPolicy::new(42, CancellationPolicy::PreservePublishedRequest).expect("remote policy"); + assert_eq!(local.deadline_unix(), 42); + assert_eq!(local.cancellation(), CancellationPolicy::LocalCooperative); + assert_eq!( + remote.cancellation(), + CancellationPolicy::PreservePublishedRequest + ); + + let capability = SignerCapability::new( + SignerKind::Remote, + CancellationSupport::BeforeAndAfterPublication, + true, + true, + ); + assert_eq!(capability.kind(), SignerKind::Remote); + assert_eq!( + capability.cancellation(), + CancellationSupport::BeforeAndAfterPublication + ); + assert!(capability.reports_progress()); + assert!(capability.may_require_authentication()); +} + +#[cfg(feature = "serde")] +#[test] +fn policy_wire_labels_are_stable_and_round_trip() { + let policy = + SignPolicy::new(42, CancellationPolicy::PreservePublishedRequest).expect("remote policy"); + let json = serde_json::to_string(&policy).expect("serialize policy"); + assert!(json.contains("preserve_published_request")); + assert_eq!( + serde_json::from_str::<SignPolicy>(&json).expect("deserialize policy"), + policy + ); +} + +#[test] +fn public_service_types_preserve_dyn_and_thread_safety() { + fn assert_dyn(_: &dyn Signer) {} + fn assert_send_sync<T: Send + Sync + ?Sized>() {} + + let _ = assert_dyn; + assert_send_sync::<dyn Signer>(); + assert_send_sync::<Error>(); +} + +#[cfg(feature = "std")] +#[test] +fn native_sources_are_opt_in_and_never_appear_in_diagnostics() { + use std::error::Error as _; + + #[derive(Debug)] + struct Sensitive; + + impl core::fmt::Display for Sensitive { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("nsec1-integration-secret") + } + } + + impl std::error::Error for Sensitive {} + + let error = Error::with_source(Kind::SignerUnavailable, Sensitive); + assert!(error.source().is_some()); + assert!(!error.to_string().contains("nsec1")); + assert!(!format!("{error:?}").contains("nsec1")); + assert!(!serde_or_debug_report(&error).contains("nsec1")); +} + +#[cfg(all(feature = "std", feature = "serde"))] +fn serde_or_debug_report(error: &Error) -> String { + serde_json::to_string(&error.to_report(None)).expect("serialize report") +} + +#[cfg(all(feature = "std", not(feature = "serde")))] +fn serde_or_debug_report(error: &Error) -> String { + format!("{:?}", error.to_report(None)) +} diff --git a/crates/signing/tests/package_boundary.rs b/crates/signing/tests/package_boundary.rs @@ -1,3 +1,5 @@ +use std::{collections::BTreeSet, fs, path::Path}; + #[allow(unused_imports)] use radroots_signing::{ Actor, Error, SignReceipt, SignRequest, Signer, SignerStatus, actor as _, capability as _, @@ -23,6 +25,37 @@ fn manifest_has_final_identity_features_and_dependencies() { "manifest is missing {required}" ); } + assert_eq!( + table_keys(MANIFEST, "[features]"), + BTreeSet::from(["default", "serde", "std"]) + ); + assert_eq!( + table_keys(MANIFEST, "[dependencies]"), + BTreeSet::from([ + "radroots_event", + "radroots_identity", + "radroots_protocol", + "serde", + ]) + ); + assert_eq!( + table_keys(MANIFEST, "[dev-dependencies]"), + BTreeSet::from(["serde_json"]) + ); + for forbidden in [ + "async-trait", + "keyring", + "nostr", + "nostr-sdk", + "reqwest", + "sqlx", + "tokio", + ] { + assert!( + !table_keys(MANIFEST, "[dependencies]").contains(forbidden), + "signing runtime must not depend on {forbidden}" + ); + } } #[test] @@ -43,6 +76,18 @@ fn crate_root_declares_the_approved_module_skeleton() { "crate root is missing {module}" ); } + assert_eq!( + root_declarations("pub mod "), + BTreeSet::from([ + "actor", + "capability", + "error", + "receipt", + "request", + "signer", + "status", + ]) + ); let _ = core::mem::size_of::<Actor>(); let _ = core::mem::size_of::<SignRequest>(); let _ = core::mem::size_of::<SignReceipt>(); @@ -60,4 +105,109 @@ fn crate_root_declares_the_approved_module_skeleton() { ] { assert!(ROOT.contains(root_export), "missing {root_export}"); } + assert_eq!( + ROOT.lines() + .map(str::trim) + .filter(|line| line.starts_with("pub use ")) + .collect::<BTreeSet<_>>(), + BTreeSet::from([ + "pub use actor::Actor;", + "pub use error::Error;", + "pub use receipt::SignReceipt;", + "pub use request::SignRequest;", + "pub use signer::Signer;", + "pub use status::SignerStatus;", + ]) + ); + assert!(!ROOT.contains("prelude")); +} + +#[test] +fn production_sources_publish_only_the_approved_traits_and_no_host_stack() { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut sources = Vec::new(); + collect_rust_sources(&source_root, &mut sources); + assert!(!sources.is_empty()); + let mut public_traits = BTreeSet::new(); + + for path in sources { + let source = fs::read_to_string(&path).expect("read signing source"); + let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); + for line in production.lines() { + let trimmed = line.trim_start(); + if let Some(name) = trimmed + .strip_prefix("pub trait ") + .and_then(|rest| rest.split([':', '<', ' ']).next()) + { + public_traits.insert(name.to_owned()); + } + for forbidden in [ + "nostr::", + "nostr_sdk::", + "reqwest::", + "sqlx::", + "tokio::", + "keyring::", + "std::fs", + "std::path", + "SecretKey", + "PrivateKey", + ] { + assert!( + !line.contains(forbidden), + "signing production source must not contain {forbidden}: {}: {trimmed}", + path.display() + ); + } + } + } + + assert_eq!( + public_traits, + ["ProgressObserver", "Signer"] + .into_iter() + .map(str::to_owned) + .collect() + ); +} + +fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { + let table = manifest + .split_once(heading) + .unwrap_or_else(|| panic!("missing manifest table {heading}")) + .1; + table + .lines() + .skip(1) + .take_while(|line| !line.trim_start().starts_with('[')) + .filter_map(|line| { + let line = line.trim(); + (line + .bytes() + .next() + .is_some_and(|byte| byte.is_ascii_lowercase() || byte == b'_') + && !line.starts_with('#')) + .then(|| line.split_once('=').map(|(key, _)| key.trim())) + .flatten() + }) + .collect() +} + +fn root_declarations(prefix: &str) -> BTreeSet<&str> { + ROOT.lines() + .map(str::trim) + .filter_map(|line| line.strip_prefix(prefix)) + .filter_map(|name| name.strip_suffix(';')) + .collect() +} + +fn collect_rust_sources(directory: &Path, paths: &mut Vec<std::path::PathBuf>) { + for entry in fs::read_dir(directory).expect("read signing source directory") { + let path = entry.expect("source directory entry").path(); + if path.is_dir() { + collect_rust_sources(&path, paths); + } else if path.extension().and_then(|value| value.to_str()) == Some("rs") { + paths.push(path); + } + } }