underhill_attestation/igvm_attest/
ak_cert.rs1use 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#[derive(Debug, PartialEq, Eq)]
15pub enum AkCertResponse {
16 Certificate(Vec<u8>),
18 NoRenewal,
20}
21
22#[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
33pub 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 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#[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
94pub 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 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 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 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 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 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 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 response[0..4].copy_from_slice(&4u32.to_le_bytes());
368 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 response.extend_from_slice(b"trailing bytes");
380 assert_eq!(
381 parse_response(&response).unwrap(),
382 AkCertResponse::Certificate(cert)
383 );
384 }
385}