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    /// 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}