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 zeroize::Zeroizing;
29
30use crate::key_material::PublicKeyDer;
31use crate::types::{
32 Attestation, CertRef, DataKey, KeyAlgorithm, KeyId, KeyMetadata, KeyPolicy, KeyProtection,
33 KeySecurityAttributes, KeySpec, PcrValue, PivDeviceInfo, PivSlot, PivSlotInfo, Pkcs11Mechanism,
34 Pkcs11TokenInfo, SignAlgorithm, TpmInfo, WrapScheme, WrappedKey, YubikeySlotMetadata,
35};
36
37/// Implement the `as_*_mut` upcasting helpers on a backend struct.
38///
39/// Each `Backend` implementor must return `Some(self)` for every trait it
40/// supports and `None` for those it does not. Without this macro that is four
41/// lines of boilerplate per capability. The macro expands to those lines
42/// automatically and allows attaching per-capability doc comments.
43///
44/// # Usage
45///
46/// ```
47/// use rite_sdk::{
48/// backend_capabilities, Backend, BackendError, KeyId, RandomBackend, SignAlgorithm,
49/// SignBackend,
50/// };
51///
52/// struct MyBackend {
53/// name: String,
54/// }
55///
56/// impl Backend for MyBackend {
57/// fn name(&self) -> &str { &self.name }
58/// fn provider(&self) -> &str { "software" }
59/// fn fingerprint(&self) -> String { "sha256:...".to_string() }
60///
61/// backend_capabilities!(
62/// /// Signs with software-held RSA and ECDSA keys.
63/// as_sign_mut: SignBackend,
64/// /// Draws ceremony randomness from the OS CSPRNG.
65/// as_random_mut: RandomBackend,
66/// );
67/// }
68///
69/// impl SignBackend for MyBackend {
70/// fn sign(&mut self, _key: &KeyId, _msg: &[u8], _alg: SignAlgorithm)
71/// -> Result<Vec<u8>, BackendError> { Ok(Vec::new()) }
72/// }
73///
74/// impl RandomBackend for MyBackend {
75/// fn generate_random(&mut self, len: usize) -> Result<Vec<u8>, BackendError> {
76/// Ok(vec![0u8; len])
77/// }
78/// }
79/// ```
80///
81/// Capabilities not listed here are not overridden, so they inherit the trait's
82/// default doc ("Not supported by this backend.") and return `None`.
83///
84/// The trait names passed to the macro must be in scope at the call site.
85///
86/// Note: `#[macro_export]` places this macro at the crate root
87/// (`rite_sdk::backend_capabilities!`), not in the `backend` module.
88/// This is standard Rust macro scoping.
89#[macro_export]
90macro_rules! backend_capabilities {
91 ($($(#[$attr:meta])* $method:ident : $trait:ident),* $(,)?) => {
92 $(
93 $(#[$attr])*
94 fn $method(&mut self) -> Option<&mut dyn $trait> {
95 Some(self)
96 }
97 )*
98 };
99}
100
101/// Core trait that all backends must implement.
102///
103/// Provides identity and fingerprinting for audit trails.
104///
105/// # Capability downcasting
106///
107/// Rust does not allow downcasting from `dyn Backend` to `dyn KeyStoreBackend`
108/// (or any other subtrait) without explicit support. The `as_*_mut` methods
109/// implement this pattern manually: a backend returns `Some(self)` for each
110/// capability it supports, and `None` for those it does not.
111///
112/// Use the [`backend_capabilities!`] macro to generate these methods without
113/// boilerplate.
114pub trait Backend: Send + Sync {
115 /// Backend name (unique identifier within a ceremony).
116 fn name(&self) -> &str;
117
118 /// Backend provider name (e.g., "software", "yubikey", "pkcs11").
119 fn provider(&self) -> &str;
120
121 /// Fingerprint for audit evidence.
122 ///
123 /// Format examples:
124 /// - Software: `"software-backend=mybackend"`
125 /// - `YubiKey`: `"yubikey-serial=12345678+firmware=5.7.1"`
126 /// - PKCS#11: `"pkcs11-module=/usr/lib/softhsm2.so+slot=0"`
127 fn fingerprint(&self) -> String;
128
129 /// Not supported by this backend, always returns `None`.
130 fn as_keystore_mut(&mut self) -> Option<&mut dyn KeyStoreBackend> {
131 None
132 }
133
134 /// Not supported by this backend, always returns `None`.
135 fn as_sign_mut(&mut self) -> Option<&mut dyn SignBackend> {
136 None
137 }
138
139 /// Not supported by this backend, always returns `None`.
140 fn as_verify_mut(&mut self) -> Option<&mut dyn VerifyBackend> {
141 None
142 }
143
144 /// Not supported by this backend, always returns `None`.
145 fn as_attest_mut(&mut self) -> Option<&mut dyn AttestationBackend> {
146 None
147 }
148
149 /// Not supported by this backend, always returns `None`.
150 fn as_piv_mut(&mut self) -> Option<&mut dyn PivBackend> {
151 None
152 }
153
154 /// Not supported by this backend, always returns `None`.
155 fn as_yubikey_mut(&mut self) -> Option<&mut dyn YubikeyBackend> {
156 None
157 }
158
159 /// Not supported by this backend, always returns `None`.
160 fn as_transport_mut(&mut self) -> Option<&mut dyn KeyTransportBackend> {
161 None
162 }
163
164 /// Not supported by this backend, always returns `None`.
165 fn as_certstore_mut(&mut self) -> Option<&mut dyn CertStoreBackend> {
166 None
167 }
168
169 /// Not supported by this backend, always returns `None`.
170 fn as_random_mut(&mut self) -> Option<&mut dyn RandomBackend> {
171 None
172 }
173
174 /// Not supported by this backend, always returns `None`.
175 fn as_pkcs11_mut(&mut self) -> Option<&mut dyn Pkcs11Backend> {
176 None
177 }
178
179 /// Not supported by this backend, always returns `None`.
180 fn as_pkcs11_admin_mut(&mut self) -> Option<&mut dyn Pkcs11AdminBackend> {
181 None
182 }
183
184 /// Not supported by this backend, always returns `None`.
185 fn as_tpm_mut(&mut self) -> Option<&mut dyn TpmBackend> {
186 None
187 }
188}
189
190/// Key generation and storage operations.
191///
192/// Backends implementing this trait can generate, import, and manage
193/// cryptographic keys.
194pub trait KeyStoreBackend: Backend {
195 /// Generate a new key.
196 ///
197 /// The backend owns the key and returns metadata including an opaque
198 /// `KeyId` reference. For hardware backends, the private key never leaves
199 /// the device.
200 fn generate_key(&mut self, spec: KeySpec) -> Result<KeyMetadata, BackendError>;
201
202 /// Import an existing key from material the caller holds.
203 ///
204 /// `spec.algorithm` says how to read `key_bytes`, because nothing in the
205 /// bytes distinguishes a 32-byte secret from any other 32 bytes: a symmetric
206 /// algorithm takes the raw key, every other one takes a private key, as
207 /// PKCS#8 DER or as PEM. The metadata that comes back carries a check value
208 /// in the first case and a public key in the second, which is the identity
209 /// each kind has.
210 ///
211 /// `passphrase` opens a private key that is encrypted, in any encoding the
212 /// backend reads. Material that needs one and is given none is refused by
213 /// name, and so is a passphrase given for material that is not encrypted:
214 /// a passphrase the transcript says was used has to have been used.
215 ///
216 /// Only supported by backends that allow key import (software, some HSMs).
217 /// Hardware security modules may reject it for security reasons, and a
218 /// device that holds only one kind of key refuses the other by name.
219 fn import_key(
220 &mut self,
221 spec: KeySpec,
222 key_bytes: &[u8],
223 passphrase: Option<&[u8]>,
224 ) -> Result<KeyMetadata, BackendError>;
225
226 /// Export the public key for `key_id`.
227 fn export_public_key(&self, key_id: &KeyId) -> Result<PublicKeyDer, BackendError>;
228
229 /// List all keys managed by this backend.
230 fn list_keys(&self) -> Result<Vec<KeyMetadata>, BackendError>;
231
232 /// Delete a key permanently.
233 fn delete_key(&mut self, key_id: &KeyId) -> Result<(), BackendError>;
234}
235
236/// Signing operations.
237pub trait SignBackend: Backend {
238 /// Sign `message` with `key_id` using `algorithm`.
239 ///
240 /// Returns raw signature bytes (format depends on algorithm).
241 fn sign(
242 &mut self,
243 key_id: &KeyId,
244 message: &[u8],
245 algorithm: SignAlgorithm,
246 ) -> Result<Vec<u8>, BackendError>;
247}
248
249/// Signature verification operations.
250///
251/// Separate from [`SignBackend`] because the two need different things. Signing
252/// needs a secret, so only the device holding it can do the work; verification
253/// needs a public key, so anything can. A card that signs and never verifies
254/// implements one trait, and a verification-only backend (a notary, an enclave)
255/// implements the other without having to refuse `sign`.
256///
257/// Most ceremonies verify in software and name no backend. Naming one is for a
258/// deployment that requires the check inside a validated boundary, or a
259/// verifier that is not local.
260pub trait VerifyBackend: Backend {
261 /// Verify `signature` over `message` under `key`.
262 ///
263 /// Takes the key rather than a reference to one the backend holds, because
264 /// the evidence a ceremony checks usually comes from outside it: a CSR from
265 /// another party, a signature made on a card that is no longer present.
266 /// There is no handle to name for those.
267 ///
268 /// `&mut self` because a verifier may need a session to work in. PKCS#11
269 /// creates a public key object, verifies against it, and destroys it.
270 fn verify_public_key(
271 &mut self,
272 key: &PublicKeyDer,
273 message: &[u8],
274 signature: &[u8],
275 algorithm: SignAlgorithm,
276 ) -> Result<bool, BackendError>;
277}
278
279/// Hardware attestation operations.
280///
281/// Backends that support attestation can cryptographically prove that a key
282/// was generated on a specific hardware device.
283pub trait AttestationBackend: Backend {
284 /// Return a cryptographic proof that `key_id` was generated on this
285 /// backend's hardware and has specific properties.
286 fn attest_key(&self, key_id: &KeyId) -> Result<Attestation, BackendError>;
287}
288
289/// Certificate storage operations.
290///
291/// Backends implementing this trait can store, read, and delete X.509
292/// certificates by reference. This is a unified interface for certificate
293/// management across PIV (slot-based), PKCS#11 (label/ID-based), and
294/// software backends.
295pub trait CertStoreBackend: Backend {
296 /// Store a DER-encoded X.509 certificate.
297 fn store_cert(&mut self, cert_ref: &CertRef, cert_der: &[u8]) -> Result<(), BackendError>;
298
299 /// Read a DER-encoded X.509 certificate.
300 fn read_cert(&self, cert_ref: &CertRef) -> Result<Vec<u8>, BackendError>;
301
302 /// Delete a certificate.
303 fn delete_cert(&mut self, cert_ref: &CertRef) -> Result<(), BackendError>;
304}
305
306/// Hardware or software random number generation.
307///
308/// Backends implementing this trait can generate random bytes. For HSM
309/// backends, this uses the device's internal RNG (e.g., PKCS#11 `C_GenerateRandom`).
310pub trait RandomBackend: Backend {
311 /// Generate `len` random bytes.
312 fn generate_random(&mut self, len: usize) -> Result<Vec<u8>, BackendError>;
313}
314
315/// Key transport (wrapping) operations.
316///
317/// Every method returns a [`WrappedKey`] carrying the scheme it was asked for
318/// and a [`WrapDescription`](crate::types::WrapDescription) of what it did, so
319/// the caller records an observation rather than a request.
320pub trait KeyTransportBackend: Backend {
321 /// Wrap `key_id` using `wrapping_key_id` under `scheme`.
322 fn wrap(
323 &mut self,
324 key_id: &KeyId,
325 wrapping_key_id: &KeyId,
326 scheme: WrapScheme,
327 ) -> Result<WrappedKey, BackendError>;
328
329 /// Unwrap `wrapped` using `unwrapping_key_id`, importing the result with
330 /// `label` under `policy`.
331 ///
332 /// The policy is the receiving ceremony's, not the origin's: nothing
333 /// travels with a wrapped key that says what it may do, so the step that
334 /// imports it declares that, exactly as the step that generated it did.
335 ///
336 /// `expected` is that same declaration one level down, because nothing
337 /// travels with a wrapped key saying what it *is* either. Recovered bytes
338 /// are a private key or a bare secret depending on what was wrapped, and
339 /// the two are told apart by what the receiving ceremony says it is
340 /// restoring rather than by what the bytes happen to parse as. `None` is a
341 /// ceremony that declared nothing, which an implementation may serve only
342 /// where the recovered form is unambiguous. A declaration that disagrees
343 /// with what came out is a failure, not something to reconcile.
344 fn unwrap(
345 &mut self,
346 wrapped: &WrappedKey,
347 unwrapping_key_id: &KeyId,
348 label: &str,
349 policy: KeyPolicy,
350 expected: Option<KeyAlgorithm>,
351 ) -> Result<KeyMetadata, BackendError>;
352
353 /// Produce a fresh content-encryption key protected by `kek`.
354 ///
355 /// Two copies of one key come back: the plaintext, for the caller to
356 /// encrypt content with and then drop, and a copy only `kek` opens, which
357 /// the container carries to the recipient.
358 ///
359 /// Content does not appear here. No key-protection
360 /// device Rite talks to is a cipher you hand a file to: a cloud KMS caps a
361 /// direct encrypt at a few kilobytes, a TPM takes a kilobyte per call over
362 /// a slow bus, and a smart card is slower still. Every one of them offers
363 /// this operation instead, so a backend implements what it can actually do
364 /// and the content pipeline stays in one place above it.
365 ///
366 /// `algorithm` must be a symmetric one, since a data key is a secret the
367 /// caller encrypts with directly. It is the length request AWS KMS spells
368 /// `KeySpec: AES_256 | AES_128`.
369 ///
370 /// The result carries a [`KeyProtection`] because the caller has to declare
371 /// the key-encryption algorithm in the container it assembles, and only the
372 /// backend knows which one it used.
373 ///
374 /// # Not yet a parameter
375 ///
376 /// An **encryption context**, which AWS calls `EncryptionContext` and Azure
377 /// calls `aad`, binds a data key to the purpose it was issued for: the same
378 /// value must be supplied to open it again, or the open fails. AWS
379 /// recommends it on every call. It is absent here because supplying it would
380 /// oblige the ceremony to store it somewhere a later run can read it back,
381 /// and CMS has no slot for it that the tag covers except `authAttrs`, which
382 /// is deferred. Adding it is the next widening this pair needs, and it wants
383 /// the transcript question answered first.
384 fn generate_data_key(
385 &mut self,
386 kek: &KeyId,
387 algorithm: KeyAlgorithm,
388 ) -> Result<DataKey, BackendError>;
389
390 /// Recover a data key from the protected copy a container carries.
391 ///
392 /// The counterpart of [`generate_data_key`](Self::generate_data_key), and
393 /// the half of the pair almost every backend implements: protecting a data
394 /// key needs only a public key or a shared secret, while opening one always
395 /// needs the device that holds the other half.
396 ///
397 /// `protection` says how `wrapped` was protected, taken from the container
398 /// rather than assumed, because the bytes say nothing about themselves and
399 /// the backend may implement several. An implementation that was not asked
400 /// which one to use could only guess, and a wrong guess on key material is
401 /// silent. A backend refuses a protection it does not implement rather than
402 /// decrypting under an assumption.
403 fn open_data_key(
404 &mut self,
405 kek: &KeyId,
406 wrapped: &[u8],
407 protection: KeyProtection,
408 ) -> Result<Zeroizing<Vec<u8>>, BackendError>;
409
410 /// Wrap `key_id` to an external recipient's public key.
411 ///
412 /// Default implementation returns `UnsupportedOperation`.
413 fn wrap_to_public(
414 &mut self,
415 key_id: &KeyId,
416 recipient_pub_key: &PublicKeyDer,
417 scheme: WrapScheme,
418 ) -> Result<WrappedKey, BackendError> {
419 let _ = (key_id, recipient_pub_key, scheme);
420 Err(BackendError::UnsupportedOperation(
421 "wrap_to_public not supported".to_string(),
422 ))
423 }
424}
425
426/// PKCS#11 token operations.
427///
428/// Provides access to PKCS#11-specific functionality: token info, key
429/// security attributes, mechanism enumeration, and session login/logout.
430pub trait Pkcs11Backend: Backend {
431 /// Get token information (label, manufacturer, serial, flags).
432 fn token_info(&self) -> Result<Pkcs11TokenInfo, BackendError>;
433
434 /// Read security attributes for `key_id` (`CKA_ALWAYS_SENSITIVE`, etc.).
435 fn key_security_attributes(
436 &self,
437 key_id: &KeyId,
438 ) -> Result<KeySecurityAttributes, BackendError>;
439
440 /// List mechanisms supported by this token.
441 fn supported_mechanisms(&self) -> Result<Vec<Pkcs11Mechanism>, BackendError>;
442
443 /// Log in to the token with a PIN.
444 fn login(&mut self, pin: &[u8]) -> Result<(), BackendError>;
445
446 /// Log out of the token.
447 fn logout(&mut self) -> Result<(), BackendError>;
448}
449
450/// PKCS#11 Security Officer (SO) role operations.
451///
452/// These operations require SO authentication and are typically performed
453/// only during initial token setup.
454pub trait Pkcs11AdminBackend: Backend {
455 /// Initialize a token (`C_InitToken`).
456 fn init_token(&mut self, so_pin: &[u8], label: &str) -> Result<(), BackendError>;
457
458 /// Initialize the user PIN (`C_InitPIN`). Requires SO session.
459 fn init_user_pin(&mut self, so_pin: &[u8], user_pin: &[u8]) -> Result<(), BackendError>;
460}
461
462/// TPM 2.0 operations.
463///
464/// Provides TPM-specific functionality: device info, PCR operations, and
465/// quote generation.
466pub trait TpmBackend: Backend {
467 /// Get TPM information (version, manufacturer, firmware).
468 fn tpm_info(&mut self) -> Result<TpmInfo, BackendError>;
469
470 /// Read PCR (Platform Configuration Register) values for the given PCR indices.
471 ///
472 /// Returns a `BTreeMap` keyed by PCR index for deterministic ordering in transcripts.
473 fn read_pcrs(&mut self, pcrs: &[u8]) -> Result<BTreeMap<u8, PcrValue>, BackendError>;
474
475 /// Extend a PCR register with new data.
476 fn extend_pcr(&mut self, pcr: u8, data: &[u8]) -> Result<(), BackendError>;
477
478 /// Generate a TPM quote (signed attestation over PCR values).
479 fn generate_quote(&mut self, pcrs: &[u8], nonce: &[u8]) -> Result<Attestation, BackendError>;
480}
481
482/// Personal Identity Verification (PIV) smart card operations.
483///
484/// Models the PIV card application interface defined by FIPS 201-3 and
485/// NIST SP 800-73-5. Methods in this trait correspond to PIV card commands
486/// and data model operations; their semantics are defined by the standard
487/// and should not diverge.
488///
489/// Backends implementing `PivBackend` should also implement the generic traits
490/// (`KeyStoreBackend`, `SignBackend`) for interoperability with non-PIV-aware actions.
491///
492/// # Standard references
493/// - FIPS 201-3: Personal Identity Verification of Federal Employees and Contractors
494/// - NIST SP 800-73-5 Part 1: PIV Card Application Data Model
495/// - NIST SP 800-73-5 Part 2: PIV Card Application Card Command Interface
496/// - NIST SP 800-78-5: Cryptographic Algorithms and Key Sizes for PIV
497pub trait PivBackend: Backend {
498 /// List populated PIV slots with metadata.
499 ///
500 /// Enumerates key references defined in SP 800-73-5 Part 1, Table 4b.
501 fn list_slots(&self) -> Result<Vec<PivSlotInfo>, BackendError>;
502
503 /// Verify the PIV Card Application PIN.
504 ///
505 /// Required before private key operations on slots with PIN access rules.
506 /// The PIN is typically 6–8 digits (ASCII-encoded).
507 /// [SP 800-73-5 Part 2, §3.3: VERIFY command]
508 fn verify_pin(&mut self, pin: &[u8]) -> Result<(), BackendError>;
509
510 /// Change the PIV Card Application PIN.
511 ///
512 /// [SP 800-73-5 Part 2, §3.4: CHANGE REFERENCE DATA command]
513 fn change_pin(&mut self, current: &[u8], new: &[u8]) -> Result<(), BackendError>;
514
515 /// Get remaining PIN retry count before lockout.
516 fn pin_retries(&mut self) -> Result<u32, BackendError>;
517
518 /// Reset a blocked PIN using the PUK (PIN Unblocking Key).
519 ///
520 /// [SP 800-73-5 Part 2, §3.4: RESET RETRY COUNTER command]
521 fn unblock_pin(&mut self, puk: &[u8], new_pin: &[u8]) -> Result<(), BackendError>;
522
523 /// Return device identity (serial, firmware version, form factor).
524 ///
525 /// Not all fields are available on every PIV card. Serial number
526 /// retrieval is vendor-specific (not standardized by NIST SP 800-73).
527 fn device_info(&self) -> Result<PivDeviceInfo, BackendError>;
528}
529
530/// `YubiKey`-specific extensions to the PIV interface.
531///
532/// Operations in this trait are Yubico vendor extensions that go beyond
533/// the NIST SP 800-73 standard. They are specific to `YubiKey` hardware
534/// and will not work with generic PIV smart cards.
535///
536/// # Vendor references
537/// - Yubico PIV documentation: <https://developers.yubico.com/PIV/>
538/// - `YubiKey` attestation: key reference F9 (Yubico-proprietary)
539/// - Touch/PIN policy configuration: Yubico-proprietary extensions
540/// to the GENERATE ASYMMETRIC KEY PAIR command
541pub trait YubikeyBackend: PivBackend {
542 /// Generate an attestation certificate for a key in `slot`.
543 ///
544 /// Proves the key was generated on-device (not imported).
545 /// Uses `YubiKey`'s attestation key (slot F9) to sign a certificate
546 /// over the public key in the target slot.
547 /// This is a Yubico extension, not available on generic PIV cards.
548 fn attest_slot(&self, slot: PivSlot) -> Result<Vec<u8>, BackendError>;
549
550 /// Authenticate with the management key (3DES or AES).
551 ///
552 /// Required before administrative operations: key generation,
553 /// certificate import, PIN/PUK retry configuration.
554 fn authenticate_management(&mut self, mgm_key: &[u8]) -> Result<(), BackendError>;
555
556 /// Change the management key.
557 fn change_management_key(&mut self, current: &[u8], new: &[u8]) -> Result<(), BackendError>;
558
559 /// Get `YubiKey`-specific metadata for `slot`.
560 ///
561 /// Returns touch policy, PIN policy, and key origin: information
562 /// not available through standard PIV commands.
563 fn slot_metadata(&self, slot: PivSlot) -> Result<YubikeySlotMetadata, BackendError>;
564
565 /// Block PUK permanently (prevents PIN unblock forever).
566 ///
567 /// High-security operation: once blocked, a forgotten PIN requires
568 /// full device reset (destroying all keys). Irreversible.
569 fn block_puk(&mut self) -> Result<(), BackendError>;
570}
571
572/// Whether an error may be re-executed by retrying the step.
573///
574/// A `Transient` error means the world wasn't ready and the step's work did not
575/// happen, so re-running it is meaningful (token reinserted, cable reseated). A
576/// `Fatal` error means re-execution is either meaningless or unsafe: a procedural
577/// result, a broken definition, or a terminal device state.
578///
579/// The classification lives on the error type, not in the ceremony definition: a
580/// ceremony author must not be able to mark, say, a signature mismatch retriable.
581#[non_exhaustive]
582#[derive(Debug, Clone, Copy, PartialEq, Eq)]
583pub enum Retriability {
584 /// Environmental and recoverable; retrying the step may succeed.
585 Transient,
586 /// Not retriable; the run must stop or escalate.
587 Fatal,
588}
589
590impl Retriability {
591 /// Convenience predicate for callers that only need a yes/no answer.
592 #[must_use]
593 pub const fn is_retriable(self) -> bool {
594 matches!(self, Retriability::Transient)
595 }
596}
597
598/// Backend-specific errors.
599#[derive(Debug, thiserror::Error)]
600#[non_exhaustive]
601pub enum BackendError {
602 /// Authentication required (not logged in).
603 #[error("Authentication required")]
604 AuthRequired,
605
606 /// PIN required to access backend.
607 #[error("PIN required")]
608 PinRequired,
609
610 /// PIN verification failed (wrong PIN). Contains remaining retries if known.
611 #[error("PIN verification failed ({0} retries remaining)")]
612 PinFailed(u32),
613
614 /// PIN is blocked (too many failed attempts).
615 #[error("PIN blocked")]
616 PinBlocked,
617
618 /// User PIN not initialized (PKCS#11: `C_InitPIN` not yet called).
619 #[error("User PIN not initialized")]
620 UserPinNotInitialized,
621
622 /// Management key authentication required.
623 #[error("Management key authentication required")]
624 ManagementKeyRequired,
625
626 /// Key not found in backend storage.
627 #[error("Key not found: {0}")]
628 KeyNotFound(String),
629
630 /// Object not found (PKCS#11: `find_objects` returned empty).
631 #[error("Object not found: {0}")]
632 ObjectNotFound(String),
633
634 /// PIV slot is empty (no key or certificate).
635 #[error("Slot empty: {0}")]
636 SlotEmpty(String),
637
638 /// Backend storage capacity exceeded.
639 #[error("Capacity exceeded")]
640 CapacityExceeded,
641
642 /// Operation not permitted (key usage flags don't allow this).
643 #[error("Operation not permitted: {0}")]
644 OperationNotPermitted(String),
645
646 /// Attribute is read-only (e.g., tried to set `CKA_ALWAYS_SENSITIVE`).
647 #[error("Attribute is read-only: {0}")]
648 AttributeReadOnly(String),
649
650 /// Algorithm not supported by this backend.
651 #[error("Unsupported algorithm: {0}")]
652 UnsupportedAlgorithm(String),
653
654 /// Operation not supported by this backend.
655 #[error("Unsupported operation: {0}")]
656 UnsupportedOperation(String),
657
658 /// Token not present in slot.
659 #[error("Token not present")]
660 TokenNotPresent,
661
662 /// Hardware failure (device disconnected, I/O error, etc.).
663 #[error("Hardware failure: {0}")]
664 HardwareFailure(String),
665
666 /// Invalid key format or data.
667 #[error("Invalid key format: {0}")]
668 InvalidKeyFormat(String),
669
670 /// Invalid data (malformed input, unexpected encoding, etc.).
671 #[error("Invalid data: {0}")]
672 InvalidData(String),
673
674 /// Backend not found in registry.
675 #[error("Backend not found: {0}")]
676 NotFound(String),
677
678 /// Configuration error (missing or invalid settings).
679 #[error("Configuration error: {0}")]
680 Configuration(String),
681
682 /// Generic backend error.
683 #[error("Backend error: {0}")]
684 Other(String),
685}
686
687impl BackendError {
688 /// Classify whether a step that failed with this error may be retried.
689 ///
690 /// The match is exhaustive with no wildcard arm on purpose: adding a new
691 /// `BackendError` variant fails to compile until it is deliberately
692 /// classified here.
693 #[must_use]
694 pub fn retriability(&self) -> Retriability {
695 match self {
696 // Environmental: the world wasn't ready, the work didn't happen.
697 // The operator can fix the precondition (log in, enter a PIN,
698 // insert the right token, initialize the device) and retry.
699 BackendError::AuthRequired
700 | BackendError::PinRequired
701 | BackendError::PinFailed(_)
702 | BackendError::ManagementKeyRequired
703 | BackendError::UserPinNotInitialized
704 | BackendError::KeyNotFound(_)
705 | BackendError::ObjectNotFound(_)
706 | BackendError::SlotEmpty(_)
707 | BackendError::TokenNotPresent
708 | BackendError::HardwareFailure(_)
709 | BackendError::Configuration(_) => Retriability::Transient,
710
711 // Fatal: re-execution is meaningless or unsafe. A terminal device
712 // state (PIN budget exhausted, capacity full), a definition that
713 // asks for something the backend cannot do, or malformed/unknown
714 // input that retrying cannot resolve.
715 BackendError::PinBlocked
716 | BackendError::CapacityExceeded
717 | BackendError::OperationNotPermitted(_)
718 | BackendError::AttributeReadOnly(_)
719 | BackendError::UnsupportedAlgorithm(_)
720 | BackendError::UnsupportedOperation(_)
721 | BackendError::InvalidKeyFormat(_)
722 | BackendError::InvalidData(_)
723 | BackendError::NotFound(_)
724 | BackendError::Other(_) => Retriability::Fatal,
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn retriability_is_retriable_predicate() {
735 assert!(Retriability::Transient.is_retriable());
736 assert!(!Retriability::Fatal.is_retriable());
737 }
738
739 #[test]
740 fn environmental_errors_are_transient() {
741 let transient = [
742 BackendError::AuthRequired,
743 BackendError::PinRequired,
744 BackendError::PinFailed(2),
745 BackendError::ManagementKeyRequired,
746 BackendError::UserPinNotInitialized,
747 BackendError::KeyNotFound("k".into()),
748 BackendError::ObjectNotFound("o".into()),
749 BackendError::SlotEmpty("9a".into()),
750 BackendError::TokenNotPresent,
751 BackendError::HardwareFailure("cable".into()),
752 BackendError::Configuration("missing".into()),
753 ];
754 for err in transient {
755 assert_eq!(
756 err.retriability(),
757 Retriability::Transient,
758 "expected transient: {err:?}"
759 );
760 }
761 }
762
763 #[test]
764 fn fatal_errors_are_not_retriable() {
765 let fatal = [
766 BackendError::PinBlocked,
767 BackendError::CapacityExceeded,
768 BackendError::OperationNotPermitted("usage".into()),
769 BackendError::AttributeReadOnly("attr".into()),
770 BackendError::UnsupportedAlgorithm("rsa1".into()),
771 BackendError::UnsupportedOperation("wrap".into()),
772 BackendError::InvalidKeyFormat("pem".into()),
773 BackendError::InvalidData("bytes".into()),
774 BackendError::NotFound("backend".into()),
775 BackendError::Other("boom".into()),
776 ];
777 for err in fatal {
778 assert_eq!(
779 err.retriability(),
780 Retriability::Fatal,
781 "expected fatal: {err:?}"
782 );
783 }
784 }
785
786 #[test]
787 fn pin_failed_retriable_but_pin_blocked_terminal() {
788 // The hardware budget: a wrong PIN can be retried; an exhausted one cannot.
789 assert!(BackendError::PinFailed(1).retriability().is_retriable());
790 assert!(!BackendError::PinBlocked.retriability().is_retriable());
791 }
792}