1use anyhow::Context;
30use der::Decode;
31use igvm::IgvmDirectiveHeader;
32use igvm::IgvmFile;
33use igvm::IgvmInitializationHeader;
34use igvm::IgvmSerializer;
35use igvm_defs::IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY;
36use igvm_defs::IGVM_VHS_SNP_ID_BLOCK_SIGNATURE;
37use igvm_defs::IgvmPlatformType;
38use x86defs::snp::SnpPspIdBlock;
39use zerocopy::FromBytes;
40use zerocopy::IntoBytes;
41
42pub const SNP_FAMILY_ID: [u8; 16] = [
51 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
52];
53pub const SNP_IMAGE_ID: [u8; 16] = *b"openhcl\0\0\0\0\0\0\0\0\0";
55
56const SHA_384_OUTPUT_SIZE_BYTES: usize = 48;
57const SNP_ID_KEY_ALGORITHM_ECDSA_P384_SHA384: u32 = 1;
58const SNP_ECDSA_CURVE_P384: u32 = 2;
59const SNP_ECC_KEY_SIZE_BYTES: usize = 48;
60const SNP_ECC_COMPONENT_SIZE_BYTES: usize = 72;
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64pub(crate) struct SnpImageIdentity {
65 family_id: [u8; 16],
66 image_id: [u8; 16],
67}
68
69impl SnpImageIdentity {
70 pub(crate) const OPENHCL: Self = Self::new(SNP_FAMILY_ID, SNP_IMAGE_ID);
72
73 pub(crate) const LINUX_DIRECT: Self = Self::new(*b"OpenVMM SNP test", *b"linux-direct\0\0\0\0");
75
76 const fn new(family_id: [u8; 16], image_id: [u8; 16]) -> Self {
77 Self {
78 family_id,
79 image_id,
80 }
81 }
82}
83
84pub fn id_block_signing_payload(ld: &[u8], guest_svn: u32, policy: u64) -> anyhow::Result<Vec<u8>> {
94 id_block_signing_payload_with_identity(ld, guest_svn, policy, SnpImageIdentity::OPENHCL)
95}
96
97pub(crate) fn id_block_signing_payload_with_identity(
99 ld: &[u8],
100 guest_svn: u32,
101 policy: u64,
102 identity: SnpImageIdentity,
103) -> anyhow::Result<Vec<u8>> {
104 let ld: [u8; SHA_384_OUTPUT_SIZE_BYTES] =
105 ld.try_into().context("SNP launch digest is not 48 bytes")?;
106 let id_block = SnpPspIdBlock {
107 ld,
108 family_id: identity.family_id,
109 image_id: identity.image_id,
110 version: 0x1,
111 guest_svn,
112 policy,
113 };
114 Ok(id_block.as_bytes().to_vec())
115}
116
117pub fn guest_policy(igvm_file: &IgvmFile, compatibility_mask: u32) -> Option<u64> {
120 igvm_file.initializations().iter().find_map(|h| match h {
121 IgvmInitializationHeader::GuestPolicy {
122 policy,
123 compatibility_mask: mask,
124 } if mask & compatibility_mask == compatibility_mask => Some(*policy),
125 _ => None,
126 })
127}
128
129pub(crate) fn add_snp_id_block_temp_key(
147 igvm_data: &[u8],
148 guest_svn: u32,
149 identity: SnpImageIdentity,
150) -> anyhow::Result<Vec<u8>> {
151 let igvm_file =
152 IgvmFile::new_from_binary(igvm_data, None).context("parsing input IGVM file")?;
153 let (compatibility_mask, policy) = snp_context(&igvm_file)?;
154
155 let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
156 let ld = snp_measurement(&serializer)?;
157
158 let psp_id_block = SnpPspIdBlock {
159 ld,
160 family_id: identity.family_id,
161 image_id: identity.image_id,
162 version: 0x1,
163 guest_svn,
164 policy,
165 };
166 tracing::info!("SNP ID Block (temporary key) {:x?}", psp_id_block);
167
168 let (signature, public_key) = sign_id_block_with_temp_key(&psp_id_block)?;
169 serializer.add_directive(id_block_directive(
170 &psp_id_block,
171 compatibility_mask,
172 signature,
173 public_key,
174 ));
175
176 finish(serializer, igvm_data.len())
177}
178
179pub fn add_snp_id_block_signed(
197 igvm_data: &[u8],
198 signing_payload: &[u8],
199 signature_der: &[u8],
200 public_key_pem: &[u8],
201) -> anyhow::Result<Vec<u8>> {
202 let igvm_file =
203 IgvmFile::new_from_binary(igvm_data, None).context("parsing input IGVM file")?;
204 let (compatibility_mask, policy) = snp_context(&igvm_file)?;
205
206 let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
207 let ld = snp_measurement(&serializer)?;
208
209 let id_block = parse_signing_payload(signing_payload)?;
214 anyhow::ensure!(
215 id_block.ld == ld,
216 "SNP ID block signing payload launch digest does not match the IGVM \
217 file measurement; the payload was generated for a different build"
218 );
219 anyhow::ensure!(
220 id_block.policy == policy,
221 "SNP ID block signing payload policy 0x{:X} does not match the IGVM \
222 file GuestPolicy 0x{policy:X}",
223 id_block.policy,
224 );
225
226 let (signature, public_key) =
227 signature_and_verify(signing_payload, signature_der, public_key_pem)?;
228 tracing::info!("SNP ID Block (out-of-band signature) {:x?}", id_block);
229 serializer.add_directive(id_block_directive(
230 &id_block,
231 compatibility_mask,
232 signature,
233 public_key,
234 ));
235
236 finish(serializer, igvm_data.len())
237}
238
239fn snp_context(igvm_file: &IgvmFile) -> anyhow::Result<(u32, u64)> {
242 let compatibility_mask = crate::platform_mask::lookup_compatibility_mask(
243 igvm_file.platforms(),
244 IgvmPlatformType::SEV_SNP,
245 )?;
246
247 if igvm_file.directives().iter().any(|h| {
251 matches!(h, IgvmDirectiveHeader::SnpIdBlock { compatibility_mask: mask, .. } if *mask == compatibility_mask)
252 }) {
253 anyhow::bail!(
254 "IGVM file already contains an SNP ID block for compatibility mask \
255 0x{compatibility_mask:X}; refusing to add a second one"
256 );
257 }
258
259 let policy = guest_policy(igvm_file, compatibility_mask)
260 .context("missing SNP GuestPolicy initialization header")?;
261
262 Ok((compatibility_mask, policy))
263}
264
265fn snp_measurement(
267 serializer: &IgvmSerializer<'_>,
268) -> anyhow::Result<[u8; SHA_384_OUTPUT_SIZE_BYTES]> {
269 serializer
270 .measurement_for(IgvmPlatformType::SEV_SNP)
271 .context("no SNP launch measurement computed for the IGVM file")?
272 .digest
273 .as_slice()
274 .try_into()
275 .context("SNP launch digest is not 48 bytes")
276}
277
278fn finish(serializer: IgvmSerializer<'_>, input_size: usize) -> anyhow::Result<Vec<u8>> {
280 let mut output = Vec::new();
281 serializer
282 .serialize(&mut output)
283 .context("serializing IGVM file with SNP ID block")?;
284 tracing::info!(
285 input_size,
286 output_size = output.len(),
287 "Added SNP ID block to IGVM file"
288 );
289 Ok(output)
290}
291
292fn parse_signing_payload(bytes: &[u8]) -> anyhow::Result<SnpPspIdBlock> {
295 anyhow::ensure!(
296 bytes.len() == size_of::<SnpPspIdBlock>(),
297 "SNP ID block signing payload must be exactly {} bytes, got {}",
298 size_of::<SnpPspIdBlock>(),
299 bytes.len()
300 );
301 let (id_block, _) = SnpPspIdBlock::read_from_prefix(bytes)
302 .map_err(|_| anyhow::anyhow!("SNP ID block signing payload is malformed"))?;
303 Ok(id_block)
304}
305
306fn left_pad_be(field: &str, be: &[u8]) -> anyhow::Result<[u8; SNP_ECC_KEY_SIZE_BYTES]> {
308 anyhow::ensure!(
309 be.len() <= SNP_ECC_KEY_SIZE_BYTES,
310 "{field} is {} bytes, exceeds {SNP_ECC_KEY_SIZE_BYTES}",
311 be.len()
312 );
313 let mut out = [0u8; SNP_ECC_KEY_SIZE_BYTES];
314 out[SNP_ECC_KEY_SIZE_BYTES - be.len()..].copy_from_slice(be);
315 Ok(out)
316}
317
318fn parse_der_ecdsa_p384(
321 der_sig: &[u8],
322) -> anyhow::Result<([u8; SNP_ECC_KEY_SIZE_BYTES], [u8; SNP_ECC_KEY_SIZE_BYTES])> {
323 #[derive(der::Sequence)]
324 struct EcdsaSigDer<'a> {
325 r: der::asn1::UintRef<'a>,
326 s: der::asn1::UintRef<'a>,
327 }
328 let sig = EcdsaSigDer::from_der(der_sig).context("parsing DER-encoded ECDSA signature")?;
329 Ok((
330 left_pad_be("signature r", sig.r.as_bytes())?,
331 left_pad_be("signature s", sig.s.as_bytes())?,
332 ))
333}
334
335fn parse_p384_public_key(public_key: &[u8]) -> anyhow::Result<crypto::ecdsa::EcdsaPublicKey> {
340 use crypto::ecdsa::EcdsaPublicKey;
341 use crypto::x509::X509Certificate;
342
343 fn key_from_cert_der(der: &[u8]) -> anyhow::Result<EcdsaPublicKey> {
347 X509Certificate::from_der(der)
348 .context("parsing certificate")?
349 .public_key()
350 .context("extracting certificate public key")?
351 .ecdsa()
352 .context("certificate public key is not an ECDSA key")
353 }
354
355 if let Ok(text) = std::str::from_utf8(public_key)
356 && text.trim_start().starts_with("-----BEGIN")
357 {
358 let (label, doc) = der::Document::from_pem(text).context("parsing public key PEM")?;
359 match label {
360 "CERTIFICATE" => key_from_cert_der(doc.as_bytes()),
361 "PUBLIC KEY" => EcdsaPublicKey::from_public_key_der(doc.as_bytes())
362 .context("parsing SubjectPublicKeyInfo"),
363 other => {
364 anyhow::bail!("unexpected PEM label {other:?}; expected CERTIFICATE or PUBLIC KEY")
365 }
366 }
367 .context("parsing SNP ID block public key")
368 } else {
369 key_from_cert_der(public_key)
371 .or_else(|_| {
372 EcdsaPublicKey::from_public_key_der(public_key)
373 .context("parsing SubjectPublicKeyInfo")
374 })
375 .context("parsing SNP ID block public key (DER certificate or SubjectPublicKeyInfo)")
376 }
377}
378
379fn signature_and_verify(
387 signed_bytes: &[u8],
388 signature_der: &[u8],
389 public_key_pem: &[u8],
390) -> anyhow::Result<(
391 IGVM_VHS_SNP_ID_BLOCK_SIGNATURE,
392 IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY,
393)> {
394 let (r, s) = parse_der_ecdsa_p384(signature_der)?;
395 let public_key = parse_p384_public_key(public_key_pem)?;
396
397 let mut sig = Vec::with_capacity(2 * SNP_ECC_KEY_SIZE_BYTES);
398 sig.extend_from_slice(&r);
399 sig.extend_from_slice(&s);
400
401 let valid = public_key
403 .verify(signed_bytes, &sig, crypto::HashAlgorithm::Sha384)
404 .context("verifying SNP ID block signature")?;
405 anyhow::ensure!(
406 valid,
407 "SNP ID block signature does not verify against the supplied public key; \
408 the signature, public key, or signing payload do not correspond"
409 );
410
411 let qxqy = public_key
414 .public_key_bytes()
415 .context("exporting SNP ID block public key")?;
416 anyhow::ensure!(
417 qxqy.len() == 2 * SNP_ECC_KEY_SIZE_BYTES,
418 "unexpected SNP ID block public key size {}",
419 qxqy.len()
420 );
421 let (qx, qy) = qxqy.split_at(SNP_ECC_KEY_SIZE_BYTES);
422
423 Ok((
424 IGVM_VHS_SNP_ID_BLOCK_SIGNATURE {
425 r_comp: padded_le_component(&r),
426 s_comp: padded_le_component(&s),
427 },
428 IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY {
429 curve: SNP_ECDSA_CURVE_P384,
430 reserved: 0,
431 qx: padded_le_component(qx),
432 qy: padded_le_component(qy),
433 },
434 ))
435}
436
437fn id_block_directive(
442 psp_id_block: &SnpPspIdBlock,
443 compatibility_mask: u32,
444 id_key_signature: IGVM_VHS_SNP_ID_BLOCK_SIGNATURE,
445 id_public_key: IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY,
446) -> IgvmDirectiveHeader {
447 IgvmDirectiveHeader::SnpIdBlock {
448 compatibility_mask,
449 author_key_enabled: 0,
450 reserved: [0; 3],
451 ld: psp_id_block.ld,
452 family_id: psp_id_block.family_id,
453 image_id: psp_id_block.image_id,
454 version: psp_id_block.version,
455 guest_svn: psp_id_block.guest_svn,
456 id_key_algorithm: SNP_ID_KEY_ALGORITHM_ECDSA_P384_SHA384,
457 author_key_algorithm: 0,
458 id_key_signature: Box::new(id_key_signature),
459 id_public_key: Box::new(id_public_key),
460 author_key_signature: Box::new(IGVM_VHS_SNP_ID_BLOCK_SIGNATURE {
461 r_comp: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
462 s_comp: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
463 }),
464 author_public_key: Box::new(IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY {
465 curve: 0,
466 reserved: 0,
467 qx: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
468 qy: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
469 }),
470 }
471}
472
473fn padded_le_component(input_be: &[u8]) -> [u8; SNP_ECC_COMPONENT_SIZE_BYTES] {
476 let mut out = [0u8; SNP_ECC_COMPONENT_SIZE_BYTES];
477 for (dst, src) in out.iter_mut().zip(input_be.iter().rev()) {
478 *dst = *src;
479 }
480 out
481}
482
483fn sign_id_block_with_temp_key(
487 id_block: &SnpPspIdBlock,
488) -> anyhow::Result<(
489 IGVM_VHS_SNP_ID_BLOCK_SIGNATURE,
490 IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY,
491)> {
492 use crypto::ecdsa::EcdsaCurve;
493 use crypto::ecdsa::EcdsaKeyPair;
494
495 let key =
497 EcdsaKeyPair::generate(EcdsaCurve::P384).context("generating temporary SNP signing key")?;
498
499 let id_block_hash: [u8; SHA_384_OUTPUT_SIZE_BYTES] =
501 crypto::sha_384::sha_384(id_block.as_bytes());
502
503 use base64::Engine as _;
504 let b64 = base64::engine::general_purpose::STANDARD;
505 tracing::info!("Input Hash Base64: {}", b64.encode(id_block_hash));
506 tracing::info!("Using Temporary Signing Key");
507
508 let signature = key
511 .sign(id_block.as_bytes(), crypto::HashAlgorithm::Sha384)
512 .context("signing SNP ID block")?;
513
514 anyhow::ensure!(
515 signature.len() == SNP_ECC_KEY_SIZE_BYTES * 2,
516 "unexpected SNP ID block signature size {}",
517 signature.len()
518 );
519
520 let (sig_r_be, sig_s_be) = signature.split_at(SNP_ECC_KEY_SIZE_BYTES);
521 let id_key_signature = IGVM_VHS_SNP_ID_BLOCK_SIGNATURE {
522 r_comp: padded_le_component(sig_r_be),
523 s_comp: padded_le_component(sig_s_be),
524 };
525
526 tracing::info!("Signature R Base64: {}", b64.encode(sig_r_be));
527 tracing::info!("Signature S Base64: {}", b64.encode(sig_s_be));
528
529 let public_key = key
531 .public_key_bytes()
532 .context("exporting temporary SNP public key")?;
533
534 anyhow::ensure!(
535 public_key.len() == SNP_ECC_KEY_SIZE_BYTES * 2,
536 "unexpected SNP ID block public key size {}",
537 public_key.len()
538 );
539
540 let (qx_be, qy_be) = public_key.split_at(SNP_ECC_KEY_SIZE_BYTES);
541
542 tracing::info!("Public Key Qx Base64: {}", b64.encode(qx_be));
543 tracing::info!("Public Key Qy Base64: {}", b64.encode(qy_be));
544 let id_public_key = IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY {
545 curve: SNP_ECDSA_CURVE_P384,
546 reserved: 0,
547 qx: padded_le_component(qx_be),
548 qy: padded_le_component(qy_be),
549 };
550
551 Ok((id_key_signature, id_public_key))
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use igvm::IgvmPlatformHeader;
558 use igvm::IgvmRevision;
559 use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM;
560 use igvm_defs::IgvmPageDataFlags;
561 use igvm_defs::IgvmPageDataType;
562 use test_with_tracing::test;
563
564 fn build_snp_igvm(mask: u32) -> Vec<u8> {
567 let platforms = vec![IgvmPlatformHeader::SupportedPlatform(
568 IGVM_VHS_SUPPORTED_PLATFORM {
569 compatibility_mask: mask,
570 highest_vtl: 0,
571 platform_type: IgvmPlatformType::SEV_SNP,
572 platform_version: 1,
573 shared_gpa_boundary: 0,
574 },
575 )];
576 let initializations = vec![IgvmInitializationHeader::GuestPolicy {
577 policy: 0x30000,
578 compatibility_mask: mask,
579 }];
580 let directives = vec![IgvmDirectiveHeader::PageData {
581 gpa: 0,
582 compatibility_mask: mask,
583 flags: IgvmPageDataFlags::new(),
584 data_type: IgvmPageDataType::NORMAL,
585 data: vec![0xAB; 4096],
586 }];
587 let igvm = IgvmFile::new(IgvmRevision::V1, platforms, initializations, directives)
588 .expect("valid SNP IgvmFile");
589 let mut out = Vec::new();
590 igvm.serialize(&mut out).expect("serialize");
591 out
592 }
593
594 fn count_id_blocks(data: &[u8]) -> usize {
595 let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM");
596 igvm.directives()
597 .iter()
598 .filter(|h| matches!(h, IgvmDirectiveHeader::SnpIdBlock { .. }))
599 .count()
600 }
601
602 fn sign_payload(signing_payload: &[u8]) -> (Vec<u8>, Vec<u8>) {
607 use crypto::ecdsa::EcdsaCurve;
608 use crypto::ecdsa::EcdsaKeyPair;
609
610 let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
611 let raw_sig = key
612 .sign(signing_payload, crypto::HashAlgorithm::Sha384)
613 .unwrap();
614 (ecdsa_raw_to_der(&raw_sig), spki_der(&key))
615 }
616
617 fn uncompressed_point(key: &crypto::ecdsa::EcdsaKeyPair) -> Vec<u8> {
619 let pk = key.public_key_bytes().unwrap();
620 let mut point = vec![0x04u8];
621 point.extend_from_slice(&pk[..SNP_ECC_KEY_SIZE_BYTES]);
622 point.extend_from_slice(&pk[SNP_ECC_KEY_SIZE_BYTES..]);
623 point
624 }
625
626 fn spki_der(key: &crypto::ecdsa::EcdsaKeyPair) -> Vec<u8> {
629 use der::Encode;
630
631 let spki = x509_cert::spki::SubjectPublicKeyInfo {
632 algorithm: x509_cert::spki::AlgorithmIdentifier {
633 oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"),
634 parameters: Some(der::asn1::ObjectIdentifier::new_unwrap("1.3.132.0.34")),
635 },
636 subject_public_key: der::asn1::BitString::from_bytes(&uncompressed_point(key)).unwrap(),
637 };
638 spki.to_der().unwrap()
639 }
640
641 fn ecdsa_raw_to_der(raw_sig: &[u8]) -> Vec<u8> {
645 use der::Encode;
646
647 fn strip_lz(b: &[u8]) -> &[u8] {
648 let mut i = 0;
649 while i + 1 < b.len() && b[i] == 0 {
650 i += 1;
651 }
652 &b[i..]
653 }
654 #[derive(der::Sequence)]
655 struct EcdsaSigDer<'a> {
656 r: der::asn1::UintRef<'a>,
657 s: der::asn1::UintRef<'a>,
658 }
659 let (r_be, s_be) = raw_sig.split_at(SNP_ECC_KEY_SIZE_BYTES);
660 EcdsaSigDer {
661 r: der::asn1::UintRef::new(strip_lz(r_be)).unwrap(),
662 s: der::asn1::UintRef::new(strip_lz(s_be)).unwrap(),
663 }
664 .to_der()
665 .unwrap()
666 }
667
668 struct SelfSignedProfile {
671 name: x509_cert::name::Name,
672 }
673
674 impl x509_cert::builder::profile::BuilderProfile for SelfSignedProfile {
675 fn get_issuer(&self, _subject: &x509_cert::name::Name) -> x509_cert::name::Name {
676 self.name.clone()
677 }
678
679 fn get_subject(&self) -> x509_cert::name::Name {
680 self.name.clone()
681 }
682
683 fn build_extensions(
684 &self,
685 _spk: x509_cert::spki::SubjectPublicKeyInfoRef<'_>,
686 _issuer_spk: x509_cert::spki::SubjectPublicKeyInfoRef<'_>,
687 _tbs: &x509_cert::TbsCertificate,
688 ) -> x509_cert::builder::Result<Vec<x509_cert::ext::Extension>> {
689 Ok(Vec::new())
690 }
691 }
692
693 struct EcdsaCertSigner<'a> {
699 key: &'a crypto::ecdsa::EcdsaKeyPair,
700 point: Vec<u8>,
701 }
702
703 #[derive(Clone)]
704 struct EcdsaVerifyingKey(Vec<u8>);
705
706 struct EcdsaDerSignature(Vec<u8>);
707
708 impl signature::Keypair for EcdsaCertSigner<'_> {
709 type VerifyingKey = EcdsaVerifyingKey;
710
711 fn verifying_key(&self) -> Self::VerifyingKey {
712 EcdsaVerifyingKey(self.point.clone())
713 }
714 }
715
716 impl x509_cert::spki::DynSignatureAlgorithmIdentifier for EcdsaCertSigner<'_> {
717 fn signature_algorithm_identifier(
718 &self,
719 ) -> x509_cert::spki::Result<x509_cert::spki::AlgorithmIdentifierOwned> {
720 Ok(x509_cert::spki::AlgorithmIdentifierOwned {
721 oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3"),
723 parameters: None,
724 })
725 }
726 }
727
728 impl signature::Signer<EcdsaDerSignature> for EcdsaCertSigner<'_> {
729 fn try_sign(&self, msg: &[u8]) -> Result<EcdsaDerSignature, signature::Error> {
730 let raw = self
731 .key
732 .sign(msg, crypto::HashAlgorithm::Sha384)
733 .map_err(|_| signature::Error::new())?;
734 Ok(EcdsaDerSignature(ecdsa_raw_to_der(&raw)))
735 }
736 }
737
738 impl x509_cert::spki::EncodePublicKey for EcdsaVerifyingKey {
739 fn to_public_key_der(&self) -> x509_cert::spki::Result<der::Document> {
740 use der::Encode;
741
742 let spki = x509_cert::spki::SubjectPublicKeyInfoOwned {
743 algorithm: x509_cert::spki::AlgorithmIdentifierOwned {
744 oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"),
745 parameters: Some(der::Any::from(der::asn1::ObjectIdentifier::new_unwrap(
746 "1.3.132.0.34",
747 ))),
748 },
749 subject_public_key: der::asn1::BitString::from_bytes(&self.0)?,
750 };
751 Ok(der::Document::try_from(spki.to_der()?)?)
752 }
753 }
754
755 impl x509_cert::spki::SignatureBitStringEncoding for EcdsaDerSignature {
756 fn to_bitstring(&self) -> der::Result<der::asn1::BitString> {
757 der::asn1::BitString::from_bytes(&self.0)
758 }
759 }
760
761 fn self_signed_p384_cert_der(key: &crypto::ecdsa::EcdsaKeyPair) -> Vec<u8> {
764 use core::str::FromStr;
765 use der::Encode;
766 use x509_cert::builder::Builder;
767
768 let point = uncompressed_point(key);
769 let spki = x509_cert::spki::SubjectPublicKeyInfoOwned {
770 algorithm: x509_cert::spki::AlgorithmIdentifierOwned {
771 oid: der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"),
772 parameters: Some(der::Any::from(der::asn1::ObjectIdentifier::new_unwrap(
773 "1.3.132.0.34",
774 ))),
775 },
776 subject_public_key: der::asn1::BitString::from_bytes(&point).unwrap(),
777 };
778
779 let name = x509_cert::name::Name::from_str("CN=snp-id-block-test").unwrap();
780 let serial = x509_cert::serial_number::SerialNumber::from(1u32);
781 let validity = x509_cert::time::Validity::new(
782 der::asn1::GeneralizedTime::from_unix_duration(std::time::Duration::from_secs(0))
783 .unwrap()
784 .into(),
785 x509_cert::time::Time::INFINITY,
786 );
787
788 let builder = x509_cert::builder::CertificateBuilder::new(
789 SelfSignedProfile { name },
790 serial,
791 validity,
792 spki,
793 )
794 .unwrap();
795
796 let signer = EcdsaCertSigner { key, point };
797 builder.build(&signer).unwrap().to_der().unwrap()
798 }
799
800 #[test]
804 fn snp_id_block_constants_byte_identity() {
805 assert_eq!(
806 SNP_FAMILY_ID,
807 [
808 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
809 0x00, 0x00,
810 ]
811 );
812 assert_eq!(SNP_IMAGE_ID, *b"openhcl\0\0\0\0\0\0\0\0\0");
813 assert_eq!(SNP_FAMILY_ID.len(), 16);
814 assert_eq!(SNP_IMAGE_ID.len(), 16);
815 }
816
817 #[test]
820 fn temp_signing_produces_expected_sizes() {
821 let id_block = SnpPspIdBlock {
822 ld: [0x11; 48],
823 family_id: SNP_FAMILY_ID,
824 image_id: SNP_IMAGE_ID,
825 version: 0x1,
826 guest_svn: 3,
827 policy: 0x30000,
828 };
829 let (sig, pubkey) = sign_id_block_with_temp_key(&id_block).expect("temp signing succeeds");
830 assert_eq!(sig.r_comp.len(), SNP_ECC_COMPONENT_SIZE_BYTES);
832 assert_eq!(sig.s_comp.len(), SNP_ECC_COMPONENT_SIZE_BYTES);
833 assert_eq!(pubkey.curve, SNP_ECDSA_CURVE_P384);
834 assert_eq!(pubkey.qx.len(), SNP_ECC_COMPONENT_SIZE_BYTES);
835 assert_eq!(pubkey.qy.len(), SNP_ECC_COMPONENT_SIZE_BYTES);
836 assert!(sig.r_comp[SNP_ECC_KEY_SIZE_BYTES..].iter().all(|&b| b == 0));
839 assert!(pubkey.qx[SNP_ECC_KEY_SIZE_BYTES..].iter().all(|&b| b == 0));
840 }
841
842 #[test]
847 fn add_snp_id_block_preserves_measurement() {
848 let igvm_data = build_snp_igvm(0x1);
849
850 let original = IgvmFile::new_from_binary(&igvm_data, None).unwrap();
852 let ref_ld = IgvmSerializer::new(&original)
853 .unwrap()
854 .measurement_for(IgvmPlatformType::SEV_SNP)
855 .unwrap()
856 .digest
857 .clone();
858
859 let out = add_snp_id_block_temp_key(&igvm_data, 7, SnpImageIdentity::LINUX_DIRECT)
860 .expect("add SNP ID block");
861 assert_eq!(count_id_blocks(&out), 1);
862
863 let parsed = IgvmFile::new_from_binary(&out, None).expect("valid output");
864 let id_block = parsed
865 .directives()
866 .iter()
867 .find_map(|h| match h {
868 IgvmDirectiveHeader::SnpIdBlock {
869 ld,
870 family_id,
871 image_id,
872 guest_svn,
873 ..
874 } => Some((*ld, *family_id, *image_id, *guest_svn)),
875 _ => None,
876 })
877 .expect("id block present");
878 assert_eq!(id_block.0.as_slice(), ref_ld.as_slice());
879 assert_eq!(id_block.1, SnpImageIdentity::LINUX_DIRECT.family_id);
880 assert_eq!(id_block.2, SnpImageIdentity::LINUX_DIRECT.image_id);
881 assert_eq!(id_block.3, 7);
882
883 let after_ld = IgvmSerializer::new(&parsed)
885 .unwrap()
886 .measurement_for(IgvmPlatformType::SEV_SNP)
887 .unwrap()
888 .digest
889 .clone();
890 assert_eq!(after_ld, ref_ld);
891 }
892
893 #[test]
895 fn add_snp_id_block_rejects_double_add() {
896 let igvm_data = build_snp_igvm(0x1);
897 let once =
898 add_snp_id_block_temp_key(&igvm_data, 1, SnpImageIdentity::OPENHCL).expect("first add");
899 let err = add_snp_id_block_temp_key(&once, 1, SnpImageIdentity::OPENHCL).unwrap_err();
900 assert!(
901 format!("{err:#}").contains("already contains an SNP ID block"),
902 "unexpected error: {err:#}"
903 );
904 }
905
906 #[test]
908 fn add_snp_id_block_requires_snp_platform() {
909 let platforms = vec![IgvmPlatformHeader::SupportedPlatform(
910 IGVM_VHS_SUPPORTED_PLATFORM {
911 compatibility_mask: 0x1,
912 highest_vtl: 0,
913 platform_type: IgvmPlatformType::VSM_ISOLATION,
914 platform_version: 1,
915 shared_gpa_boundary: 0,
916 },
917 )];
918 let directives = vec![IgvmDirectiveHeader::PageData {
919 gpa: 0,
920 compatibility_mask: 0x1,
921 flags: IgvmPageDataFlags::new(),
922 data_type: IgvmPageDataType::NORMAL,
923 data: vec![0xCD; 4096],
924 }];
925 let igvm = IgvmFile::new(IgvmRevision::V1, platforms, vec![], directives).unwrap();
926 let mut data = Vec::new();
927 igvm.serialize(&mut data).unwrap();
928
929 let err = add_snp_id_block_temp_key(&data, 1, SnpImageIdentity::OPENHCL).unwrap_err();
930 assert!(format!("{err:#}").to_lowercase().contains("platform"));
931 }
932
933 #[test]
939 fn add_snp_id_block_signed_round_trip() {
940 let igvm_data = build_snp_igvm(0x1);
941
942 let original = IgvmFile::new_from_binary(&igvm_data, None).unwrap();
944 let ref_ld = IgvmSerializer::new(&original)
945 .unwrap()
946 .measurement_for(IgvmPlatformType::SEV_SNP)
947 .unwrap()
948 .digest
949 .clone();
950
951 let signing_payload = id_block_signing_payload(&ref_ld, 11, 0x30000).expect("payload");
953 let (sig_der, spki_der) = sign_payload(&signing_payload);
955
956 let out = add_snp_id_block_signed(&igvm_data, &signing_payload, &sig_der, &spki_der)
958 .expect("signed add");
959 assert_eq!(count_id_blocks(&out), 1);
960
961 let parsed = IgvmFile::new_from_binary(&out, None).expect("valid output");
962 let (ld, svn) = parsed
963 .directives()
964 .iter()
965 .find_map(|h| match h {
966 IgvmDirectiveHeader::SnpIdBlock { ld, guest_svn, .. } => Some((*ld, *guest_svn)),
967 _ => None,
968 })
969 .expect("id block present");
970 assert_eq!(ld.as_slice(), ref_ld.as_slice());
971 assert_eq!(svn, 11);
972 }
973
974 #[test]
976 fn add_snp_id_block_signed_rejects_malformed_signature() {
977 let igvm_data = build_snp_igvm(0x1);
978 let original = IgvmFile::new_from_binary(&igvm_data, None).unwrap();
979 let ref_ld = IgvmSerializer::new(&original)
980 .unwrap()
981 .measurement_for(IgvmPlatformType::SEV_SNP)
982 .unwrap()
983 .digest
984 .clone();
985 let signing_payload = id_block_signing_payload(&ref_ld, 1, 0x30000).unwrap();
986 let (_sig_der, spki_der) = sign_payload(&signing_payload);
987
988 let err = add_snp_id_block_signed(&igvm_data, &signing_payload, b"not-der", &spki_der)
989 .unwrap_err();
990 assert!(
991 format!("{err:#}").contains("DER-encoded ECDSA signature"),
992 "unexpected error: {err:#}"
993 );
994 }
995
996 #[test]
999 fn add_snp_id_block_signed_rejects_wrong_public_key() {
1000 let igvm_data = build_snp_igvm(0x1);
1001 let ref_ld = IgvmSerializer::new(&IgvmFile::new_from_binary(&igvm_data, None).unwrap())
1002 .unwrap()
1003 .measurement_for(IgvmPlatformType::SEV_SNP)
1004 .unwrap()
1005 .digest
1006 .clone();
1007 let signing_payload = id_block_signing_payload(&ref_ld, 1, 0x30000).unwrap();
1008 let (sig_der, _spki_a) = sign_payload(&signing_payload);
1010 let (_sig_b, spki_b) = sign_payload(&signing_payload);
1011
1012 let err =
1013 add_snp_id_block_signed(&igvm_data, &signing_payload, &sig_der, &spki_b).unwrap_err();
1014 assert!(
1015 format!("{err:#}").contains("does not verify"),
1016 "unexpected error: {err:#}"
1017 );
1018 }
1019
1020 #[test]
1023 fn add_snp_id_block_signed_rejects_stale_payload() {
1024 let igvm_data = build_snp_igvm(0x1);
1025 let signing_payload = id_block_signing_payload(&[0x22; 48], 1, 0x30000).unwrap();
1027 let (sig_der, spki_der) = sign_payload(&signing_payload);
1028
1029 let err =
1030 add_snp_id_block_signed(&igvm_data, &signing_payload, &sig_der, &spki_der).unwrap_err();
1031 assert!(
1032 format!("{err:#}").contains("does not match the IGVM file"),
1033 "unexpected error: {err:#}"
1034 );
1035 }
1036
1037 #[test]
1039 fn add_snp_id_block_signed_rejects_wrong_size_payload() {
1040 let igvm_data = build_snp_igvm(0x1);
1041 let (sig_der, spki_der) =
1042 sign_payload(&id_block_signing_payload(&[0u8; 48], 1, 0x30000).unwrap());
1043 let err =
1044 add_snp_id_block_signed(&igvm_data, b"too-short", &sig_der, &spki_der).unwrap_err();
1045 assert!(
1046 format!("{err:#}").contains("must be exactly"),
1047 "unexpected error: {err:#}"
1048 );
1049 }
1050
1051 #[test]
1057 fn add_snp_id_block_signed_accepts_x509_certificate() {
1058 use crypto::ecdsa::EcdsaCurve;
1059 use crypto::ecdsa::EcdsaKeyPair;
1060
1061 let igvm_data = build_snp_igvm(0x1);
1062 let ref_ld = IgvmSerializer::new(&IgvmFile::new_from_binary(&igvm_data, None).unwrap())
1063 .unwrap()
1064 .measurement_for(IgvmPlatformType::SEV_SNP)
1065 .unwrap()
1066 .digest
1067 .clone();
1068 let signing_payload = id_block_signing_payload(&ref_ld, 5, 0x30000).unwrap();
1069
1070 let key = EcdsaKeyPair::generate(EcdsaCurve::P384).unwrap();
1073 let sig_der = ecdsa_raw_to_der(
1074 &key.sign(&signing_payload, crypto::HashAlgorithm::Sha384)
1075 .unwrap(),
1076 );
1077 let cert_der = self_signed_p384_cert_der(&key);
1078 let cert_pem = der::Document::from_der(&cert_der)
1079 .unwrap()
1080 .to_pem("CERTIFICATE", der::pem::LineEnding::LF)
1081 .unwrap();
1082
1083 for public_key in [cert_der.clone(), cert_pem.into_bytes()] {
1084 let out = add_snp_id_block_signed(&igvm_data, &signing_payload, &sig_der, &public_key)
1085 .expect("certificate signer input accepted");
1086 assert_eq!(count_id_blocks(&out), 1);
1087 let (ld, svn) = IgvmFile::new_from_binary(&out, None)
1088 .unwrap()
1089 .directives()
1090 .iter()
1091 .find_map(|h| match h {
1092 IgvmDirectiveHeader::SnpIdBlock { ld, guest_svn, .. } => {
1093 Some((*ld, *guest_svn))
1094 }
1095 _ => None,
1096 })
1097 .expect("id block present");
1098 assert_eq!(ld.as_slice(), ref_ld.as_slice());
1099 assert_eq!(svn, 5);
1100 }
1101 }
1102}