Skip to main content

scll_core/
error.rs

1//! Typed error and warning surface — PDD §8.
2//!
3//! Errors are a real enum from day one (no stringly-typed codes), so tests can
4//! assert exact variants and the SW→error mapping can be exhaustively checked.
5//! The v0.5 category prefixes (`T_`/`S_`/`K_`/`C_`/`L_`/`P_`/`I_`/`V_`) survive
6//! only as the comment groupings below.
7//!
8//! `no_std`: the `thiserror::Error` derive is kept — `error_in_core` is stable
9//! since Rust 1.81, and `thiserror` ≥2 with `default-features = false` derives
10//! `core::error::Error`. Former `String`/`Vec<u8>` payloads are fixed-capacity
11//! `heapless` collections sized from [`crate::limits`].
12
13use heapless::{String, Vec};
14
15use crate::cap::CapError;
16use crate::command::BuildError;
17use crate::limits::{MAX_KEYTYPE_SUPPORTED, OTHER_DETAIL_MAX, WARNING_DETAIL_MAX};
18use crate::tlv::TlvError;
19
20/// Fatal error returned by every public workflow function (`Result<XReport, ScllError>`).
21#[derive(thiserror::Error, Debug)]
22#[non_exhaustive]
23pub enum ScllError {
24    // --- Transport (was T_*) ---
25    #[error("transport unavailable")]
26    TransportUnavailable,
27    #[error("card removed")]
28    CardRemoved,
29    #[error("reader gone")]
30    ReaderGone,
31    #[error("transport timeout")]
32    Timeout,
33
34    // --- Secure channel (was S_*) ---
35    #[error("no SCP protocol the library supports")]
36    ScpProtocolUnsupported,
37    #[error("no common security level")]
38    NoCommonSecurityLevel,
39    #[error("KVN mismatch (card vs supplied keys)")]
40    KvnMismatch,
41    #[error("card cryptogram verification failed")]
42    CardCryptogramFail,
43    #[error("pseudo-random card challenge verification failed")]
44    CardChallengeFail,
45    #[error("EXTERNAL AUTHENTICATE failed (sw={sw:#06x})")]
46    ExternalAuthFail { sw: u16 },
47    #[error("security status not satisfied")]
48    SecurityStatusNotSatisfied,
49    #[error("no secure channel is open on this manager")]
50    NoOpenChannel, // CardManager in-session method called before open_scp (PDD §3.6)
51
52    // --- Keys (was K_*) ---
53    #[error("referenced key not found")]
54    KeyNotFound,
55    #[error("key type unsupported")]
56    KeyTypeUnsupported {
57        offered: u8,
58        supported: Vec<u8, MAX_KEYTYPE_SUPPORTED>,
59    },
60    #[error("key check value mismatch")]
61    KeyCheckValueMismatch,
62    #[error("cannot delete the active keyset")]
63    CannotDeleteActiveKeyset,
64
65    // --- Content / CAP (was C_*) ---
66    #[error("AID length {len} out of range (must be 5..=16 bytes)")]
67    InvalidAid { len: usize },
68    #[error("AID already exists")]
69    AidAlreadyExists,
70    #[error("package AID already exists")]
71    PackageAidExists,
72    #[error("package not found")]
73    PackageNotFound,
74    #[error("resident SD module not found")]
75    ResidentSdNotFound,
76    #[error("load file too large for short APDUs")]
77    LoadTooLarge,
78    #[error("SSD still has applets")]
79    SsdHasApplets,
80    #[error("ELF has other instances")]
81    ElfHasOtherInstances,
82
83    // --- Life-cycle (was L_*) ---
84    #[error("card not usable (misconfigured or TERMINATED)")]
85    CardNotUsable,
86    #[error("illegal life-cycle transition")]
87    IllegalLifecycleTransition,
88    #[error("conditions of use not satisfied")]
89    ConditionsNotSatisfied,
90    #[error("ISD AID not found")]
91    IsdAidNotFound,
92    #[error("session is not against the ISD")]
93    SessionNotIsd,
94    #[error("target no longer exists")]
95    TargetNoLongerExists,
96    #[error("TERMINATED is out of scope as a set target")]
97    TerminateOutOfScope,
98
99    // --- Privilege (was P_*) ---
100    #[error("parent lacks Authorized Management")]
101    ParentLacksAm,
102    #[error("unsupported privilege")]
103    UnsupportedPrivilege,
104
105    // --- Card said no, with an SW the library does not map to a specific case ---
106    #[error("card returned status word {sw:#06x}")]
107    Card { sw: u16 },
108
109    // --- Internal parse / build (wrapped sub-errors; `?` from those layers) ---
110    #[error("malformed card response: {0}")]
111    MalformedResponse(#[from] TlvError),
112    #[error("APDU build error: {0}")]
113    Build(#[from] BuildError),
114    #[error("CAP parse error: {0}")]
115    Cap(#[from] CapError),
116
117    // --- Backend / crypto / key-handle (was V_*) ---
118    #[error(transparent)]
119    Backend(#[from] BackendError),
120}
121
122impl ScllError {
123    /// Map a *context-free* command status word to its [`ScllError`].
124    ///
125    /// This covers only the **general** error conditions of GPCS v2.3.1
126    /// Table 11-10 — the two status words whose meaning is invariant across
127    /// every GP command (`6982` security status, `6985` conditions of use).
128    /// Everything else falls through to the [`ScllError::Card`] catch-all, so
129    /// the mapping is *total*: every `u16` yields exactly one variant.
130    ///
131    /// Per-command refinements are deliberately **not** decided here. The same
132    /// SW means different things per command — e.g. `6985` is
133    /// `IllegalLifecycleTransition` for SET STATUS (Table 11-87) but
134    /// `ElfHasOtherInstances` for DELETE (Table 11-26); `6A88` is
135    /// `IsdAidNotFound`, `KeyNotFound`, or `PackageNotFound` by context. Those
136    /// live in the workflow layer, which inspects the SW before delegating the
137    /// residue to this general mapper (PDD §8 status-word coverage).
138    ///
139    /// Note: `9000` (success) is not an error; callers check for success before
140    /// calling this. Passing it returns `Card { sw: 0x9000 }` (harmless).
141    #[must_use]
142    pub fn from_general_sw(sw: u16) -> ScllError {
143        match sw {
144            // GPCS v2.3.1 Table 11-10 (general error conditions).
145            0x6982 => ScllError::SecurityStatusNotSatisfied,
146            0x6985 => ScllError::ConditionsNotSatisfied,
147            // Sole catch-all (PDD §8): no dedicated, context-free variant.
148            other => ScllError::Card { sw: other },
149        }
150    }
151}
152
153/// Non-fatal warning attached to a report's `warnings` (PDD §7/§8).
154#[derive(Debug, Clone)]
155pub struct Warning {
156    pub kind: WarningKind,
157    pub detail: String<WARNING_DETAIL_MAX>,
158}
159
160/// Typed warning kinds (PDD §8). No `String` codes.
161#[derive(Debug, Clone)]
162#[non_exhaustive]
163pub enum WarningKind {
164    CardRecognitionDataMissing, // was D_*
165    KeyInformationTemplateMissing,
166    CardCapabilityInfoMissing,
167    UnknownLifecycleByte(u8),
168    GetStatusParseFailed,
169    LifecycleNoOp, // was L_LifecycleNoOp
170    /// `get_card_inventory` (§5.12a) reached a `CardInventory` capacity bound
171    /// (`MAX_SDS` / `MAX_APPLETS` / `MAX_ELFS`) or the per-scope page cap
172    /// (`MAX_STATUS_PAGES`) before the card was exhausted; the returned
173    /// inventory is a valid prefix, not the full card content.
174    InventoryTruncated,
175}
176
177/// Coarse error surface returned by every backend trait method (PDD §3.3/§8).
178/// Carries an opaque `detail` so software and HSM/PKCS#11 backends can attach
179/// context without leaking key material.
180#[derive(thiserror::Error, Debug)]
181#[non_exhaustive]
182pub enum BackendError {
183    #[error("key import failed: {0}")]
184    KeyImport(String<OTHER_DETAIL_MAX>), // was V_KeyImport
185    #[error("key generation failed: {0}")]
186    KeyGen(String<OTHER_DETAIL_MAX>), // was V_KeyGen
187    #[error("crypto operation failed: {0}")]
188    Crypto(String<OTHER_DETAIL_MAX>), // was V_Crypto
189    #[error("RNG failure: {0}")]
190    Rng(String<OTHER_DETAIL_MAX>), // was V_Rng
191    #[error("operation unsupported by this backend: {0}")]
192    Unsupported(String<OTHER_DETAIL_MAX>),
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    /// The two dedicated arms map exactly, and the mapping is *total* with
200    /// `Card { sw }` as the sole catch-all: every other `u16` round-trips its
201    /// own `sw` through `Card`. Exhaustive over the whole `u16` space (PDD §8).
202    #[test]
203    fn general_sw_map_is_total_with_card_as_sole_catch_all() {
204        for sw in 0x0000u16..=0xFFFF {
205            match ScllError::from_general_sw(sw) {
206                ScllError::SecurityStatusNotSatisfied => assert_eq!(sw, 0x6982),
207                ScllError::ConditionsNotSatisfied => assert_eq!(sw, 0x6985),
208                ScllError::Card { sw: got } => {
209                    assert_eq!(got, sw, "Card must carry the input sw verbatim");
210                    assert!(
211                        sw != 0x6982 && sw != 0x6985,
212                        "dedicated SWs must not fall through"
213                    );
214                }
215                other => panic!("sw {sw:#06x} mapped to an unexpected variant: {other:?}"),
216            }
217        }
218    }
219
220    #[test]
221    fn dedicated_general_sws_map_to_their_variants() {
222        assert!(matches!(
223            ScllError::from_general_sw(0x6982),
224            ScllError::SecurityStatusNotSatisfied
225        ));
226        assert!(matches!(
227            ScllError::from_general_sw(0x6985),
228            ScllError::ConditionsNotSatisfied
229        ));
230    }
231
232    #[test]
233    fn success_word_is_not_special_cased() {
234        // 9000 is success, not an error; the general mapper has no opinion and
235        // returns the catch-all carrying the verbatim sw.
236        assert!(matches!(
237            ScllError::from_general_sw(0x9000),
238            ScllError::Card { sw: 0x9000 }
239        ));
240    }
241}