dig_keystore/hardware/provider.rs
1//! The [`HardwareProvider`] seam — the whole contract a platform binding must
2//! satisfy.
3//!
4//! # Why the seam is this narrow
5//!
6//! A hardware trusted component cannot wrap a keystore blob directly. A TPM 2.0
7//! key reached through CNG is asymmetric (RSA/ECC) and bounded by the modulus
8//! size; a Secure Enclave key is a P-256 key that never leaves the chip. So the
9//! envelope is **hybrid**: a random 32-byte content key encrypts the blob with
10//! AES-256-GCM in this crate, and the hardware only ever wraps *that key*.
11//!
12//! Keeping the trait to two 32-byte operations means each platform binding is a
13//! few dozen lines of FFI instead of a second envelope format, and every
14//! platform shares one audited AEAD path.
15//!
16//! # Where implementations live
17//!
18//! Not in this package. It sets `unsafe_code = "forbid"` as a spec-pinned
19//! security property (`SPEC.md` §12/§13.2, conformance C-15), and every platform
20//! trusted-component API is FFI — so the bindings live in the
21//! `dig-keystore-hardware` workspace member under `hardware/`, mirroring the
22//! `wasm/` split, and reach this crate only through this trait. Windows (CNG),
23//! macOS on Apple silicon (Secure Enclave) and Linux (TPM 2.0) are implemented
24//! there.
25//!
26//! Passing no provider resolves
27//! [`Software(NotRequested)`](super::DegradeReason::NotRequested): honest, and
28//! explicitly not a claim of hardware protection.
29
30use zeroize::Zeroizing;
31
32use super::tier::{HardwareKind, HardwareProbe};
33use crate::error::Result;
34
35/// Length of the symmetric content key a provider wraps.
36pub const CONTENT_KEY_LEN: usize = 32;
37
38/// The content key that a hardware provider wraps and unwraps.
39///
40/// Zeroized on drop: this is the only plaintext key material that transits the
41/// provider boundary.
42pub type ContentKey = Zeroizing<[u8; CONTENT_KEY_LEN]>;
43
44/// Where a provider's wrapping key physically lives.
45///
46/// This is the property hardware binding actually buys, so it is reported
47/// explicitly rather than inferred from the provider's name.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum KeyCustody {
50 /// The wrapping key was generated inside the hardware component and cannot
51 /// be exported from it. Copying the sealed blob to another machine does not
52 /// let an attacker open it.
53 ///
54 /// An implementation MUST NOT report this unless the key was created
55 /// non-exportable *and* an export attempt is refused by the platform.
56 NonExportable,
57
58 /// The wrapping key exists in process memory at some point. Offers no
59 /// cross-machine binding; a provider reporting this MUST NOT be treated as
60 /// a hardware tier.
61 ProcessMemory,
62}
63
64impl KeyCustody {
65 /// Whether this custody level is strong enough to claim a hardware tier.
66 pub const fn is_hardware_grade(self) -> bool {
67 matches!(self, Self::NonExportable)
68 }
69}
70
71/// A binding to one OS hardware trusted component.
72///
73/// Implementations are injected into
74/// [`HardwareBoundBackend`](super::HardwareBoundBackend), which probes and
75/// self-tests them before claiming a hardware tier — an implementation is never
76/// taken at its word.
77pub trait HardwareProvider: Send + Sync + 'static {
78 /// Which hardware class this provider binds to.
79 fn kind(&self) -> HardwareKind;
80
81 /// Inspect the host for usable hardware.
82 ///
83 /// MUST return [`HardwareProbe::Indeterminate`] — never
84 /// [`Absent`](HardwareProbe::Absent) — when the inspection itself fails
85 /// (an error, a timeout, an empty or unintelligible response). "I could not
86 /// tell" and "there is none" are different answers and the caller acts
87 /// differently on each.
88 fn probe(&self) -> HardwareProbe;
89
90 /// Where this provider's wrapping key lives.
91 fn custody(&self) -> KeyCustody;
92
93 /// Encrypt `content_key` to the hardware wrapping key.
94 ///
95 /// The returned bytes are opaque to this crate and are stored verbatim in
96 /// the envelope header.
97 fn wrap_key(&self, content_key: &ContentKey) -> Result<Vec<u8>>;
98
99 /// Decrypt a previously wrapped content key using the hardware key.
100 ///
101 /// MUST fail — never return arbitrary bytes — when `wrapped` was sealed by a
102 /// different hardware key (for instance a blob copied from another machine).
103 /// That failure *is* the cross-machine binding guarantee.
104 ///
105 /// **The same failure occurs when this device is the original one and its
106 /// key has since been destroyed** (a TPM clear, a mainboard swap), which is
107 /// permanent loss rather than a refusal. A provider cannot tell the two
108 /// apart — it holds a key, not a history — so it MUST NOT describe the
109 /// failure as recoverable or imply the blob can simply be moved back. See
110 /// `SPEC.md` §17.5b, which is normative for how this is reported.
111 fn unwrap_key(&self, wrapped: &[u8]) -> Result<ContentKey>;
112}