dig_keystore/hardware/double.rs
1//! [`FakeDevice`] — a configurable [`HardwareProvider`] double.
2//!
3//! # Why a double, and what it does and does not prove
4//!
5//! Real TPM / Secure Enclave hardware is absent from CI (this repo's workflows
6//! run `ubuntu-latest` only) and cannot be made to lie on demand. `FakeDevice`
7//! exists to exercise the parts that *are* platform-independent: the tier
8//! decision, the fail-closed rules, the envelope codec, and the cross-machine
9//! binding property.
10//!
11//! It models non-exportability **structurally**: the device key lives only
12//! inside the `FakeDevice` and is never written into an envelope, so a second
13//! device with a different key cannot open the first device's blobs — exactly
14//! the situation of a sealed blob copied to another machine. What the double
15//! cannot prove is that a *real* platform key is non-exportable; that assertion
16//! belongs against the platform itself (it is made by attempting an export and
17//! requiring the platform to refuse).
18//!
19//! The double is deliberately **wide**. A double that can vary only one field
20//! cannot express a multi-field lie, and the interesting failures here are
21//! precisely providers that are inconsistent with themselves: one that probes
22//! `Available` but cannot wrap, one that claims a hardware kind it does not
23//! bind, one that reports `NonExportable` custody over a key held in process
24//! memory.
25
26use zeroize::Zeroizing;
27
28use std::sync::Arc;
29
30use parking_lot::Mutex;
31
32use super::provider::{ContentKey, HardwareProvider, KeyCustody, CONTENT_KEY_LEN};
33use super::tier::{HardwareKind, HardwareProbe};
34use crate::cipher;
35use crate::error::{KeystoreError, Result};
36
37/// How a [`FakeDevice`] behaves when asked to wrap or unwrap.
38///
39/// Each variant is an adversary the tier self-test must refute. The vocabulary is
40/// deliberately wide: **a fixture set that cannot express a given lie reports the
41/// guard against it as safe**, so every clause of the self-test contract needs a
42/// variant that violates exactly that clause.
43#[non_exhaustive]
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum WrapBehaviour {
46 /// Correct: AES-256-GCM under the device key, round-trips.
47 #[default]
48 Honest,
49 /// `wrap_key` errors — models hardware that probes present but cannot be used.
50 FailWrap,
51 /// `wrap_key` succeeds, `unwrap_key` errors.
52 FailUnwrap,
53 /// `wrap_key` returns the content key verbatim — models a "wrapping" that
54 /// wraps nothing, leaving the key exportable with the blob.
55 Passthrough,
56 /// `unwrap_key` returns a *different* key rather than failing — models a
57 /// provider whose round-trip silently does not reproduce the key.
58 WrongKeyOnUnwrap,
59
60 /// `wrap_key` returns an **empty** wrapped key while `unwrap_key` still
61 /// reproduces the right one from an internal slot.
62 ///
63 /// Models a provider that emits nothing into the envelope but remembers the
64 /// key out of band. It is the only way to violate *just* the
65 /// "a wrap that returns nothing" clause of the self-test contract: a device
66 /// that returned empty and then failed to unwrap would be refuted by the
67 /// round-trip clause instead, leaving the empty-wrap guard untested. Such a
68 /// provider would write envelopes carrying no wrapped key at all.
69 EmptyWrapWithRecall,
70}
71
72/// A configurable stand-in for a hardware trusted component.
73#[derive(Debug, Clone)]
74pub struct FakeDevice {
75 kind: HardwareKind,
76 probe: HardwareProbe,
77 custody: KeyCustody,
78 behaviour: WrapBehaviour,
79 /// Per-device key. Never leaves this struct and is never written into an
80 /// envelope — this is what makes one device's blobs unopenable by another.
81 ///
82 /// Shared and mutable so a clone of the device observes a
83 /// [`rotate_device_key`](FakeDevice::rotate_device_key) — the double's model
84 /// of a trusted component being CLEARED while the same handle is held.
85 device_key: Arc<Mutex<[u8; 32]>>,
86 /// How many more `unwrap_key` calls succeed before the device stops
87 /// unwrapping, or `None` for a device that always works.
88 ///
89 /// A device that is broken from the start is refuted by the constructor
90 /// self-test and never reaches the operations under test, so expressing
91 /// "honest at construction, broken afterwards" needs a budget rather than
92 /// another [`WrapBehaviour`] variant. Shared like the key, for the same
93 /// reason.
94 unwraps_left: Arc<Mutex<Option<usize>>>,
95 /// The last content key this device was asked to wrap.
96 ///
97 /// Serves two purposes: it lets a test assert that the plaintext content key
98 /// does not appear in the stored envelope, and it lets
99 /// [`WrapBehaviour::EmptyWrapWithRecall`] reproduce a key it never wrote out.
100 /// Shared behind an `Arc` so a clone of the device observes the same slot.
101 last_wrapped: Arc<Mutex<Option<[u8; CONTENT_KEY_LEN]>>>,
102}
103
104impl FakeDevice {
105 /// An honest, working device of `kind`, keyed by `device_id`.
106 ///
107 /// Two devices with different `device_id`s stand for two different machines.
108 pub fn working(kind: HardwareKind, device_id: u8) -> Self {
109 Self {
110 kind,
111 probe: HardwareProbe::Available(kind),
112 custody: KeyCustody::NonExportable,
113 behaviour: WrapBehaviour::Honest,
114 device_key: Arc::new(Mutex::new([device_id; 32])),
115 unwraps_left: Arc::default(),
116 last_wrapped: Arc::default(),
117 }
118 }
119
120 /// The last content key this device was asked to wrap, if any.
121 ///
122 /// Test-only observability: it is what makes "the content key never appears
123 /// in the stored bytes" an assertion about the actual key rather than about a
124 /// value the test invented.
125 pub fn last_wrapped_content_key(&self) -> Option<[u8; CONTENT_KEY_LEN]> {
126 *self.last_wrapped.lock()
127 }
128
129 /// A host with definitively no hardware.
130 pub fn absent(kind: HardwareKind) -> Self {
131 Self {
132 probe: HardwareProbe::Absent,
133 ..Self::working(kind, 0)
134 }
135 }
136
137 /// A host whose hardware could not be inspected.
138 pub fn indeterminate(kind: HardwareKind, detail: &str) -> Self {
139 Self {
140 probe: HardwareProbe::indeterminate(detail),
141 ..Self::working(kind, 0)
142 }
143 }
144
145 /// Override the probe outcome.
146 pub fn with_probe(mut self, probe: HardwareProbe) -> Self {
147 self.probe = probe;
148 self
149 }
150
151 /// Override the reported custody.
152 pub fn with_custody(mut self, custody: KeyCustody) -> Self {
153 self.custody = custody;
154 self
155 }
156
157 /// Override the wrap/unwrap behaviour.
158 pub fn with_behaviour(mut self, behaviour: WrapBehaviour) -> Self {
159 self.behaviour = behaviour;
160 self
161 }
162
163 /// Override the advertised kind independently of the probed kind, so the
164 /// double can contradict itself.
165 pub fn with_kind(mut self, kind: HardwareKind) -> Self {
166 self.kind = kind;
167 self
168 }
169
170 /// Replace this device's key material, as a TPM clear / re-enrolment does.
171 ///
172 /// Blobs sealed before the rotation become unopenable by this device, which
173 /// is precisely what a user sees after a firmware update clears the TPM.
174 /// Clones share the slot, so a backend already holding this provider sees
175 /// the change.
176 pub fn rotate_device_key(&self, device_id: u8) {
177 *self.device_key.lock() = [device_id; 32];
178 }
179
180 /// Succeed at `n` more unwraps, then fail every one after that.
181 ///
182 /// Models a component that is healthy when inspected and unusable moments
183 /// later. The constructor self-test spends exactly one unwrap, so
184 /// `failing_unwrap_after(1)` yields a device that resolves a genuine
185 /// hardware tier and then cannot reopen the next thing it seals.
186 pub fn failing_unwrap_after(self, n: usize) -> Self {
187 *self.unwraps_left.lock() = Some(n);
188 self
189 }
190
191 /// Length of the per-wrap nonce prefixed to a wrapped blob.
192 const NONCE_LEN: usize = 12;
193
194 /// Draw a fresh nonce for one wrap.
195 ///
196 /// A fixed nonce would be wrong even in a double: every wrap encrypts a
197 /// *different* freshly-generated content key under the same device key, so a
198 /// constant nonce is AES-GCM nonce reuse across distinct plaintexts. The
199 /// nonce is prefixed to the wrapped blob so `unwrap_key` can recover it.
200 fn fresh_nonce() -> [u8; Self::NONCE_LEN] {
201 let mut nonce = <[u8; Self::NONCE_LEN]>::default();
202 rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut nonce);
203 nonce
204 }
205}
206
207impl HardwareProvider for FakeDevice {
208 fn kind(&self) -> HardwareKind {
209 self.kind
210 }
211
212 fn probe(&self) -> HardwareProbe {
213 self.probe.clone()
214 }
215
216 fn custody(&self) -> KeyCustody {
217 self.custody
218 }
219
220 fn wrap_key(&self, content_key: &ContentKey) -> Result<Vec<u8>> {
221 *self.last_wrapped.lock() = Some(**content_key);
222 match self.behaviour {
223 WrapBehaviour::FailWrap => Err(KeystoreError::HardwareWrapFailed {
224 detail: "fake device refuses to wrap".to_owned(),
225 }),
226 WrapBehaviour::Passthrough => Ok(content_key.to_vec()),
227 // Emits nothing, but see `unwrap_key` — the key is remembered.
228 WrapBehaviour::EmptyWrapWithRecall => Ok(Vec::new()),
229 _ => {
230 // nonce || AES-256-GCM(content key) under the device key.
231 let nonce = Self::fresh_nonce();
232 let device_key = *self.device_key.lock();
233 let sealed = cipher::encrypt(&device_key, &nonce, content_key.as_slice(), b"")?;
234 let mut out = Vec::with_capacity(nonce.len() + sealed.len());
235 out.extend_from_slice(&nonce);
236 out.extend_from_slice(&sealed);
237 Ok(out)
238 }
239 }
240 }
241
242 fn unwrap_key(&self, wrapped: &[u8]) -> Result<ContentKey> {
243 // Spend the unwrap budget first: an exhausted device refuses regardless
244 // of behaviour, which is what makes "healthy at construction, broken
245 // afterwards" expressible.
246 {
247 let mut left = self.unwraps_left.lock();
248 if let Some(remaining) = left.as_mut() {
249 if *remaining == 0 {
250 return Err(KeystoreError::HardwareUnwrapFailed {
251 detail: "fake device stopped unwrapping".to_owned(),
252 });
253 }
254 *remaining -= 1;
255 }
256 }
257 match self.behaviour {
258 WrapBehaviour::FailUnwrap => {
259 return Err(KeystoreError::HardwareUnwrapFailed {
260 detail: "fake device refuses to unwrap".to_owned(),
261 })
262 }
263 WrapBehaviour::WrongKeyOnUnwrap => {
264 // Any key OTHER than the one wrapped; derived, not a literal.
265 let mut wrong = <[u8; CONTENT_KEY_LEN]>::default();
266 rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut wrong);
267 return Ok(Zeroizing::new(wrong));
268 }
269 WrapBehaviour::EmptyWrapWithRecall => {
270 return self.last_wrapped.lock().map(Zeroizing::new).ok_or_else(|| {
271 KeystoreError::HardwareUnwrapFailed {
272 detail: "recall device has wrapped nothing yet".to_owned(),
273 }
274 })
275 }
276 WrapBehaviour::Passthrough => {
277 let bytes: [u8; CONTENT_KEY_LEN] =
278 wrapped
279 .try_into()
280 .map_err(|_| KeystoreError::HardwareUnwrapFailed {
281 detail: "passthrough device got a non-key blob".to_owned(),
282 })?;
283 return Ok(Zeroizing::new(bytes));
284 }
285 _ => {}
286 }
287
288 // Split the prefixed nonce back off. A blob too short to carry one was
289 // not produced by this device.
290 if wrapped.len() <= Self::NONCE_LEN {
291 return Err(KeystoreError::HardwareUnwrapFailed {
292 detail: "wrapped blob is too short to carry a nonce".to_owned(),
293 });
294 }
295 let (nonce, sealed) = wrapped.split_at(Self::NONCE_LEN);
296 let nonce: [u8; Self::NONCE_LEN] = nonce.try_into().expect("checked length");
297
298 // A blob sealed by a *different* device key fails here — the
299 // cross-machine binding guarantee.
300 let device_key = *self.device_key.lock();
301 let plain = cipher::decrypt(&device_key, &nonce, sealed, b"").map_err(|_| {
302 KeystoreError::HardwareUnwrapFailed {
303 detail: "wrapped key was not sealed by this device".to_owned(),
304 }
305 })?;
306 let bytes: [u8; CONTENT_KEY_LEN] =
307 plain
308 .as_slice()
309 .try_into()
310 .map_err(|_| KeystoreError::HardwareUnwrapFailed {
311 detail: "unwrapped key has the wrong length".to_owned(),
312 })?;
313 Ok(Zeroizing::new(bytes))
314 }
315}