commit 2219a79cc9ea05afd53a424d683cf895c66a1732
parent 23ee816e25f36062535c6ef33b0f772c6c8a9a69
Author: triesap <tyson@radroots.org>
Date: Thu, 30 Jul 2026 11:32:57 +0000
signing: normalize signing errors from one authority
- replace temporary construction failures with one normalized signing error
- derive code class retry and recovery metadata from governed catalogs
- preserve native sources while redacting diagnostics and protocol reports
- add signer-output protocol authority and refresh generated inventory
Diffstat:
9 files changed, 374 insertions(+), 146 deletions(-)
diff --git a/contracts/codegen/protocol_v1.inventory.json b/contracts/codegen/protocol_v1.inventory.json
@@ -42,7 +42,7 @@
{
"module": "error::v1",
"path": "crates/protocol/src/error/v1.rs",
- "sha256": "59553954506a56aca8c2cdef385b67e593514f846bbda3ac38907f73f58a8da6",
+ "sha256": "f6f8578988a7a3cf57e0a3081fa592a5e16998e666696e24c41368ea93226b80",
"types": [
{
"rust_path": "radroots_protocol::error::v1::CapabilityId",
diff --git a/contracts/codegen/protocol_v1.inventory.sha256 b/contracts/codegen/protocol_v1.inventory.sha256
@@ -1 +1 @@
-36d98bfc7a76f8ac9c0bd36f6775b8ae2024ff821593d76ceef57ebc559a331c
+86a317bf2eb502ca0c89f627a823c3df9652b95a4b1a71169b03ffb9e2f9b4ce
diff --git a/crates/protocol/src/error/v1.rs b/crates/protocol/src/error/v1.rs
@@ -156,6 +156,7 @@ error_catalog! {
SignerRejected => ("signer_rejected", Signer, false, [SelectAuthorizedActor]),
SignerTimeout => ("signer_timeout", Signer, true, [RetryAfterTransportFailure]),
SignerCancelled => ("signer_cancelled", Signer, false, [ConfigureSigner]),
+ SignerOutputInvalid => ("signer_output_invalid", Signer, false, [ConfigureSigner]),
RelayAuthRequired => ("relay_auth_required", Network, true, [CompleteSignerAuthentication]),
RelayAuthRejected => ("relay_auth_rejected", Network, false, [CompleteSignerAuthentication]),
RelayPaymentRequired => ("relay_payment_required", Network, false, [ConfigureTransportTargets]),
@@ -762,7 +763,7 @@ mod tests {
#[test]
fn generated_catalog_is_complete_unique_and_self_consistent() {
- assert_eq!(CATALOG.len(), 56);
+ assert_eq!(CATALOG.len(), 57);
assert_eq!(KnownCode::ALL.len(), CATALOG.len());
let mut codes = BTreeSet::new();
for (index, descriptor) in CATALOG.iter().enumerate() {
diff --git a/crates/signing/src/actor.rs b/crates/signing/src/actor.rs
@@ -4,10 +4,11 @@
//! It describes host selection and provenance but does not select accounts,
//! acquire keys, or prove that a host granted a role.
-use core::fmt;
use radroots_event::contract::AuthorRole;
use radroots_identity::{AccountId, PublicKey};
+use crate::{Error, error::Kind};
+
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeSet;
#[cfg(feature = "std")]
@@ -102,29 +103,6 @@ impl ActorResolutionRequest {
}
}
-/// Validation failures while constructing actor provenance.
-#[non_exhaustive]
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum ActorError {
- /// Text did not contain a canonical public key.
- InvalidPublicKey,
- /// Account provenance did not identify the same public key as the actor.
- AccountPublicKeyMismatch,
-}
-
-impl fmt::Display for ActorError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self {
- Self::InvalidPublicKey => "actor public key is invalid",
- Self::AccountPublicKeyMismatch => {
- "actor account identifier does not match the actor public key"
- }
- })
- }
-}
-
-impl core::error::Error for ActorError {}
-
/// An actor-role claim and its public provenance.
///
/// Construction validates that account-backed provenance and the public key
@@ -139,14 +117,14 @@ pub struct Actor {
impl Actor {
/// Creates actor provenance from canonical public values.
- pub fn new<I>(public_key: PublicKey, source: ActorSource, roles: I) -> Result<Self, ActorError>
+ pub fn new<I>(public_key: PublicKey, source: ActorSource, roles: I) -> Result<Self, Error>
where
I: IntoIterator<Item = AuthorRole>,
{
if let Some(account_id) = source.account_id()
&& account_id.as_bytes() != public_key.as_bytes()
{
- return Err(ActorError::AccountPublicKeyMismatch);
+ return Err(Error::new(Kind::InvalidArgument));
}
Ok(Self {
public_key,
@@ -160,12 +138,12 @@ impl Actor {
public_key: &str,
source: ActorSource,
roles: I,
- ) -> Result<Self, ActorError>
+ ) -> Result<Self, Error>
where
I: IntoIterator<Item = AuthorRole>,
{
let public_key =
- PublicKey::from_hex(public_key).map_err(|_| ActorError::InvalidPublicKey)?;
+ PublicKey::from_hex(public_key).map_err(|_| Error::new(Kind::InvalidArgument))?;
Self::new(public_key, source, roles)
}
@@ -245,22 +223,21 @@ mod tests {
assert_eq!(actor.account_id(), Some(account_id(ALICE)));
}
- assert_eq!(
- Actor::new(
- public_key(ALICE),
- ActorSource::LocalAccount(account_id(BOB)),
- [AuthorRole::Farmer],
- ),
- Err(ActorError::AccountPublicKeyMismatch)
- );
+ let error = Actor::new(
+ public_key(ALICE),
+ ActorSource::LocalAccount(account_id(BOB)),
+ [AuthorRole::Farmer],
+ )
+ .expect_err("mismatched account must fail");
+ assert_eq!(error.kind(), Kind::InvalidArgument);
}
#[test]
fn invalid_public_key_text_is_rejected() {
- assert_eq!(
- Actor::from_public_key_hex("not-a-public-key", ActorSource::ExplicitPublicKey, []),
- Err(ActorError::InvalidPublicKey)
- );
+ let error =
+ Actor::from_public_key_hex("not-a-public-key", ActorSource::ExplicitPublicKey, [])
+ .expect_err("invalid public key must fail");
+ assert_eq!(error.kind(), Kind::InvalidArgument);
}
#[test]
diff --git a/crates/signing/src/error.rs b/crates/signing/src/error.rs
@@ -1,19 +1,302 @@
-//! Normalized signing failures.
+//! Normalized, secret-safe signing failures.
use core::fmt;
-/// A signing failure.
+use radroots_protocol::{
+ error::v1::{Class, Descriptor as ProtocolDescriptor, ErrorReport, KnownCode, RecoveryAction},
+ runtime::v1::OperationId,
+};
+
+#[cfg(feature = "std")]
+use std::boxed::Box;
+
+/// Stable native signing failure kinds.
///
-/// Step 104 replaces this opaque pre-release value with the governed error
-/// catalog. It intentionally carries no dependency-specific or secret data.
+/// These variants describe the signing contract rather than any concrete
+/// signer library. Additive variants remain possible before 1.0.
#[non_exhaustive]
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct Error;
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum Kind {
+ InvalidArgument,
+ AuthorizationDenied,
+ SignerCapabilityMissing,
+ SignerUnavailable,
+ SignerRejected,
+ SignerTimeout,
+ SignerCancelled,
+ SignerOutputInvalid,
+ DeadlineExceeded,
+ InternalError,
+}
+
+/// One signing error descriptor generated from the native-to-protocol map.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct Descriptor {
+ kind: Kind,
+ code: KnownCode,
+ message: &'static str,
+}
+
+impl Descriptor {
+ #[must_use]
+ pub const fn kind(self) -> Kind {
+ self.kind
+ }
+
+ #[must_use]
+ pub const fn known_code(self) -> KnownCode {
+ self.code
+ }
+
+ #[must_use]
+ pub const fn code(self) -> &'static str {
+ self.code.as_str()
+ }
+
+ #[must_use]
+ pub const fn class(self) -> Class {
+ self.protocol_descriptor().class
+ }
+
+ #[must_use]
+ pub const fn retryable(self) -> bool {
+ self.protocol_descriptor().retryable
+ }
+
+ #[must_use]
+ pub const fn recovery_actions(self) -> &'static [RecoveryAction] {
+ self.protocol_descriptor().recovery_actions
+ }
+
+ #[must_use]
+ pub const fn message(self) -> &'static str {
+ self.message
+ }
+
+ const fn protocol_descriptor(self) -> ProtocolDescriptor {
+ self.code.descriptor()
+ }
+}
+
+macro_rules! signing_error_catalog {
+ ($( $variant:ident => ($code:ident, $message:literal) ),+ $(,)?) => {
+ impl Kind {
+ /// Every native signing failure kind in stable catalog order.
+ pub const ALL: &'static [Self] = &[$(Self::$variant),+];
+
+ /// Returns metadata generated from this package's single mapping
+ /// and the protocol catalog's single class/recovery authority.
+ #[must_use]
+ pub const fn descriptor(self) -> Descriptor {
+ match self {
+ $(Self::$variant => Descriptor {
+ kind: Self::$variant,
+ code: KnownCode::$code,
+ message: $message,
+ },)+
+ }
+ }
+ }
+
+ /// Complete stable native signing error catalog.
+ pub const CATALOG: &[Descriptor] = &[
+ $(Descriptor {
+ kind: Kind::$variant,
+ code: KnownCode::$code,
+ message: $message,
+ },)+
+ ];
+ };
+}
+
+signing_error_catalog! {
+ InvalidArgument => (InvalidArgument, "signing request is invalid"),
+ AuthorizationDenied => (AuthorizationDenied, "signing authorization was denied"),
+ SignerCapabilityMissing => (SignerCapabilityMissing, "required signer capability is missing"),
+ SignerUnavailable => (SignerUnavailable, "signer is unavailable"),
+ SignerRejected => (SignerRejected, "signer rejected the request"),
+ SignerTimeout => (SignerTimeout, "signer timed out"),
+ SignerCancelled => (SignerCancelled, "signing was cancelled"),
+ SignerOutputInvalid => (SignerOutputInvalid, "signer output did not match the frozen draft"),
+ DeadlineExceeded => (DeadlineExceeded, "signing deadline was exceeded"),
+ InternalError => (InternalError, "internal signing failure"),
+}
+
+/// A normalized signing failure with an optional native source.
+///
+/// Display and debug output are stable and never copy source text. Under the
+/// `std` feature callers may inspect the explicit `source()` chain for local
+/// diagnostics; protocol conversion always discards it.
+pub struct Error {
+ kind: Kind,
+ #[cfg(feature = "std")]
+ source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
+}
+
+impl Error {
+ /// Creates a source-free normalized failure.
+ #[must_use]
+ pub const fn new(kind: Kind) -> Self {
+ Self {
+ kind,
+ #[cfg(feature = "std")]
+ source: None,
+ }
+ }
+
+ /// Preserves a native source without exposing it through display, debug,
+ /// or protocol serialization.
+ #[cfg(feature = "std")]
+ pub fn with_source<E>(kind: Kind, source: E) -> Self
+ where
+ E: std::error::Error + Send + Sync + 'static,
+ {
+ Self {
+ kind,
+ source: Some(Box::new(source)),
+ }
+ }
+
+ #[must_use]
+ pub const fn kind(&self) -> Kind {
+ self.kind
+ }
+
+ #[must_use]
+ pub const fn descriptor(&self) -> Descriptor {
+ self.kind.descriptor()
+ }
+
+ #[must_use]
+ pub const fn code(&self) -> &'static str {
+ self.descriptor().code()
+ }
+
+ #[must_use]
+ pub const fn class(&self) -> Class {
+ self.descriptor().class()
+ }
+
+ #[must_use]
+ pub const fn retryable(&self) -> bool {
+ self.descriptor().retryable()
+ }
+
+ #[must_use]
+ pub const fn recovery_actions(&self) -> &'static [RecoveryAction] {
+ self.descriptor().recovery_actions()
+ }
+
+ /// Produces the versioned boundary report without copying source text.
+ #[must_use]
+ pub fn to_report(&self, operation_id: Option<OperationId>) -> ErrorReport {
+ ErrorReport::redacted_from_source(self.descriptor().known_code(), operation_id, None)
+ }
+}
+
+impl fmt::Debug for Error {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut value = formatter.debug_struct("Error");
+ value.field("kind", &self.kind);
+ #[cfg(feature = "std")]
+ value.field("source", &self.source.as_ref().map(|_| "[redacted]"));
+ value.finish()
+ }
+}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str("signing operation failed")
+ formatter.write_str(self.descriptor().message())
}
}
-impl core::error::Error for Error {}
+impl core::error::Error for Error {
+ #[cfg(feature = "std")]
+ fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
+ self.source
+ .as_deref()
+ .map(|source| source as &(dyn core::error::Error + 'static))
+ }
+}
+
+impl From<&Error> for ErrorReport {
+ fn from(error: &Error) -> Self {
+ error.to_report(None)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[cfg(feature = "std")]
+ use std::{collections::BTreeSet, error::Error as _};
+
+ #[test]
+ fn catalog_codes_are_unique_and_metadata_matches_protocol_authority() {
+ assert_eq!(CATALOG.len(), Kind::ALL.len());
+ let mut codes = alloc_or_std_set();
+ for (index, descriptor) in CATALOG.iter().copied().enumerate() {
+ assert!(codes.insert(descriptor.code()));
+ assert_eq!(descriptor.kind(), Kind::ALL[index]);
+ assert_eq!(descriptor.kind().descriptor(), descriptor);
+ let protocol = descriptor.known_code().descriptor();
+ assert_eq!(descriptor.class(), protocol.class);
+ assert_eq!(descriptor.retryable(), protocol.retryable);
+ assert_eq!(descriptor.recovery_actions(), protocol.recovery_actions);
+ assert!(!descriptor.message().is_empty());
+ let report = Error::new(descriptor.kind()).to_report(None);
+ assert_eq!(report.code().known_code(), Some(descriptor.known_code()));
+ assert_eq!(report.class(), descriptor.class());
+ assert_eq!(report.retryable(), descriptor.retryable());
+ assert_eq!(report.recovery_actions(), descriptor.recovery_actions());
+ assert_eq!(report.message().as_str(), "[redacted]");
+ }
+ }
+
+ #[cfg(feature = "std")]
+ #[test]
+ fn native_source_is_preserved_but_diagnostics_and_reports_are_redacted() {
+ #[derive(Debug)]
+ struct SensitiveSource;
+
+ impl fmt::Display for SensitiveSource {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("nsec1-do-not-disclose")
+ }
+ }
+
+ impl std::error::Error for SensitiveSource {}
+
+ let error = Error::with_source(Kind::SignerUnavailable, SensitiveSource);
+ assert_eq!(
+ error.source().expect("source").to_string(),
+ "nsec1-do-not-disclose"
+ );
+ assert!(!error.to_string().contains("nsec1"));
+ assert!(!format!("{error:?}").contains("nsec1"));
+ assert_eq!(error.code(), "signer_unavailable");
+ assert!(error.retryable());
+
+ let report = error.to_report(Some(OperationId::SyncPush));
+ assert_eq!(report.code().as_str(), "signer_unavailable");
+ assert_eq!(report.operation_id(), Some(OperationId::SyncPush));
+ assert_eq!(report.message().as_str(), "[redacted]");
+ #[cfg(feature = "serde")]
+ assert!(
+ !serde_json::to_string(&report)
+ .expect("report")
+ .contains("nsec1")
+ );
+ }
+
+ #[cfg(feature = "std")]
+ fn alloc_or_std_set() -> BTreeSet<&'static str> {
+ BTreeSet::new()
+ }
+
+ #[cfg(not(feature = "std"))]
+ fn alloc_or_std_set() -> alloc::collections::BTreeSet<&'static str> {
+ alloc::collections::BTreeSet::new()
+ }
+}
diff --git a/crates/signing/src/receipt.rs b/crates/signing/src/receipt.rs
@@ -4,7 +4,7 @@ use core::fmt;
use radroots_event::{SignedEvent, draft::validate_signed_nostr_event_matches_draft};
use radroots_protocol::runtime::v1::OperationId;
-use crate::{Error, SignRequest};
+use crate::{Error, SignRequest, error::Kind};
/// Successful signer output with portable operation provenance.
#[non_exhaustive]
@@ -36,8 +36,19 @@ impl SignReceipt {
signed_event: SignedEvent,
completed_at_unix: u64,
) -> Result<Self, Error> {
- validate_signed_nostr_event_matches_draft(&signed_event, request.draft())
- .map_err(|_| Error)?;
+ validate_signed_nostr_event_matches_draft(&signed_event, request.draft()).map_err(
+ |source| {
+ #[cfg(feature = "std")]
+ {
+ Error::with_source(Kind::SignerOutputInvalid, source)
+ }
+ #[cfg(not(feature = "std"))]
+ {
+ let _ = source;
+ Error::new(Kind::SignerOutputInvalid)
+ }
+ },
+ )?;
Ok(Self {
operation_id: request.operation_id(),
signed_event,
@@ -207,10 +218,9 @@ mod tests {
];
for event in cases {
- assert_eq!(
- SignReceipt::from_signed_event(&request, event, 42),
- Err(Error)
- );
+ let error =
+ SignReceipt::from_signed_event(&request, event, 42).expect_err("drift must fail");
+ assert_eq!(error.kind(), Kind::SignerOutputInvalid);
}
}
}
diff --git a/crates/signing/src/request.rs b/crates/signing/src/request.rs
@@ -9,7 +9,7 @@ use alloc::sync::Arc;
#[cfg(feature = "std")]
use std::sync::Arc;
-use crate::{Actor, Error, status::SignProgress};
+use crate::{Actor, Error, error::Kind, status::SignProgress};
/// How a signer must interpret cancellation around remote publication.
#[non_exhaustive]
@@ -36,12 +36,9 @@ pub struct SignPolicy {
impl SignPolicy {
/// Creates a bounded policy. Unix timestamp zero is never a valid deadline.
- pub const fn new(
- deadline_unix: u64,
- cancellation: CancellationPolicy,
- ) -> Result<Self, SignPolicyError> {
+ pub const fn new(deadline_unix: u64, cancellation: CancellationPolicy) -> Result<Self, Error> {
if deadline_unix == 0 {
- return Err(SignPolicyError::InvalidDeadline);
+ return Err(Error::new(Kind::InvalidArgument));
}
Ok(Self {
deadline_unix,
@@ -62,21 +59,6 @@ impl SignPolicy {
}
}
-/// Invalid signing policy input.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum SignPolicyError {
- /// The deadline was the Unix epoch sentinel rather than a real bound.
- InvalidDeadline,
-}
-
-impl fmt::Display for SignPolicyError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str("signing deadline must be greater than zero")
- }
-}
-
-impl core::error::Error for SignPolicyError {}
-
/// Runtime-local observer for signing progress.
///
/// Observers are not serialized, persisted, or invoked by hidden workers.
@@ -105,7 +87,7 @@ impl SignRequest {
draft: EventDraft,
policy: SignPolicy,
) -> Result<Self, Error> {
- authorize_actor_for_draft(&actor, &draft).map_err(|_| Error)?;
+ authorize_actor_for_draft(&actor, &draft).map_err(authorization_error)?;
Ok(Self {
operation_id,
actor,
@@ -180,6 +162,14 @@ fn authorize_actor_for_draft(
Ok(())
}
+fn authorization_error(failure: AuthorizationFailure) -> Error {
+ match failure {
+ AuthorizationFailure::InvalidDraft => Error::new(Kind::InvalidArgument),
+ AuthorizationFailure::ActorRoleUnsatisfied
+ | AuthorizationFailure::ActorPublicKeyMismatch => Error::new(Kind::AuthorizationDenied),
+ }
+}
+
impl fmt::Debug for SignRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
@@ -254,10 +244,9 @@ mod tests {
#[test]
fn policy_requires_a_real_deadline() {
- assert_eq!(
- SignPolicy::new(0, CancellationPolicy::LocalCooperative),
- Err(SignPolicyError::InvalidDeadline)
- );
+ let error = SignPolicy::new(0, CancellationPolicy::LocalCooperative)
+ .expect_err("zero deadline must fail");
+ assert_eq!(error.kind(), Kind::InvalidArgument);
}
#[test]
diff --git a/crates/signing/src/signer.rs b/crates/signing/src/signer.rs
@@ -40,6 +40,7 @@ mod tests {
use crate::{
Actor,
actor::ActorSource,
+ error::Kind,
request::{CancellationPolicy, SignPolicy},
};
use core::sync::atomic::{AtomicUsize, Ordering};
@@ -67,7 +68,7 @@ mod tests {
}
fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> {
- Box::pin(async { Err(Error) })
+ Box::pin(async { Err(Error::new(Kind::InternalError)) })
}
}
@@ -77,7 +78,7 @@ mod tests {
}
fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> {
- Box::pin(async { Err(Error) })
+ Box::pin(async { Err(Error::new(Kind::InternalError)) })
}
}
@@ -88,7 +89,7 @@ mod tests {
fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> {
self.0.fetch_add(1, Ordering::Relaxed);
- Box::pin(async { Err(Error) })
+ Box::pin(async { Err(Error::new(Kind::InternalError)) })
}
}
@@ -169,7 +170,10 @@ mod tests {
),
];
for request in requests {
- assert!(request.is_err());
+ assert_eq!(
+ request.as_ref().expect_err("request must fail").kind(),
+ Kind::AuthorizationDenied
+ );
if let Ok(request) = request {
drop(signer.sign(request));
}
diff --git a/crates/signing/src/status.rs b/crates/signing/src/status.rs
@@ -7,7 +7,7 @@ use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::{string::String, vec::Vec};
-use crate::capability::SignerCapability;
+use crate::{Error, capability::SignerCapability, error::Kind};
const MAX_AUTH_URI_BYTES: usize = 2_048;
@@ -28,19 +28,19 @@ impl AuthChallenge {
uri: impl Into<String>,
required_at_unix: u64,
expires_at_unix: Option<u64>,
- ) -> Result<Self, AuthChallengeError> {
+ ) -> Result<Self, Error> {
let uri = uri.into();
if uri.len() > MAX_AUTH_URI_BYTES
|| uri.trim() != uri
|| !uri.starts_with("https://")
|| uri.chars().any(char::is_control)
{
- return Err(AuthChallengeError::InvalidUri);
+ return Err(Error::new(Kind::InvalidArgument));
}
if let Some(expires_at_unix) = expires_at_unix
&& expires_at_unix < required_at_unix
{
- return Err(AuthChallengeError::ExpiresBeforeRequired);
+ return Err(Error::new(Kind::InvalidArgument));
}
Ok(Self {
uri,
@@ -99,26 +99,6 @@ impl<'de> serde::Deserialize<'de> for AuthChallenge {
}
}
-/// Invalid authentication challenge input.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum AuthChallengeError {
- /// The URI was not bounded canonical HTTPS text.
- InvalidUri,
- /// The challenge expiry preceded the required timestamp.
- ExpiresBeforeRequired,
-}
-
-impl fmt::Display for AuthChallengeError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self {
- Self::InvalidUri => "authentication challenge URI is invalid",
- Self::ExpiresBeforeRequired => "authentication challenge expires before it is required",
- })
- }
-}
-
-impl core::error::Error for AuthChallengeError {}
-
/// Stable signing progress stages.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -146,9 +126,9 @@ pub struct SignProgress {
impl SignProgress {
/// Creates a progress update without an authentication challenge.
- pub const fn stage(stage: SignProgressStage) -> Result<Self, SignProgressError> {
+ pub const fn stage(stage: SignProgressStage) -> Result<Self, Error> {
if matches!(stage, SignProgressStage::AwaitingAuthentication) {
- return Err(SignProgressError::MissingAuthenticationChallenge);
+ return Err(Error::new(Kind::InvalidArgument));
}
Ok(Self {
stage,
@@ -196,37 +176,15 @@ impl<'de> serde::Deserialize<'de> for SignProgress {
(SignProgressStage::AwaitingAuthentication, Some(challenge)) => {
Ok(Self::authentication(challenge))
}
- (SignProgressStage::AwaitingAuthentication, None) => Err(serde::de::Error::custom(
- SignProgressError::MissingAuthenticationChallenge,
- )),
- (_, Some(_)) => Err(serde::de::Error::custom(
- SignProgressError::UnexpectedAuthenticationChallenge,
- )),
+ (SignProgressStage::AwaitingAuthentication, None) => {
+ Err(serde::de::Error::custom(Error::new(Kind::InvalidArgument)))
+ }
+ (_, Some(_)) => Err(serde::de::Error::custom(Error::new(Kind::InvalidArgument))),
(stage, None) => Self::stage(stage).map_err(serde::de::Error::custom),
}
}
}
-/// Invalid progress construction.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum SignProgressError {
- MissingAuthenticationChallenge,
- UnexpectedAuthenticationChallenge,
-}
-
-impl fmt::Display for SignProgressError {
- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
- formatter.write_str(match self {
- Self::MissingAuthenticationChallenge => "authentication progress requires a challenge",
- Self::UnexpectedAuthenticationChallenge => {
- "only authentication progress may carry a challenge"
- }
- })
- }
-}
-
-impl core::error::Error for SignProgressError {}
-
/// Current signer availability.
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -306,20 +264,26 @@ mod tests {
assert_eq!(challenge.expires_at_unix(), Some(20));
assert!(!format!("{challenge:?}").contains("sensitive"));
assert_eq!(
- AuthChallenge::new("http://auth.example", 10, None),
- Err(AuthChallengeError::InvalidUri)
+ AuthChallenge::new("http://auth.example", 10, None)
+ .expect_err("HTTP challenge must fail")
+ .kind(),
+ Kind::InvalidArgument
);
assert_eq!(
- AuthChallenge::new("https://auth.example", 20, Some(10)),
- Err(AuthChallengeError::ExpiresBeforeRequired)
+ AuthChallenge::new("https://auth.example", 20, Some(10))
+ .expect_err("invalid expiry must fail")
+ .kind(),
+ Kind::InvalidArgument
);
}
#[test]
fn progress_requires_challenges_only_at_the_authentication_stage() {
assert_eq!(
- SignProgress::stage(SignProgressStage::AwaitingAuthentication),
- Err(SignProgressError::MissingAuthenticationChallenge)
+ SignProgress::stage(SignProgressStage::AwaitingAuthentication)
+ .expect_err("missing challenge must fail")
+ .kind(),
+ Kind::InvalidArgument
);
let challenge =
AuthChallenge::new("https://auth.example/approve", 10, None).expect("challenge");