Skip to main content

soft_fido2_ctap/
status.rs

1//! CTAP2 status codes
2//!
3//! Status codes defined in FIDO2 specification:
4//! <https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#error-responses>
5
6use core::fmt;
7use core::str::FromStr;
8
9/// CTAP2 status codes
10///
11/// These status codes are returned in CTAP responses to indicate success or various error conditions.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14#[repr(u8)]
15pub enum StatusCode {
16    /// Successful completion of command
17    Success = 0x00,
18
19    /// Invalid command
20    InvalidCommand = 0x01,
21
22    /// Invalid parameter in request
23    InvalidParameter = 0x02,
24
25    /// Invalid message or item length
26    InvalidLength = 0x03,
27
28    /// Invalid message sequencing
29    InvalidSeq = 0x04,
30
31    /// Message timed out
32    Timeout = 0x05,
33
34    /// Channel busy
35    ChannelBusy = 0x06,
36
37    /// Command requires channel lock
38    LockRequired = 0x0A,
39
40    /// Invalid channel
41    InvalidChannel = 0x0B,
42
43    /// CBOR unexpected type
44    CborUnexpectedType = 0x11,
45
46    /// Invalid CBOR encoding
47    InvalidCbor = 0x12,
48
49    /// Missing required parameter
50    MissingParameter = 0x14,
51
52    /// Limit exceeded
53    LimitExceeded = 0x15,
54
55    /// Unsupported extension
56    UnsupportedExtension = 0x16,
57
58    /// Credential excluded (already exists)
59    CredentialExcluded = 0x19,
60
61    /// Processing (e.g. waiting for user presence)
62    Processing = 0x21,
63
64    /// Invalid credential
65    InvalidCredential = 0x22,
66
67    /// User action pending
68    UserActionPending = 0x23,
69
70    /// Operation pending
71    OperationPending = 0x24,
72
73    /// No operations pending
74    NoOperations = 0x25,
75
76    /// Unsupported algorithm
77    UnsupportedAlgorithm = 0x26,
78
79    /// Operation denied by user
80    OperationDenied = 0x27,
81
82    /// Key store full
83    KeyStoreFull = 0x28,
84
85    /// Not busy
86    NotBusy = 0x29,
87
88    /// No operation pending
89    NoOperationPending = 0x2A,
90
91    /// Unsupported option
92    UnsupportedOption = 0x2B,
93
94    /// Invalid option
95    InvalidOption = 0x2C,
96
97    /// Keepalive cancel
98    KeepaliveCancel = 0x2D,
99
100    /// No credentials found
101    NoCredentials = 0x2E,
102
103    /// User action timeout
104    UserActionTimeout = 0x2F,
105
106    /// Not allowed
107    NotAllowed = 0x30,
108
109    /// PIN invalid
110    PinInvalid = 0x31,
111
112    /// PIN blocked
113    PinBlocked = 0x32,
114
115    /// PIN/UV auth parameter invalid
116    PinAuthInvalid = 0x33,
117
118    /// PIN/UV auth blocked
119    PinAuthBlocked = 0x34,
120
121    /// PIN not set
122    PinNotSet = 0x35,
123
124    /// PIN/UV auth token required
125    PuatRequired = 0x36,
126
127    /// PIN policy violation
128    PinPolicyViolation = 0x37,
129
130    /// Request too large
131    RequestTooLarge = 0x39,
132
133    /// Action timeout
134    ActionTimeout = 0x3A,
135
136    /// User presence required
137    UpRequired = 0x3B,
138
139    /// User verification blocked
140    UvBlocked = 0x3C,
141
142    /// Integrity failure
143    IntegrityFailure = 0x3D,
144
145    /// Invalid subcommand
146    InvalidSubcommand = 0x3E,
147
148    /// User verification invalid
149    UvInvalid = 0x3F,
150
151    /// Unauthorized permission
152    UnauthorizedPermission = 0x40,
153
154    /// Other unspecified error
155    Other = 0x7F,
156}
157
158impl fmt::Display for StatusCode {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        let msg = match self {
161            Self::Success => "Success",
162            Self::InvalidCommand => "Invalid command",
163            Self::InvalidParameter => "Invalid parameter",
164            Self::InvalidLength => "Invalid length",
165            Self::InvalidSeq => "Invalid sequence",
166            Self::Timeout => "Timeout",
167            Self::ChannelBusy => "Channel busy",
168            Self::LockRequired => "Lock required",
169            Self::InvalidChannel => "Invalid channel",
170            Self::CborUnexpectedType => "CBOR unexpected type",
171            Self::InvalidCbor => "Invalid CBOR",
172            Self::MissingParameter => "Missing parameter",
173            Self::LimitExceeded => "Limit exceeded",
174            Self::UnsupportedExtension => "Unsupported extension",
175            Self::CredentialExcluded => "Credential excluded",
176            Self::Processing => "Processing",
177            Self::InvalidCredential => "Invalid credential",
178            Self::UserActionPending => "User action pending",
179            Self::OperationPending => "Operation pending",
180            Self::NoOperations => "No operations",
181            Self::UnsupportedAlgorithm => "Unsupported algorithm",
182            Self::OperationDenied => "Operation denied",
183            Self::KeyStoreFull => "Key store full",
184            Self::NotBusy => "Not busy",
185            Self::NoOperationPending => "No operation pending",
186            Self::UnsupportedOption => "Unsupported option",
187            Self::InvalidOption => "Invalid option",
188            Self::KeepaliveCancel => "Keepalive cancel",
189            Self::NoCredentials => "No credentials",
190            Self::UserActionTimeout => "User action timeout",
191            Self::NotAllowed => "Not allowed",
192            Self::PinInvalid => "PIN invalid",
193            Self::PinBlocked => "PIN blocked",
194            Self::PinAuthInvalid => "PIN auth invalid",
195            Self::PinAuthBlocked => "PIN auth blocked",
196            Self::PinNotSet => "PIN not set",
197            Self::PuatRequired => "PIN/UV auth token required",
198            Self::PinPolicyViolation => "PIN policy violation",
199            Self::RequestTooLarge => "Request too large",
200            Self::ActionTimeout => "Action timeout",
201            Self::UpRequired => "UP required",
202            Self::UvBlocked => "UV blocked",
203            Self::IntegrityFailure => "Integrity failure",
204            Self::InvalidSubcommand => "Invalid subcommand",
205            Self::UvInvalid => "UV invalid",
206            Self::UnauthorizedPermission => "Unauthorized permission",
207            Self::Other => "Other error",
208        };
209        write!(f, "{}", msg)
210    }
211}
212
213/// Implement std::error::Error only when std is available
214#[cfg(feature = "std")]
215impl std::error::Error for StatusCode {}
216
217impl StatusCode {
218    /// Deprecated CTAP 2.0 source-level alias. Serializes as
219    /// CTAP2_ERR_PUAT_REQUIRED (0x36) under the modern registry.
220    #[allow(non_upper_case_globals)]
221    #[deprecated(note = "use StatusCode::PuatRequired")]
222    pub const PinRequired: Self = Self::PuatRequired;
223
224    /// Deprecated CTAP 2.0 source-level alias. The 0x38 wire value is
225    /// reserved in modern CTAP, so expired tokens map to PIN_AUTH_INVALID.
226    #[allow(non_upper_case_globals)]
227    #[deprecated(note = "use StatusCode::PinAuthInvalid")]
228    pub const PinTokenExpired: Self = Self::PinAuthInvalid;
229
230    /// Convert status code to byte value
231    pub fn to_u8(self) -> u8 {
232        self as u8
233    }
234
235    /// Create status code from byte value
236    pub fn from_u8(value: u8) -> Self {
237        match value {
238            0x00 => Self::Success,
239            0x01 => Self::InvalidCommand,
240            0x02 => Self::InvalidParameter,
241            0x03 => Self::InvalidLength,
242            0x04 => Self::InvalidSeq,
243            0x05 => Self::Timeout,
244            0x06 => Self::ChannelBusy,
245            0x0A => Self::LockRequired,
246            0x0B => Self::InvalidChannel,
247            0x11 => Self::CborUnexpectedType,
248            0x12 => Self::InvalidCbor,
249            0x14 => Self::MissingParameter,
250            0x15 => Self::LimitExceeded,
251            0x16 => Self::UnsupportedExtension,
252            0x19 => Self::CredentialExcluded,
253            0x21 => Self::Processing,
254            0x22 => Self::InvalidCredential,
255            0x23 => Self::UserActionPending,
256            0x24 => Self::OperationPending,
257            0x25 => Self::NoOperations,
258            0x26 => Self::UnsupportedAlgorithm,
259            0x27 => Self::OperationDenied,
260            0x28 => Self::KeyStoreFull,
261            0x29 => Self::NotBusy,
262            0x2A => Self::NoOperationPending,
263            0x2B => Self::UnsupportedOption,
264            0x2C => Self::InvalidOption,
265            0x2D => Self::KeepaliveCancel,
266            0x2E => Self::NoCredentials,
267            0x2F => Self::UserActionTimeout,
268            0x30 => Self::NotAllowed,
269            0x31 => Self::PinInvalid,
270            0x32 => Self::PinBlocked,
271            0x33 => Self::PinAuthInvalid,
272            0x34 => Self::PinAuthBlocked,
273            0x35 => Self::PinNotSet,
274            0x36 => Self::PuatRequired,
275            0x37 => Self::PinPolicyViolation,
276            0x39 => Self::RequestTooLarge,
277            0x3A => Self::ActionTimeout,
278            0x3B => Self::UpRequired,
279            0x3C => Self::UvBlocked,
280            0x3D => Self::IntegrityFailure,
281            0x3E => Self::InvalidSubcommand,
282            0x3F => Self::UvInvalid,
283            0x40 => Self::UnauthorizedPermission,
284            _ => Self::Other,
285        }
286    }
287
288    /// Check if this is a success status
289    pub fn is_success(self) -> bool {
290        self == Self::Success
291    }
292}
293
294impl From<StatusCode> for u8 {
295    fn from(status: StatusCode) -> u8 {
296        status.to_u8()
297    }
298}
299
300impl From<u8> for StatusCode {
301    fn from(value: u8) -> Self {
302        Self::from_u8(value)
303    }
304}
305
306impl From<soft_fido2_crypto::CryptoError> for StatusCode {
307    fn from(err: soft_fido2_crypto::CryptoError) -> Self {
308        match err {
309            soft_fido2_crypto::CryptoError::InvalidPublicKey => Self::InvalidParameter,
310            soft_fido2_crypto::CryptoError::InvalidPrivateKey => Self::InvalidParameter,
311            soft_fido2_crypto::CryptoError::InvalidSignature => Self::InvalidParameter,
312            soft_fido2_crypto::CryptoError::DecryptionFailed => Self::PinAuthInvalid,
313            soft_fido2_crypto::CryptoError::EncryptionFailed => Self::Other,
314            soft_fido2_crypto::CryptoError::InvalidKeyLength { .. } => Self::InvalidParameter,
315            soft_fido2_crypto::CryptoError::KeyAgreementFailed => Self::Other,
316            soft_fido2_crypto::CryptoError::InvalidCoseKey => Self::InvalidParameter,
317            _ => Self::Other,
318        }
319    }
320}
321
322/// Error type for parsing status codes from strings
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub struct ParseStatusCodeError;
325
326impl fmt::Display for ParseStatusCodeError {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        write!(f, "invalid status code string")
329    }
330}
331
332impl FromStr for StatusCode {
333    type Err = ParseStatusCodeError;
334
335    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
336        match s {
337            "Success" => Ok(Self::Success),
338            "InvalidCommand" => Ok(Self::InvalidCommand),
339            "InvalidParameter" => Ok(Self::InvalidParameter),
340            "InvalidLength" => Ok(Self::InvalidLength),
341            "InvalidSeq" => Ok(Self::InvalidSeq),
342            "Timeout" => Ok(Self::Timeout),
343            "ChannelBusy" => Ok(Self::ChannelBusy),
344            "LockRequired" => Ok(Self::LockRequired),
345            "InvalidChannel" => Ok(Self::InvalidChannel),
346            "CborUnexpectedType" => Ok(Self::CborUnexpectedType),
347            "InvalidCbor" => Ok(Self::InvalidCbor),
348            "MissingParameter" => Ok(Self::MissingParameter),
349            "LimitExceeded" => Ok(Self::LimitExceeded),
350            "UnsupportedExtension" => Ok(Self::UnsupportedExtension),
351            "CredentialExcluded" => Ok(Self::CredentialExcluded),
352            "Processing" => Ok(Self::Processing),
353            "InvalidCredential" => Ok(Self::InvalidCredential),
354            "UserActionPending" => Ok(Self::UserActionPending),
355            "OperationPending" => Ok(Self::OperationPending),
356            "NoOperations" => Ok(Self::NoOperations),
357            "UnsupportedAlgorithm" => Ok(Self::UnsupportedAlgorithm),
358            "OperationDenied" => Ok(Self::OperationDenied),
359            "KeyStoreFull" => Ok(Self::KeyStoreFull),
360            "NotBusy" => Ok(Self::NotBusy),
361            "NoOperationPending" => Ok(Self::NoOperationPending),
362            "UnsupportedOption" => Ok(Self::UnsupportedOption),
363            "InvalidOption" => Ok(Self::InvalidOption),
364            "KeepaliveCancel" => Ok(Self::KeepaliveCancel),
365            "NoCredentials" => Ok(Self::NoCredentials),
366            "UserActionTimeout" => Ok(Self::UserActionTimeout),
367            "NotAllowed" => Ok(Self::NotAllowed),
368            "PinInvalid" => Ok(Self::PinInvalid),
369            "PinBlocked" => Ok(Self::PinBlocked),
370            "PinAuthInvalid" => Ok(Self::PinAuthInvalid),
371            "PinAuthBlocked" => Ok(Self::PinAuthBlocked),
372            "PinNotSet" => Ok(Self::PinNotSet),
373            "PinRequired" => Ok(Self::PuatRequired),
374            "PinPolicyViolation" => Ok(Self::PinPolicyViolation),
375            "PinTokenExpired" => Ok(Self::PinAuthInvalid),
376            "RequestTooLarge" => Ok(Self::RequestTooLarge),
377            "ActionTimeout" => Ok(Self::ActionTimeout),
378            "UpRequired" => Ok(Self::UpRequired),
379            "UvBlocked" => Ok(Self::UvBlocked),
380            "IntegrityFailure" => Ok(Self::IntegrityFailure),
381            "InvalidSubcommand" => Ok(Self::InvalidSubcommand),
382            "UvInvalid" => Ok(Self::UvInvalid),
383            "UnauthorizedPermission" => Ok(Self::UnauthorizedPermission),
384            "PuatRequired" => Ok(Self::PuatRequired),
385            "Other" => Ok(Self::Other),
386            _ => Err(ParseStatusCodeError),
387        }
388    }
389}
390
391/// Result type for CTAP operations
392pub type Result<T> = core::result::Result<T, StatusCode>;
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_status_code_round_trip() {
400        let codes = vec![
401            StatusCode::Success,
402            StatusCode::InvalidCommand,
403            StatusCode::PinInvalid,
404            StatusCode::OperationDenied,
405        ];
406
407        for code in codes {
408            let byte = code.to_u8();
409            let recovered = StatusCode::from_u8(byte);
410            assert_eq!(code, recovered);
411        }
412    }
413
414    #[test]
415    fn test_unknown_status_code() {
416        let unknown = StatusCode::from_u8(0xFF);
417        assert_eq!(unknown, StatusCode::Other);
418    }
419
420    #[test]
421    fn test_is_success() {
422        assert!(StatusCode::Success.is_success());
423        assert!(!StatusCode::InvalidCommand.is_success());
424    }
425
426    #[test]
427    fn test_from_crypto_error() {
428        let status: StatusCode = soft_fido2_crypto::CryptoError::InvalidPublicKey.into();
429        assert_eq!(status, StatusCode::InvalidParameter);
430
431        let status: StatusCode = soft_fido2_crypto::CryptoError::DecryptionFailed.into();
432        assert_eq!(status, StatusCode::PinAuthInvalid);
433    }
434
435    #[test]
436    fn test_from_str() {
437        assert_eq!("Success".parse::<StatusCode>(), Ok(StatusCode::Success));
438        assert_eq!(
439            "InvalidCommand".parse::<StatusCode>(),
440            Ok(StatusCode::InvalidCommand)
441        );
442        assert_eq!(
443            "PinInvalid".parse::<StatusCode>(),
444            Ok(StatusCode::PinInvalid)
445        );
446        assert!("InvalidString".parse::<StatusCode>().is_err());
447    }
448
449    #[test]
450    fn ctap_2_3_pin_uv_status_values_are_wire_correct() {
451        assert_eq!(StatusCode::PuatRequired.to_u8(), 0x36);
452        assert_eq!(StatusCode::UnauthorizedPermission.to_u8(), 0x40);
453        assert_eq!(StatusCode::from_u8(0x36), StatusCode::PuatRequired);
454    }
455
456    #[test]
457    fn reserved_and_non_standard_pin_uv_values_are_not_accepted() {
458        assert_eq!(StatusCode::from_u8(0x38), StatusCode::Other);
459        assert_eq!(StatusCode::from_u8(0x41), StatusCode::Other);
460    }
461
462    #[test]
463    #[allow(deprecated)]
464    fn legacy_source_aliases_use_modern_wire_values() {
465        assert_eq!(StatusCode::PinRequired.to_u8(), 0x36);
466        assert_eq!(StatusCode::PinTokenExpired.to_u8(), 0x33);
467    }
468}