microvms_core/
identity.rs1use protocol::identity::{SEED_BYTES, seed_from_bytes};
25
26use crate::error::{Error, ErrorKind};
27
28pub struct LaunchIdentity {
35 vm_seed: [u8; SEED_BYTES],
36 host_seed: [u8; SEED_BYTES],
37}
38
39impl std::fmt::Debug for LaunchIdentity {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str("LaunchIdentity(<two seeds>)")
44 }
45}
46
47impl LaunchIdentity {
48 pub fn generate() -> Result<Self, Error> {
54 let mut vm_seed = [0_u8; SEED_BYTES];
55 let mut host_seed = [0_u8; SEED_BYTES];
56 getrandom::fill(&mut vm_seed)
57 .and_then(|()| getrandom::fill(&mut host_seed))
58 .map_err(|error| {
59 Error::new(
60 ErrorKind::Unexpected,
61 format!("the OS random pool is unavailable: {error}"),
62 )
63 })?;
64 seed_from_bytes(&vm_seed)
67 .and_then(|_| seed_from_bytes(&host_seed))
68 .map_err(|error| Error::new(ErrorKind::Unexpected, error.to_string()))?;
69 Ok(Self { vm_seed, host_seed })
70 }
71
72 #[cfg(test)]
74 pub fn from_seeds(vm_seed: [u8; SEED_BYTES], host_seed: [u8; SEED_BYTES]) -> Self {
75 Self { vm_seed, host_seed }
76 }
77
78 pub fn seed_field(&self) -> String {
80 use base64::Engine as _;
81 base64::engine::general_purpose::STANDARD.encode(self.vm_seed)
82 }
83
84 pub fn host_public_field(&self) -> String {
87 use base64::Engine as _;
88 base64::engine::general_purpose::STANDARD.encode(public_of(&self.host_seed))
89 }
90
91 pub fn keep(self) -> TunnelIdentity {
98 TunnelIdentity {
99 host_seed: self.host_seed,
100 vm_public_key: public_of(&self.vm_seed),
101 }
102 }
103}
104
105#[derive(Clone)]
107pub struct TunnelIdentity {
108 host_seed: [u8; SEED_BYTES],
109 vm_public_key: [u8; SEED_BYTES],
110}
111
112impl std::fmt::Debug for TunnelIdentity {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 write!(
116 f,
117 "TunnelIdentity(vm_public_key: {}, host_seed: <secret>)",
118 const_hex::encode(self.vm_public_key)
119 )
120 }
121}
122
123impl TunnelIdentity {
124 pub fn from_parts(host_seed: &[u8], vm_public_key: &[u8]) -> Result<Self, Error> {
130 let host_seed = seed_from_bytes(host_seed)
131 .map_err(|error| Error::invalid_arg(format!("the stored host seed: {error}")))?;
132 let vm_public_key: [u8; SEED_BYTES] = vm_public_key.try_into().map_err(|_| {
133 Error::invalid_arg(format!(
134 "the stored VM public key is {} bytes, and an x25519 public key is exactly \
135 {SEED_BYTES}",
136 vm_public_key.len()
137 ))
138 })?;
139 Ok(Self {
140 host_seed,
141 vm_public_key,
142 })
143 }
144
145 pub fn vm_public_key(&self) -> [u8; SEED_BYTES] {
147 self.vm_public_key
148 }
149
150 pub fn host_seed(&self) -> [u8; SEED_BYTES] {
157 self.host_seed
158 }
159
160 pub fn host_seed_base64(&self) -> String {
166 use base64::Engine as _;
167 base64::engine::general_purpose::STANDARD.encode(self.host_seed)
168 }
169
170 pub fn vm_public_key_base64(&self) -> String {
172 use base64::Engine as _;
173 base64::engine::general_purpose::STANDARD.encode(self.vm_public_key)
174 }
175
176 pub fn from_encoded_parts(host_seed: &str, vm_public_key: &str) -> Result<Self, Error> {
178 use base64::Engine as _;
179 let b64 = base64::engine::general_purpose::STANDARD;
180 let host = b64.decode(host_seed).map_err(|_| {
181 Error::invalid_arg(
182 "the identity host seed is not valid base64. It is the identityHostSeed \
183 value from `run --identity`'s envelope, or the registry record's \
184 identityHostSeed field."
185 .to_string(),
186 )
187 })?;
188 let vm = b64.decode(vm_public_key).map_err(|_| {
189 Error::invalid_arg(
190 "the identity VM public key is not valid base64. It is the \
191 identityVmPublicKey value from `run --identity`'s envelope."
192 .to_string(),
193 )
194 })?;
195 Self::from_parts(&host, &vm)
196 }
197
198 pub fn initiator(&self) -> Result<snow::HandshakeState, Error> {
202 snow::Builder::new(
203 protocol::identity::NOISE_PATTERN
204 .parse()
205 .expect("the pattern is a compile-time constant the protocol crate tests"),
206 )
207 .local_private_key(&self.host_seed)
208 .and_then(|builder| builder.remote_public_key(&self.vm_public_key))
209 .and_then(|builder| builder.build_initiator())
210 .map_err(|error| {
211 Error::new(
212 ErrorKind::Unexpected,
213 format!("could not build the identity initiator: {error}"),
214 )
215 })
216 }
217}
218
219fn public_of(seed: &[u8; SEED_BYTES]) -> [u8; SEED_BYTES] {
221 *x25519_dalek::PublicKey::from(&x25519_dalek::StaticSecret::from(*seed)).as_bytes()
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn generated_material_is_fresh_and_valid() {
230 let first = LaunchIdentity::generate().expect("the pool is available");
231 let second = LaunchIdentity::generate().expect("the pool is available");
232 assert_ne!(first.seed_field(), second.seed_field());
234 assert_ne!(first.host_public_field(), second.host_public_field());
235 assert_eq!(first.seed_field().len(), 44);
237 assert_eq!(first.host_public_field().len(), 44);
238 }
239
240 #[test]
242 fn keeping_an_identity_drops_the_vm_secret() {
243 let vm_seed = [7_u8; SEED_BYTES];
244 let host_seed = [9_u8; SEED_BYTES];
245 let kept = LaunchIdentity::from_seeds(vm_seed, host_seed).keep();
246
247 assert_eq!(kept.vm_public_key(), public_of(&vm_seed));
248 assert_eq!(kept.host_seed(), host_seed);
249 assert_ne!(kept.vm_public_key(), vm_seed);
252 }
253
254 #[test]
255 fn a_kept_identity_round_trips_through_its_stored_parts() {
256 let kept = LaunchIdentity::from_seeds([7_u8; 32], [9_u8; 32]).keep();
257 let restored = TunnelIdentity::from_parts(&kept.host_seed(), &kept.vm_public_key())
258 .expect("what was stored restores");
259 assert_eq!(restored.vm_public_key(), kept.vm_public_key());
260 assert_eq!(restored.host_seed(), kept.host_seed());
261 }
262
263 #[test]
264 fn stored_parts_of_the_wrong_shape_are_refused_with_the_length() {
265 let error = TunnelIdentity::from_parts(&[1_u8; 16], &[2_u8; 32])
266 .expect_err("a short host seed is refused");
267 assert!(error.to_string().contains("16"), "{error}");
268
269 let error = TunnelIdentity::from_parts(&[1_u8; 32], &[2_u8; 31])
270 .expect_err("a short public key is refused");
271 assert!(error.to_string().contains("31"), "{error}");
272 }
273
274 #[test]
276 fn debug_prints_the_pin_and_never_a_secret() {
277 let launch = LaunchIdentity::from_seeds([0x5A_u8; 32], [0x3C_u8; 32]);
278 let rendered = format!("{launch:?}");
279 assert!(!rendered.to_lowercase().contains("5a5a"), "{rendered}");
280 assert!(!rendered.to_lowercase().contains("3c3c"), "{rendered}");
281
282 let kept = launch.keep();
283 let rendered = format!("{kept:?}");
284 assert!(
287 rendered.contains(&const_hex::encode(kept.vm_public_key())),
288 "{rendered}"
289 );
290 assert!(!rendered.to_lowercase().contains("3c3c3c"), "{rendered}");
291 }
292
293 #[test]
300 fn the_initiator_completes_against_a_responder_built_from_the_payload_fields() {
301 use base64::Engine as _;
302 let b64 = base64::engine::general_purpose::STANDARD;
303
304 let launch = LaunchIdentity::from_seeds([7_u8; 32], [9_u8; 32]);
305 let vm_seed_bytes: [u8; 32] = b64
307 .decode(launch.seed_field())
308 .expect("valid base64")
309 .try_into()
310 .expect("32 bytes");
311 let host_public_bytes: [u8; 32] = b64
312 .decode(launch.host_public_field())
313 .expect("valid base64")
314 .try_into()
315 .expect("32 bytes");
316
317 let mut responder =
318 snow::Builder::new(protocol::identity::NOISE_PATTERN.parse().expect("parses"))
319 .local_private_key(&vm_seed_bytes)
320 .expect("a 32-byte secret")
321 .remote_public_key(&host_public_bytes)
322 .expect("a 32-byte key")
323 .build_responder()
324 .expect("builds");
325
326 let kept = launch.keep();
327 let mut initiator = kept.initiator().expect("builds");
328
329 let mut buffer = [0_u8; 1024];
330 let mut scratch = [0_u8; 1024];
331 let written = initiator.write_message(&[], &mut buffer).expect("writes");
332 responder
333 .read_message(&buffer[..written], &mut scratch)
334 .expect("the daemon accepts the launching host");
335 let written = responder.write_message(&[], &mut buffer).expect("writes");
336 initiator
337 .read_message(&buffer[..written], &mut scratch)
338 .expect("the host accepts its own VM");
339
340 let mut host_transport = initiator.into_transport_mode().expect("transport");
342 let mut vm_transport = responder.into_transport_mode().expect("transport");
343 let sent = host_transport
344 .write_message(b"ping", &mut buffer)
345 .expect("encrypts");
346 let received = vm_transport
347 .read_message(&buffer[..sent], &mut scratch)
348 .expect("decrypts");
349 assert_eq!(&scratch[..received], b"ping");
350 }
351}