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:
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);
+ }
+ }
}