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