Skip to main content

microvms_core/
identity.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The host's half of the tunnel identity proof: seed generation, derivation, and the pin.
3//!
4//! `protocol::identity` carries the wire contract and the full design argument (why Noise KK
5//! rather than rustls — a measured cross-compile constraint on the daemon's shipping target —
6//! and the honest ptrace limit). This module is what a *launcher* holds: the two seeds it
7//! generated, the payload fields they become, and the pin it stores to recognise its VM later.
8//!
9//! # Two seeds, and who learns what
10//!
11//! At launch the host generates two 32-byte x25519 static secrets:
12//!
13//! * the **VM seed**, delivered to the daemon in the run-hook payload. After the launch the
14//!   host needs only its *public* half — the pin — and the secret half should not outlive the
15//!   launch call on the host side.
16//! * the **host seed**, which never leaves this process's trust domain. Its public half is
17//!   delivered to the daemon, which pins it in return. Holding this secret is what
18//!   distinguishes the launching host from anyone else with the agent token.
19//!
20//! Mutual authentication falls out: the client proves it holds the host secret, the daemon
21//! proves it holds the VM seed, and each side pinned the other's public half before the first
22//! byte moved.
23
24pub use microvms_domain::identity::*;
25
26// Seed generation and the Noise initiator draw from the OS random pool, so they're
27// `crate::prelude::LaunchIdentityExt` and `TunnelIdentityExt`; the rest is the domain's.
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32    use crate::prelude::*;
33
34    #[test]
35    fn generated_material_is_fresh_and_valid() {
36        let first = LaunchIdentity::generate().expect("the pool is available");
37        let second = LaunchIdentity::generate().expect("the pool is available");
38        // Distinct across calls — the property a seeded identity exists to provide.
39        assert_ne!(first.seed_field(), second.seed_field());
40        assert_ne!(first.host_public_field(), second.host_public_field());
41        // And the fields are the size the payload budget accounts for: 44 chars of base64.
42        assert_eq!(first.seed_field().len(), 44);
43        assert_eq!(first.host_public_field().len(), 44);
44    }
45
46    /// The client's derivation agrees with the daemon's, end to end in one process.
47    ///
48    /// The two sides derive independently — `microvms-domain` with `x25519-dalek` directly, the
49    /// daemon through the same crate but its own code path — and this is the assertion that
50    /// the payload fields this module emits produce a daemon whose handshake this module's
51    /// initiator completes.
52    #[test]
53    fn the_initiator_completes_against_a_responder_built_from_the_payload_fields() {
54        use base64::Engine as _;
55        let b64 = base64::engine::general_purpose::STANDARD;
56
57        let launch = LaunchIdentity::from_seeds([7_u8; 32], [9_u8; 32]).expect("valid seeds");
58        // What the daemon receives: the two payload fields, decoded its way.
59        let vm_seed_bytes: [u8; 32] = b64
60            .decode(launch.seed_field())
61            .expect("valid base64")
62            .try_into()
63            .expect("32 bytes");
64        let host_public_bytes: [u8; 32] = b64
65            .decode(launch.host_public_field())
66            .expect("valid base64")
67            .try_into()
68            .expect("32 bytes");
69
70        let mut responder =
71            snow::Builder::new(protocol::identity::NOISE_PATTERN.parse().expect("parses"))
72                .local_private_key(&vm_seed_bytes)
73                .expect("a 32-byte secret")
74                .remote_public_key(&host_public_bytes)
75                .expect("a 32-byte key")
76                .build_responder()
77                .expect("builds");
78
79        let kept = launch.keep();
80        let mut initiator = kept.initiator().expect("builds");
81
82        let mut buffer = [0_u8; 1024];
83        let mut scratch = [0_u8; 1024];
84        let written = initiator.write_message(&[], &mut buffer).expect("writes");
85        responder
86            .read_message(&buffer[..written], &mut scratch)
87            .expect("the daemon accepts the launching host");
88        let written = responder.write_message(&[], &mut buffer).expect("writes");
89        initiator
90            .read_message(&buffer[..written], &mut scratch)
91            .expect("the host accepts its own VM");
92
93        // Both reach transport mode: the handshake genuinely completed.
94        let mut host_transport = initiator.into_transport_mode().expect("transport");
95        let mut vm_transport = responder.into_transport_mode().expect("transport");
96        let sent = host_transport
97            .write_message(b"ping", &mut buffer)
98            .expect("encrypts");
99        let received = vm_transport
100            .read_message(&buffer[..sent], &mut scratch)
101            .expect("decrypts");
102        assert_eq!(&scratch[..received], b"ping");
103    }
104}