Skip to main content

rite_sdk/
backend.rs

1//! Backend trait hierarchy for cryptographic hardware abstraction.
2//!
3//! ## Design philosophy
4//!
5//! Backends are **orthogonal to roles and participants**. They represent the
6//! execution mechanism (how an action is implemented), not the human assignment
7//! (who performs the action).
8//!
9//! The four layers of abstraction:
10//! - **Step**: Unit of execution in the workflow
11//! - **Role**: WHO performs the step (human/participant)
12//! - **Action**: WHAT semantic operation happens
13//! - **Backend**: HOW the action is implemented (software/hardware)
14//!
15//! ## Trait composition
16//!
17//! Backends implement only the traits they're capable of. There are no universal
18//! requirements: traits are composable based on backend capabilities.
19//!
20//! Example trait support matrix:
21//! - Software: `KeyStore + Sign + KeyTransport`
22//! - `YubiKey` PIV: `KeyStore + Sign + Piv + Yubikey`
23//! - PKCS#11 HSM: `KeyStore + Sign + KeyTransport + Pkcs11`
24//! - Mock: All traits (for testing)
25
26use std::collections::BTreeMap;
27
28use crate::types::{
29    Attestation, CertRef, KeyId, KeyMetadata, KeySecurityAttributes, KeySpec, PcrValue,
30    PivDeviceInfo, PivSlot, PivSlotInfo, Pkcs11Mechanism, Pkcs11TokenInfo, SignAlgorithm, TpmInfo,
31    WrapAlgorithm, WrappedKey, YubikeySlotMetadata,
32};
33
34/// Implement the `as_*_mut` upcasting helpers on a backend struct.
35///
36/// Each `Backend` implementor must return `Some(self)` for every trait it
37/// supports and `None` for those it does not. Without this macro that is four
38/// lines of boilerplate per capability. The macro expands to those lines
39/// automatically and allows attaching per-capability doc comments.
40///
41/// # Usage
42///
43/// ```
44/// use rite_sdk::{
45///     backend_capabilities, Backend, BackendError, KeyId, RandomBackend, SignAlgorithm,
46///     SignBackend,
47/// };
48///
49/// struct MyBackend {
50///     name: String,
51/// }
52///
53/// impl Backend for MyBackend {
54///     fn name(&self) -> &str { &self.name }
55///     fn provider(&self) -> &str { "software" }
56///     fn fingerprint(&self) -> String { "sha256:...".to_string() }
57///
58///     backend_capabilities!(
59///         /// Signs with software-held RSA and ECDSA keys.
60///         as_sign_mut: SignBackend,
61///         /// Draws ceremony randomness from the OS CSPRNG.
62///         as_random_mut: RandomBackend,
63///     );
64/// }
65///
66/// impl SignBackend for MyBackend {
67///     fn sign(&mut self, _key: &KeyId, _msg: &[u8], _alg: SignAlgorithm)
68///         -> Result<Vec<u8>, BackendError> { Ok(Vec::new()) }
69///     fn verify(&self, _key: &KeyId, _msg: &[u8], _sig: &[u8], _alg: SignAlgorithm)
70///         -> Result<bool, BackendError> { Ok(true) }
71/// }
72///
73/// impl RandomBackend for MyBackend {
74///     fn generate_random(&mut self, len: usize) -> Result<Vec<u8>, BackendError> {
75///         Ok(vec![0u8; len])
76///     }
77/// }
78/// ```
79///
80/// Capabilities not listed here are not overridden, so they inherit the trait's
81/// default doc ("Not supported by this backend.") and return `None`.
82///
83/// The trait names passed to the macro must be in scope at the call site.
84///
85/// Note: `#[macro_export]` places this macro at the crate root
86/// (`rite_sdk::backend_capabilities!`), not in the `backend` module.
87/// This is standard Rust macro scoping.
88#[macro_export]
89macro_rules! backend_capabilities {
90    ($($(#[$attr:meta])* $method:ident : $trait:ident),* $(,)?) => {
91        $(
92            $(#[$attr])*
93            fn $method(&mut self) -> Option<&mut dyn $trait> {
94                Some(self)
95            }
96        )*
97    };
98}
99
100/// Core trait that all backends must implement.
101///
102/// Provides identity and fingerprinting for audit trails.
103///
104/// # Capability downcasting
105///
106/// Rust does not allow downcasting from `dyn Backend` to `dyn KeyStoreBackend`
107/// (or any other subtrait) without explicit support. The `as_*_mut` methods
108/// implement this pattern manually: a backend returns `Some(self)` for each
109/// capability it supports, and `None` for those it does not.
110///
111/// Use the [`backend_capabilities!`] macro to generate these methods without
112/// boilerplate.
113pub trait Backend: Send + Sync {
114    /// Backend name (unique identifier within a ceremony).
115    fn name(&self) -> &str;
116
117    /// Backend provider name (e.g., "software", "yubikey", "pkcs11").
118    fn provider(&self) -> &str;
119
120    /// Fingerprint for audit evidence.
121    ///
122    /// Format examples:
123    /// - Software: `"software-backend=mybackend"`
124    /// - `YubiKey`: `"yubikey-serial=12345678+firmware=5.7.1"`
125    /// - PKCS#11: `"pkcs11-module=/usr/lib/softhsm2.so+slot=0"`
126    fn fingerprint(&self) -> String;
127
128    /// Not supported by this backend, always returns `None`.
129    fn as_keystore_mut(&mut self) -> Option<&mut dyn KeyStoreBackend> {
130        None
131    }
132
133    /// Not supported by this backend, always returns `None`.
134    fn as_sign_mut(&mut self) -> Option<&mut dyn SignBackend> {
135        None
136    }
137
138    /// Not supported by this backend, always returns `None`.
139    fn as_attest_mut(&mut self) -> Option<&mut dyn AttestationBackend> {
140        None
141    }
142
143    /// Not supported by this backend, always returns `None`.
144    fn as_piv_mut(&mut self) -> Option<&mut dyn PivBackend> {
145        None
146    }
147
148    /// Not supported by this backend, always returns `None`.
149    fn as_yubikey_mut(&mut self) -> Option<&mut dyn YubikeyBackend> {
150        None
151    }
152
153    /// Not supported by this backend, always returns `None`.
154    fn as_transport_mut(&mut self) -> Option<&mut dyn KeyTransportBackend> {
155        None
156    }
157
158    /// Not supported by this backend, always returns `None`.
159    fn as_certstore_mut(&mut self) -> Option<&mut dyn CertStoreBackend> {
160        None
161    }
162
163    /// Not supported by this backend, always returns `None`.
164    fn as_random_mut(&mut self) -> Option<&mut dyn RandomBackend> {
165        None
166    }
167
168    /// Not supported by this backend, always returns `None`.
169    fn as_pkcs11_mut(&mut self) -> Option<&mut dyn Pkcs11Backend> {
170        None
171    }
172
173    /// Not supported by this backend, always returns `None`.
174    fn as_pkcs11_admin_mut(&mut self) -> Option<&mut dyn Pkcs11AdminBackend> {
175        None
176    }
177
178    /// Not supported by this backend, always returns `None`.
179    fn as_tpm_mut(&mut self) -> Option<&mut dyn TpmBackend> {
180        None
181    }
182}
183
184/// Key generation and storage operations.
185///
186/// Backends implementing this trait can generate, import, and manage
187/// cryptographic keys.
188pub trait KeyStoreBackend: Backend {
189    /// Generate a new key.
190    ///
191    /// The backend owns the key and returns metadata including an opaque
192    /// `KeyId` reference. For hardware backends, the private key never leaves
193    /// the device.
194    fn generate_key(&mut self, spec: KeySpec) -> Result<KeyMetadata, BackendError>;
195
196    /// Import an existing private key.
197    ///
198    /// Only supported by backends that allow key import (software, some HSMs).
199    /// Hardware security modules may reject key import for security reasons.
200    ///
201    /// `key_bytes` must be in PKCS#8 DER format.
202    fn import_private_key(
203        &mut self,
204        spec: KeySpec,
205        key_bytes: &[u8],
206    ) -> Result<KeyMetadata, BackendError>;
207
208    /// Export the public key for `key_id` in `SubjectPublicKeyInfo` (SPKI) DER format.
209    fn export_public_key(&self, key_id: &KeyId) -> Result<Vec<u8>, BackendError>;
210
211    /// List all keys managed by this backend.
212    fn list_keys(&self) -> Result<Vec<KeyMetadata>, BackendError>;
213
214    /// Delete a key permanently.
215    fn delete_key(&mut self, key_id: &KeyId) -> Result<(), BackendError>;
216}
217
218/// Signing and verification operations.
219pub trait SignBackend: Backend {
220    /// Sign `message` with `key_id` using `algorithm`.
221    ///
222    /// Returns raw signature bytes (format depends on algorithm).
223    fn sign(
224        &mut self,
225        key_id: &KeyId,
226        message: &[u8],
227        algorithm: SignAlgorithm,
228    ) -> Result<Vec<u8>, BackendError>;
229
230    /// Verify `signature` over `message` with `key_id` and `algorithm`.
231    fn verify(
232        &self,
233        key_id: &KeyId,
234        message: &[u8],
235        signature: &[u8],
236        algorithm: SignAlgorithm,
237    ) -> Result<bool, BackendError>;
238}
239
240/// Hardware attestation operations.
241///
242/// Backends that support attestation can cryptographically prove that a key
243/// was generated on a specific hardware device.
244pub trait AttestationBackend: Backend {
245    /// Return a cryptographic proof that `key_id` was generated on this
246    /// backend's hardware and has specific properties.
247    fn attest_key(&self, key_id: &KeyId) -> Result<Attestation, BackendError>;
248}
249
250/// Certificate storage operations.
251///
252/// Backends implementing this trait can store, read, and delete X.509
253/// certificates by reference. This is a unified interface for certificate
254/// management across PIV (slot-based), PKCS#11 (label/ID-based), and
255/// software backends.
256pub trait CertStoreBackend: Backend {
257    /// Store a DER-encoded X.509 certificate.
258    fn store_cert(&mut self, cert_ref: &CertRef, cert_der: &[u8]) -> Result<(), BackendError>;
259
260    /// Read a DER-encoded X.509 certificate.
261    fn read_cert(&self, cert_ref: &CertRef) -> Result<Vec<u8>, BackendError>;
262
263    /// Delete a certificate.
264    fn delete_cert(&mut self, cert_ref: &CertRef) -> Result<(), BackendError>;
265}
266
267/// Hardware or software random number generation.
268///
269/// Backends implementing this trait can generate random bytes. For HSM
270/// backends, this uses the device's internal RNG (e.g., PKCS#11 `C_GenerateRandom`).
271pub trait RandomBackend: Backend {
272    /// Generate `len` random bytes.
273    fn generate_random(&mut self, len: usize) -> Result<Vec<u8>, BackendError>;
274}
275
276/// Key transport (wrapping) operations.
277pub trait KeyTransportBackend: Backend {
278    /// Wrap `key_id` using `wrapping_key_id` with `algorithm`.
279    fn wrap(
280        &mut self,
281        key_id: &KeyId,
282        wrapping_key_id: &KeyId,
283        algorithm: WrapAlgorithm,
284    ) -> Result<WrappedKey, BackendError>;
285
286    /// Unwrap `wrapped` using `unwrapping_key_id`, importing the result with `label`.
287    fn unwrap(
288        &mut self,
289        wrapped: &WrappedKey,
290        unwrapping_key_id: &KeyId,
291        label: &str,
292    ) -> Result<KeyMetadata, BackendError>;
293
294    /// Wrap `key_id` to an external recipient's raw public key.
295    ///
296    /// Default implementation returns `UnsupportedOperation`.
297    fn wrap_to_public(
298        &mut self,
299        key_id: &KeyId,
300        recipient_pub_key: &[u8],
301        algorithm: WrapAlgorithm,
302    ) -> Result<WrappedKey, BackendError> {
303        let _ = (key_id, recipient_pub_key, algorithm);
304        Err(BackendError::UnsupportedOperation(
305            "wrap_to_public not supported".to_string(),
306        ))
307    }
308}
309
310/// PKCS#11 token operations.
311///
312/// Provides access to PKCS#11-specific functionality: token info, key
313/// security attributes, mechanism enumeration, and session login/logout.
314pub trait Pkcs11Backend: Backend {
315    /// Get token information (label, manufacturer, serial, flags).
316    fn token_info(&self) -> Result<Pkcs11TokenInfo, BackendError>;
317
318    /// Read security attributes for `key_id` (`CKA_ALWAYS_SENSITIVE`, etc.).
319    fn key_security_attributes(
320        &self,
321        key_id: &KeyId,
322    ) -> Result<KeySecurityAttributes, BackendError>;
323
324    /// List mechanisms supported by this token.
325    fn supported_mechanisms(&self) -> Result<Vec<Pkcs11Mechanism>, BackendError>;
326
327    /// Log in to the token with a PIN.
328    fn login(&mut self, pin: &[u8]) -> Result<(), BackendError>;
329
330    /// Log out of the token.
331    fn logout(&mut self) -> Result<(), BackendError>;
332}
333
334/// PKCS#11 Security Officer (SO) role operations.
335///
336/// These operations require SO authentication and are typically performed
337/// only during initial token setup.
338pub trait Pkcs11AdminBackend: Backend {
339    /// Initialize a token (`C_InitToken`).
340    fn init_token(&mut self, so_pin: &[u8], label: &str) -> Result<(), BackendError>;
341
342    /// Initialize the user PIN (`C_InitPIN`). Requires SO session.
343    fn init_user_pin(&mut self, so_pin: &[u8], user_pin: &[u8]) -> Result<(), BackendError>;
344}
345
346/// TPM 2.0 operations.
347///
348/// Provides TPM-specific functionality: device info, PCR operations, and
349/// quote generation.
350pub trait TpmBackend: Backend {
351    /// Get TPM information (version, manufacturer, firmware).
352    fn tpm_info(&mut self) -> Result<TpmInfo, BackendError>;
353
354    /// Read PCR (Platform Configuration Register) values for the given PCR indices.
355    ///
356    /// Returns a `BTreeMap` keyed by PCR index for deterministic ordering in transcripts.
357    fn read_pcrs(&mut self, pcrs: &[u8]) -> Result<BTreeMap<u8, PcrValue>, BackendError>;
358
359    /// Extend a PCR register with new data.
360    fn extend_pcr(&mut self, pcr: u8, data: &[u8]) -> Result<(), BackendError>;
361
362    /// Generate a TPM quote (signed attestation over PCR values).
363    fn generate_quote(&mut self, pcrs: &[u8], nonce: &[u8]) -> Result<Attestation, BackendError>;
364}
365
366/// Personal Identity Verification (PIV) smart card operations.
367///
368/// Models the PIV card application interface defined by FIPS 201-3 and
369/// NIST SP 800-73-5. Methods in this trait correspond to PIV card commands
370/// and data model operations; their semantics are defined by the standard
371/// and should not diverge.
372///
373/// Backends implementing `PivBackend` should also implement the generic traits
374/// (`KeyStoreBackend`, `SignBackend`) for interoperability with non-PIV-aware actions.
375///
376/// # Standard references
377/// - FIPS 201-3: Personal Identity Verification of Federal Employees and Contractors
378/// - NIST SP 800-73-5 Part 1: PIV Card Application Data Model
379/// - NIST SP 800-73-5 Part 2: PIV Card Application Card Command Interface
380/// - NIST SP 800-78-5: Cryptographic Algorithms and Key Sizes for PIV
381pub trait PivBackend: Backend {
382    /// List populated PIV slots with metadata.
383    ///
384    /// Enumerates key references defined in SP 800-73-5 Part 1, Table 4b.
385    fn list_slots(&self) -> Result<Vec<PivSlotInfo>, BackendError>;
386
387    /// Verify the PIV Card Application PIN.
388    ///
389    /// Required before private key operations on slots with PIN access rules.
390    /// The PIN is typically 6–8 digits (ASCII-encoded).
391    /// [SP 800-73-5 Part 2, §3.3: VERIFY command]
392    fn verify_pin(&mut self, pin: &[u8]) -> Result<(), BackendError>;
393
394    /// Change the PIV Card Application PIN.
395    ///
396    /// [SP 800-73-5 Part 2, §3.4: CHANGE REFERENCE DATA command]
397    fn change_pin(&mut self, current: &[u8], new: &[u8]) -> Result<(), BackendError>;
398
399    /// Get remaining PIN retry count before lockout.
400    fn pin_retries(&mut self) -> Result<u32, BackendError>;
401
402    /// Reset a blocked PIN using the PUK (PIN Unblocking Key).
403    ///
404    /// [SP 800-73-5 Part 2, §3.4: RESET RETRY COUNTER command]
405    fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), BackendError>;
406
407    /// Return device identity (serial, firmware version, form factor).
408    ///
409    /// Not all fields are available on every PIV card. Serial number
410    /// retrieval is vendor-specific (not standardized by NIST SP 800-73).
411    fn device_info(&self) -> Result<PivDeviceInfo, BackendError>;
412}
413
414/// `YubiKey`-specific extensions to the PIV interface.
415///
416/// Operations in this trait are Yubico vendor extensions that go beyond
417/// the NIST SP 800-73 standard. They are specific to `YubiKey` hardware
418/// and will not work with generic PIV smart cards.
419///
420/// # Vendor references
421/// - Yubico PIV documentation: <https://developers.yubico.com/PIV/>
422/// - `YubiKey` attestation: key reference F9 (Yubico-proprietary)
423/// - Touch/PIN policy configuration: Yubico-proprietary extensions
424///   to the GENERATE ASYMMETRIC KEY PAIR command
425pub trait YubikeyBackend: PivBackend {
426    /// Generate an attestation certificate for a key in `slot`.
427    ///
428    /// Proves the key was generated on-device (not imported).
429    /// Uses `YubiKey`'s attestation key (slot F9) to sign a certificate
430    /// over the public key in the target slot.
431    /// This is a Yubico extension, not available on generic PIV cards.
432    fn attest_slot(&self, slot: PivSlot) -> Result<Vec<u8>, BackendError>;
433
434    /// Authenticate with the management key (3DES or AES).
435    ///
436    /// Required before administrative operations: key generation,
437    /// certificate import, PIN/PUK retry configuration.
438    fn authenticate_management(&mut self, mgm_key: &[u8]) -> Result<(), BackendError>;
439
440    /// Change the management key.
441    fn change_management_key(&mut self, current: &[u8], new: &[u8]) -> Result<(), BackendError>;
442
443    /// Get `YubiKey`-specific metadata for `slot`.
444    ///
445    /// Returns touch policy, PIN policy, and key origin: information
446    /// not available through standard PIV commands.
447    fn slot_metadata(&self, slot: PivSlot) -> Result<YubikeySlotMetadata, BackendError>;
448
449    /// Block PUK permanently (prevents PIN unblock forever).
450    ///
451    /// High-security operation: once blocked, a forgotten PIN requires
452    /// full device reset (destroying all keys). Irreversible.
453    fn block_puk(&mut self) -> Result<(), BackendError>;
454}
455
456/// Whether an error may be re-executed by retrying the step.
457///
458/// A `Transient` error means the world wasn't ready and the step's work did not
459/// happen, so re-running it is meaningful (token reinserted, cable reseated). A
460/// `Fatal` error means re-execution is either meaningless or unsafe: a procedural
461/// result, a broken definition, or a terminal device state.
462///
463/// The classification lives on the error type, not in the ceremony definition: a
464/// ceremony author must not be able to mark, say, a signature mismatch retriable.
465#[non_exhaustive]
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum Retriability {
468    /// Environmental and recoverable; retrying the step may succeed.
469    Transient,
470    /// Not retriable; the run must stop or escalate.
471    Fatal,
472}
473
474impl Retriability {
475    /// Convenience predicate for callers that only need a yes/no answer.
476    #[must_use]
477    pub const fn is_retriable(self) -> bool {
478        matches!(self, Retriability::Transient)
479    }
480}
481
482/// Backend-specific errors.
483#[derive(Debug, thiserror::Error)]
484#[non_exhaustive]
485pub enum BackendError {
486    /// Authentication required (not logged in).
487    #[error("Authentication required")]
488    AuthRequired,
489
490    /// PIN required to access backend.
491    #[error("PIN required")]
492    PinRequired,
493
494    /// PIN verification failed (wrong PIN). Contains remaining retries if known.
495    #[error("PIN verification failed ({0} retries remaining)")]
496    PinFailed(u32),
497
498    /// PIN is blocked (too many failed attempts).
499    #[error("PIN blocked")]
500    PinBlocked,
501
502    /// User PIN not initialized (PKCS#11: `C_InitPIN` not yet called).
503    #[error("User PIN not initialized")]
504    UserPinNotInitialized,
505
506    /// Management key authentication required.
507    #[error("Management key authentication required")]
508    ManagementKeyRequired,
509
510    /// Key not found in backend storage.
511    #[error("Key not found: {0}")]
512    KeyNotFound(String),
513
514    /// Object not found (PKCS#11: `find_objects` returned empty).
515    #[error("Object not found: {0}")]
516    ObjectNotFound(String),
517
518    /// PIV slot is empty (no key or certificate).
519    #[error("Slot empty: {0}")]
520    SlotEmpty(String),
521
522    /// Backend storage capacity exceeded.
523    #[error("Capacity exceeded")]
524    CapacityExceeded,
525
526    /// Operation not permitted (key usage flags don't allow this).
527    #[error("Operation not permitted: {0}")]
528    OperationNotPermitted(String),
529
530    /// Attribute is read-only (e.g., tried to set `CKA_ALWAYS_SENSITIVE`).
531    #[error("Attribute is read-only: {0}")]
532    AttributeReadOnly(String),
533
534    /// Algorithm not supported by this backend.
535    #[error("Unsupported algorithm: {0}")]
536    UnsupportedAlgorithm(String),
537
538    /// Operation not supported by this backend.
539    #[error("Unsupported operation: {0}")]
540    UnsupportedOperation(String),
541
542    /// Token not present in slot.
543    #[error("Token not present")]
544    TokenNotPresent,
545
546    /// Hardware failure (device disconnected, I/O error, etc.).
547    #[error("Hardware failure: {0}")]
548    HardwareFailure(String),
549
550    /// Invalid key format or data.
551    #[error("Invalid key format: {0}")]
552    InvalidKeyFormat(String),
553
554    /// Invalid data (malformed input, unexpected encoding, etc.).
555    #[error("Invalid data: {0}")]
556    InvalidData(String),
557
558    /// Backend not found in registry.
559    #[error("Backend not found: {0}")]
560    NotFound(String),
561
562    /// Configuration error (missing or invalid settings).
563    #[error("Configuration error: {0}")]
564    Configuration(String),
565
566    /// Generic backend error.
567    #[error("Backend error: {0}")]
568    Other(String),
569}
570
571impl BackendError {
572    /// Classify whether a step that failed with this error may be retried.
573    ///
574    /// The match is exhaustive with no wildcard arm on purpose: adding a new
575    /// `BackendError` variant fails to compile until it is deliberately
576    /// classified here.
577    #[must_use]
578    pub fn retriability(&self) -> Retriability {
579        match self {
580            // Environmental: the world wasn't ready, the work didn't happen.
581            // The operator can fix the precondition (log in, enter a PIN,
582            // insert the right token, initialize the device) and retry.
583            BackendError::AuthRequired
584            | BackendError::PinRequired
585            | BackendError::PinFailed(_)
586            | BackendError::ManagementKeyRequired
587            | BackendError::UserPinNotInitialized
588            | BackendError::KeyNotFound(_)
589            | BackendError::ObjectNotFound(_)
590            | BackendError::SlotEmpty(_)
591            | BackendError::TokenNotPresent
592            | BackendError::HardwareFailure(_)
593            | BackendError::Configuration(_) => Retriability::Transient,
594
595            // Fatal: re-execution is meaningless or unsafe. A terminal device
596            // state (PIN budget exhausted, capacity full), a definition that
597            // asks for something the backend cannot do, or malformed/unknown
598            // input that retrying cannot resolve.
599            BackendError::PinBlocked
600            | BackendError::CapacityExceeded
601            | BackendError::OperationNotPermitted(_)
602            | BackendError::AttributeReadOnly(_)
603            | BackendError::UnsupportedAlgorithm(_)
604            | BackendError::UnsupportedOperation(_)
605            | BackendError::InvalidKeyFormat(_)
606            | BackendError::InvalidData(_)
607            | BackendError::NotFound(_)
608            | BackendError::Other(_) => Retriability::Fatal,
609        }
610    }
611}
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    #[test]
618    fn retriability_is_retriable_predicate() {
619        assert!(Retriability::Transient.is_retriable());
620        assert!(!Retriability::Fatal.is_retriable());
621    }
622
623    #[test]
624    fn environmental_errors_are_transient() {
625        let transient = [
626            BackendError::AuthRequired,
627            BackendError::PinRequired,
628            BackendError::PinFailed(2),
629            BackendError::ManagementKeyRequired,
630            BackendError::UserPinNotInitialized,
631            BackendError::KeyNotFound("k".into()),
632            BackendError::ObjectNotFound("o".into()),
633            BackendError::SlotEmpty("9a".into()),
634            BackendError::TokenNotPresent,
635            BackendError::HardwareFailure("cable".into()),
636            BackendError::Configuration("missing".into()),
637        ];
638        for err in transient {
639            assert_eq!(
640                err.retriability(),
641                Retriability::Transient,
642                "expected transient: {err:?}"
643            );
644        }
645    }
646
647    #[test]
648    fn fatal_errors_are_not_retriable() {
649        let fatal = [
650            BackendError::PinBlocked,
651            BackendError::CapacityExceeded,
652            BackendError::OperationNotPermitted("usage".into()),
653            BackendError::AttributeReadOnly("attr".into()),
654            BackendError::UnsupportedAlgorithm("rsa1".into()),
655            BackendError::UnsupportedOperation("wrap".into()),
656            BackendError::InvalidKeyFormat("pem".into()),
657            BackendError::InvalidData("bytes".into()),
658            BackendError::NotFound("backend".into()),
659            BackendError::Other("boom".into()),
660        ];
661        for err in fatal {
662            assert_eq!(
663                err.retriability(),
664                Retriability::Fatal,
665                "expected fatal: {err:?}"
666            );
667        }
668    }
669
670    #[test]
671    fn pin_failed_retriable_but_pin_blocked_terminal() {
672        // The hardware budget: a wrong PIN can be retried; an exhausted one cannot.
673        assert!(BackendError::PinFailed(1).retriability().is_retriable());
674        assert!(!BackendError::PinBlocked.retriability().is_retriable());
675    }
676}