Skip to main content

igvmfilegen/
snp_id_block.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! SEV-SNP ID block generation and signing.
5//!
6//! Adds a signed SNP ID block to an already-built IGVM file as an
7//! [`IgvmDirectiveHeader::SnpIdBlock`] directive. Two signing modes are
8//! supported:
9//!
10//! - **Out-of-band (production):** `manifest` emits the ID block signing
11//!   payload as `<base>-snp.idblock` -- the raw [`SnpPspIdBlock`] bytes, i.e.
12//!   exactly the content the SNP firmware hashes (SHA-384) and validates. A
13//!   generic file-content signer (e.g. `openssl dgst -sha384 -sign key -out
14//!   sig.der file`) signs those bytes and emits a DER-encoded ECDSA signature
15//!   file. That signature, plus the signing public key (X.509 cert or SPKI
16//!   PEM), is fed back via [`add_snp_id_block_signed`], which reconstructs the
17//!   directive without ever holding a private key.
18//! - **Temporary key (development/test):** [`add_snp_id_block_temp_key`]
19//!   generates an ephemeral ECDSA P-384 key, signs the block in-process, and
20//!   embeds the result. This is for local testing only.
21//!
22//! Either way, the launch digest embedded in the block is the SNP measurement
23//! that the `igvm` crate's [`IgvmSerializer`] computes eagerly at construction
24//! time, so the file is measured exactly once. The SNP measurement algorithm
25//! only hashes page-data directives, so adding the `SnpIdBlock` directive
26//! afterwards does not perturb that launch digest -- the embedded `ld` stays
27//! valid. Its presence signals the IGVM loader to set `id_block_en = 1`.
28
29use 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
42/// SNP family identifier for OpenHCL guests.
43///
44/// Layout convention:
45/// - `byte[3] == 0x01` identifies OpenHCL.
46/// - all other bytes are reserved and must remain `0x00`.
47///
48/// This value is baked into externally-consumed SNP ID blocks; changing it
49/// alters attestation identity, so any edit must be deliberate.
50pub const SNP_FAMILY_ID: [u8; 16] = [
51    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
52];
53/// SNP image identifier for OpenHCL guests.
54pub 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/// Identity fields included in an SNP ID block.
63#[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    /// The OpenHCL SNP image identity.
71    pub(crate) const OPENHCL: Self = Self::new(SNP_FAMILY_ID, SNP_IMAGE_ID);
72
73    /// The identity used by the direct-Linux SNP test image.
74    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
84/// Build the SNP ID block signing payload for an IGVM file.
85///
86/// Called by `manifest` to emit `<base>-snp.idblock`. The returned bytes are
87/// the raw [`SnpPspIdBlock`] -- exactly the content the SNP firmware hashes
88/// (SHA-384) and validates. A file-content signer signs these bytes directly
89/// (SHA-384 + ECDSA P-384, DER-encoded signature), so the emitted signature is
90/// valid for the firmware without any repackaging. `ld` is the SNP launch
91/// measurement, `policy` comes from the file's `GuestPolicy`, and `guest_svn`
92/// from the manifest.
93pub 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
97/// Build an SNP ID block signing payload with an explicit image identity.
98pub(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
117/// Read the SNP `GuestPolicy` value for `compatibility_mask` from an IGVM file,
118/// if present.
119pub 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
129/// Add an SNP ID block signed by an ephemeral key (development/test only).
130///
131/// The SNP launch digest is taken from the measurement that
132/// [`IgvmSerializer::new`] computes eagerly, so the file is measured exactly
133/// once. A random ECDSA P-384 key signs the block in-process. Production flows
134/// should instead use [`add_snp_id_block_signed`] with an out-of-band
135/// signature.
136///
137/// # Arguments
138/// * `igvm_data` - Input IGVM file; must contain an SEV-SNP platform header and
139///   a matching [`IgvmInitializationHeader::GuestPolicy`].
140/// * `guest_svn` - Guest security version number to embed.
141/// * `identity` - Family and image identifiers to embed.
142///
143/// # Errors
144/// Returns an error if the file has no SEV-SNP platform, already contains an
145/// SNP ID block, lacks an SNP measurement/guest policy, or if signing fails.
146pub(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
179/// Add an SNP ID block using an out-of-band signature (production).
180///
181/// `signing_payload` is the `<base>-snp.idblock` emitted by `manifest` (raw
182/// [`SnpPspIdBlock`], see [`id_block_signing_payload`]); `signature_der` is the
183/// DER-encoded ECDSA signature a file-content signer produced over those exact
184/// bytes; `public_key_pem` is the signer's public key as an X.509 certificate
185/// or SPKI public key (PEM or DER). The payload's launch digest and policy are
186/// checked against the IGVM file being patched, and the signature is
187/// cryptographically verified over the payload bytes with the supplied public
188/// key, so that a stale payload, wrong key, or corrupt signature fails at build
189/// time rather than only when the guest fails to launch on real hardware.
190///
191/// # Errors
192/// Returns an error if the file has no SEV-SNP platform, already contains an
193/// SNP ID block, lacks an SNP measurement/guest policy, if the signing payload
194/// is malformed or does not match the file, if the signature/public key cannot
195/// be parsed, or if the signature does not verify.
196pub 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    // The out-of-band signature was produced over the signing payload's exact
210    // bytes, so the directive must be reconstructed from the payload's ID block
211    // verbatim. Verify it matches the file we are patching so a stale payload
212    // fails clearly instead of yielding a file that will not attest.
213    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
239/// Locate the SEV-SNP compatibility mask, reject a pre-existing ID block, and
240/// return `(compatibility_mask, guest_policy)`.
241fn 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    // Refuse to double-add for this compatibility mask: a second ID block for
248    // the same mask would make the file ambiguous. Callers wanting to re-sign
249    // must start from a file without one for this mask.
250    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
265/// Fetch the cached SNP launch measurement (48-byte SHA-384) from a serializer.
266fn 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
278/// Serialize the staged serializer to bytes with a trace of the result size.
279fn 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
292/// Parse and validate an SNP ID block signing payload (the raw
293/// [`SnpPspIdBlock`] bytes emitted by `manifest`).
294fn 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
306/// Left-pad a big-endian ECC scalar/coordinate to 48 bytes.
307fn 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
318/// Parse a DER-encoded ECDSA signature (`SEQUENCE { INTEGER r, INTEGER s }`)
319/// into its big-endian 48-byte P-384 `(r, s)` components.
320fn 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
335/// Extract the signer's ECDSA P-384 public key from a supplied public key.
336///
337/// Accepts an X.509 certificate or a bare `SubjectPublicKeyInfo`, in PEM or
338/// DER form. The actual key parsing is delegated to the `crypto` crate.
339fn parse_p384_public_key(public_key: &[u8]) -> anyhow::Result<crypto::ecdsa::EcdsaPublicKey> {
340    use crypto::ecdsa::EcdsaPublicKey;
341    use crypto::x509::X509Certificate;
342
343    // Extract the ECDSA public key from a DER-encoded X.509 certificate,
344    // routing through the `crypto` X.509 parser rather than doing our own DER
345    // work here.
346    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        // DER: try a certificate first, then a bare SubjectPublicKeyInfo.
370        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
379/// Parse a DER ECDSA signature + public key, cryptographically verify the
380/// signature over `signed_bytes` (the signing payload the signer signed), and
381/// return the IGVM ID block signature and public-key structures (big-endian in,
382/// PSP little-endian layout out).
383///
384/// Verifying here catches a wrong key or corrupt signature at build time,
385/// rather than only when the guest fails to launch on real hardware.
386fn 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    // Verify (public key, r||s) over the SHA-384 of the signed content.
402    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    // Export the verified key as `Qx || Qy` (big-endian, 48 bytes each) for the
412    // PSP ID block public-key structure.
413    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
437/// Assemble an [`IgvmDirectiveHeader::SnpIdBlock`] from an ID block plus its
438/// signature and public key. Author-key fields are left zeroed (author signing
439/// is not used); the directive's presence signals the loader to set
440/// `id_block_en = 1`.
441fn 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
473/// Zero-pads and reverses a big-endian ECC component into a 72-byte
474/// little-endian array as required by the PSP ID block format.
475fn 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
483/// Generate a temporary ECDSA P-384 key pair using the selected `crypto`
484/// backend, sign the SHA-384 hash of the ID block, and return the signature
485/// + public key in the format expected by `IGVM_VHS_SNP_ID_BLOCK`.
486fn 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    // Generate a random P-384 key pair for ECDSA signing.
496    let key =
497        EcdsaKeyPair::generate(EcdsaCurve::P384).context("generating temporary SNP signing key")?;
498
499    // Hash the ID block with SHA-384.
500    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    // Sign the ID block bytes; `EcdsaKeyPair::sign` hashes them with SHA-384
509    // internally. Returns r || s in big-endian, each 48 bytes for P-384.
510    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    // Export the public key as Qx || Qy in big-endian, each 48 bytes for P-384.
530    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    /// Build a minimal, measurable SNP IGVM file: one SEV-SNP platform, a
565    /// matching `GuestPolicy`, and a single measured page.
566    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    /// Simulate a file-content signer: sign the SHA-384 of the signing payload
603    /// with a fresh P-384 key, returning `(DER ECDSA signature, DER SPKI public
604    /// key)` -- exactly what `openssl dgst -sha384 -sign` plus the signer's
605    /// public key would provide.
606    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    /// The uncompressed EC point `0x04 || Qx || Qy` for `key`.
618    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    /// DER-encode `key`'s public key as a `SubjectPublicKeyInfo` (the bare
627    /// `PUBLIC KEY` form).
628    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    /// DER-encode a raw `r || s` ECDSA signature (each `SNP_ECC_KEY_SIZE_BYTES`
642    /// big-endian) as a `SEQUENCE { r INTEGER, s INTEGER }` -- the form emitted
643    /// by e.g. `openssl dgst -sha384 -sign`.
644    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    /// A minimal self-signed `BuilderProfile` (issuer == subject, no
669    /// extensions).
670    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    /// `signature`/`spki` adapter over a P-384 [`crypto::ecdsa::EcdsaKeyPair`]
694    /// so the `x509-cert` builder can produce a self-signed ECDSA certificate.
695    /// Only the embedded `SubjectPublicKeyInfo` is consumed by
696    /// [`parse_p384_public_key`]; the self-signature is real but its
697    /// verification is not part of this path.
698    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                // ecdsa-with-SHA384
722                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    /// Build a self-signed P-384 certificate (DER) whose
762    /// `SubjectPublicKeyInfo` carries `key`'s public point.
763    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    /// Pin the SNP ID block identity constants to their exact byte values.
801    /// These are baked into externally-consumed SNP ID blocks, so any change
802    /// must be a deliberate, reviewed edit.
803    #[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    /// The temporary signing path must produce correctly-sized signature and
818    /// public-key components in the PSP little-endian layout.
819    #[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        // r/s each occupy 48 significant bytes zero-padded into a 72-byte field.
831        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        // The top (most-significant) bytes beyond the 48-byte component must
837        // be zero padding.
838        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    /// End-to-end: adding an ID block yields a valid file with exactly one
843    /// `SnpIdBlock`, and its embedded `ld` equals the measurement of the
844    /// original (ID-block-free) file -- proving the ID block does not perturb
845    /// the launch digest.
846    #[test]
847    fn add_snp_id_block_preserves_measurement() {
848        let igvm_data = build_snp_igvm(0x1);
849
850        // Reference digest computed on the original file.
851        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        // The measurement of the patched file must be unchanged.
884        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    /// Adding a second ID block must be refused.
894    #[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    /// A file with no SEV-SNP platform must be rejected.
907    #[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    /// End-to-end out-of-band flow: `manifest` emits the signing payload, a
934    /// file-content signer produces a DER signature + public key, and
935    /// `add_snp_id_block_signed` reconstructs a valid directive whose
936    /// `ld`/`guest_svn` come from the payload and whose launch digest matches
937    /// the file measurement.
938    #[test]
939    fn add_snp_id_block_signed_round_trip() {
940        let igvm_data = build_snp_igvm(0x1);
941
942        // Reference measurement + policy of the original file.
943        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        // manifest-side: emit the signing payload from the measurement.
952        let signing_payload = id_block_signing_payload(&ref_ld, 11, 0x30000).expect("payload");
953        // signer-side: produce the DER signature + public key.
954        let (sig_der, spki_der) = sign_payload(&signing_payload);
955
956        // add-side: reconstruct and attach.
957        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    /// A malformed DER signature is rejected.
975    #[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    /// A valid signature under the WRONG public key is rejected by the
997    /// build-time cryptographic verification.
998    #[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        // Signature from one key, public key from a different key.
1009        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    /// A signing payload whose launch digest does not match the file is
1021    /// rejected before the signature is even consulted.
1022    #[test]
1023    fn add_snp_id_block_signed_rejects_stale_payload() {
1024        let igvm_data = build_snp_igvm(0x1);
1025        // Signing payload built for a *different* launch digest.
1026        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    /// A wrong-length signing payload (not the raw SnpPspIdBlock) is rejected.
1038    #[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    /// End-to-end with the X.509 certificate signer input (rather than a bare
1052    /// `SubjectPublicKeyInfo`): the signer's public key is supplied as a P-384
1053    /// certificate in both DER and PEM form, and `add_snp_id_block_signed`
1054    /// accepts it and validates the signature. This exercises the certificate
1055    /// parsing branch of `parse_p384_public_key`.
1056    #[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        // Sign the payload and wrap the signer's public key in a self-signed
1071        // P-384 certificate, using the same key for both.
1072        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}