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,"interactive-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            interactive_console_enabled: false,
497            secure_boot: false,
498            tpm_enabled: false,
499            tpm_persisted: false,
500            filtered_vpci_devices_allowed: true,
501            vm_unique_id: String::new(),
502        };
503        let result = serde_json::to_string(&attestation_vm_config);
504        assert!(result.is_ok());
505
506        let vm_config = result.unwrap();
507        assert_eq!(vm_config, EXPECTED_JWK);
508    }
509
510    #[test]
511    fn test_vm_configuration_with_time() {
512        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":""}"#;
513
514        let attestation_vm_config = AttestationVmConfig {
515            current_time: None,
516            root_cert_thumbprint: String::new(),
517            console_enabled: false,
518            interactive_console_enabled: false,
519            secure_boot: false,
520            tpm_enabled: false,
521            tpm_persisted: false,
522            filtered_vpci_devices_allowed: true,
523            vm_unique_id: String::new(),
524        };
525        let attestation_vm_config =
526            attestation_vm_config_with_time(&attestation_vm_config, 1691103220);
527        let result = serde_json::to_string(&attestation_vm_config);
528        assert!(result.is_ok());
529
530        let vm_config = result.unwrap();
531        assert_eq!(vm_config, EXPECTED_JWK);
532    }
533
534    #[test]
535    fn test_empty_response() {
536        let result = parse_response_header(&[]);
537        assert!(result.is_err());
538        assert_eq!(
539            result.unwrap_err().to_string(),
540            Error::ResponseSizeTooSmall { response_size: 0 }.to_string()
541        );
542    }
543
544    #[test]
545    fn test_invalid_response_size_smaller_than_header_size() {
546        const INVALID_RESPONSE: [u8; 4] = [0x04, 0x00, 0x00, 0x00];
547        let result = parse_response_header(&INVALID_RESPONSE);
548        assert!(result.is_err());
549        assert_eq!(
550            result.unwrap_err().to_string(),
551            Error::ResponseSizeTooSmall { response_size: 4 }.to_string()
552        );
553    }
554
555    #[test]
556    fn test_valid_v1_response_size_match() {
557        const VALID_RESPONSE: [u8; 42] = [
558            0x2a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
559            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
560            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
561        ];
562
563        let result = parse_response_header(&VALID_RESPONSE);
564        assert!(result.is_ok());
565        let header = result.unwrap();
566        assert_eq!(VALID_RESPONSE.len(), header.data_size as usize);
567        assert_eq!(IgvmAttestResponseVersion::VERSION_1, header.version);
568    }
569
570    #[test]
571    fn test_valid_v2_response_size_match() {
572        const VALID_RESPONSE: [u8; 42] = [
573            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
574            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
575            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
576        ];
577
578        let result = parse_response_header(&VALID_RESPONSE);
579        assert!(result.is_ok());
580        let header = result.unwrap();
581        assert_eq!(VALID_RESPONSE.len(), header.data_size as usize);
582        assert_eq!(IgvmAttestResponseVersion::VERSION_2, header.version);
583    }
584
585    #[test]
586    fn test_valid_v1_response_size_smaller_than_specified() {
587        const VALID_RESPONSE: [u8; 42] = [
588            0x29, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
589            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
590            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
591        ];
592
593        let header = parse_response_header(&VALID_RESPONSE);
594        assert!(header.is_ok());
595        assert_eq!(0x29, header.unwrap().data_size as usize);
596    }
597
598    #[test]
599    fn test_valid_v2_response_size_smaller_than_specified() {
600        const VALID_RESPONSE: [u8; 42] = [
601            0x29, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
602            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
603            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
604        ];
605
606        let header = parse_response_header(&VALID_RESPONSE);
607        assert!(header.is_ok());
608        assert_eq!(0x29, header.unwrap().data_size as usize);
609    }
610
611    #[test]
612    fn test_invalid_v1_response_size() {
613        const INVALID_RESPONSE: [u8; 42] = [
614            0x2b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
615            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
616            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
617        ];
618
619        let result = parse_response_header(&INVALID_RESPONSE);
620        assert!(result.is_err());
621        assert_eq!(
622            result.unwrap_err().to_string(),
623            Error::ResponseSizeMismatch {
624                size: INVALID_RESPONSE.len(),
625                specified_size: 0x2b
626            }
627            .to_string()
628        );
629    }
630
631    #[test]
632    fn test_invalid_v2_response_size() {
633        const INVALID_RESPONSE: [u8; 42] = [
634            0x2b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
635            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
636            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
637        ];
638
639        let result = parse_response_header(&INVALID_RESPONSE);
640        assert!(result.is_err());
641        assert_eq!(
642            result.unwrap_err().to_string(),
643            Error::ResponseSizeMismatch {
644                size: INVALID_RESPONSE.len(),
645                specified_size: 0x2b
646            }
647            .to_string()
648        );
649    }
650
651    #[test]
652    fn test_invalid_header_version() {
653        const INVALID_RESPONSE: [u8; 42] = [
654            0x2a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x30, 0x82, 0x03, 0xeb, 0x30, 0x82,
655            0x02, 0xd3, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x3b, 0xa3, 0x33, 0x97, 0xef,
656            0x2f, 0x9e, 0xef, 0xbd, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
657        ];
658
659        let result = parse_response_header(&INVALID_RESPONSE);
660        assert!(result.is_err());
661        assert_eq!(
662            result.unwrap_err().to_string(),
663            Error::InvalidResponseHeaderVersion {
664                version: IgvmAttestResponseVersion(3),
665                latest_version: IGVM_ATTEST_RESPONSE_CURRENT_VERSION
666            }
667            .to_string()
668        );
669    }
670
671    #[test]
672    fn test_invalid_v2_response_size_smaller_than_specified_header_size() {
673        const INVALID_RESPONSE: [u8; 28] = [
674            0x1c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
675            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
676        ];
677
678        let result = parse_response_header(&INVALID_RESPONSE);
679        assert!(result.is_err());
680        assert_eq!(
681            result.unwrap_err().to_string(),
682            Error::ResponseHeaderInvalidFormat {
683                response_size: 0x1c
684            }
685            .to_string()
686        );
687    }
688
689    #[test]
690    fn test_failed_response_with_retryable_error() {
691        // error_code: 1103 (0x44f), http_status_code: 403 (0x193), retryable
692        const INVALID_RESPONSE: [u8; 42] = [
693            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0x93, 0x01,
694            0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
695            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
696        ];
697
698        let result = parse_response_header(&INVALID_RESPONSE);
699        assert!(result.is_err());
700        assert_eq!(
701            result.unwrap_err().to_string(),
702            Error::Attestation {
703                igvm_error_code: 1103,
704                http_status_code: 403,
705                retry_signal: true
706            }
707            .to_string()
708        );
709    }
710
711    #[test]
712    fn test_failed_response_with_non_retryable_error() {
713        // error_code: 1103 (0x44f), http_status_code: 503 (0x1f7), not retryable
714        const INVALID_RESPONSE: [u8; 42] = [
715            0x2a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x4f, 0x04, 0x00, 0x00, 0xf7, 0x01,
716            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
717            0x00, 0x00, 0x00, 0x00, 0x35, 0x5e, 0xda, 0xdd, 0x27, 0x38, 0x42, 0x30, 0x0d, 0x06,
718        ];
719
720        let result = parse_response_header(&INVALID_RESPONSE);
721        assert!(result.is_err());
722        assert_eq!(
723            result.unwrap_err().to_string(),
724            Error::Attestation {
725                igvm_error_code: 1103,
726                http_status_code: 503,
727                retry_signal: false
728            }
729            .to_string()
730        );
731    }
732}