Skip to main content

underhill_attestation/igvm_attest/
mod.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 base64_serde::base64_serde_type;
9use openhcl_attestation_protocol::igvm_attest::get::IGVM_ATTEST_RESPONSE_CURRENT_VERSION;
10use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader;
11use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestHashType;
12use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestReportType;
13use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestType;
14use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestVersion;
15use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion;
16use openhcl_attestation_protocol::igvm_attest::get::IgvmCapabilityBitMap;
17use openhcl_attestation_protocol::igvm_attest::get::IgvmErrorInfo;
18use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig;
19use tee_call::TeeType;
20use thiserror::Error;
21use zerocopy::FromBytes;
22use zerocopy::FromZeros;
23use zerocopy::IntoBytes;
24
25pub mod ak_cert;
26pub mod key_release;
27pub mod wrapped_key;
28
29base64_serde_type!(Base64Url, base64::engine::general_purpose::URL_SAFE_NO_PAD);
30
31#[expect(missing_docs)] // self-explanatory fields
32#[derive(Debug, Error)]
33pub enum Error {
34    #[error(
35        "the size of the attestation report {report_size} is invalid, expected {expected_size}"
36    )]
37    InvalidAttestationReportSize {
38        report_size: usize,
39        expected_size: usize,
40    },
41    #[error("the size of the attestation response {response_size} is too small to parse")]
42    ResponseSizeTooSmall { response_size: usize },
43    #[error(
44        "the header of the attestation response (size {response_size}) is not in correct format"
45    )]
46    ResponseHeaderInvalidFormat { response_size: usize },
47    #[error(
48        "response size {specified_size} specified in the header not match the actual size {size}"
49    )]
50    ResponseSizeMismatch { size: usize, specified_size: usize },
51    #[error("response header version {version:?} larger than current version {latest_version:?}")]
52    InvalidResponseHeaderVersion {
53        version: IgvmAttestResponseVersion,
54        latest_version: IgvmAttestResponseVersion,
55    },
56    #[error(
57        "attest failed ({igvm_error_code}-{http_status_code}), retry recommendation ({retry_signal}), skip hw unsealing recommendation ({skip_hw_unsealing_signal})"
58    )]
59    Attestation {
60        igvm_error_code: u32,
61        http_status_code: u32,
62        retry_signal: bool,
63        skip_hw_unsealing_signal: bool,
64    },
65}
66
67/// Rust-style enum for `IgvmAttestReportType`
68pub enum ReportType {
69    /// VBS report
70    Vbs,
71    /// SNP report
72    Snp,
73    /// TDX report
74    Tdx,
75    /// CCA report
76    Cca,
77    /// Trusted VM report
78    Tvm,
79}
80
81impl ReportType {
82    /// Map the value to `IgvmAttestReportType`
83    fn to_external_type(&self) -> IgvmAttestReportType {
84        match self {
85            Self::Vbs => IgvmAttestReportType::VBS_VM_REPORT,
86            Self::Snp => IgvmAttestReportType::SNP_VM_REPORT,
87            Self::Tdx => IgvmAttestReportType::TDX_VM_REPORT,
88            Self::Cca => IgvmAttestReportType::CCA_VM_REPORT,
89            Self::Tvm => IgvmAttestReportType::TVM_REPORT,
90        }
91    }
92}
93
94/// Helper struct to create `IgvmAttestRequest` in raw bytes.
95pub struct IgvmAttestRequestHelper {
96    /// The request type.
97    request_type: IgvmAttestRequestType,
98    /// The report type.
99    report_type: ReportType,
100    /// Raw bytes of `RuntimeClaims`.
101    runtime_claims: Vec<u8>,
102    /// The hash of the `runtime_claims` to be included in the
103    /// `report_data` field of the attestation report.
104    runtime_claims_hash: [u8; tee_call::REPORT_DATA_SIZE],
105    /// THe hash type of the `runtime_claims_hash`.
106    hash_type: IgvmAttestHashType,
107}
108
109impl IgvmAttestRequestHelper {
110    /// Prepare the data necessary for creating the `KEY_RELEASE` request.
111    pub fn prepare_key_release_request(
112        tee_type: TeeType,
113        rsa_exponent: &[u8],
114        rsa_modulus: &[u8],
115        host_time: i64,
116        attestation_vm_config: &AttestationVmConfig,
117    ) -> Self {
118        let report_type = match tee_type {
119            TeeType::Snp => ReportType::Snp,
120            TeeType::Tdx => ReportType::Tdx,
121            TeeType::Cca => ReportType::Cca,
122            TeeType::Vbs => ReportType::Vbs,
123        };
124
125        let attestation_vm_config =
126            attestation_vm_config_with_time(attestation_vm_config, host_time);
127        let runtime_claims =
128            openhcl_attestation_protocol::igvm_attest::get::runtime_claims::RuntimeClaims::key_release_request_runtime_claims(rsa_exponent, rsa_modulus, &attestation_vm_config);
129        let runtime_claims = runtime_claims_to_bytes(&runtime_claims);
130
131        let hash_type = IgvmAttestHashType::SHA_256;
132        let hash = crypto::sha_256::sha_256(runtime_claims.as_bytes());
133        let mut runtime_claims_hash = [0u8; tee_call::REPORT_DATA_SIZE];
134        runtime_claims_hash[0..hash.len()].copy_from_slice(&hash);
135
136        Self {
137            request_type: IgvmAttestRequestType::KEY_RELEASE_REQUEST,
138            report_type,
139            runtime_claims,
140            runtime_claims_hash,
141            hash_type,
142        }
143    }
144
145    /// Prepare the data necessary for creating the `AK_CERT` request.
146    pub fn prepare_ak_cert_request(
147        tee_type: Option<TeeType>,
148        ak_pub_exponent: &[u8],
149        ak_pub_modulus: &[u8],
150        ek_pub_exponent: &[u8],
151        ek_pub_modulus: &[u8],
152        attestation_vm_config: &AttestationVmConfig,
153        guest_input: &[u8],
154    ) -> Self {
155        let report_type = match tee_type {
156            Some(TeeType::Snp) => ReportType::Snp,
157            Some(TeeType::Tdx) => ReportType::Tdx,
158            Some(TeeType::Cca) => ReportType::Cca,
159            Some(TeeType::Vbs) => ReportType::Vbs,
160            None => ReportType::Tvm,
161        };
162
163        let runtime_claims =
164            openhcl_attestation_protocol::igvm_attest::get::runtime_claims::RuntimeClaims::ak_cert_runtime_claims(
165                ak_pub_exponent,
166                ak_pub_modulus,
167                ek_pub_exponent,
168                ek_pub_modulus,
169                attestation_vm_config,
170                guest_input,
171            );
172
173        let runtime_claims = runtime_claims_to_bytes(&runtime_claims);
174
175        let hash_type = IgvmAttestHashType::SHA_256;
176        let hash = crypto::sha_256::sha_256(runtime_claims.as_bytes());
177        let mut runtime_claims_hash = [0u8; tee_call::REPORT_DATA_SIZE];
178        runtime_claims_hash[0..hash.len()].copy_from_slice(&hash);
179
180        Self {
181            request_type: IgvmAttestRequestType::AK_CERT_REQUEST,
182            report_type,
183            runtime_claims,
184            runtime_claims_hash,
185            hash_type,
186        }
187    }
188
189    /// Return the `runtime_claims_hash`.
190    pub fn get_runtime_claims_hash(&self) -> &[u8; tee_call::REPORT_DATA_SIZE] {
191        &self.runtime_claims_hash
192    }
193
194    /// Set the `request_type`.
195    pub fn set_request_type(&mut self, request_type: IgvmAttestRequestType) {
196        self.request_type = request_type
197    }
198
199    /// Create the request in raw bytes.
200    pub fn create_request(
201        &self,
202        version: IgvmAttestRequestVersion,
203        attestation_report: &[u8],
204    ) -> Result<Vec<u8>, Error> {
205        create_request(
206            version,
207            self.request_type,
208            &self.runtime_claims,
209            attestation_report,
210            &self.report_type,
211            self.hash_type,
212        )
213    }
214}
215
216/// Verify response header and try to extract IgvmErrorInfo from the header
217pub fn parse_response_header(response: &[u8]) -> Result<IgvmAttestCommonResponseHeader, Error> {
218    // Extract common header fields regardless of header version or request type
219    // For V1 request, response buffer should be empty in case of attestation failure
220    let header = IgvmAttestCommonResponseHeader::read_from_prefix(response)
221        .map_err(|_| Error::ResponseSizeTooSmall {
222            response_size: response.len(),
223        })?
224        .0; // TODO: zerocopy: err (https://github.com/microsoft/openvmm/issues/759)
225
226    // Check header data_size and version
227    if header.data_size as usize > response.len() {
228        Err(Error::ResponseSizeMismatch {
229            size: response.len(),
230            specified_size: header.data_size as usize,
231        })?
232    }
233    if header.version > IGVM_ATTEST_RESPONSE_CURRENT_VERSION {
234        Err(Error::InvalidResponseHeaderVersion {
235            version: header.version,
236            latest_version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION,
237        })?
238    }
239
240    // IgvmErrorInfo is added in response header since version 2
241    if header.version >= IgvmAttestResponseVersion::VERSION_2 {
242        // Extract result info from response header
243        let igvm_error_info = IgvmErrorInfo::read_from_prefix(
244            &response[size_of::<IgvmAttestCommonResponseHeader>()..],
245        )
246        .map_err(|_| Error::ResponseHeaderInvalidFormat {
247            response_size: response.len(),
248        })?
249        .0; // TODO: zerocopy: err (https://github.com/microsoft/openvmm/issues/759)
250
251        if 0 != igvm_error_info.error_code {
252            Err(Error::Attestation {
253                igvm_error_code: igvm_error_info.error_code,
254                http_status_code: igvm_error_info.http_status_code,
255                retry_signal: igvm_error_info.igvm_signal.retry(),
256                skip_hw_unsealing_signal: igvm_error_info.igvm_signal.skip_hw_unsealing(),
257            })?
258        }
259    }
260    Ok(IgvmAttestCommonResponseHeader {
261        data_size: header.data_size,
262        version: header.version,
263    })
264}
265
266/// Create a request in raw bytes.
267/// A request looks like:
268///   `IgvmAttestRequestBase` (raw bytes) | `IgvmAttestRequestDataExt` (raw bytes) if version >= 2 | `runtime_claims` (raw bytes)
269fn create_request(
270    version: IgvmAttestRequestVersion,
271    request_type: IgvmAttestRequestType,
272    runtime_claims: &[u8],
273    attestation_report: &[u8],
274    report_type: &ReportType,
275    hash_type: IgvmAttestHashType,
276) -> Result<Vec<u8>, Error> {
277    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestBase;
278    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestData;
279    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestDataExt;
280    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestHeader;
281
282    let expected_report_size = get_report_size(report_type);
283    if attestation_report.len() != expected_report_size {
284        Err(Error::InvalidAttestationReportSize {
285            report_size: attestation_report.len(),
286            expected_size: expected_report_size,
287        })?
288    }
289
290    let runtime_claims_len = runtime_claims.len();
291    // Determine if request data extension structure is needed (introduced in version 2)
292    let include_extension = version >= IgvmAttestRequestVersion::VERSION_2;
293    let extension_size = if include_extension {
294        size_of::<IgvmAttestRequestDataExt>()
295    } else {
296        0
297    };
298    let report_size = size_of::<IgvmAttestRequestBase>() + extension_size + runtime_claims_len;
299    let user_data_size = size_of::<IgvmAttestRequestData>() + extension_size + runtime_claims_len;
300    let mut request = IgvmAttestRequestBase::new_zeroed();
301
302    request.header = IgvmAttestRequestHeader::new(report_size as u32, request_type, 0);
303
304    request.attestation_report[..attestation_report.len()].copy_from_slice(attestation_report);
305
306    request.request_data = IgvmAttestRequestData::new(
307        version,
308        user_data_size as u32,
309        report_type.to_external_type(),
310        hash_type,
311        runtime_claims_len as u32,
312    );
313
314    let mut buffer = Vec::with_capacity(report_size);
315    buffer.extend_from_slice(request.as_bytes());
316
317    if include_extension {
318        let capability_bitmap = IgvmCapabilityBitMap::new()
319            .with_error_code(true)
320            .with_retry(true)
321            .with_skip_hw_unsealing(true);
322        let ext = IgvmAttestRequestDataExt::new(capability_bitmap);
323        buffer.extend_from_slice(ext.as_bytes());
324    }
325
326    buffer.extend_from_slice(runtime_claims);
327
328    Ok(buffer)
329}
330
331/// Get the expected size of the given report type.
332fn get_report_size(report_type: &ReportType) -> usize {
333    match report_type {
334        ReportType::Vbs => openhcl_attestation_protocol::igvm_attest::get::VBS_VM_REPORT_SIZE,
335        ReportType::Snp => openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE,
336        ReportType::Tdx => openhcl_attestation_protocol::igvm_attest::get::TDX_VM_REPORT_SIZE,
337        ReportType::Tvm => openhcl_attestation_protocol::igvm_attest::get::TVM_REPORT_SIZE,
338        ReportType::Cca => todo!(),
339    }
340}
341
342/// Helper function that returns the given config with the `current_time` set.
343fn attestation_vm_config_with_time(
344    vm_config: &AttestationVmConfig,
345    host_epoch: i64,
346) -> AttestationVmConfig {
347    let mut vm_config = vm_config.clone();
348    vm_config.current_time = Some(host_epoch);
349    vm_config
350}
351
352/// Helper function that converts the `RuntimeClaims` to raw bytes.
353fn runtime_claims_to_bytes(
354    runtime_claims: &openhcl_attestation_protocol::igvm_attest::get::runtime_claims::RuntimeClaims,
355) -> Vec<u8> {
356    let runtime_claims = serde_json::to_string(runtime_claims).expect("JSON serialization failed");
357    runtime_claims.as_bytes().to_vec()
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn test_create_request() {
366        use openhcl_attestation_protocol::igvm_attest::get::IGVM_ATTEST_REQUEST_CURRENT_VERSION;
367
368        let result = create_request(
369            IGVM_ATTEST_REQUEST_CURRENT_VERSION,
370            IgvmAttestRequestType::AK_CERT_REQUEST,
371            &[],
372            &[0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE],
373            &ReportType::Snp,
374            IgvmAttestHashType::SHA_256,
375        );
376        assert!(result.is_ok());
377
378        let result = create_request(
379            IGVM_ATTEST_REQUEST_CURRENT_VERSION,
380            IgvmAttestRequestType::AK_CERT_REQUEST,
381            &[],
382            &[0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE + 1],
383            &ReportType::Snp,
384            IgvmAttestHashType::SHA_256,
385        );
386        assert!(result.is_err());
387    }
388
389    #[test]
390    fn test_create_request_version1_no_extension() {
391        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestBase;
392        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestVersion;
393
394        let runtime_claims = vec![1u8, 2, 3];
395        let attestation_report =
396            vec![0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE];
397
398        let buffer = create_request(
399            IgvmAttestRequestVersion::VERSION_1,
400            IgvmAttestRequestType::AK_CERT_REQUEST,
401            &runtime_claims,
402            &attestation_report,
403            &ReportType::Snp,
404            IgvmAttestHashType::SHA_256,
405        )
406        .expect("request generation");
407
408        let (request, _) =
409            IgvmAttestRequestBase::read_from_prefix(&buffer).expect("parse IgvmAttestRequest");
410        assert_eq!(
411            request.request_data.version,
412            IgvmAttestRequestVersion::VERSION_1
413        );
414
415        let expected_size = (size_of::<
416            openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestData,
417        >() + runtime_claims.len()) as u32;
418        assert_eq!(request.request_data.data_size, expected_size);
419
420        let header_size = size_of::<IgvmAttestRequestBase>();
421        assert_eq!(
422            buffer.len(),
423            header_size + runtime_claims.len(),
424            "no extension appended for version 1"
425        );
426        assert_eq!(&buffer[header_size..], runtime_claims.as_slice());
427    }
428
429    #[test]
430    fn test_create_request_version2_with_extension() {
431        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestBase;
432        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestDataExt;
433        use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestVersion;
434
435        let runtime_claims = vec![4u8, 5, 6, 7];
436        let attestation_report =
437            vec![0u8; openhcl_attestation_protocol::igvm_attest::get::SNP_VM_REPORT_SIZE];
438
439        let buffer = create_request(
440            IgvmAttestRequestVersion::VERSION_2,
441            IgvmAttestRequestType::AK_CERT_REQUEST,
442            &runtime_claims,
443            &attestation_report,
444            &ReportType::Snp,
445            IgvmAttestHashType::SHA_256,
446        )
447        .expect("request generation");
448
449        let (request, _) =
450            IgvmAttestRequestBase::read_from_prefix(&buffer).expect("parse IgvmAttestRequest");
451        assert_eq!(
452            request.request_data.version,
453            IgvmAttestRequestVersion::VERSION_2
454        );
455
456        let expected_extension_size = size_of::<IgvmAttestRequestDataExt>();
457        let expected_size = (size_of::<
458            openhcl_attestation_protocol::igvm_attest::get::IgvmAttestRequestData,
459        >() + expected_extension_size
460            + runtime_claims.len()) as u32;
461        assert_eq!(request.request_data.data_size, expected_size);
462
463        let header_size = size_of::<IgvmAttestRequestBase>();
464        let ext_offset = header_size;
465
466        let (ext, _) = IgvmAttestRequestDataExt::read_from_prefix(&buffer[ext_offset..])
467            .expect("parse IgvmAttestRequestDataExt");
468        assert!(ext.capability_bitmap.error_code());
469        assert!(ext.capability_bitmap.retry());
470        assert!(ext.capability_bitmap.skip_hw_unsealing());
471
472        assert_eq!(
473            buffer.len(),
474            header_size + expected_extension_size + runtime_claims.len()
475        );
476        assert_eq!(
477            &buffer[header_size + expected_extension_size..],
478            runtime_claims.as_slice()
479        );
480    }
481
482    #[test]
483    fn test_transfer_key_jwk() {
484        const EXPECTED_JWK: &str = r#"[{"kid":"HCLTransferKey","key_ops":["encrypt"],"kty":"RSA","e":"RVhQT05FTlQ","n":"TU9EVUxVUw"}]"#;
485
486        let rsa_jwk = openhcl_attestation_protocol::igvm_attest::get::runtime_claims::RsaJwk::get_transfer_key_jwks(
487            b"EXPONENT",
488            b"MODULUS",
489        );
490
491        let result = serde_json::to_string(&rsa_jwk);
492        assert!(result.is_ok());
493
494        let transfer_key_jwk = result.unwrap();
495        assert_eq!(transfer_key_jwk, EXPECTED_JWK);
496    }
497
498    #[test]
499    fn test_vm_configuration_no_time() {
500        const EXPECTED_JWK: &str = r#"{"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#;
501
502        let attestation_vm_config = AttestationVmConfig {
503            current_time: None,
504            root_cert_thumbprint: String::new(),
505            console_enabled: false,
506            interactive_console_enabled: false,
507            secure_boot: false,
508            tpm_enabled: false,
509            tpm_persisted: false,
510            filtered_vpci_devices_allowed: true,
511            vm_unique_id: String::new(),
512            vmgs_provisioner: None,
513        };
514        let result = serde_json::to_string(&attestation_vm_config);
515        assert!(result.is_ok());
516
517        let vm_config = result.unwrap();
518        assert_eq!(vm_config, EXPECTED_JWK);
519    }
520
521    #[test]
522    fn test_vm_configuration_with_time() {
523        const EXPECTED_JWK: &str = r#"{"current-time":1691103220,"root-cert-thumbprint":"","console-enabled":false,"interactive-console-enabled":false,"secure-boot":false,"tpm-enabled":false,"tpm-persisted":false,"filtered-vpci-devices-allowed":true,"vmUniqueId":""}"#;
524
525        let attestation_vm_config = AttestationVmConfig {
526            current_time: None,
527            root_cert_thumbprint: String::new(),
528            console_enabled: false,
529            interactive_console_enabled: false,
530            secure_boot: false,
531            tpm_enabled: false,
532            tpm_persisted: false,
533            filtered_vpci_devices_allowed: true,
534            vm_unique_id: String::new(),
535            vmgs_provisioner: None,
536        };
537        let attestation_vm_config =
538            attestation_vm_config_with_time(&attestation_vm_config, 1691103220);
539        let result = serde_json::to_string(&attestation_vm_config);
540        assert!(result.is_ok());
541
542        let vm_config = result.unwrap();
543        assert_eq!(vm_config, EXPECTED_JWK);
544    }
545
546    #[test]
547    fn test_empty_response() {
548        let result = parse_response_header(&[]);
549        assert!(result.is_err());
550        assert_eq!(
551            result.unwrap_err().to_string(),
552            Error::ResponseSizeTooSmall { response_size: 0 }.to_string()
553        );
554    }
555
556    #[test]
557    fn test_invalid_response_size_smaller_than_header_size() {
558        const INVALID_RESPONSE: [u8; 4] = [0x04, 0x00, 0x00, 0x00];
559        let result = parse_response_header(&INVALID_RESPONSE);
560        assert!(result.is_err());
561        assert_eq!(
562            result.unwrap_err().to_string(),
563            Error::ResponseSizeTooSmall { response_size: 4 }.to_string()
564        );
565    }
566
567    #[test]
568    fn test_valid_v1_response_size_match() {
569        const VALID_RESPONSE: [u8; 42] = [
570            0x2a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
571            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
572            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
573        ];
574
575        let result = parse_response_header(&VALID_RESPONSE);
576        assert!(result.is_ok());
577        let header = result.unwrap();
578        assert_eq!(VALID_RESPONSE.len(), header.data_size as usize);
579        assert_eq!(IgvmAttestResponseVersion::VERSION_1, header.version);
580    }
581
582    #[test]
583    fn test_valid_v2_response_size_match() {
584        const VALID_RESPONSE: [u8; 42] = [
585            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
586            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
587            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
588        ];
589
590        let result = parse_response_header(&VALID_RESPONSE);
591        assert!(result.is_ok());
592        let header = result.unwrap();
593        assert_eq!(VALID_RESPONSE.len(), header.data_size as usize);
594        assert_eq!(IgvmAttestResponseVersion::VERSION_2, header.version);
595    }
596
597    #[test]
598    fn test_valid_v1_response_size_smaller_than_specified() {
599        const VALID_RESPONSE: [u8; 42] = [
600            0x29, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
601            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
602            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
603        ];
604
605        let header = parse_response_header(&VALID_RESPONSE);
606        assert!(header.is_ok());
607        assert_eq!(0x29, header.unwrap().data_size as usize);
608    }
609
610    #[test]
611    fn test_valid_v2_response_size_smaller_than_specified() {
612        const VALID_RESPONSE: [u8; 42] = [
613            0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
614            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
615            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
616        ];
617
618        let header = parse_response_header(&VALID_RESPONSE);
619        assert!(header.is_ok());
620        assert_eq!(0x29, header.unwrap().data_size as usize);
621    }
622
623    #[test]
624    fn test_invalid_v1_response_size() {
625        const INVALID_RESPONSE: [u8; 42] = [
626            0x2b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
627            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
628            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
629        ];
630
631        let result = parse_response_header(&INVALID_RESPONSE);
632        assert!(result.is_err());
633        assert_eq!(
634            result.unwrap_err().to_string(),
635            Error::ResponseSizeMismatch {
636                size: INVALID_RESPONSE.len(),
637                specified_size: 0x2b
638            }
639            .to_string()
640        );
641    }
642
643    #[test]
644    fn test_invalid_v2_response_size() {
645        const INVALID_RESPONSE: [u8; 42] = [
646            0x2b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
647            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
648            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
649        ];
650
651        let result = parse_response_header(&INVALID_RESPONSE);
652        assert!(result.is_err());
653        assert_eq!(
654            result.unwrap_err().to_string(),
655            Error::ResponseSizeMismatch {
656                size: INVALID_RESPONSE.len(),
657                specified_size: 0x2b
658            }
659            .to_string()
660        );
661    }
662
663    #[test]
664    fn test_invalid_header_version() {
665        const INVALID_RESPONSE: [u8; 42] = [
666            0x2a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
667            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
668            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
669        ];
670
671        let result = parse_response_header(&INVALID_RESPONSE);
672        assert!(result.is_err());
673        assert_eq!(
674            result.unwrap_err().to_string(),
675            Error::InvalidResponseHeaderVersion {
676                version: IgvmAttestResponseVersion(3),
677                latest_version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION
678            }
679            .to_string()
680        );
681    }
682
683    #[test]
684    fn test_invalid_v2_response_size_smaller_than_specified_header_size() {
685        const INVALID_RESPONSE: [u8; 28] = [
686            0x1c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
687            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
688        ];
689
690        let result = parse_response_header(&INVALID_RESPONSE);
691        assert!(result.is_err());
692        assert_eq!(
693            result.unwrap_err().to_string(),
694            Error::ResponseHeaderInvalidFormat {
695                response_size: 0x1c
696            }
697            .to_string()
698        );
699    }
700
701    #[test]
702    fn test_failed_response_with_retryable_error() {
703        // error_code: 1103 (0x44f), http_status_code: 403 (0x193), retryable
704        const INVALID_RESPONSE: [u8; 42] = [
705            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x93, 0x01,
706            0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
707            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
708        ];
709
710        let result = parse_response_header(&INVALID_RESPONSE);
711        assert!(result.is_err());
712        assert_eq!(
713            result.unwrap_err().to_string(),
714            Error::Attestation {
715                igvm_error_code: 1103,
716                http_status_code: 403,
717                retry_signal: true,
718                skip_hw_unsealing_signal: false
719            }
720            .to_string()
721        );
722    }
723
724    #[test]
725    fn test_failed_response_with_non_retryable_error() {
726        // error_code: 1103 (0x44f), http_status_code: 503 (0x1f7), not retryable
727        const INVALID_RESPONSE: [u8; 42] = [
728            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0xf7, 0x01,
729            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
730            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
731        ];
732
733        let result = parse_response_header(&INVALID_RESPONSE);
734        assert!(result.is_err());
735        assert_eq!(
736            result.unwrap_err().to_string(),
737            Error::Attestation {
738                igvm_error_code: 1103,
739                http_status_code: 503,
740                retry_signal: false,
741                skip_hw_unsealing_signal: false
742            }
743            .to_string()
744        );
745    }
746
747    #[test]
748    fn test_failed_response_with_skip_hw_unsealing_signal() {
749        // error_code: 1103 (0x44f), http_status_code: 400 (0x190),
750        // igvm_signal: retry=true, skip_hw_unsealing=true (0x03 = bits 0 and 1 set)
751        const INVALID_RESPONSE: [u8; 42] = [
752            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x90, 0x01,
753            0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
754            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
755        ];
756
757        let result = parse_response_header(&INVALID_RESPONSE);
758        assert!(result.is_err());
759        assert_eq!(
760            result.unwrap_err().to_string(),
761            Error::Attestation {
762                igvm_error_code: 1103,
763                http_status_code: 400,
764                retry_signal: true,
765                skip_hw_unsealing_signal: true
766            }
767            .to_string()
768        );
769    }
770
771    #[test]
772    fn test_failed_response_with_skip_hw_unsealing_only() {
773        // error_code: 1103 (0x44f), http_status_code: 400 (0x190),
774        // igvm_signal: retry=false, skip_hw_unsealing=true (0x02 = bit 1 set)
775        const INVALID_RESPONSE: [u8; 42] = [
776            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x90, 0x01,
777            0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
778            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
779        ];
780
781        let result = parse_response_header(&INVALID_RESPONSE);
782        assert!(result.is_err());
783        assert_eq!(
784            result.unwrap_err().to_string(),
785            Error::Attestation {
786                igvm_error_code: 1103,
787                http_status_code: 400,
788                retry_signal: false,
789                skip_hw_unsealing_signal: true
790            }
791            .to_string()
792        );
793    }
794}