1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
2
3#[cfg(any(test, feature = "signing"))]
4use ed25519_dalek::{Signer, SigningKey};
5
6use sha2::{Digest, Sha384};
7
8#[cfg(any(test, feature = "signing"))]
9use sha2::{digest::Output, Sha256};
10
11use serde::{Deserialize, Serialize};
12
13#[derive(thiserror::Error, Debug)]
14pub enum SignatureVerificationError {
15 #[error("Failed to verify signature: {0}")]
16 SignatureError(#[from] ed25519_dalek::SignatureError),
17 #[error("The specified key is unknown")]
18 UnknownKeyId,
19}
20
21const VMSA_PAGE_SIZE: usize = 4096;
22
23#[derive(thiserror::Error, Debug)]
24pub enum LaunchMeasurementError {
25 #[error("Launch measurement must be 48 bytes, got {0} bytes")]
26 InvalidMeasurementLength(usize),
27 #[error("VMSA must be at most {VMSA_PAGE_SIZE} bytes, got {0} bytes")]
28 InvalidVmsaLength(usize),
29}
30
31#[derive(Clone, Debug, Deserialize, Serialize)]
33#[serde(
34 untagged,
35 expecting = "a base64-encoded string or a launch measurement object"
36)]
37pub enum LaunchMeasurement {
38 V1(LaunchMeasurementV1),
39 V2(LaunchMeasurementV2),
40}
41
42#[derive(Clone, Debug, Deserialize, Serialize)]
45pub struct LaunchMeasurementV1(#[serde(with = "base64_compat")] pub Vec<u8>);
46
47#[derive(Clone, Debug, Deserialize, Serialize)]
51pub struct LaunchMeasurementV2 {
52 #[serde(with = "base64_compat")]
54 pub vmsa: Vec<u8>,
55 #[serde(with = "base64_compat")]
57 pub value: Vec<u8>,
58}
59
60impl LaunchMeasurement {
61 #[must_use]
72 pub fn resolve(&self, vcpus: usize) -> [u8; 48] {
73 self.try_resolve(vcpus).expect("Invalid launch measurement")
74 }
75
76 pub fn try_resolve(&self, vcpus: usize) -> Result<[u8; 48], LaunchMeasurementError> {
84 match self {
85 Self::V1(measurement) => measurement.try_resolve(),
86 Self::V2(measurement) => measurement.try_resolve(vcpus),
87 }
88 }
89
90 fn extend_signing_input(&self, input: &mut Vec<u8>) {
92 match self {
93 Self::V1(measurement) => input.extend_from_slice(&measurement.0),
94 Self::V2(measurement) => {
95 input.extend_from_slice(&measurement.vmsa);
96 input.extend_from_slice(&measurement.value);
97 }
98 }
99 }
100}
101
102impl LaunchMeasurementV1 {
103 pub fn try_resolve(&self) -> Result<[u8; 48], LaunchMeasurementError> {
109 self.0
110 .as_slice()
111 .try_into()
112 .map_err(|_| LaunchMeasurementError::InvalidMeasurementLength(self.0.len()))
113 }
114}
115
116impl LaunchMeasurementV2 {
117 pub fn try_resolve(&self, vcpus: usize) -> Result<[u8; 48], LaunchMeasurementError> {
125 if self.vmsa.len() > VMSA_PAGE_SIZE {
126 return Err(LaunchMeasurementError::InvalidVmsaLength(self.vmsa.len()));
127 }
128 let pre_vmsa_digest = self
129 .value
130 .as_slice()
131 .try_into()
132 .map_err(|_| LaunchMeasurementError::InvalidMeasurementLength(self.value.len()))?;
133 Ok(resolve_v2(&pre_vmsa_digest, &self.vmsa, vcpus))
134 }
135}
136
137fn resolve_v2(pre_vmsa_digest: &[u8; 48], vmsa: &[u8], vcpus: usize) -> [u8; 48] {
145 const PAGE_INFO_SIZE: u16 = 112;
146 const VMSA_GPA: u64 = 0xffff_ffff_f000;
147
148 let mut padded = [0u8; VMSA_PAGE_SIZE];
150 padded[..vmsa.len()].copy_from_slice(vmsa);
151 let contents: [u8; 48] = Sha384::digest(padded).into();
152
153 let mut digest: [u8; 48] = *pre_vmsa_digest;
154
155 for _ in 0..vcpus {
156 let mut page_info = [0u8; PAGE_INFO_SIZE as usize];
157 page_info[0..48].copy_from_slice(&digest); page_info[48..96].copy_from_slice(&contents); page_info[96..98].copy_from_slice(&PAGE_INFO_SIZE.to_le_bytes()); page_info[98] = 2; page_info[104..112].copy_from_slice(&VMSA_GPA.to_le_bytes()); digest = Sha384::digest(page_info).into();
163 }
164
165 digest
166}
167
168#[derive(Clone, Debug, Deserialize, Serialize)]
169#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
170#[serde(rename_all = "camelCase")]
171pub struct PublishedVersionEntry {
172 pub kind: String,
174 pub generation: usize,
176 #[serde(serialize_with = "rfc_3339_no_z")]
177 pub created_at: chrono::DateTime<chrono::offset::FixedOffset>,
178 pub ref_: String,
180 #[cfg_attr(feature = "utoipa", schema(value_type = serde_json::Value))]
184 pub launch_measurement: LaunchMeasurement,
185 pub signature: PublishedVersionEntrySignature,
187}
188
189impl PublishedVersionEntry {
190 pub fn verify_signature(&self) -> Result<(), SignatureVerificationError> {
197 let Some(verifying_key) = verifying_key_by_id(&self.signature.key_id) else {
198 return Err(SignatureVerificationError::UnknownKeyId);
199 };
200
201 self.verify_signature_with_key(&verifying_key)
202 }
203
204 fn verify_signature_with_key(
205 &self,
206 verifying_key: &VerifyingKey,
207 ) -> Result<(), SignatureVerificationError> {
208 let mut input = Vec::new();
209 input.extend_from_slice(self.kind.as_bytes());
210 input.extend_from_slice(self.generation.to_string().as_bytes());
211 input.extend_from_slice(self.created_at.to_rfc3339().as_bytes());
212 input.extend_from_slice(self.ref_.as_bytes());
213 self.launch_measurement.extend_signing_input(&mut input);
214
215 let signature = Signature::from_slice(&self.signature.value)
216 .map_err(SignatureVerificationError::SignatureError)?;
217
218 verifying_key
219 .verify(&input, &signature)
220 .map_err(SignatureVerificationError::SignatureError)
221 }
222}
223
224#[serde_with::serde_as]
225#[derive(Clone, Debug, Deserialize, Serialize)]
226#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
227#[serde(rename_all = "camelCase")]
228pub struct PublishedVersionEntrySignature {
229 #[serde(with = "base64_compat")]
231 #[cfg_attr(feature = "utoipa", schema(value_type = String, format = "byte", example = "dYa685dhHap8RSUtB4DDy1l4UcycsGhklBnV5a/4HSg="))]
232 pub key_id: Vec<u8>,
233 #[serde(with = "base64_compat")]
235 #[cfg_attr(feature = "utoipa", schema(value_type = String, format = "byte", example = "V89vbJZCP3ZNskERuNN1MkzaL5of68dPZ36DigmXmgBUn2fthx6M57Iyh5USOIR5GZyJi119yf3erp7Zi1mZCQ=="))]
236 pub value: Vec<u8>,
237}
238
239#[cfg(not(any(test, feature = "test-signing-keys")))]
240fn verifying_key_by_id(key_id: &[u8]) -> Option<VerifyingKey> {
241 if key_id == b"\x02\x1a\xd4^\xc5\x7f\\\x0b\xc8\x13n\x11C\xf5m\xff5\xe8.\x87o\x8dz\x16\x0fg\xe1,k\x913\xb5" {
242 Some(VerifyingKey::from_bytes(b"e\xbcL!\xf5\x9b*O\xd6\xb5}1D\x85W\xe8{[\xd1\x15\xef$\xc3\xd7R\x8c\xa0\xc3\xeco\xb4\xe2").unwrap())
244 } else if key_id == b"\x0f}3E\x87\xbcR\x0f{\xdff\xf8=\xed\x81\x03\x05\x80^\x7f\xd3\xe4\xdb,\xd8rN\xa7y\x0b\x0b\\" {
245 Some(VerifyingKey::from_bytes(b"\x08\xcbka\x11\x8c\x10\xf0\x1d\xc5h\x1a\xb3sV\x02\xd2Ik\xd3.\"Y\xaeu\xc1~\xd7\x0c\x85j%").unwrap())
247 } else {
248 None
249 }
250}
251
252#[cfg(any(test, feature = "test-signing-keys"))]
253fn verifying_key_by_id(key_id: &[u8]) -> Option<VerifyingKey> {
254 if key_id == b"u\x86\xba\xf3\x97a\x1d\xaa|E%-\x07\x80\xc3\xcbYxQ\xcc\x9c\xb0hd\x94\x19\xd5\xe5\xaf\xf8\x1d(" {
256 Some(VerifyingKey::from_bytes(b"\xab2\xe0\x0et^\x81\xb8\x90\xcd\x8e5\xea/\x7f0)k\x98\xcav\xe27\x07\xcd\xa5\x82Y%\x92\xba\xd0").unwrap())
258 } else {
259 None
260 }
261}
262
263#[cfg(any(test, feature = "signing"))]
264#[must_use]
265pub fn key_id(verifying_key: &VerifyingKey) -> Output<Sha256> {
266 let mut hasher = Sha256::new();
267 hasher.update(verifying_key.to_bytes());
268 hasher.finalize()
269}
270
271#[cfg(any(test, feature = "signing"))]
275#[must_use]
276pub fn signed_version_entry(
277 launch_measurement: LaunchMeasurement,
278 flake_url: &str,
279 signing_key: &SigningKey,
280) -> PublishedVersionEntry {
281 let kind = "release".to_string();
282 let generation = match &launch_measurement {
283 LaunchMeasurement::V1(_) => 1,
284 LaunchMeasurement::V2(_) => 2,
285 };
286 let created_at: chrono::DateTime<chrono::offset::FixedOffset> =
287 chrono::offset::Utc::now().into();
288
289 let mut input = Vec::new();
290 input.extend_from_slice(kind.as_bytes());
291 input.extend_from_slice(generation.to_string().as_bytes());
292 input.extend_from_slice(created_at.to_rfc3339().as_bytes());
293 input.extend_from_slice(flake_url.as_bytes());
294 launch_measurement.extend_signing_input(&mut input);
295
296 let signature = {
297 let signature = signing_key.sign(&input);
298 PublishedVersionEntrySignature {
299 key_id: key_id(&signing_key.verifying_key()).to_vec(),
300 value: signature.to_vec(),
301 }
302 };
303
304 PublishedVersionEntry {
305 kind,
306 generation,
307 created_at,
308 ref_: flake_url.into(),
309 launch_measurement,
310 signature,
311 }
312}
313
314fn rfc_3339_no_z<S: serde::Serializer>(
317 source: &chrono::DateTime<chrono::offset::FixedOffset>,
318 serializer: S,
319) -> Result<S::Ok, S::Error> {
320 source.to_rfc3339().serialize(serializer)
321}
322
323mod base64_compat {
324 use base64::prelude::{Engine, BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
325 use serde::{de, Deserialize, Serialize, Serializer};
326
327 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
328 where
329 D: serde::Deserializer<'de>,
330 {
331 let value: String = Deserialize::deserialize(deserializer)?;
332 let url_safe = value.replace('+', "-").replace('/', "_");
333 let unpadded = url_safe.trim_end_matches('=');
334 BASE64_URL_SAFE_NO_PAD
335 .decode(unpadded)
336 .map_err(de::Error::custom)
337 }
338
339 pub fn serialize<S: Serializer, T: AsRef<[u8]>>(
340 source: &T,
341 serializer: S,
342 ) -> Result<S::Ok, S::Error> {
343 BASE64_STANDARD.encode(source).serialize(serializer)
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use base64::Engine;
352 use ed25519_dalek::Signature;
353 use rand::{rand_core::UnwrapErr, rngs::SysRng};
354
355 use crate::versions::verifying_key_by_id;
356
357 use super::{
358 signed_version_entry, LaunchMeasurement, LaunchMeasurementError, LaunchMeasurementV1,
359 LaunchMeasurementV2, PublishedVersionEntry,
360 };
361
362 #[test]
363 fn deserialize_published_version_entry() {
364 let entry: PublishedVersionEntry =
365 serde_json::from_str(published_version_entry_json()).unwrap();
366 assert_published_version_entry_fields(&entry);
367 }
368
369 #[test]
370 fn serialization_round_trip() {
371 let entry: PublishedVersionEntry =
372 serde_json::from_str(published_version_entry_json()).unwrap();
373 let serialized = serde_json::to_string_pretty(&entry).unwrap();
374 let entry: PublishedVersionEntry = serde_json::from_str(&serialized).unwrap();
375 assert_published_version_entry_fields(&entry);
376 }
377
378 #[test]
379 fn test_signature() {
380 let entry = published_version_entry();
381 assert!(entry.verify_signature().is_ok());
382 }
383
384 #[test]
385 fn unknown_key_id_fails() {
386 let mut entry = published_version_entry();
387 entry.signature.key_id[0] = entry.signature.key_id[0].wrapping_add(1);
388 assert!(entry.verify_signature().is_err());
389 }
390
391 #[test]
392 fn wrong_signature_fails() {
393 let mut entry = published_version_entry();
394 entry.signature.value[5] = entry.signature.value[5].wrapping_add(1);
395 assert!(entry.verify_signature().is_err());
396 }
397
398 #[test]
399 fn deserializes_standard_base64_for_compatibility() {
400 let entry: PublishedVersionEntry =
401 serde_json::from_str(published_version_entry_standard_base64_json()).unwrap();
402 assert_published_version_entry_fields(&entry);
403 }
404
405 #[test]
406 fn serializes_to_standard_base64_for_compatibility() {
407 use base64::prelude::BASE64_STANDARD;
408
409 let serialized = serde_json::to_string(&published_version_entry()).unwrap();
410 let deserialized: serde_json::Value = serde_json::from_str(&serialized).unwrap();
411
412 let launch_measurement = deserialized
413 .as_object()
414 .and_then(|obj| {
415 obj.get("launchMeasurement")
416 .and_then(|value| value.as_str())
417 })
418 .unwrap();
419 match BASE64_STANDARD.decode(launch_measurement) {
420 Ok(measurement) => assert_launch_measurement(&measurement),
421 Err(err) => panic!("Could not base64 deserialize launch measurement: {err:?}"),
422 }
423 }
424
425 #[test]
426 fn test_signed_version_entry() {
427 use ed25519_dalek::SigningKey;
428
429 let signing_key = SigningKey::generate(&mut UnwrapErr(SysRng));
430
431 let entry = signed_version_entry(
432 published_version_entry().launch_measurement,
433 "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70",
434 &signing_key,
435 );
436
437 assert_eq!(entry.kind, "release");
438 assert_eq!(entry.generation, 1);
439 assert_eq!(
440 entry.ref_,
441 "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70".to_string()
442 );
443 assert_v1_launch_measurement(&entry);
444 assert!(entry
445 .verify_signature_with_key(&signing_key.verifying_key())
446 .is_ok());
447 }
448
449 #[test]
450 fn test_deserializes_entry_with_z_offset() {
451 let entry: Result<PublishedVersionEntry, _> =
452 serde_json::from_str(published_version_entry_json_z_offset());
453 assert!(entry.is_ok());
454 }
455
456 #[test]
457 fn test_signature_uses_json_time_value() {
458 #[derive(serde::Deserialize)]
460 #[serde(rename_all = "camelCase")]
461 #[allow(unused)]
462 struct RawVersionEntry {
463 pub kind: String,
464 pub generation: usize,
465 pub created_at: String,
466 pub ref_: String,
467 #[serde(with = "super::base64_compat")]
468 pub launch_measurement: Vec<u8>,
469 pub signature: super::PublishedVersionEntrySignature,
470 }
471 let version_entry: RawVersionEntry =
472 serde_json::from_str(published_version_entry_json()).unwrap();
473
474 let mut input = Vec::new();
475 input.extend_from_slice(version_entry.kind.as_bytes());
476 input.extend_from_slice(version_entry.generation.to_string().as_bytes());
477 input.extend_from_slice(version_entry.created_at.as_bytes());
478 input.extend_from_slice(version_entry.ref_.as_bytes());
479 input.extend_from_slice(&version_entry.launch_measurement);
480
481 let signature = Signature::from_slice(&version_entry.signature.value).unwrap();
482 let key = verifying_key_by_id(&version_entry.signature.key_id).unwrap();
483 key.verify_strict(&input, &signature).unwrap();
484 }
485
486 #[test]
487 fn v2_signed_entry_is_valid() {
488 use ed25519_dalek::SigningKey;
489
490 let signing_key = SigningKey::generate(&mut UnwrapErr(SysRng));
491
492 let vmsa = vec![0xabu8; 1992];
493 let measurement = vec![0x42u8; 48];
494
495 let entry = signed_version_entry(
496 LaunchMeasurement::V2(LaunchMeasurementV2 {
497 vmsa: vmsa.clone(),
498 value: measurement.clone(),
499 }),
500 "github:yaxitech/cvms/v0.1.0",
501 &signing_key,
502 );
503
504 assert_eq!(entry.kind, "release");
505 assert_eq!(entry.generation, 2);
506 assert_eq!(entry.ref_, "github:yaxitech/cvms/v0.1.0".to_string());
507
508 match &entry.launch_measurement {
509 LaunchMeasurement::V1(_) => panic!("Expected V2 launch measurement"),
510 LaunchMeasurement::V2(v2) => {
511 assert_eq!(v2.vmsa, vmsa);
512 assert_eq!(v2.value, measurement);
513 }
514 }
515
516 assert!(entry
517 .verify_signature_with_key(&signing_key.verifying_key())
518 .is_ok());
519 }
520
521 #[test]
522 fn v2_serialization_round_trip() {
523 use ed25519_dalek::SigningKey;
524
525 let signing_key = SigningKey::generate(&mut UnwrapErr(SysRng));
526 let entry = signed_version_entry(
527 LaunchMeasurement::V2(LaunchMeasurementV2 {
528 vmsa: vec![0x01u8; 1992],
529 value: vec![0x02u8; 48],
530 }),
531 "github:yaxitech/cvms/v1.0.0",
532 &signing_key,
533 );
534
535 let serialized = serde_json::to_string_pretty(&entry).unwrap();
536 let deserialized: serde_json::Value = serde_json::from_str(&serialized).unwrap();
537
538 assert!(
540 deserialized["launchMeasurement"].is_object(),
541 "gen 2 launchMeasurement should serialize as object"
542 );
543 assert!(deserialized["launchMeasurement"]["vmsa"].is_string());
544 assert!(deserialized["launchMeasurement"]["value"].is_string());
545
546 let restored: PublishedVersionEntry = serde_json::from_str(&serialized).unwrap();
548 assert!(restored
549 .verify_signature_with_key(&signing_key.verifying_key())
550 .is_ok());
551 }
552
553 #[test]
554 fn v2_wrong_signature_fails() {
555 use ed25519_dalek::SigningKey;
556
557 let signing_key = SigningKey::generate(&mut UnwrapErr(SysRng));
558 let mut entry = signed_version_entry(
559 LaunchMeasurement::V2(LaunchMeasurementV2 {
560 vmsa: vec![0xabu8; 1992],
561 value: vec![0x42u8; 48],
562 }),
563 "github:yaxitech/cvms/v0.1.0",
564 &signing_key,
565 );
566
567 entry.signature.value[5] = entry.signature.value[5].wrapping_add(1);
568 assert!(entry
569 .verify_signature_with_key(&signing_key.verifying_key())
570 .is_err());
571 }
572
573 #[test]
574 fn launch_measurement_deserializes_both_variants() {
575 let v1: LaunchMeasurement = serde_json::from_str(r#""WUFYSQ==""#).unwrap();
577 assert!(matches!(v1, LaunchMeasurement::V1(ref m) if m.0 == b"YAXI"));
578
579 let v2: LaunchMeasurement =
581 serde_json::from_str(r#"{"vmsa": "_wD_", "value": "AP__"}"#).unwrap();
582 assert!(matches!(v2, LaunchMeasurement::V2(ref m)
583 if m.vmsa == [0xff, 0x00, 0xff] && m.value == [0x00, 0xff, 0xff]));
584 }
585
586 fn published_version_entry() -> PublishedVersionEntry {
587 serde_json::from_str(published_version_entry_json()).unwrap()
588 }
589
590 fn published_version_entry_standard_base64_json() -> &'static str {
591 r#"
592 {
593 "kind": "release",
594 "generation": 1,
595 "createdAt": "2025-03-03T21:53:07.650800415+00:00",
596 "ref": "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70",
597 "launchMeasurement": "QZPtHQhKa5bVGeNnPOBwzH1ycRDNYFy1/yyQl2Tq+T+VOHelRODRYI5kPd6u5dgV",
598 "signature": {
599 "keyId": "dYa685dhHap8RSUtB4DDy1l4UcycsGhklBnV5a/4HSg=",
600 "value": "NJpXrwzxi570HwBMGrlDndesoFiOPyaMXzAYjwzr+LGHnlr+gGrnX6pIOAXKdH9XVOwziAX7hYs44Ii9Gxc2CA=="
601 }
602 }
603 "#
604 }
605
606 fn published_version_entry_json() -> &'static str {
607 r#"
608 {
609 "kind": "release",
610 "generation": 1,
611 "createdAt": "2025-03-03T21:53:07.650800415+00:00",
612 "ref": "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70",
613 "launchMeasurement": "QZPtHQhKa5bVGeNnPOBwzH1ycRDNYFy1_yyQl2Tq-T-VOHelRODRYI5kPd6u5dgV",
614 "signature": {
615 "keyId": "dYa685dhHap8RSUtB4DDy1l4UcycsGhklBnV5a_4HSg=",
616 "value": "NJpXrwzxi570HwBMGrlDndesoFiOPyaMXzAYjwzr-LGHnlr-gGrnX6pIOAXKdH9XVOwziAX7hYs44Ii9Gxc2CA=="
617 }
618 }
619 "#
620 }
621
622 fn published_version_entry_json_z_offset() -> &'static str {
623 r#"
624 {
625 "kind": "routex-release",
626 "generation": 1,
627 "createdAt": "2024-03-26T19:16:40.269247621Z",
628 "ref": "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70",
629 "launchMeasurement": "QZPtHQhKa5bVGeNnPOBwzH1ycRDNYFy1_yyQl2Tq-T-VOHelRODRYI5kPd6u5dgV",
630 "signature": {
631 "keyId": "dYa685dhHap8RSUtB4DDy1l4UcycsGhklBnV5a_4HSg=",
632 "value": "w9kKh0xKFNKm5Mcn1-VSiMgCWs_jAZIAkJfGjiTkFQnj4CYD5qskb2LnQeE4CJa9CrgmT21l5pkIJR7gUP6UDA=="
633 }
634 }
635 "#
636 }
637
638 fn assert_published_version_entry_fields(entry: &PublishedVersionEntry) {
639 assert_eq!(entry.kind, "release");
640 assert_eq!(entry.generation, 1);
641 assert_eq!(
642 entry.ref_,
643 "github:yaxitech/nixos/a24091a8d5f8dc74f64435c029816c7c2c0c7a70".to_string()
644 );
645 assert_v1_launch_measurement(entry);
646 assert_eq!(
647 entry.signature.key_id,
648 &[
649 117, 134, 186, 243, 151, 97, 29, 170, 124, 69, 37, 45, 7, 128, 195, 203, 89, 120,
650 81, 204, 156, 176, 104, 100, 148, 25, 213, 229, 175, 248, 29, 40
651 ],
652 );
653 }
654
655 fn assert_v1_launch_measurement(entry: &PublishedVersionEntry) {
656 match &entry.launch_measurement {
657 LaunchMeasurement::V1(measurement) => assert_launch_measurement(&measurement.0),
658 LaunchMeasurement::V2(_) => panic!("Expected V1 launch measurement"),
659 }
660 }
661
662 fn assert_launch_measurement(actual: &[u8]) {
663 assert_eq!(
664 actual,
665 &[
666 65, 147, 237, 29, 8, 74, 107, 150, 213, 25, 227, 103, 60, 224, 112, 204, 125, 114,
667 113, 16, 205, 96, 92, 181, 255, 44, 144, 151, 100, 234, 249, 63, 149, 56, 119, 165,
668 68, 224, 209, 96, 142, 100, 61, 222, 174, 229, 216, 21
669 ]
670 );
671 }
672
673 #[test]
674 fn v1_resolve_returns_stored_digest() {
675 let bytes = [0x42u8; 48];
676 let measurement = LaunchMeasurement::V1(LaunchMeasurementV1(bytes.to_vec()));
677 assert_eq!(measurement.resolve(4), bytes);
678 }
679
680 #[test]
681 fn v2_resolve_with_zero_vcpus_returns_pre_vmsa_digest() {
682 let digest = [0x42u8; 48];
683 let measurement = LaunchMeasurement::V2(LaunchMeasurementV2 {
684 vmsa: vec![0xab; 1992],
685 value: digest.to_vec(),
686 });
687 assert_eq!(measurement.resolve(0), digest);
688 }
689
690 #[test]
691 fn v2_resolve_changes_with_vcpu_count() {
692 let measurement = LaunchMeasurement::V2(LaunchMeasurementV2 {
693 vmsa: vec![0xab; 1992],
694 value: vec![0x42; 48],
695 });
696 assert_ne!(measurement.resolve(1), measurement.resolve(2));
697 }
698
699 #[test]
700 fn v1_try_resolve_rejects_invalid_measurement_length() {
701 let measurement = LaunchMeasurementV1(vec![0x42; 47]);
702 assert!(matches!(
703 measurement.try_resolve(),
704 Err(LaunchMeasurementError::InvalidMeasurementLength(47))
705 ));
706 }
707
708 #[test]
709 fn v2_try_resolve_rejects_invalid_measurement_length() {
710 let measurement = LaunchMeasurementV2 {
711 vmsa: vec![0xab; 1992],
712 value: vec![0x42; 49],
713 };
714 assert!(matches!(
715 measurement.try_resolve(1),
716 Err(LaunchMeasurementError::InvalidMeasurementLength(49))
717 ));
718 }
719
720 #[test]
721 fn v2_try_resolve_rejects_invalid_vmsa_length() {
722 let measurement = LaunchMeasurementV2 {
723 vmsa: vec![0xab; 4097],
724 value: vec![0x42; 48],
725 };
726 assert!(matches!(
727 measurement.try_resolve(1),
728 Err(LaunchMeasurementError::InvalidVmsaLength(4097))
729 ));
730 }
731}