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