1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! Error types for the webauthn library.
//!
//! Every variant produces a message that aids debugging without leaking
//! security-sensitive material (key bytes, challenge values, etc.).
use thiserror::Error;
/// All errors that can be returned by WebAuthn ceremony verification.
#[derive(Debug, Error)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WebAuthnError {
/// The client data JSON could not be decoded or is structurally invalid.
#[error("Invalid client data: {0}")]
InvalidClientData(String),
/// The challenge inside the client data does not match the issued challenge.
///
/// Security-critical: a mismatch means the response was not produced for
/// this ceremony instance.
#[error("Challenge mismatch: expected challenge does not match response")]
ChallengeMismatch,
/// The `origin` field in the client data does not match the expected origin.
///
/// Prevents a credential from one origin being replayed at another.
#[error("Origin mismatch: expected {expected}, got {got}")]
OriginMismatch { expected: String, got: String },
/// The RP ID hash in authenticator data does not equal SHA-256(rp_id).
///
/// Ensures the authenticator bound the credential to the correct relying party.
#[error("RP ID hash mismatch")]
RpIdHashMismatch,
/// The User Present (UP) flag is not set in the authenticator data flags byte.
#[error("User Present flag not set")]
UserNotPresent,
/// The User Verification (UV) flag is not set, but the relying party has
/// `require_user_verification` enabled.
///
/// The authenticator must perform user verification (PIN, biometric, etc.)
/// before the assertion is accepted.
#[error("User Verification flag not set")]
UserNotVerified,
/// The attestation object could not be decoded or is missing required fields.
#[error("Invalid attestation object: {0}")]
InvalidAttestationObject(String),
/// The authenticator data bytes are malformed or too short.
#[error("Invalid authenticator data: {0}")]
InvalidAuthenticatorData(String),
/// The COSE public key inside the credential data is invalid.
#[error("Invalid public key: {0}")]
InvalidPublicKey(String),
/// ECDSA signature verification returned a failure.
///
/// The message was either tampered with or signed by the wrong key.
#[error("Signature verification failed")]
SignatureVerificationFailed,
/// The sign count in the assertion is not greater than the stored sign count.
///
/// Indicates a possible authenticator clone or replay attack.
#[error("Sign count invalid: stored {stored}, received {received}")]
SignCountInvalid { stored: u32, received: u32 },
/// A CBOR decoding step failed.
#[error("CBOR decode error: {0}")]
CborDecodeError(String),
/// A base64url decoding step failed.
#[error("Base64 decode error: {0}")]
Base64DecodeError(String),
/// The challenge was issued too long ago and is no longer valid.
///
/// Callers should generate a new challenge and restart the ceremony.
#[error("Challenge expired")]
ChallengeExpired,
/// The challenge has already been consumed by a previous ceremony.
///
/// This error is only returned when the relying party has opted in to
/// single-use challenge enforcement via
/// [`crate::RelyingParty::enforce_single_use_challenges`]. Issue a fresh
/// challenge and restart the ceremony.
#[error("Challenge was already used in a previous ceremony")]
ChallengePreviouslyUsed,
/// The COSE algorithm identifier is not supported by this library.
///
/// The `i64` is the raw COSE algorithm integer (e.g. `-7` = ES256, `-257` = RS256).
#[error("Unsupported algorithm: {0}")]
UnsupportedAlgorithm(i64),
/// `clientDataJSON` contains `crossOrigin: true` but the relying party has
/// `reject_cross_origin` enabled.
///
/// Cross-origin credentials allow assertions from an iframe whose origin
/// differs from the top-level origin. When the RP does not expect embedded
/// usage, `crossOrigin: true` may indicate credential abuse.
#[error("Cross-origin credential use is not permitted by this relying party")]
CrossOriginNotAllowed,
/// The credential has the Backup Eligibility (BE) flag set, but this relying
/// party has `reject_backup_eligible` enabled.
///
/// Use this policy when your threat model requires hardware-bound keys that
/// cannot be synced to a cloud or platform account.
#[error("Credential is backup-eligible but this relying party does not permit backed-up credentials")]
BackupEligibleNotAllowed,
/// The credential does not have the Backup Eligibility (BE) flag set, but
/// this relying party has `require_backup_eligible` enabled.
///
/// Use this policy for consumer passkey deployments that depend on credential
/// sync (e.g. cross-device sign-in via iCloud Keychain or Google Password Manager).
#[error("Credential is not backup-eligible but this relying party requires backup-eligible credentials")]
BackupEligibilityRequired,
/// The Backup Eligibility (BE) flag in the authenticator data differs from
/// the value recorded at registration time.
///
/// BE is immutable per spec — a mismatch indicates a possible credential
/// substitution attack (a different authenticator presenting the same credential ID).
#[error(
"Backup Eligibility flag changed since registration — credential may have been substituted"
)]
BackupEligibilityChanged,
/// The `x5c` certificate chain in an attestation statement is structurally
/// invalid: a certificate in the chain is not signed by the next certificate,
/// or the DER encoding is malformed.
///
/// The inner string identifies which link in the chain failed and why.
#[error("Attestation certificate chain invalid: {0}")]
AttestationChainInvalid(String),
/// The `x5c` chain is structurally valid but its root certificate is not
/// signed by any of the configured trust anchors.
///
/// This error is only returned when the relying party has configured at least
/// one trust anchor via [`crate::RelyingParty::trust_anchors`]. When no
/// trust anchors are configured the chain structure is still verified but
/// the root is accepted unconditionally.
#[error("Attestation root certificate is not trusted by any configured trust anchor")]
AttestationRootUntrusted,
}
/// Convenience alias so callers write `webauthn::Result<T>`.
pub type Result<T> = std::result::Result<T, WebAuthnError>;