openhcl_attestation_protocol/igvm_attest/get.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The module helps preparing requests and parsing responses that are
5//! sent to and received from the IGVm agent runs on the host via GET
6//! `IGVM_ATTEST` host request.
7
8use bitfield_struct::bitfield;
9use open_enum::open_enum;
10use zerocopy::FromBytes;
11use zerocopy::Immutable;
12use zerocopy::IntoBytes;
13use zerocopy::KnownLayout;
14
15const ATTESTATION_VERSION: u32 = 2;
16const ATTESTATION_SIGNATURE: u32 = 0x414c4348; // 'HCLA'
17/// The value is based on the maximum report size of the supported isolated VM
18/// Currently it's the size of a SNP report.
19const ATTESTATION_REPORT_SIZE_MAX: usize = SNP_VM_REPORT_SIZE;
20
21pub const VBS_VM_REPORT_SIZE: usize = hvdef::vbs::VBS_REPORT_SIZE;
22pub const SNP_VM_REPORT_SIZE: usize = x86defs::snp::SNP_REPORT_SIZE;
23pub const TDX_VM_REPORT_SIZE: usize = x86defs::tdx::TDX_REPORT_SIZE;
24/// No TEE attestation report for TVM
25pub const TVM_REPORT_SIZE: usize = 0;
26
27const PAGE_SIZE: usize = 4096;
28
29/// Number of pages required by the response buffer of WRAPPED_KEY request
30/// Currently the number matches the maximum value defined by `get_protocol`
31pub const WRAPPED_KEY_RESPONSE_BUFFER_SIZE: usize = 16 * PAGE_SIZE;
32/// Number of pages required by the response buffer of KEY_RELEASE request
33/// Currently the number matches the maximum value defined by `get_protocol`
34pub const KEY_RELEASE_RESPONSE_BUFFER_SIZE: usize = 16 * PAGE_SIZE;
35/// Number of pages required by the response buffer of AK_CERT request
36/// Currently the AK cert request only requires 1 page.
37pub const AK_CERT_RESPONSE_BUFFER_SIZE: usize = PAGE_SIZE;
38
39/// Current IGVM Attest response header version.
40pub const IGVM_ATTEST_RESPONSE_CURRENT_VERSION: IgvmAttestResponseVersion =
41 IgvmAttestResponseVersion::VERSION_2;
42
43open_enum! {
44 /// IGVM Attest response header versions.
45 #[derive(Default, IntoBytes, Immutable, KnownLayout, FromBytes)]
46 pub enum IgvmAttestResponseVersion: u32 {
47 /// Version 1
48 VERSION_1 = 1,
49 /// Version 2
50 VERSION_2 = 2,
51 }
52}
53
54/// Current IGVM Attest request header version.
55pub const IGVM_ATTEST_REQUEST_CURRENT_VERSION: IgvmAttestRequestVersion =
56 IgvmAttestRequestVersion::VERSION_2;
57
58open_enum! {
59 /// IGVM Attest request header versions.
60 #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
61 pub enum IgvmAttestRequestVersion: u32 {
62 /// Version 1
63 VERSION_1 = 1,
64 /// Version 2
65 VERSION_2 = 2,
66 }
67}
68
69/// Request base structure (C-style)
70/// The struct (includes the appended [`runtime_claims::RuntimeClaims`]) also serves as the
71/// attestation report in vTPM guest attestation.
72#[repr(C)]
73#[derive(Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
74pub struct IgvmAttestRequestBase {
75 /// Header (unmeasured)
76 pub header: IgvmAttestRequestHeader,
77 /// TEE attestation report
78 pub attestation_report: [u8; ATTESTATION_REPORT_SIZE_MAX],
79 /// Request data (unmeasured)
80 pub request_data: IgvmAttestRequestData,
81 // Data to be appended at the end of the struct:
82 // - Optional Extended request data [`IgvmAttestRequestDataExt`] (request version 2+).
83 // - Variable-length [`runtime_claims::RuntimeClaims`] (JSON string)
84 // The hash of [`runtime_claims::RuntimeClaims`] in [`IgvmAttestHashType`] will be captured
85 // in the `report_data` or equivalent field of the TEE attestation report.
86}
87
88open_enum! {
89 /// TEE attestation report type (C-style enum)
90 #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
91 pub enum IgvmAttestReportType: u32 {
92 /// Invalid report
93 INVALID_REPORT = 0,
94 /// VBS report
95 VBS_VM_REPORT = 1,
96 /// SNP report
97 SNP_VM_REPORT = 2,
98 /// Trusted VM report
99 TVM_REPORT = 3,
100 /// TDX report
101 TDX_VM_REPORT = 4,
102 /// CCA report
103 CCA_VM_REPORT = 5,
104 }
105}
106
107open_enum! {
108 /// Request type (C-style enum)
109 #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
110 pub enum IgvmAttestRequestType: u32 {
111 /// Invalid request
112 INVALID_REQUEST = 0,
113 /// Request for getting wrapped key from AKV.
114 KEY_RELEASE_REQUEST = 1,
115 /// Request to getting attestation key certificate.
116 AK_CERT_REQUEST = 2,
117 /// Request for getting VMMD blob from CPS.
118 WRAPPED_KEY_REQUEST = 3,
119 }
120}
121
122open_enum! {
123 /// Hash algorithm used for content of report data (C-style enum)
124 #[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
125 pub enum IgvmAttestHashType: u32 {
126 /// Invalid hash
127 INVALID_HASH = 0,
128 /// SHA-256
129 SHA_256 = 1,
130 /// SHA-384
131 SHA_384 = 2,
132 /// SHA-512
133 SHA_512 = 3,
134 }
135}
136
137/// Unmeasured data used to provide transport sanity and versioning (C-style struct)
138#[repr(C)]
139#[derive(Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
140pub struct IgvmAttestRequestHeader {
141 /// Signature
142 pub signature: u32,
143 /// Version
144 pub version: u32,
145 /// Report size
146 pub report_size: u32,
147 /// Request type
148 pub request_type: IgvmAttestRequestType,
149 /// Status
150 pub status: u32,
151 /// Reserved
152 pub reserved: [u32; 3],
153}
154
155impl IgvmAttestRequestHeader {
156 /// Create an `HardwareKeyProtectorHeader` instance.
157 pub fn new(report_size: u32, request_type: IgvmAttestRequestType, status: u32) -> Self {
158 Self {
159 signature: ATTESTATION_SIGNATURE,
160 version: ATTESTATION_VERSION,
161 report_size,
162 request_type,
163 status,
164 reserved: [0u32; 3],
165 }
166 }
167}
168
169/// Bitmap of additional Igvm request attributes.
170/// 0 - error_code: Requesting IGVM Agent Error code
171/// 1 - retry: Retry preference
172/// 2 - skip_hw_unsealing: Skip hardware unsealing in case key release request fails
173/// 3 - use_rsa_aes_key_wrap_384: Request that the IGVM Agent ask Azure Key Vault
174/// (AKV) to wrap and release the key with the SHA-384 variant of the
175/// composite RSA+AES key-wrap scheme (PKCS#11 CKM_RSA_AES_KEY_WRAP /
176/// AKV's RSA_AES_KEY_WRAP_384): RSA-OAEP-SHA384 (MGF1-SHA-384) wraps an
177/// AES-256 KEK that performs AES Key Wrap on the released key. The default
178/// is the same CKM_RSA_AES_KEY_WRAP scheme with the inner RSA-OAEP using
179/// SHA-1 (MGF1-SHA-1).
180/// 4 - corim_endorsement: Request that the IGVM Agent fetch the CoRIM launch
181/// endorsement for the guest and include it in the attestation flow.
182#[bitfield(u32)]
183#[derive(IntoBytes, FromBytes, Immutable, KnownLayout)]
184pub struct IgvmCapabilityBitMap {
185 pub error_code: bool,
186 pub retry: bool,
187 pub skip_hw_unsealing: bool,
188 pub use_rsa_aes_key_wrap_384: bool,
189 pub corim_endorsement: bool,
190 #[bits(27)]
191 _reserved: u32,
192}
193
194/// Unmeasured user data, used for host attestation requests (C-style struct)
195#[repr(C)]
196#[derive(Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
197pub struct IgvmAttestRequestData {
198 /// Data size
199 pub data_size: u32,
200 /// Version
201 pub version: IgvmAttestRequestVersion,
202 /// Report type
203 pub report_type: IgvmAttestReportType,
204 /// Report data hash type
205 pub report_data_hash_type: IgvmAttestHashType,
206 /// Size of the appended raw runtime claims
207 pub variable_data_size: u32,
208}
209
210impl IgvmAttestRequestData {
211 /// Create an `IgvmAttestRequestData` instance.
212 pub fn new(
213 version: IgvmAttestRequestVersion,
214 data_size: u32,
215 report_type: IgvmAttestReportType,
216 report_data_hash_type: IgvmAttestHashType,
217 variable_data_size: u32,
218 ) -> Self {
219 Self {
220 data_size,
221 version,
222 report_type,
223 report_data_hash_type,
224 variable_data_size,
225 }
226 }
227}
228
229/// Unmeasured user data appended to `IgvmAttestRequestData` for version 2+,
230/// used for host attestation requests (C-style struct).
231#[repr(C)]
232#[derive(Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
233pub struct IgvmAttestRequestDataExt {
234 /// Bitmap of additional requested attributes
235 pub capability_bitmap: IgvmCapabilityBitMap,
236}
237
238impl IgvmAttestRequestDataExt {
239 /// Create an `IgvmAttestRequestDataExt` instance.
240 pub fn new(capability_bitmap: IgvmCapabilityBitMap) -> Self {
241 Self { capability_bitmap }
242 }
243}
244
245/// Bitmap indicates a signal to requestor
246/// 0 - IGVM_SIGNAL_RETRY_RECOMMENDED_BIT: Retry recommendation
247/// 1 - IGVM_SIGNAL_SKIP_HW_UNSEALING_RECOMMENDED_BIT: Skip hardware unsealing
248/// 2 - IGVM_SIGNAL_RSA_AES_KEY_WRAP_384_USED_BIT: Set by the IGVM Agent to
249/// indicate that the agent asked AKV for the SHA-384 variant
250/// (RSA_AES_KEY_WRAP_384) and AKV used it to wrap the key in the payload.
251/// If this bit is clear, AKV wrapped the key with the default
252/// CKM_RSA_AES_KEY_WRAP scheme (inner RSA-OAEP using SHA-1).
253/// 3 - IGVM_SIGNAL_CORIM_ENDORSEMENT_REQUESTED_BIT: Set by the IGVM Agent to
254/// indicate that the agent requested the CoRIM endorsement.
255/// 5 - IGVM_SIGNAL_NO_RENEW_CERT_BIT: Keep the AK certificate described in
256/// the request instead of issuing a replacement.
257#[bitfield(u32)]
258#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)]
259pub struct IgvmSignal {
260 pub retry: bool,
261 pub skip_hw_unsealing: bool,
262 pub rsa_aes_key_wrap_384_used: bool,
263 pub corim_endorsement_requested: bool,
264 #[bits(1)]
265 _reserved_bit4: u32,
266 pub no_renew_cert: bool,
267 #[bits(26)]
268 _reserved: u32,
269}
270
271/// The common response header that comply with both V1 and V2 Igvm attest response
272#[repr(C)]
273#[derive(Default, Debug, IntoBytes, FromBytes)]
274pub struct IgvmAttestCommonResponseHeader {
275 /// Data size
276 pub data_size: u32,
277 /// Version
278 pub version: IgvmAttestResponseVersion,
279}
280
281/// The response header for `IGVM_ERROR_INFO` (C-style struct)
282#[repr(C)]
283#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
284pub struct IgvmErrorInfo {
285 /// ErrorCode propagated from IgvmAgent
286 pub error_code: u32,
287 /// HttpStatusCode propagated from IgvmAgent that enhances the ErrorCode
288 pub http_status_code: u32,
289 /// Igvm signal from response
290 pub igvm_signal: IgvmSignal,
291 /// Reserved
292 pub reserved: [u32; 3],
293}
294
295/// The response header for `KEY_RELEASE_REQUEST` (C-style struct)
296#[repr(C)]
297#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
298pub struct IgvmAttestKeyReleaseResponseHeader {
299 /// Data size
300 pub data_size: u32,
301 /// Version
302 pub version: IgvmAttestResponseVersion,
303 /// IgvmErrorInfo that contains RPC result and retry recommendation
304 pub error_info: IgvmErrorInfo,
305}
306
307/// The response header for `WRAPPED_KEY_REQUEST` (C-style struct)
308/// Currently the definition is the same as [`IgvmAttestKeyReleaseResponseHeader`].
309#[repr(C)]
310#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
311pub struct IgvmAttestWrappedKeyResponseHeader {
312 /// Data size
313 pub data_size: u32,
314 /// Version
315 pub version: IgvmAttestResponseVersion,
316 /// IgvmErrorInfo that contains RPC result and retry recommendation
317 pub error_info: IgvmErrorInfo,
318}
319
320/// The response header for `AK_CERT_REQUEST` (C-style struct)
321#[repr(C)]
322#[derive(Default, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)]
323pub struct IgvmAttestAkCertResponseHeader {
324 /// Data size
325 pub data_size: u32,
326 /// Version
327 pub version: IgvmAttestResponseVersion,
328 /// IgvmErrorInfo that contains RPC result and retry recommendation
329 pub error_info: IgvmErrorInfo,
330}
331
332/// Definition of the runt-time claims, which will be appended to the
333/// `IgvmAttestRequestBase` in raw bytes.
334pub mod runtime_claims {
335 use base64_serde::base64_serde_type;
336 use guid::Guid;
337 use mesh::MeshPayload;
338 use serde::Deserialize;
339 use serde::Serialize;
340
341 base64_serde_type!(Base64Url, base64::engine::general_purpose::URL_SAFE_NO_PAD);
342
343 /// Metadata describing the existing AK certificate for renewal decisions.
344 /// These claims do not establish certificate trust.
345 #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
346 pub struct AkCertMetadata {
347 /// Issuer common name.
348 pub issuer: String,
349 /// Subject common name.
350 pub subject: String,
351 /// ASN.1 UTCTime or GeneralizedTime content, including the trailing `Z`.
352 pub not_before: String,
353 /// ASN.1 UTCTime or GeneralizedTime content, including the trailing `Z`.
354 pub not_after: String,
355 /// Uppercase hex-encoded authority key identifier, or empty if absent.
356 pub authority: String,
357 }
358
359 /// Measured runtime claim in JSON format.
360 /// The hash of the data is expected be put into the user_data field of
361 /// the attestation report.
362 #[derive(Debug, Deserialize, Serialize)]
363 #[serde(rename_all = "kebab-case")]
364 pub struct RuntimeClaims {
365 /// An array of [`RsaJwk`]
366 pub keys: Vec<RsaJwk>,
367 /// VM configuration
368 pub vm_configuration: AttestationVmConfig,
369 /// Optional user data
370 #[serde(default, skip_serializing_if = "String::is_empty")]
371 pub user_data: String,
372 /// Existing AK certificate metadata, present only on renewal requests.
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub akcert: Option<AkCertMetadata>,
375 }
376
377 impl RuntimeClaims {
378 /// Create runtime claims for `KEY_RELEASE_REQUEST`.
379 pub fn key_release_request_runtime_claims(
380 exponent: &[u8],
381 modulus: &[u8],
382 attestation_vm_config: &AttestationVmConfig,
383 ) -> Self {
384 let transfer_key_jwks = RsaJwk::get_transfer_key_jwks(exponent, modulus);
385 Self {
386 keys: transfer_key_jwks,
387 vm_configuration: attestation_vm_config.clone(),
388 user_data: "".to_string(),
389 akcert: None,
390 }
391 }
392
393 /// Helper function for creating runtime claims of `AK_CERT_REQUEST`.
394 pub fn ak_cert_runtime_claims(
395 ak_pub_exponent: &[u8],
396 ak_pub_modulus: &[u8],
397 ek_pub_exponent: &[u8],
398 ek_pub_modulus: &[u8],
399 attestation_vm_config: &AttestationVmConfig,
400 user_data: &[u8],
401 ) -> Self {
402 let tpm_jwks = RsaJwk::get_tpm_jwks(
403 ak_pub_exponent,
404 ak_pub_modulus,
405 ek_pub_exponent,
406 ek_pub_modulus,
407 );
408 Self {
409 keys: tpm_jwks,
410 vm_configuration: attestation_vm_config.clone(),
411 user_data: hex::encode(user_data),
412 akcert: None,
413 }
414 }
415 }
416
417 /// JWK for an RSA key
418 #[derive(Debug, Deserialize, Serialize)]
419 pub struct RsaJwk {
420 /// Key id
421 pub kid: String,
422 /// Key operations
423 pub key_ops: Vec<String>,
424 /// Key type
425 pub kty: String,
426 /// RSA public exponent
427 #[serde(with = "Base64Url")]
428 pub e: Vec<u8>,
429 /// RSA public modulus
430 #[serde(with = "Base64Url")]
431 pub n: Vec<u8>,
432 }
433
434 impl RsaJwk {
435 /// Create a JWKS from inputs.
436 pub fn get_transfer_key_jwks(exponent: &[u8], modulus: &[u8]) -> Vec<RsaJwk> {
437 let jwk = RsaJwk {
438 kid: "HCLTransferKey".to_string(),
439 key_ops: vec!["encrypt".to_string()],
440 kty: "RSA".to_string(),
441 e: exponent.to_vec(),
442 n: modulus.to_vec(),
443 };
444
445 vec![jwk]
446 }
447
448 /// Create a JWKS from inputs.
449 pub fn get_tpm_jwks(
450 ak_pub_exponent: &[u8],
451 ak_pub_modulus: &[u8],
452 ek_pub_exponent: &[u8],
453 ek_pub_modulus: &[u8],
454 ) -> Vec<RsaJwk> {
455 let ak_pub = RsaJwk {
456 kid: "HCLAkPub".to_string(),
457 key_ops: vec!["sign".to_string()],
458 kty: "RSA".to_string(),
459 e: ak_pub_exponent.to_vec(),
460 n: ak_pub_modulus.to_vec(),
461 };
462 let ek_pub = RsaJwk {
463 kid: "HCLEkPub".to_string(),
464 key_ops: vec!["encrypt".to_string()],
465 kty: "RSA".to_string(),
466 e: ek_pub_exponent.to_vec(),
467 n: ek_pub_modulus.to_vec(),
468 };
469
470 vec![ak_pub, ek_pub]
471 }
472 }
473
474 /// Claims for VMGS provenance.
475 #[derive(Clone, Debug, Deserialize, Serialize, MeshPayload)]
476 #[serde(rename_all = "kebab-case")]
477 pub struct VmgsProvisioner {
478 /// VMGS ID
479 #[serde(with = "serde_helpers::as_string")]
480 pub id: Guid,
481 /// Signer (root cert thumbprint + leaf subject name as a decentralized
482 /// identifier)
483 pub signer: String,
484 }
485
486 /// Supported hardware sealing policy
487 #[derive(Clone, Copy, Debug, Deserialize, Serialize, MeshPayload)]
488 pub enum HardwareSealingPolicy {
489 #[serde(rename = "none")]
490 None,
491 #[serde(rename = "hash")]
492 Hash,
493 #[serde(rename = "signer")]
494 Signer,
495 }
496
497 /// TPM reference implementation version.
498 #[derive(Clone, Copy, Debug, Deserialize, Serialize, MeshPayload)]
499 pub enum AttestationTpmVersion {
500 /// TPM reference implementation version 1.38
501 #[serde(rename = "1.38")]
502 V138,
503 /// TPM reference implementation version 1.85
504 #[serde(rename = "185")]
505 V185,
506 }
507
508 /// VM configuration to be included in the `RuntimeClaims`.
509 #[derive(Clone, Debug, Deserialize, Serialize, MeshPayload)]
510 #[serde(rename_all = "kebab-case")]
511 pub struct AttestationVmConfig {
512 /// Time stamp
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub current_time: Option<i64>,
515 /// Base64-encoded hash of the provisioning cert
516 pub root_cert_thumbprint: String,
517 /// Whether the serial console is enabled
518 pub console_enabled: bool,
519 /// Whether the serial console, if enabled, is interactive
520 pub interactive_console_enabled: bool,
521 /// Whether the IPMI KCS interface is enabled
522 pub ipmi_enabled: bool,
523 /// Whether secure boot is enabled
524 pub secure_boot: bool,
525 /// Whether the TPM is enabled
526 pub tpm_enabled: bool,
527 /// TPM reference implementation version
528 pub tpm_version: AttestationTpmVersion,
529 /// Whether the VM is in stateful mode (i.e. attestation is not
530 /// suppressed).
531 ///
532 /// NOTE: This is a legacy field. Its name (`tpm-persisted` on the wire)
533 /// predates stateless + hardware sealing and does NOT describe whether
534 /// TPM state is actually persisted to the VMGS at runtime — that broader
535 /// decision is made by the VMM (see `no_persistent_secrets` in
536 /// `underhill_core`). The name and value semantics are kept unchanged to
537 /// preserve the attestation runtime-claims contract and the
538 /// hardware-derived key KDF input.
539 pub tpm_persisted: bool,
540 /// Whether certain vPCI devices are allowed through the device filter
541 pub filtered_vpci_devices_allowed: bool,
542 /// VM id
543 #[serde(rename = "vmUniqueId")]
544 pub vm_unique_id: String,
545 /// VMGS provenance data
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub vmgs_provisioner: Option<VmgsProvisioner>,
548 /// Hardware sealing policy
549 pub hardware_sealing_policy: HardwareSealingPolicy,
550 }
551}