Skip to main content

canokey_protocol/
error.rs

1//! Structured protocol failures, independent of transport and binding errors.
2use crate::StatusWord;
3/// Stable semantic categories for protocol failures. Transport errors stay outside
4/// the core; callers must allow future variants of this non-exhaustive enum.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum ErrorKind {
8    /// A caller value or options combination is invalid.
9    InvalidArgument,
10    /// PIN/PUK bytes violate applet input rules.
11    InvalidPin,
12    /// Response framing or semantic content is malformed.
13    InvalidResponse,
14    /// The exchange sequence violates its conversation rules.
15    ProtocolViolation,
16    /// A frame, input, response, nesting, or exchange budget was exceeded.
17    LimitExceeded,
18    /// Explicit credential verification failed; retry information may be present.
19    AuthenticationFailed,
20    /// The card's mutual-authentication cryptogram did not match the host challenge.
21    DeviceAuthenticationFailed,
22    /// A credential reference is blocked.
23    PinBlocked,
24    /// Required authentication/security state is absent.
25    SecurityStatusNotSatisfied,
26    /// The card reports unmet execution conditions.
27    ConditionsNotSatisfied,
28    /// A requested applet object/reference was not found.
29    NotFound,
30    /// The required applet/device could not be selected.
31    UnsupportedDevice,
32    /// A capability or instruction is known to be unavailable.
33    UnsupportedFeature,
34    /// The requested algorithm cannot be used for this operation.
35    UnsupportedAlgorithm,
36    /// Available evidence cannot establish required capability support.
37    CapabilityUnknown,
38    /// An observed format/version is not understood (including certificate encoding).
39    UnsupportedProtocolVersion,
40    /// An unmapped status was returned; inspect `Error::status_word` or
41    /// `Error::application_status`.
42    UnexpectedStatusWord,
43    /// A local lifecycle method was called in an invalid state.
44    OperationStateError,
45}
46/// Context in which a protocol error was classified.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Phase {
49    /// Input/configuration validation or an error without a more specific phase.
50    Construction,
51    /// Applet selection.
52    Select,
53    /// Target applet command.
54    Command,
55    /// Credential verification or authentication exchange.
56    Authentication,
57    /// Response or structured-byte decoding.
58    Parsing,
59    /// Physical continuation, correction, or segmentation.
60    Conversation,
61}
62/// Credential involved in an authentication error; never contains its bytes.
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64pub enum SecretReference {
65    /// PIV user PIN.
66    Pin,
67    /// PIV PIN unblocking key.
68    Puk,
69    /// PIV management key.
70    ManagementKey,
71    /// Admin applet PIN.
72    AdminPin,
73    /// OATH access-code key, distinct from a PIN retry counter.
74    OathAccess,
75    /// OpenPGP PW1 for signatures (reference 81).
76    Pw1Sign,
77    /// OpenPGP PW1 for decipher/authentication (reference 82).
78    Pw1Other,
79    /// OpenPGP administrative password (reference 83).
80    Pw3,
81    /// OpenPGP resetting code.
82    ResetCode,
83}
84/// Owned, cloneable protocol failure with no secret payload.
85/// Display is diagnostic English; applications own localization and transport errors.
86#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
87#[error("{kind:?} during {phase:?}")]
88pub struct Error {
89    /// Semantic failure category.
90    pub kind: ErrorKind,
91    /// Command/parser context used to classify the error.
92    pub phase: Phase,
93    /// Original status when the failure came from a card status word.
94    pub status_word: Option<StatusWord>,
95    /// Applet-level non-ISO status byte reported in the response payload
96    /// (e.g. the CTAP status byte), when the failure came from such a status.
97    /// Distinct from `status_word`, which is always an ISO 7816 SW1-SW2.
98    pub application_status: Option<u8>,
99    /// Credential reference when known; absent for non-authentication failures.
100    pub reference: Option<SecretReference>,
101    /// Retry count from an authentication 63Cx status; absent when not reported.
102    pub retries_remaining: Option<u8>,
103}
104impl Error {
105    /// Construct a failure in Construction phase with no card status or credential.
106    pub fn new(kind: ErrorKind) -> Self {
107        Self {
108            kind,
109            phase: Phase::Construction,
110            status_word: None,
111            application_status: None,
112            reference: None,
113            retries_remaining: None,
114        }
115    }
116    /// Replace the phase while retaining all other error details.
117    pub fn at(mut self, phase: Phase) -> Self {
118        self.phase = phase;
119        self
120    }
121    /// Classify a card status using its phase and optional credential reference.
122    ///
123    /// SELECT 6A82/6A88 maps to UnsupportedDevice; elsewhere it maps to NotFound.
124    /// 63Cx is AuthenticationFailed only with a credential reference. Unknown
125    /// statuses remain UnexpectedStatusWord with their raw value. This function
126    /// is for failure paths: even 9000 maps to UnexpectedStatusWord here.
127    pub fn status(sw: StatusWord, phase: Phase, reference: Option<SecretReference>) -> Self {
128        let pin_reference = matches!(
129            reference,
130            Some(
131                SecretReference::Pin
132                    | SecretReference::Puk
133                    | SecretReference::AdminPin
134                    | SecretReference::Pw1Sign
135                    | SecretReference::Pw1Other
136                    | SecretReference::Pw3
137                    | SecretReference::ResetCode
138            )
139        );
140        let kind = match sw.raw() {
141            0x6983 if pin_reference => ErrorKind::PinBlocked,
142            0x6982 => ErrorKind::SecurityStatusNotSatisfied,
143            0x6985 => ErrorKind::ConditionsNotSatisfied,
144            0x6a82 | 0x6a88 if phase == Phase::Select => ErrorKind::UnsupportedDevice,
145            0x6a82 | 0x6a88 => ErrorKind::NotFound,
146            0x6d00 | 0x6e00 => ErrorKind::UnsupportedFeature,
147            n if n & 0xfff0 == 0x63c0 && reference.is_some() => ErrorKind::AuthenticationFailed,
148            _ => ErrorKind::UnexpectedStatusWord,
149        };
150        Self {
151            kind,
152            phase,
153            status_word: Some(sw),
154            application_status: None,
155            reference,
156            retries_remaining: if pin_reference && sw.raw() & 0xfff0 == 0x63c0 {
157                Some((sw.raw() & 15) as u8)
158            } else {
159                None
160            },
161        }
162    }
163}