Skip to main content

underhill_attestation/igvm_attest/
ak_cert.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The module for `AK_CERT_REQUEST` request type that supports parsing the
5//! response.
6use crate::igvm_attest::Error as CommonError;
7use crate::igvm_attest::parse_response_header;
8use crypto::x509::X509Certificate;
9use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AkCertMetadata;
10use thiserror::Error;
11use zerocopy::FromBytes;
12
13/// The certificate or renewal decision returned by the host.
14#[derive(Debug, PartialEq, Eq)]
15pub enum AkCertResponse {
16    /// Certificate bytes, possibly empty if the service is unavailable.
17    Certificate(Vec<u8>),
18    /// Keep the existing certificate.
19    NoRenewal,
20}
21
22/// An error extracting renewal metadata from a certificate.
23#[derive(Debug, Error)]
24pub enum AkCertMetadataError {
25    #[error("invalid AK certificate")]
26    Certificate(#[from] crypto::x509::X509Error),
27    #[error("invalid AK certificate validity")]
28    Validity(#[from] crypto::x509::X509ValidityError),
29    #[error("AK certificate has no {0} common name")]
30    MissingCommonName(&'static str),
31}
32
33/// Extract renewal hints, not a certificate trust decision.
34///
35/// TPM NV storage can contain padding or stale bytes after the DER certificate.
36pub fn parse_metadata(data: &[u8]) -> Result<AkCertMetadata, AkCertMetadataError> {
37    let certificate = X509Certificate::from_der(data)?;
38    let (not_before, not_after) = certificate.validity()?;
39    Ok(AkCertMetadata {
40        issuer: certificate
41            .issuer_common_name()?
42            .ok_or(AkCertMetadataError::MissingCommonName("issuer"))?,
43        subject: certificate
44            .subject_common_name()?
45            .ok_or(AkCertMetadataError::MissingCommonName("subject"))?,
46        not_before: format_validity_time(not_before),
47        not_after: format_validity_time(not_after),
48        authority: certificate
49            .authority_key_identifier()?
50            .map(hex::encode_upper)
51            .unwrap_or_default(),
52    })
53}
54
55fn format_validity_time(time: jiff::civil::DateTime) -> String {
56    let year = time.year();
57    // Jiff's %y supports 1969-2068, not X.509's 1950-2049 range.
58    let year = if (1950..=2049).contains(&year) {
59        format!("{:02}", year % 100)
60    } else {
61        format!("{year:04}")
62    };
63    format!(
64        "{year}{:02}{:02}{:02}{:02}{:02}Z",
65        time.month(),
66        time.day(),
67        time.hour(),
68        time.minute(),
69        time.second(),
70    )
71}
72
73/// AkCertError is returned by parse_ak_cert_response() in emuplat/tpm.rs
74#[derive(Debug, Error)]
75pub enum AkCertError {
76    #[error(
77        "AK cert response is too small to parse. Found {size} bytes but expected at least {minimum_size}"
78    )]
79    SizeTooSmall { size: usize, minimum_size: usize },
80    #[error(
81        "AK cert response size {specified_size} specified in the header is larger then the actual size {size}"
82    )]
83    SizeMismatch { size: usize, specified_size: usize },
84    #[error(
85        "AK cert response header version {version} does match the expected version {expected_version}"
86    )]
87    HeaderVersionMismatch { version: u32, expected_version: u32 },
88    #[error("error in parsing response header")]
89    ParseHeader(#[source] CommonError),
90    #[error("invalid response header version: {0}")]
91    InvalidResponseVersion(u32),
92}
93
94/// Parse an `AK_CERT_REQUEST` response, including V2 renewal decisions.
95pub fn parse_response(response: &[u8]) -> Result<AkCertResponse, AkCertError> {
96    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestAkCertResponseHeader;
97    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader;
98    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion;
99
100    let header = parse_response_header(response).map_err(AkCertError::ParseHeader)?;
101
102    // Extract payload as per header version
103    let header_size = match header.version {
104        IgvmAttestResponseVersion::VERSION_1 => size_of::<IgvmAttestCommonResponseHeader>(),
105        IgvmAttestResponseVersion::VERSION_2 => size_of::<IgvmAttestAkCertResponseHeader>(),
106        invalid_version => return Err(AkCertError::InvalidResponseVersion(invalid_version.0)),
107    };
108    let data_size = header.data_size as usize;
109
110    if data_size < header_size {
111        return Err(AkCertError::SizeTooSmall {
112            size: data_size,
113            minimum_size: header_size,
114        });
115    }
116
117    if header.version == IgvmAttestResponseVersion::VERSION_2 {
118        let (header, _) = IgvmAttestAkCertResponseHeader::read_from_prefix(&response[..data_size])
119            .map_err(|_| AkCertError::SizeTooSmall {
120                size: data_size,
121                minimum_size: header_size,
122            })?;
123        if header.error_info.igvm_signal.no_renew_cert() {
124            return Ok(AkCertResponse::NoRenewal);
125        }
126    }
127
128    Ok(AkCertResponse::Certificate(
129        response[header_size..data_size].to_vec(),
130    ))
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crypto::rsa::RsaKeyPair;
137    use crypto::x509::X509SelfSignedOptions;
138    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestAkCertResponseHeader;
139    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestCommonResponseHeader;
140    use openhcl_attestation_protocol::igvm_attest::get::IgvmAttestResponseVersion;
141    use openhcl_attestation_protocol::igvm_attest::get::IgvmSignal;
142    use test_igvm_agent_lib::ak_cert::certificate;
143    use test_with_tracing::test;
144    use zerocopy::IntoBytes;
145
146    #[test]
147    fn test_certificate_metadata() {
148        let cert = certificate();
149        let metadata = parse_metadata(&cert).unwrap();
150        assert_eq!(metadata.issuer, "Test \"AK\" issuer");
151        assert_eq!(metadata.subject, "Test \"AK\" issuer");
152        assert_eq!(metadata.not_before, "261009180232Z");
153        assert_eq!(metadata.not_after, "20561120180232Z");
154        assert_eq!(
155            metadata.authority,
156            "9FF3935C2C83D84F1DB5242A0E7D3C9B1D191D3B"
157        );
158
159        let mut padded = cert.clone();
160        padded.resize(4096, 0);
161        assert_eq!(parse_metadata(&padded).unwrap(), metadata);
162        padded[cert.len()..].fill(0xa5);
163        assert_eq!(parse_metadata(&padded).unwrap(), metadata);
164    }
165
166    #[test]
167    fn test_certificate_without_authority_key_identifier() {
168        let key = RsaKeyPair::generate(2048).unwrap();
169        let cert = X509Certificate::build_self_signed((&key).into(), &Default::default())
170            .unwrap()
171            .to_der()
172            .unwrap();
173        assert_eq!(parse_metadata(&cert).unwrap().authority, "");
174    }
175
176    #[test]
177    fn test_certificate_no_expiration_metadata() {
178        let key = RsaKeyPair::generate(2048).unwrap();
179        let cert = X509Certificate::build_self_signed(
180            (&key).into(),
181            &X509SelfSignedOptions {
182                common_name: "No expiration",
183                not_before: "261009180232Z",
184                not_after: "99991231235959Z",
185                authority_key_identifier: Some(&[0x01, 0xab, 0x00, 0xff]),
186                ..Default::default()
187            },
188        )
189        .unwrap()
190        .to_der()
191        .unwrap();
192        let metadata = parse_metadata(&cert).unwrap();
193        assert_eq!(metadata.issuer, "No expiration");
194        assert_eq!(metadata.subject, "No expiration");
195        assert_eq!(metadata.not_before, "261009180232Z");
196        assert_eq!(metadata.not_after, "99991231235959Z");
197        assert_eq!(metadata.authority, "01AB00FF");
198    }
199
200    #[test]
201    fn test_certificate_missing_common_name() {
202        let key = RsaKeyPair::generate(2048).unwrap();
203        let cert = X509Certificate::build_self_signed(
204            (&key).into(),
205            &X509SelfSignedOptions {
206                common_name: "",
207                ..Default::default()
208            },
209        )
210        .unwrap()
211        .to_der()
212        .unwrap();
213        assert!(matches!(
214            parse_metadata(&cert),
215            Err(AkCertMetadataError::MissingCommonName("issuer"))
216        ));
217    }
218
219    #[test]
220    fn test_metadata_invalid_storage_buffer() {
221        let cert = certificate();
222        assert!(parse_metadata(&[]).is_err());
223        assert!(parse_metadata(&cert[..cert.len() - 1]).is_err());
224        assert!(parse_metadata(&[0; 4096]).is_err());
225    }
226
227    #[test]
228    fn test_metadata_time_formatting() {
229        for (time, expected) in [
230            ("1949-12-31T23:59:59", "19491231235959Z"),
231            ("1950-01-01T00:00:00", "500101000000Z"),
232            ("1970-01-01T00:00:00", "700101000000Z"),
233            ("2000-02-29T00:00:00", "000229000000Z"),
234            ("2049-12-31T23:59:59", "491231235959Z"),
235            ("2050-01-01T00:00:00", "20500101000000Z"),
236            ("2056-11-20T18:02:32", "20561120180232Z"),
237            ("9999-12-30T00:00:00", "99991230000000Z"),
238            ("9999-12-31T23:59:59", "99991231235959Z"),
239        ] {
240            assert_eq!(format_validity_time(time.parse().unwrap()), expected);
241        }
242    }
243
244    fn v2_response(signal: IgvmSignal, payload: &[u8]) -> Vec<u8> {
245        let header = IgvmAttestAkCertResponseHeader {
246            data_size: (size_of::<IgvmAttestAkCertResponseHeader>() + payload.len()) as u32,
247            version: IgvmAttestResponseVersion::VERSION_2,
248            error_info: openhcl_attestation_protocol::igvm_attest::get::IgvmErrorInfo {
249                igvm_signal: signal,
250                ..Default::default()
251            },
252        };
253        let mut response = header.as_bytes().to_vec();
254        response.extend_from_slice(payload);
255        response
256    }
257
258    #[test]
259    fn test_v2_certificate_and_no_renewal() {
260        for payload in [b"".as_slice(), b"certificate"] {
261            assert_eq!(
262                parse_response(&v2_response(IgvmSignal::new(), payload)).unwrap(),
263                AkCertResponse::Certificate(payload.to_vec())
264            );
265            // The signal takes precedence over any payload, as in the legacy agent ABI.
266            let signal = IgvmSignal::new().with_no_renew_cert(true);
267            assert_eq!(signal.as_bytes(), &32u32.to_le_bytes());
268            assert_eq!(
269                parse_response(&v2_response(signal, payload)).unwrap(),
270                AkCertResponse::NoRenewal
271            );
272        }
273        for bit in [0, 1, 2, 3, 4, 6, 31] {
274            let signal = IgvmSignal::from(1u32 << bit);
275            assert_eq!(
276                parse_response(&v2_response(signal, b"cert")).unwrap(),
277                AkCertResponse::Certificate(b"cert".to_vec())
278            );
279        }
280    }
281
282    #[test]
283    fn test_no_renewal_does_not_override_errors_or_bad_sizes() {
284        let response = v2_response(IgvmSignal::new().with_no_renew_cert(true), &[]);
285        for size in 0..response.len() {
286            assert!(parse_response(&response[..size]).is_err());
287            // Bytes present outside the declared size must not supply the signal.
288            let mut invalid = response.clone();
289            invalid[..4].copy_from_slice(&(size as u32).to_le_bytes());
290            assert!(parse_response(&invalid).is_err());
291        }
292        let mut invalid = response.clone();
293        invalid[..4].copy_from_slice(&((response.len() + 1) as u32).to_le_bytes());
294        assert!(parse_response(&invalid).is_err());
295
296        let mut error = response.clone();
297        error[8..12].copy_from_slice(&1u32.to_le_bytes());
298        assert!(matches!(
299            parse_response(&error),
300            Err(AkCertError::ParseHeader(CommonError::Attestation { .. }))
301        ));
302        for version in [0u32, 3, u32::MAX] {
303            let mut invalid = response.clone();
304            invalid[4..8].copy_from_slice(&version.to_le_bytes());
305            assert!(parse_response(&invalid).is_err());
306        }
307    }
308
309    #[test]
310    fn test_undersized_response() {
311        const HEADER_SIZE: usize = size_of::<IgvmAttestAkCertResponseHeader>();
312        let properly_sized_response: [u8; HEADER_SIZE] = [1; HEADER_SIZE];
313        let undersized_response = &properly_sized_response[..HEADER_SIZE - 1];
314
315        // Empty response counts as an undersized response
316        let result = parse_response(&[]);
317        assert!(result.is_err());
318        assert_eq!(
319            result.unwrap_err().to_string(),
320            AkCertError::ParseHeader(CommonError::ResponseSizeTooSmall { response_size: 0 })
321                .to_string()
322        );
323
324        // Response has to be at least `HEADER_SIZE` bytes long, so `HEADER_SIZE - 1` bytes is too small.
325        let undersized_parse_ = parse_response(undersized_response);
326        assert!(undersized_parse_.is_err());
327        assert_eq!(
328            undersized_parse_.unwrap_err().to_string(),
329            AkCertError::ParseHeader(CommonError::ResponseSizeTooSmall {
330                response_size: HEADER_SIZE - 1
331            })
332            .to_string()
333        );
334
335        // When we finally have `HEADER_SIZE` bytes, we no longer see the failure as `AkCertError::SizeTooSmall`,
336        // but we still see a different error since the response is not valid.
337        let properly_sized_parse = parse_response(&properly_sized_response);
338        assert!(
339            !properly_sized_parse
340                .unwrap_err()
341                .to_string()
342                .starts_with("AK cert response is too small to parse"),
343        );
344    }
345
346    fn v1_response(payload: &[u8]) -> Vec<u8> {
347        let data_size = (size_of::<IgvmAttestCommonResponseHeader>() + payload.len()) as u32;
348        let mut response = data_size.to_le_bytes().to_vec();
349        response.extend_from_slice(&IgvmAttestResponseVersion::VERSION_1.0.to_le_bytes());
350        response.extend_from_slice(payload);
351        response
352    }
353
354    #[test]
355    fn test_valid_response_size_match() {
356        let cert = certificate();
357        assert_eq!(
358            parse_response(&v1_response(&cert)).unwrap(),
359            AkCertResponse::Certificate(cert)
360        );
361    }
362
363    #[test]
364    fn test_parse_response_small_size() {
365        let mut response = [0u8; 8];
366        // data_size = 4 (little-endian u32)
367        response[0..4].copy_from_slice(&4u32.to_le_bytes());
368        // version = VERSION_1 = 1 (little-endian u32)
369        response[4..8].copy_from_slice(&1u32.to_le_bytes());
370
371        assert!(parse_response(&response).is_err());
372    }
373
374    #[test]
375    fn test_valid_response_size_smaller_than_specified() {
376        let cert = certificate();
377        let mut response = v1_response(&cert);
378        // Bytes beyond the declared response size must not become payload.
379        response.extend_from_slice(b"trailing bytes");
380        assert_eq!(
381            parse_response(&response).unwrap(),
382            AkCertResponse::Certificate(cert)
383        );
384    }
385}