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