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 will live
17//!
18//! **No platform provider ships yet** — this release contains the trait and the
19//! envelope, and nothing that binds to real hardware. Passing no provider
20//! resolves [`Software(NotRequested)`](super::DegradeReason::NotRequested).
21//!
22//! This package sets `unsafe_code = "forbid"` as a spec-pinned security property
23//! (`SPEC.md` §12/§13.2, conformance C-15), so raw CNG / Security Framework FFI
24//! cannot live here. Real bindings are therefore *planned* for a separate
25//! `hardware/` workspace member (`dig-keystore-hardware`) that will mirror the
26//! existing `wasm/` split, tracked as **dig_ecosystem #1693**; they will be
27//! injected through this trait.
28
29use zeroize::Zeroizing;
30
31use super::tier::{HardwareKind, HardwareProbe};
32use crate::error::Result;
33
34/// Length of the symmetric content key a provider wraps.
35pub const CONTENT_KEY_LEN: usize = 32;
36
37/// The content key that a hardware provider wraps and unwraps.
38///
39/// Zeroized on drop: this is the only plaintext key material that transits the
40/// provider boundary.
41pub type ContentKey = Zeroizing<[u8; CONTENT_KEY_LEN]>;
42
43/// Where a provider's wrapping key physically lives.
44///
45/// This is the property hardware binding actually buys, so it is reported
46/// explicitly rather than inferred from the provider's name.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum KeyCustody {
49 /// The wrapping key was generated inside the hardware component and cannot
50 /// be exported from it. Copying the sealed blob to another machine does not
51 /// let an attacker open it.
52 ///
53 /// An implementation MUST NOT report this unless the key was created
54 /// non-exportable *and* an export attempt is refused by the platform.
55 NonExportable,
56
57 /// The wrapping key exists in process memory at some point. Offers no
58 /// cross-machine binding; a provider reporting this MUST NOT be treated as
59 /// a hardware tier.
60 ProcessMemory,
61}
62
63impl KeyCustody {
64 /// Whether this custody level is strong enough to claim a hardware tier.
65 pub const fn is_hardware_grade(self) -> bool {
66 matches!(self, Self::NonExportable)
67 }
68}
69
70/// A binding to one OS hardware trusted component.
71///
72/// Implementations are injected into
73/// [`HardwareBoundBackend`](super::HardwareBoundBackend), which probes and
74/// self-tests them before claiming a hardware tier — an implementation is never
75/// taken at its word.
76pub trait HardwareProvider: Send + Sync + 'static {
77 /// Which hardware class this provider binds to.
78 fn kind(&self) -> HardwareKind;
79
80 /// Inspect the host for usable hardware.
81 ///
82 /// MUST return [`HardwareProbe::Indeterminate`] — never
83 /// [`Absent`](HardwareProbe::Absent) — when the inspection itself fails
84 /// (an error, a timeout, an empty or unintelligible response). "I could not
85 /// tell" and "there is none" are different answers and the caller acts
86 /// differently on each.
87 fn probe(&self) -> HardwareProbe;
88
89 /// Where this provider's wrapping key lives.
90 fn custody(&self) -> KeyCustody;
91
92 /// Encrypt `content_key` to the hardware wrapping key.
93 ///
94 /// The returned bytes are opaque to this crate and are stored verbatim in
95 /// the envelope header.
96 fn wrap_key(&self, content_key: &ContentKey) -> Result<Vec<u8>>;
97
98 /// Decrypt a previously wrapped content key using the hardware key.
99 ///
100 /// MUST fail — never return arbitrary bytes — when `wrapped` was sealed by a
101 /// different hardware key (for instance a blob copied from another machine).
102 /// That failure *is* the cross-machine binding guarantee.
103 fn unwrap_key(&self, wrapped: &[u8]) -> Result<ContentKey>;
104}