Skip to main content

microvms_app/
entropy.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The entropy port: where idempotency nonces, agent tokens and identity seeds come from.
3//!
4//! # Why a port for randomness
5//!
6//! Three values a launch mints are random by requirement, and each has a different reason:
7//!
8//! * the `clientToken` nonce must never repeat across attempts, because a replayed token
9//!   wedges an image in `CREATING` for fifteen hours (TRAP-1, [`crate::control::token`]);
10//! * the agent token is a bearer credential, so it must be unguessable;
11//! * the identity seeds are x25519 secrets.
12//!
13//! Drawing them through [`Entropy`] rather than calling the OS pool inline keeps the use cases
14//! free of that I/O, and lets a test pin exactly which draw became which value.
15//!
16//! # No weak default
17//!
18//! Every production control plane gets `OsEntropy` (in `microvms-edges`) unless a caller swaps it, including one built over a
19//! caller's transport. A plane with a deterministic source would mint repeating client tokens,
20//! which is the exact failure TRAP-1 exists to prevent, so the only way to get one is to ask
21//! for it by name.
22
23use std::fmt;
24
25use protocol::identity::SEED_BYTES;
26
27use crate::error::{Error, ErrorKind};
28use crate::identity::LaunchIdentity;
29
30/// A source of random bytes.
31pub trait Entropy: Send + Sync + fmt::Debug {
32    /// Fills `buf` with fresh random bytes.
33    ///
34    /// Fails only when the source is unavailable. A caller refuses the operation then, never
35    /// falls back to a clock: a clock-derived nonce risks the collision the nonce exists to
36    /// prevent, and a clock-derived credential is guessable.
37    fn fill(&self, buf: &mut [u8]) -> Result<(), Error>;
38
39    /// A fresh identity seed.
40    fn seed(&self) -> Result<[u8; SEED_BYTES], Error> {
41        let mut seed = [0_u8; SEED_BYTES];
42        self.fill(&mut seed)?;
43        Ok(seed)
44    }
45}
46
47/// A launch identity from two fresh seeds drawn from `entropy`.
48///
49/// An all-zero draw isn't reachable from a working pool. It's still refused, because the
50/// protocol crate refuses a zero seed (a shared identity proves nothing), and a launch must
51/// never hand out material the daemon would reject at bootstrap. The refusal is the source's
52/// fault, not the caller's, so it isn't an invalid argument.
53pub fn launch_identity(entropy: &dyn Entropy) -> Result<LaunchIdentity, Error> {
54    let vm_seed = entropy.seed()?;
55    let host_seed = entropy.seed()?;
56    LaunchIdentity::from_seeds(vm_seed, host_seed)
57        .map_err(|error| Error::new(ErrorKind::Unexpected, error.to_string()))
58}
59
60#[cfg(any(test, feature = "test-support"))]
61#[cfg_attr(test, mutants::skip)] // a test double; see .cargo/mutants.toml
62pub(crate) mod testing {
63    //! The deterministic source the tests share.
64
65    use std::sync::atomic::{AtomicU64, Ordering};
66
67    use super::*;
68
69    /// Deterministic bytes, distinct per call, so a test can say which draw became which value.
70    ///
71    /// Call `n` (from 1) fills byte `i` with byte `i % 8` of `n`, little-endian, XORed with
72    /// `i`. The first eight bytes encode `n` one-to-one, so two calls never produce the same
73    /// fill, and no fill longer than eight bytes is all zero, so a seed drawn from it passes the
74    /// protocol's check.
75    #[derive(Debug, Default)]
76    pub struct SequenceEntropy {
77        calls: AtomicU64,
78        unavailable: bool,
79    }
80
81    impl SequenceEntropy {
82        pub fn new() -> Self {
83            Self::default()
84        }
85
86        /// A source whose every draw fails, the way an unavailable OS pool does.
87        pub fn unavailable() -> Self {
88            Self {
89                unavailable: true,
90                ..Self::default()
91            }
92        }
93
94        /// The bytes call `n` (from 1) fills a buffer of `len` with.
95        pub fn draw(n: u64, len: usize) -> Vec<u8> {
96            let counter = n.to_le_bytes();
97            (0..len)
98                .map(|index| counter[index % counter.len()] ^ (index as u8))
99                .collect()
100        }
101
102        /// How many fills were asked for, failed ones included.
103        pub fn calls(&self) -> u64 {
104            self.calls.load(Ordering::SeqCst)
105        }
106    }
107
108    impl Entropy for SequenceEntropy {
109        fn fill(&self, buf: &mut [u8]) -> Result<(), Error> {
110            let n = self.calls.fetch_add(1, Ordering::SeqCst) + 1;
111            if self.unavailable {
112                return Err(Error::new(
113                    ErrorKind::Unexpected,
114                    "the scripted random pool is unavailable",
115                ));
116            }
117            buf.copy_from_slice(&Self::draw(n, buf.len()));
118            Ok(())
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::collections::HashSet;
126
127    use super::testing::SequenceEntropy;
128    use super::*;
129
130    /// The scripted source is distinct per call and never all zero, which is what lets a
131    /// test's seeds pass the protocol's check.
132    #[test]
133    fn the_sequence_source_is_distinct_per_call_and_never_zero() {
134        let entropy = SequenceEntropy::new();
135        let seeds: HashSet<[u8; SEED_BYTES]> = (0..300)
136            .map(|_| entropy.seed().expect("scripted"))
137            .collect();
138        assert_eq!(seeds.len(), 300);
139        assert!(seeds.iter().all(|seed| seed.iter().any(|byte| *byte != 0)));
140    }
141
142    /// Both seeds of a launch identity come from the source, in order: VM seed, then host.
143    ///
144    /// **Falsification**: draw the seeds from `OsEntropy` inside `launch_identity` and the VM
145    /// seed stops being the first scripted draw.
146    #[test]
147    fn a_launch_identity_is_the_source_s_next_two_seeds() {
148        use base64::Engine as _;
149        let entropy = SequenceEntropy::new();
150        let identity = launch_identity(&entropy).expect("valid seeds");
151        let expected =
152            base64::engine::general_purpose::STANDARD.encode(SequenceEntropy::draw(1, SEED_BYTES));
153        assert_eq!(identity.seed_field(), expected);
154        assert_eq!(entropy.calls(), 2);
155    }
156
157    /// An unavailable source refuses the identity; it doesn't panic.
158    #[test]
159    fn an_unavailable_source_refuses_the_identity() {
160        let err = launch_identity(&SequenceEntropy::unavailable()).expect_err("no pool");
161        assert_eq!(err.kind(), ErrorKind::Unexpected);
162    }
163}