Skip to main content

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    /// How many more `probe` calls report the configured outcome before the
96    /// device reports [`HardwareProbe::Indeterminate`], or `None` for a device
97    /// whose probe never changes.
98    ///
99    /// Temporal variation, for the same reason `unwraps_left` exists: a device
100    /// that is uninspectable from the start never reaches the composed paths
101    /// under test, because the FIRST resolution already refuses it. Expressing
102    /// "inspectable once, uninspectable moments later" — a TPM that becomes
103    /// contended between two resolutions — needs a budget rather than a fixed
104    /// probe. Shared like the key, so a clone observes the same countdown.
105    probes_left: Arc<Mutex<Option<usize>>>,
106    /// The last content key this device was asked to wrap.
107    ///
108    /// Serves two purposes: it lets a test assert that the plaintext content key
109    /// does not appear in the stored envelope, and it lets
110    /// [`WrapBehaviour::EmptyWrapWithRecall`] reproduce a key it never wrote out.
111    /// Shared behind an `Arc` so a clone of the device observes the same slot.
112    last_wrapped: Arc<Mutex<Option<[u8; CONTENT_KEY_LEN]>>>,
113}
114
115impl FakeDevice {
116    /// An honest, working device of `kind`, keyed by `device_id`.
117    ///
118    /// Two devices with different `device_id`s stand for two different machines.
119    pub fn working(kind: HardwareKind, device_id: u8) -> Self {
120        Self {
121            kind,
122            probe: HardwareProbe::Available(kind),
123            custody: KeyCustody::NonExportable,
124            behaviour: WrapBehaviour::Honest,
125            device_key: Arc::new(Mutex::new([device_id; 32])),
126            unwraps_left: Arc::default(),
127            probes_left: Arc::default(),
128            last_wrapped: Arc::default(),
129        }
130    }
131
132    /// The last content key this device was asked to wrap, if any.
133    ///
134    /// Test-only observability: it is what makes "the content key never appears
135    /// in the stored bytes" an assertion about the actual key rather than about a
136    /// value the test invented.
137    pub fn last_wrapped_content_key(&self) -> Option<[u8; CONTENT_KEY_LEN]> {
138        *self.last_wrapped.lock()
139    }
140
141    /// A host with definitively no hardware.
142    pub fn absent(kind: HardwareKind) -> Self {
143        Self {
144            probe: HardwareProbe::Absent,
145            ..Self::working(kind, 0)
146        }
147    }
148
149    /// A host whose hardware could not be inspected.
150    pub fn indeterminate(kind: HardwareKind, detail: &str) -> Self {
151        Self {
152            probe: HardwareProbe::indeterminate(detail),
153            ..Self::working(kind, 0)
154        }
155    }
156
157    /// Override the probe outcome.
158    pub fn with_probe(mut self, probe: HardwareProbe) -> Self {
159        self.probe = probe;
160        self
161    }
162
163    /// Override the reported custody.
164    pub fn with_custody(mut self, custody: KeyCustody) -> Self {
165        self.custody = custody;
166        self
167    }
168
169    /// Override the wrap/unwrap behaviour.
170    pub fn with_behaviour(mut self, behaviour: WrapBehaviour) -> Self {
171        self.behaviour = behaviour;
172        self
173    }
174
175    /// Override the advertised kind independently of the probed kind, so the
176    /// double can contradict itself.
177    pub fn with_kind(mut self, kind: HardwareKind) -> Self {
178        self.kind = kind;
179        self
180    }
181
182    /// Replace this device's key material, as a TPM clear / re-enrolment does.
183    ///
184    /// Blobs sealed before the rotation become unopenable by this device, which
185    /// is precisely what a user sees after a firmware update clears the TPM.
186    /// Clones share the slot, so a backend already holding this provider sees
187    /// the change.
188    pub fn rotate_device_key(&self, device_id: u8) {
189        *self.device_key.lock() = [device_id; 32];
190    }
191
192    /// Report the configured probe outcome `n` more times, then report
193    /// [`HardwareProbe::Indeterminate`] for every probe after that.
194    ///
195    /// Models a trusted component that is inspectable when first asked and
196    /// contended moments later — a TPM busy with BitLocker, Credential Guard or
197    /// Windows Hello. A composition that resolves the tier TWICE inspects such a
198    /// device twice and gets two different answers, which is the only way to
199    /// exercise the second resolution's policy handling.
200    #[must_use]
201    pub fn indeterminate_probe_after(self, n: usize) -> Self {
202        *self.probes_left.lock() = Some(n);
203        self
204    }
205
206    /// Succeed at `n` more unwraps, then fail every one after that.
207    ///
208    /// Models a component that is healthy when inspected and unusable moments
209    /// later. The constructor self-test spends exactly one unwrap, so
210    /// `failing_unwrap_after(1)` yields a device that resolves a genuine
211    /// hardware tier and then cannot reopen the next thing it seals.
212    #[must_use]
213    pub fn failing_unwrap_after(self, n: usize) -> Self {
214        *self.unwraps_left.lock() = Some(n);
215        self
216    }
217
218    /// Length of the per-wrap nonce prefixed to a wrapped blob.
219    const NONCE_LEN: usize = 12;
220
221    /// Draw a fresh nonce for one wrap.
222    ///
223    /// A fixed nonce would be wrong even in a double: every wrap encrypts a
224    /// *different* freshly-generated content key under the same device key, so a
225    /// constant nonce is AES-GCM nonce reuse across distinct plaintexts. The
226    /// nonce is prefixed to the wrapped blob so `unwrap_key` can recover it.
227    fn fresh_nonce() -> [u8; Self::NONCE_LEN] {
228        let mut nonce = <[u8; Self::NONCE_LEN]>::default();
229        rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut nonce);
230        nonce
231    }
232}
233
234impl HardwareProvider for FakeDevice {
235    fn kind(&self) -> HardwareKind {
236        self.kind
237    }
238
239    fn probe(&self) -> HardwareProbe {
240        let mut budget = self.probes_left.lock();
241        match budget.as_mut() {
242            Some(0) => HardwareProbe::Indeterminate {
243                detail: "fake device became uninspectable".to_owned(),
244            },
245            Some(left) => {
246                *left -= 1;
247                self.probe.clone()
248            }
249            None => self.probe.clone(),
250        }
251    }
252
253    fn custody(&self) -> KeyCustody {
254        self.custody
255    }
256
257    fn wrap_key(&self, content_key: &ContentKey) -> Result<Vec<u8>> {
258        *self.last_wrapped.lock() = Some(**content_key);
259        match self.behaviour {
260            WrapBehaviour::FailWrap => Err(KeystoreError::HardwareWrapFailed {
261                detail: "fake device refuses to wrap".to_owned(),
262            }),
263            WrapBehaviour::Passthrough => Ok(content_key.to_vec()),
264            // Emits nothing, but see `unwrap_key` — the key is remembered.
265            WrapBehaviour::EmptyWrapWithRecall => Ok(Vec::new()),
266            _ => {
267                // nonce || AES-256-GCM(content key) under the device key.
268                let nonce = Self::fresh_nonce();
269                let device_key = *self.device_key.lock();
270                let sealed = cipher::encrypt(&device_key, &nonce, content_key.as_slice(), b"")?;
271                let mut out = Vec::with_capacity(nonce.len() + sealed.len());
272                out.extend_from_slice(&nonce);
273                out.extend_from_slice(&sealed);
274                Ok(out)
275            }
276        }
277    }
278
279    fn unwrap_key(&self, wrapped: &[u8]) -> Result<ContentKey> {
280        // Spend the unwrap budget first: an exhausted device refuses regardless
281        // of behaviour, which is what makes "healthy at construction, broken
282        // afterwards" expressible.
283        {
284            let mut left = self.unwraps_left.lock();
285            if let Some(remaining) = left.as_mut() {
286                if *remaining == 0 {
287                    return Err(KeystoreError::HardwareUnwrapFailed {
288                        detail: "fake device stopped unwrapping".to_owned(),
289                    });
290                }
291                *remaining -= 1;
292            }
293        }
294        match self.behaviour {
295            WrapBehaviour::FailUnwrap => {
296                return Err(KeystoreError::HardwareUnwrapFailed {
297                    detail: "fake device refuses to unwrap".to_owned(),
298                })
299            }
300            WrapBehaviour::WrongKeyOnUnwrap => {
301                // Any key OTHER than the one wrapped; derived, not a literal.
302                let mut wrong = <[u8; CONTENT_KEY_LEN]>::default();
303                rand_core::RngCore::fill_bytes(&mut rand_core::OsRng, &mut wrong);
304                return Ok(Zeroizing::new(wrong));
305            }
306            WrapBehaviour::EmptyWrapWithRecall => {
307                return self.last_wrapped.lock().map(Zeroizing::new).ok_or_else(|| {
308                    KeystoreError::HardwareUnwrapFailed {
309                        detail: "recall device has wrapped nothing yet".to_owned(),
310                    }
311                })
312            }
313            WrapBehaviour::Passthrough => {
314                let bytes: [u8; CONTENT_KEY_LEN] =
315                    wrapped
316                        .try_into()
317                        .map_err(|_| KeystoreError::HardwareUnwrapFailed {
318                            detail: "passthrough device got a non-key blob".to_owned(),
319                        })?;
320                return Ok(Zeroizing::new(bytes));
321            }
322            _ => {}
323        }
324
325        // Split the prefixed nonce back off. A blob too short to carry one was
326        // not produced by this device.
327        if wrapped.len() <= Self::NONCE_LEN {
328            return Err(KeystoreError::HardwareUnwrapFailed {
329                detail: "wrapped blob is too short to carry a nonce".to_owned(),
330            });
331        }
332        let (nonce, sealed) = wrapped.split_at(Self::NONCE_LEN);
333        let nonce: [u8; Self::NONCE_LEN] = nonce.try_into().expect("checked length");
334
335        // A blob sealed by a *different* device key fails here — the
336        // cross-machine binding guarantee.
337        let device_key = *self.device_key.lock();
338        let plain = cipher::decrypt(&device_key, &nonce, sealed, b"").map_err(|_| {
339            KeystoreError::HardwareUnwrapFailed {
340                detail: "wrapped key was not sealed by this device".to_owned(),
341            }
342        })?;
343        let bytes: [u8; CONTENT_KEY_LEN] =
344            plain
345                .as_slice()
346                .try_into()
347                .map_err(|_| KeystoreError::HardwareUnwrapFailed {
348                    detail: "unwrapped key has the wrong length".to_owned(),
349                })?;
350        Ok(Zeroizing::new(bytes))
351    }
352}