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