webauthn_kit/error.rs
1//! Error type for all `webauthn-kit` operations.
2
3/// Error type for `WebAuthn` operations.
4///
5/// The type is `#[non_exhaustive]`: matching on it requires a wildcard arm.
6///
7/// # Security note
8///
9/// Error strings may echo attacker-controlled fragments of the request being
10/// verified (e.g. a base64 payload or an origin string). They are intended for
11/// server-side logs and debugging. Do **not** forward raw error messages to
12/// untrusted clients; surface only a generic verification-failed response.
13#[non_exhaustive]
14#[derive(Debug, thiserror::Error)]
15pub enum WebauthnError {
16 /// A caller-supplied configuration value is invalid or inconsistent.
17 #[error("Configuration error: {0}")]
18 Config(String),
19 /// The presented challenge is unknown, malformed, or was already consumed
20 /// (single-use replay protection triggered).
21 #[error("Invalid challenge: {0}")]
22 InvalidChallenge(String),
23 /// The referenced credential does not exist for the given user/scope.
24 #[error("Credential not found: {0}")]
25 CredentialNotFound(String),
26 /// Generic verification failure (challenge mismatch, bad origin, wrong
27 /// `type` field, truncated authenticator data, RP ID hash mismatch, ...).
28 #[error("Verification failed: {0}")]
29 VerificationFailed(String),
30 /// The registration presented a credential ID that is already registered.
31 #[error("Duplicate credential: {0}")]
32 DuplicateCredential(String),
33 /// The referenced user does not exist.
34 #[error("User not found: {0}")]
35 UserNotFound(String),
36 /// The challenge was consumed after its freshness window elapsed.
37 #[error("Challenge expired")]
38 ChallengeExpired,
39 /// The COSE algorithm is not one of the supported/allowed algorithms.
40 #[error("Unsupported algorithm: {0}")]
41 UnsupportedAlgorithm(i32),
42 /// `ring` rejected the cryptographic signature.
43 #[error("Signature verification failed")]
44 SignatureVerificationFailed,
45 /// The attestation object or statement could not be parsed, or
46 /// attestation verification failed for a non-signature reason
47 /// (unsupported/unknown format, untrusted chain, expired certificate,
48 /// AAGUID extension mismatch, ...).
49 ///
50 /// Signature failures surface as [`WebauthnError::SignatureVerificationFailed`].
51 #[error("Attestation error: {0}")]
52 AttestationError(String),
53 /// A ceremony required user verification
54 /// ([`crate::policy::UserVerificationPolicy::Required`]) but the
55 /// authenticator's UV flag was clear.
56 #[error("User verification required but not performed")]
57 UserVerificationRequired,
58 /// The presented credential violates the caller's
59 /// [`crate::policy::CredentialPolicy`] (backup-eligibility policy, ...).
60 #[error("Credential policy violation: {0}")]
61 PolicyViolation(String),
62}
63
64// Tests exercise failure paths and invariants directly; unwrap/expect,
65// slicing, and panicking asserts are acceptable here — violations
66// surface as test failures, not production panics.
67#[allow(
68 clippy::unwrap_used,
69 clippy::expect_used,
70 clippy::indexing_slicing,
71 clippy::panic
72)]
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_error_display_variants() {
79 let errors = vec![
80 WebauthnError::Config("test".to_string()),
81 WebauthnError::InvalidChallenge("test".to_string()),
82 WebauthnError::CredentialNotFound("test".to_string()),
83 WebauthnError::VerificationFailed("test".to_string()),
84 WebauthnError::DuplicateCredential("test".to_string()),
85 WebauthnError::UserNotFound("test".to_string()),
86 WebauthnError::ChallengeExpired,
87 WebauthnError::UnsupportedAlgorithm(-7),
88 WebauthnError::SignatureVerificationFailed,
89 WebauthnError::AttestationError("test".to_string()),
90 WebauthnError::UserVerificationRequired,
91 WebauthnError::PolicyViolation("test".to_string()),
92 ];
93 for err in errors {
94 assert!(!format!("{err}").is_empty());
95 assert!(!format!("{err:?}").is_empty());
96 }
97 }
98}