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}