1#[cfg(feature = "seal-verify")]
8mod verify_impl {
9 use cms::content_info::ContentInfo;
10 use cms::signed_data::SignedData;
11 use der::{Decode, Encode};
12 use sha2::{Digest, Sha256};
13
14 use crate::error::{AhuError, Result};
15
16 const ID_SIGNED_DATA: der::asn1::ObjectIdentifier =
17 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.2");
18 const ID_MESSAGE_DIGEST: der::asn1::ObjectIdentifier =
19 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.4");
20
21 #[derive(Debug)]
23 pub struct SealVerification {
24 pub signature_valid: bool,
25 pub digest_matches: bool,
26 pub signer_subject: String,
27 pub signer_sha256: String,
28 }
29
30 pub fn verify_seal(manifest_bytes: &[u8], seal_bytes: &[u8]) -> Result<SealVerification> {
37 if seal_bytes.is_empty() {
38 return Err(AhuError::SealInvalid("seal section is empty".into()));
39 }
40
41 let content_info = ContentInfo::from_der(seal_bytes)
43 .map_err(|e| AhuError::SealInvalid(format!("parse ContentInfo: {e}")))?;
44
45 if content_info.content_type != ID_SIGNED_DATA {
46 return Err(AhuError::SealInvalid(format!(
47 "unexpected content type: {}",
48 content_info.content_type
49 )));
50 }
51
52 let signed_data = content_info
54 .content
55 .decode_as::<SignedData>()
56 .map_err(|e| AhuError::SealInvalid(format!("parse SignedData: {e}")))?;
57
58 let signer_infos = &signed_data.signer_infos.0;
60 if signer_infos.len() != 1 {
61 return Err(AhuError::SealInvalid(
62 "exactly one signer is required".into(),
63 ));
64 }
65 let signer_info = &signer_infos.as_slice()[0];
66 let sha256 = der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1");
67 if signer_info.digest_alg.oid != sha256
68 || signed_data.digest_algorithms.len() != 1
69 || signed_data.digest_algorithms.as_slice()[0].oid != sha256
70 {
71 return Err(AhuError::SealInvalid(
72 "only SHA-256 digest attributes are supported".into(),
73 ));
74 }
75 let data_oid = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.1");
76 if signed_data.encap_content_info.econtent_type != data_oid
77 || signed_data.encap_content_info.econtent.is_some()
78 {
79 return Err(AhuError::SealInvalid(
80 "expected detached CMS data content".into(),
81 ));
82 }
83 let content_oid = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.113549.1.9.3");
84 let attrs = signer_info
85 .signed_attrs
86 .as_ref()
87 .ok_or_else(|| AhuError::SealInvalid("missing signed attributes".into()))?;
88 let content_attrs: Vec<_> = attrs.iter().filter(|a| a.oid == content_oid).collect();
89 if content_attrs.len() != 1
90 || content_attrs[0].values.len() != 1
91 || content_attrs[0].values.as_slice()[0]
92 .decode_as::<der::asn1::ObjectIdentifier>()
93 .ok()
94 != Some(data_oid)
95 {
96 return Err(AhuError::SealInvalid(
97 "invalid content-type attribute".into(),
98 ));
99 }
100
101 let manifest_digest = Sha256::digest(manifest_bytes);
103 let digest_matches = verify_message_digest(signer_info, &manifest_digest)?;
104
105 if !digest_matches {
106 return Err(AhuError::SealInvalid(
107 "message-digest attribute does not match manifest hash".into(),
108 ));
109 }
110
111 let cert = extract_signer_cert(&signed_data)?;
113
114 let signer_subject = format!("{}", cert.tbs_certificate().subject());
115
116 let signature_valid = verify_signature(signer_info, cert)?;
118 if !signature_valid {
119 return Err(AhuError::SealInvalid("invalid CMS signature".into()));
120 }
121 let signer_sha256 = hex::encode(Sha256::digest(
122 cert.to_der()
123 .map_err(|e| AhuError::SealInvalid(e.to_string()))?,
124 ));
125
126 Ok(SealVerification {
127 signature_valid,
128 digest_matches,
129 signer_subject,
130 signer_sha256,
131 })
132 }
133
134 fn verify_message_digest(
135 signer_info: &cms::signed_data::SignerInfo,
136 expected_digest: &[u8],
137 ) -> Result<bool> {
138 let attrs = signer_info
139 .signed_attrs
140 .as_ref()
141 .ok_or_else(|| AhuError::SealInvalid("no signed attributes".into()))?;
142
143 let matches: Vec<_> = attrs
144 .iter()
145 .filter(|a| a.oid == ID_MESSAGE_DIGEST)
146 .collect();
147 if matches.len() != 1 || matches[0].values.len() != 1 {
148 return Err(AhuError::SealInvalid(
149 "exactly one message-digest value required".into(),
150 ));
151 }
152 let octet = matches[0].values.as_slice()[0]
153 .decode_as::<der::asn1::OctetString>()
154 .map_err(|e| AhuError::SealInvalid(format!("invalid digest attribute: {e}")))?;
155 Ok(octet.as_bytes() == expected_digest)
156 }
157
158 fn extract_signer_cert(
159 signed_data: &SignedData,
160 ) -> Result<&x509_cert::certificate::Certificate> {
161 use cms::signed_data::SignerIdentifier;
162 use x509_cert::ext::pkix::SubjectKeyIdentifier;
163 let signer = signed_data
164 .signer_infos
165 .0
166 .as_slice()
167 .first()
168 .ok_or_else(|| AhuError::SealInvalid("missing signer".into()))?;
169 let certs = signed_data
170 .certificates
171 .as_ref()
172 .ok_or_else(|| AhuError::SealInvalid("missing certificates".into()))?;
173 let mut found = None;
174 for choice in certs.0.iter() {
175 if let cms::cert::CertificateChoices::Certificate(cert) = choice {
176 let tbs = cert.tbs_certificate();
177 let matches = match &signer.sid {
178 SignerIdentifier::IssuerAndSerialNumber(id) => {
179 &id.issuer == tbs.issuer() && &id.serial_number == tbs.serial_number()
180 }
181 SignerIdentifier::SubjectKeyIdentifier(id) => tbs
182 .get_extension::<SubjectKeyIdentifier>()
183 .ok()
184 .flatten()
185 .is_some_and(|(_, ski)| &ski == id),
186 };
187 if matches {
188 if found.is_some() {
189 return Err(AhuError::SealInvalid("ambiguous signer certificate".into()));
190 }
191 found = Some(cert);
192 }
193 }
194 }
195 found.ok_or_else(|| {
196 AhuError::SealInvalid("signer identifier has no matching certificate".into())
197 })
198 }
199
200 pub fn verify_seal_with_anchors(
205 manifest: &[u8],
206 seal: &[u8],
207 anchors_der: &[Vec<u8>],
208 now: u64,
209 ) -> Result<SealVerification> {
210 use x509_cert::certificate::Certificate;
211 let verification = verify_seal(manifest, seal)?;
212 let ci = ContentInfo::from_der(seal).map_err(|e| AhuError::SealInvalid(e.to_string()))?;
213 let sd = ci
214 .content
215 .decode_as::<SignedData>()
216 .map_err(|e| AhuError::SealInvalid(e.to_string()))?;
217 let signer = extract_signer_cert(&sd)?;
218 validate_cert_profile(signer, now, false)?;
219 if anchors_der.is_empty() {
220 return Err(AhuError::SealInvalid("no trust anchors".into()));
221 }
222 let anchors = anchors_der
223 .iter()
224 .map(|bytes| {
225 Certificate::from_der(bytes)
226 .map_err(|e| AhuError::SealInvalid(format!("invalid trust anchor: {e}")))
227 })
228 .collect::<Result<Vec<_>>>()?;
229 for anchor in &anchors {
230 validate_cert_profile(anchor, now, true)?;
231 }
232 for anchor in &anchors {
233 if signer == anchor {
234 return Ok(verification);
235 }
236 if signer.tbs_certificate().issuer() != anchor.tbs_certificate().subject() {
237 continue;
238 }
239 if signer.signature_algorithm() != signer.tbs_certificate().signature() {
240 return Err(AhuError::SealInvalid(
241 "certificate signature algorithms disagree".into(),
242 ));
243 }
244 let bytes = signer
245 .tbs_certificate()
246 .to_der()
247 .map_err(|e| AhuError::SealInvalid(e.to_string()))?;
248 let sig = signer.signature().as_bytes().ok_or_else(|| {
249 AhuError::SealInvalid("invalid certificate signature bits".into())
250 })?;
251 if verify_bytes(&bytes, sig, signer.signature_algorithm().oid, anchor)? {
252 return Ok(verification);
253 }
254 }
255 Err(AhuError::SealInvalid("signer is not directly issued by a configured CA trust anchor (intermediate paths unsupported)".into()))
256 }
257
258 pub fn verify_seal_with_pins(
261 manifest: &[u8],
262 seal: &[u8],
263 pins_der: &[Vec<u8>],
264 now: u64,
265 ) -> Result<SealVerification> {
266 let verification = verify_seal(manifest, seal)?;
267 let ci = ContentInfo::from_der(seal).map_err(|e| AhuError::SealInvalid(e.to_string()))?;
268 let sd = ci
269 .content
270 .decode_as::<SignedData>()
271 .map_err(|e| AhuError::SealInvalid(e.to_string()))?;
272 let signer = extract_signer_cert(&sd)?;
273 validate_cert_profile(signer, now, false)?;
274 let signer_der = signer
275 .to_der()
276 .map_err(|e| AhuError::SealInvalid(e.to_string()))?;
277 let pins = pins_der
280 .iter()
281 .map(|pin| {
282 x509_cert::certificate::Certificate::from_der(pin)
283 .and_then(|cert| cert.to_der())
284 .map_err(|e| AhuError::SealInvalid(format!("invalid signer pin: {e}")))
285 })
286 .collect::<Result<Vec<_>>>()?;
287 if !pins.iter().any(|pin| pin == &signer_der) {
288 return Err(AhuError::SealInvalid(
289 "signer certificate does not match an explicit pin".into(),
290 ));
291 }
292 Ok(verification)
293 }
294
295 fn validate_cert_profile(
296 cert: &x509_cert::certificate::Certificate,
297 now: u64,
298 ca: bool,
299 ) -> Result<()> {
300 use x509_cert::ext::pkix::{BasicConstraints, KeyUsage};
301 let tbs = cert.tbs_certificate();
302 let validity = tbs.validity();
303 if now < validity.not_before.to_unix_duration().as_secs()
304 || now >= validity.not_after.to_unix_duration().as_secs()
305 {
306 return Err(AhuError::SealInvalid(
307 "certificate outside validity period".into(),
308 ));
309 }
310 if let Some(exts) = tbs.extensions() {
311 let mut seen = std::collections::HashSet::new();
312 for ext in exts {
313 let oid = ext.extn_id.to_string();
314 if !seen.insert(oid.clone()) {
315 return Err(AhuError::SealInvalid(
316 "duplicate certificate extension".into(),
317 ));
318 }
319 if !matches!(
322 oid.as_str(),
323 "2.5.29.14" | "2.5.29.15" | "2.5.29.19" | "2.5.29.35"
324 ) {
325 return Err(AhuError::SealInvalid(format!(
326 "unsupported seal certificate extension {oid}"
327 )));
328 }
329 }
330 }
331 let usage = tbs
332 .get_extension::<KeyUsage>()
333 .map_err(|e| AhuError::SealInvalid(e.to_string()))?;
334 if usage.is_some_and(|(_, ku)| {
335 if ca {
336 !ku.key_cert_sign()
337 } else {
338 !ku.digital_signature()
339 }
340 }) {
341 return Err(AhuError::SealInvalid(
342 "certificate key usage does not permit operation".into(),
343 ));
344 }
345 if ca
346 && !tbs
347 .get_extension::<BasicConstraints>()
348 .map_err(|e| AhuError::SealInvalid(e.to_string()))?
349 .is_some_and(|(_, bc)| bc.ca)
350 {
351 return Err(AhuError::SealInvalid(
352 "trust anchor must be a CA certificate; signer pins are a separate policy".into(),
353 ));
354 }
355 Ok(())
356 }
357
358 const ID_ECDSA_SHA256_V: der::asn1::ObjectIdentifier =
359 der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
360 const ID_ML_DSA_44_V: der::asn1::ObjectIdentifier =
361 der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.17");
362 const ID_ML_DSA_65_V: der::asn1::ObjectIdentifier =
363 der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.18");
364 const ID_ML_DSA_87_V: der::asn1::ObjectIdentifier =
365 der::asn1::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.3.19");
366
367 fn verify_signature(
368 signer_info: &cms::signed_data::SignerInfo,
369 cert: &x509_cert::certificate::Certificate,
370 ) -> Result<bool> {
371 let signed_attrs = signer_info
372 .signed_attrs
373 .as_ref()
374 .ok_or_else(|| AhuError::SealInvalid("no signed attrs for verification".into()))?;
375
376 let attrs_der = signed_attrs
377 .to_der()
378 .map_err(|e| AhuError::SealInvalid(format!("encode signed attrs: {e}")))?;
379
380 verify_bytes(
381 &attrs_der,
382 signer_info.signature.as_bytes(),
383 signer_info.signature_algorithm.oid,
384 cert,
385 )
386 }
387
388 fn verify_bytes(
389 bytes: &[u8],
390 sig_bytes: &[u8],
391 sig_alg_oid: der::asn1::ObjectIdentifier,
392 cert: &x509_cert::certificate::Certificate,
393 ) -> Result<bool> {
394 let spki = cert.tbs_certificate().subject_public_key_info();
395 let pub_key_bytes = spki
396 .subject_public_key
397 .as_bytes()
398 .ok_or_else(|| AhuError::SealInvalid("no public key bits".into()))?;
399 if sig_alg_oid == ID_ECDSA_SHA256_V {
400 let ec = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
401 let curve = der::asn1::ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
402 if spki.algorithm.oid != ec
403 || spki
404 .algorithm
405 .parameters
406 .as_ref()
407 .and_then(|p| p.decode_as::<der::asn1::ObjectIdentifier>().ok())
408 != Some(curve)
409 {
410 return Err(AhuError::SealInvalid("expected P-256 key algorithm".into()));
411 }
412 verify_ecdsa_seal(bytes, sig_bytes, pub_key_bytes)
413 } else if sig_alg_oid == ID_ML_DSA_44_V && spki.algorithm.oid == sig_alg_oid {
414 verify_ml_dsa_seal::<ml_dsa::MlDsa44>(bytes, sig_bytes, pub_key_bytes)
415 } else if sig_alg_oid == ID_ML_DSA_65_V && spki.algorithm.oid == sig_alg_oid {
416 verify_ml_dsa_seal::<ml_dsa::MlDsa65>(bytes, sig_bytes, pub_key_bytes)
417 } else if sig_alg_oid == ID_ML_DSA_87_V && spki.algorithm.oid == sig_alg_oid {
418 verify_ml_dsa_seal::<ml_dsa::MlDsa87>(bytes, sig_bytes, pub_key_bytes)
419 } else {
420 Err(AhuError::SealInvalid(format!(
421 "unsupported/mismatched signature algorithm {sig_alg_oid}"
422 )))
423 }
424 }
425
426 fn verify_ecdsa_seal(attrs_der: &[u8], sig_bytes: &[u8], pub_key_bytes: &[u8]) -> Result<bool> {
427 let attrs_hash = Sha256::digest(attrs_der);
428
429 let verifying_key = p256::ecdsa::VerifyingKey::from_sec1_bytes(pub_key_bytes)
430 .map_err(|e| AhuError::SealInvalid(format!("parse P-256 public key: {e}")))?;
431
432 let signature = p256::ecdsa::DerSignature::from_bytes(sig_bytes)
433 .map_err(|e| AhuError::SealInvalid(format!("parse ECDSA signature: {e}")))?;
434
435 use p256::ecdsa::signature::hazmat::PrehashVerifier;
436 match verifying_key.verify_prehash(&attrs_hash, &signature) {
437 Ok(()) => Ok(true),
438 Err(_) => Ok(false),
439 }
440 }
441
442 fn verify_ml_dsa_seal<P>(
443 attrs_der: &[u8],
444 sig_bytes: &[u8],
445 pub_key_bytes: &[u8],
446 ) -> Result<bool>
447 where
448 P: ml_dsa::MlDsaParams,
449 {
450 let encoded = ml_dsa::EncodedVerifyingKey::<P>::try_from(pub_key_bytes)
451 .map_err(|_| AhuError::SealInvalid("invalid ML-DSA public key size".into()))?;
452 let vk = ml_dsa::VerifyingKey::<P>::decode(&encoded);
453
454 let sig = ml_dsa::Signature::<P>::try_from(sig_bytes)
455 .map_err(|_| AhuError::SealInvalid("invalid ML-DSA signature".into()))?;
456
457 use ml_dsa::Verifier;
458 match vk.verify(attrs_der, &sig) {
459 Ok(()) => Ok(true),
460 Err(_) => Ok(false),
461 }
462 }
463}
464
465#[cfg(feature = "seal-verify")]
466pub use verify_impl::{
467 SealVerification, verify_seal, verify_seal_with_anchors, verify_seal_with_pins,
468};