rust_cktap/
error.rs

1// Copyright (c) 2025 rust-cktap contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::Deserialize;
5use std::fmt::Debug;
6
7/// Errors returned by the card, CBOR deserialization or value encoding, or the APDU transport.
8#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
9pub enum CkTapError {
10    #[error(transparent)]
11    Card(#[from] CardError),
12    #[error("CBOR deserialization error: {0}")]
13    CborDe(String),
14    #[error("CBOR value error: {0}")]
15    CborValue(String),
16    #[error("APDU transport error: {0}")]
17    Transport(String),
18    #[error("Unknown card type")]
19    UnknownCardType,
20}
21
22/// Errors returned by the CkTap card.
23#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)]
24pub enum CardError {
25    #[error("Rare or unlucky value used/occurred. Start again")]
26    UnluckyNumber,
27    #[error("Invalid/incorrect/incomplete arguments provided to command")]
28    BadArguments,
29    #[error("Authentication details (CVC/epubkey) are wrong")]
30    BadAuth,
31    #[error("Command requires auth, and none was provided")]
32    NeedsAuth,
33    #[error("The 'cmd' field is an unsupported command")]
34    UnknownCommand,
35    #[error("Command is not valid at this time, no point retrying")]
36    InvalidCommand,
37    #[error("You can't do that right now when card is in this state")]
38    InvalidState,
39    #[error("Nonce is not unique-looking enough")]
40    WeakNonce,
41    #[error("Unable to decode CBOR data stream")]
42    BadCBOR,
43    #[error("Can't change CVC without doing a backup first")]
44    BackupFirst,
45    #[error("Due to auth failures, delay required")]
46    RateLimited,
47}
48
49impl CardError {
50    pub fn error_from_code(code: u16) -> Option<CardError> {
51        match code {
52            205 => Some(CardError::UnluckyNumber),
53            400 => Some(CardError::BadArguments),
54            401 => Some(CardError::BadAuth),
55            403 => Some(CardError::NeedsAuth),
56            404 => Some(CardError::UnknownCommand),
57            405 => Some(CardError::InvalidCommand),
58            406 => Some(CardError::InvalidState),
59            417 => Some(CardError::WeakNonce),
60            422 => Some(CardError::BadCBOR),
61            425 => Some(CardError::BackupFirst),
62            429 => Some(CardError::RateLimited),
63            _ => None,
64        }
65    }
66
67    pub fn error_code(&self) -> u16 {
68        match self {
69            CardError::UnluckyNumber => 205,
70            CardError::BadArguments => 400,
71            CardError::BadAuth => 401,
72            CardError::NeedsAuth => 403,
73            CardError::UnknownCommand => 404,
74            CardError::InvalidCommand => 405,
75            CardError::InvalidState => 406,
76            CardError::WeakNonce => 417,
77            CardError::BadCBOR => 422,
78            CardError::BackupFirst => 425,
79            CardError::RateLimited => 429,
80        }
81    }
82}
83
84impl<T> From<ciborium::de::Error<T>> for CkTapError
85where
86    T: Debug,
87{
88    fn from(e: ciborium::de::Error<T>) -> Self {
89        CkTapError::CborDe(e.to_string())
90    }
91}
92
93impl From<ciborium::value::Error> for CkTapError {
94    fn from(e: ciborium::value::Error) -> Self {
95        CkTapError::CborValue(e.to_string())
96    }
97}
98
99#[cfg(feature = "pcsc")]
100impl From<pcsc::Error> for CkTapError {
101    fn from(e: pcsc::Error) -> Self {
102        CkTapError::Transport(e.to_string())
103    }
104}
105
106#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
107pub struct ErrorResponse {
108    pub error: String,
109    pub code: u16,
110}
111
112/// Errors returned by the `status` command.
113#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
114pub enum StatusError {
115    #[error(transparent)]
116    CkTap(#[from] CkTapError),
117    #[error(transparent)]
118    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
119}
120
121#[cfg(feature = "pcsc")]
122impl From<pcsc::Error> for StatusError {
123    fn from(e: pcsc::Error) -> Self {
124        StatusError::CkTap(CkTapError::Transport(e.to_string()))
125    }
126}
127
128/// Errors returned by the `change` command.
129#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
130pub enum ChangeError {
131    #[error(transparent)]
132    CkTap(#[from] CkTapError),
133    #[error("new cvc is too short, must be at least 6 bytes, was only {0} bytes")]
134    TooShort(usize),
135    #[error("new cvc is too long, must be at most 32 bytes, was {0} bytes")]
136    TooLong(usize),
137    #[error("new cvc is the same as the old one")]
138    SameAsOld,
139}
140
141/// Errors returned by the `read` command.
142#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
143pub enum ReadError {
144    #[error(transparent)]
145    CkTap(#[from] CkTapError),
146    #[error(transparent)]
147    Secp256k1(#[from] bitcoin::secp256k1::Error),
148    #[error(transparent)]
149    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
150}
151
152impl From<ReadError> for CertsError {
153    fn from(e: ReadError) -> Self {
154        match e {
155            ReadError::CkTap(e) => CertsError::CkTap(e),
156            ReadError::Secp256k1(e) => CertsError::Secp256k1(e),
157            ReadError::KeyFromSlice(e) => CertsError::KeyFromSlice(e),
158        }
159    }
160}
161
162/// Errors returned by the `certs` command.
163#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
164pub enum CertsError {
165    #[error(transparent)]
166    CkTap(#[from] CkTapError),
167    #[error(transparent)]
168    Secp256k1(#[from] bitcoin::secp256k1::Error),
169    #[error(transparent)]
170    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
171    #[error("Root cert is not from Coinkite. Card is counterfeit: {0}")]
172    InvalidRootCert(String),
173}
174
175/// Errors returned by the `derive` command.
176#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
177pub enum DeriveError {
178    #[error(transparent)]
179    CkTap(#[from] CkTapError),
180    #[error(transparent)]
181    Secp256k1(#[from] bitcoin::secp256k1::Error),
182    #[error(transparent)]
183    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
184    #[error("Invalid chain code: {0}")]
185    InvalidChainCode(String),
186}
187
188/// Errors returned by the `xpub` command.
189#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
190pub enum XpubError {
191    #[error(transparent)]
192    CkTap(#[from] CkTapError),
193    #[error(transparent)]
194    Bip32(#[from] bitcoin::bip32::Error),
195}
196
197/// Errors returned by the `unseal` command.
198#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
199pub enum UnsealError {
200    #[error(transparent)]
201    CkTap(#[from] CkTapError),
202    #[error(transparent)]
203    Secp256k1(#[from] bitcoin::secp256k1::Error),
204    #[error(transparent)]
205    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
206}
207
208/// Errors returned by the `dump` command.
209#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
210pub enum DumpError {
211    #[error(transparent)]
212    CkTap(#[from] CkTapError),
213    #[error(transparent)]
214    Secp256k1(#[from] bitcoin::secp256k1::Error),
215    #[error(transparent)]
216    KeyFromSlice(#[from] bitcoin::key::FromSliceError),
217    #[error("Slot is sealed: {0}")]
218    SlotSealed(u8),
219    #[error("Slot is unused: {0}")]
220    SlotUnused(u8),
221    /// If the slot was unsealed due to confusion or uncertainty about its status.
222    /// In other words, if the card unsealed itself rather than via a
223    /// successful `unseal` command.
224    #[error("Slot was unsealed improperly: {0}")]
225    SlotTampered(u8),
226}
227
228#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
229pub enum SignPsbtError {
230    #[error("Invalid path at index: {0}")]
231    InvalidPath(usize),
232    #[error("Invalid script at index: {0}")]
233    InvalidScript(usize),
234    #[error("Missing pubkey at index: {0}")]
235    MissingPubkey(usize),
236    #[error("Missing UTXO at index: {0}")]
237    MissingUtxo(usize),
238    #[error("Pubkey mismatch at index: {0}")]
239    PubkeyMismatch(usize),
240    #[error("Sighash error: {0}")]
241    SighashError(String),
242    #[error("Signature error: {0}")]
243    SignatureError(String),
244    #[error("Signing slot is not unsealed: {0}")]
245    SlotNotUnsealed(u8),
246    #[error(transparent)]
247    CkTap(#[from] CkTapError),
248    #[error("Witness program error: {0}")]
249    WitnessProgram(String),
250}