1pub(crate) mod additional;
2pub(crate) mod constraints;
3pub(crate) mod device;
4pub(crate) mod locations;
5pub(crate) mod policies;
6pub(crate) mod transparency;
7
8use crate::names::inspect_name;
9use crate::{
10 AccessDescription, AuthorityKeyIdentifier, DistributionPoint, NameAttribute, OidNames,
11};
12use x509_cert::der::Decode;
13use x509_parser::{
14 asn1_rs::{BitString, FromDer, Oid},
15 extensions as backend,
16};
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[cfg_attr(
23 feature = "serde",
24 serde(tag = "kind", content = "value", rename_all = "snake_case")
25)]
26#[non_exhaustive]
27pub enum GeneralName {
28 Dns(String),
30 Email(String),
32 Uri(String),
34 Ip(String),
36 Directory(Vec<Vec<NameAttribute>>),
38 RegisteredId(String),
40 OtherName {
42 oid: String,
44 name: Option<String>,
46 value_der_hex: String,
50 },
51 X400Address {
53 constructed: bool,
55 content_hex: String,
57 },
58 EdiPartyName {
60 constructed: bool,
62 content_hex: String,
64 },
65 Unsupported(u32),
69 Malformed(u32),
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78#[non_exhaustive]
79pub struct KeyUsage {
80 pub digital_signature: bool,
82 pub content_commitment: bool,
84 pub key_encipherment: bool,
86 pub data_encipherment: bool,
88 pub key_agreement: bool,
90 pub key_cert_sign: bool,
92 pub crl_sign: bool,
94 pub encipher_only: bool,
96 pub decipher_only: bool,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104#[non_exhaustive]
105pub struct KeyPurpose {
106 pub oid: String,
108 pub name: Option<String>,
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize))]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[cfg_attr(
118 feature = "serde",
119 serde(tag = "kind", content = "value", rename_all = "snake_case")
120)]
121#[non_exhaustive]
122pub enum ExtensionDetails {
123 SubjectAlternativeName(Vec<GeneralName>),
125 KeyUsage(KeyUsage),
127 ExtendedKeyUsage(Vec<KeyPurpose>),
129 BasicConstraints {
131 ca: bool,
133 path_len_constraint: Option<u32>,
135 },
136 SubjectKeyIdentifier(String),
138 AuthorityKeyIdentifier(AuthorityKeyIdentifier),
140 AuthorityInfoAccess(Vec<AccessDescription>),
142 SubjectInfoAccess(Vec<AccessDescription>),
144 IssuerAlternativeName(Vec<GeneralName>),
146 CrlDistributionPoints(Vec<DistributionPoint>),
148 FreshestCrl(Vec<DistributionPoint>),
150 CertificatePolicies(Vec<crate::CertificatePolicy>),
152 NameConstraints(crate::NameConstraints),
154 PolicyConstraints(crate::PolicyConstraints),
156 PolicyMappings(Vec<crate::PolicyMapping>),
158 InhibitAnyPolicy(u32),
160 PrivateKeyUsagePeriod(crate::PrivateKeyUsagePeriod),
162 SubjectDirectoryAttributes(Vec<crate::DirectoryAttribute>),
164 TlsFeatures(Vec<u16>),
167 OcspNoCheck,
169 CtPoison,
171 SignedCertificateTimestamps(Vec<crate::SctEntry>),
173 NetscapeCertificateType(crate::NetscapeCertificateType),
175 NetscapeComment(String),
177 FidoAaguid(String),
179 FidoTransports(crate::FidoTransports),
181 MicrosoftTemplateName(String),
183 MicrosoftCertificateTemplate(crate::CertificateTemplate),
185 Unsupported,
187 Malformed,
190}
191
192#[derive(Clone, Debug, PartialEq, Eq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize))]
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196#[non_exhaustive]
197pub struct ExtensionInfo {
198 pub oid: String,
200 pub critical: bool,
202 pub duplicate: bool,
204 pub value_der: Vec<u8>,
206 pub details: ExtensionDetails,
208}
209
210pub(crate) fn general_name(name: &backend::GeneralName<'_>, names: &OidNames) -> GeneralName {
211 use backend::GeneralName as B;
212 match name {
213 B::DNSName(s) => GeneralName::Dns((*s).into()),
214 B::RFC822Name(s) => GeneralName::Email((*s).into()),
215 B::URI(s) => GeneralName::Uri((*s).into()),
216 B::DirectoryName(n) => GeneralName::Directory(inspect_name(n, names).rdns),
217 B::RegisteredID(oid) => GeneralName::RegisteredId(oid.to_id_string()),
218 B::IPAddress(bytes) => match bytes.len() {
219 4 => GeneralName::Ip(
220 std::net::Ipv4Addr::from(<[u8; 4]>::try_from(*bytes).expect("length checked"))
221 .to_string(),
222 ),
223 16 => GeneralName::Ip(
224 std::net::Ipv6Addr::from(<[u8; 16]>::try_from(*bytes).expect("length checked"))
225 .to_string(),
226 ),
227 _ => GeneralName::Malformed(7),
228 },
229 B::OtherName(oid, value) => {
230 let oid = oid.to_id_string();
231 GeneralName::OtherName {
232 name: names.get(&oid).map(str::to_owned),
233 oid,
234 value_der_hex: hex::encode(value),
235 }
236 }
237 B::X400Address(any) => GeneralName::X400Address {
238 constructed: any.header.is_constructed(),
239 content_hex: hex::encode(any.data),
240 },
241 B::EDIPartyName(any) => GeneralName::EdiPartyName {
242 constructed: any.header.is_constructed(),
243 content_hex: hex::encode(any.data),
244 },
245 B::Invalid(tag, _) => GeneralName::Malformed(tag.0),
246 }
247}
248
249#[cfg(test)]
250fn decode(oid: &str, bytes: &[u8]) -> ExtensionDetails {
251 decode_with_names(oid, bytes, &OidNames::default())
252}
253
254pub(crate) fn decode_with_names(oid: &str, bytes: &[u8], names: &OidNames) -> ExtensionDetails {
255 decode_supported(oid, bytes, names).unwrap_or(ExtensionDetails::Malformed)
256}
257
258fn decode_supported(oid: &str, bytes: &[u8], names: &OidNames) -> Option<ExtensionDetails> {
259 if let Some(value) = crate::extensions::device::decode(oid, bytes).ok()? {
260 return Some(value);
261 }
262 match oid {
263 "2.5.29.17" | "2.5.29.18" => {
264 let (rest, san) = backend::SubjectAlternativeName::from_der(bytes).ok()?;
265 if !rest.is_empty() || san.general_names.is_empty() {
266 return None;
267 }
268 let entries = san
269 .general_names
270 .iter()
271 .map(|n| general_name(n, names))
272 .collect();
273 Some(if oid == "2.5.29.17" {
274 ExtensionDetails::SubjectAlternativeName(entries)
275 } else {
276 ExtensionDetails::IssuerAlternativeName(entries)
277 })
278 }
279 "2.5.29.19" => {
280 let (rest, bc) = backend::BasicConstraints::from_der(bytes).ok()?;
281 if !rest.is_empty() {
282 return None;
283 }
284 Some(ExtensionDetails::BasicConstraints {
285 ca: bc.ca,
286 path_len_constraint: bc.path_len_constraint,
287 })
288 }
289 "2.5.29.15" => {
290 let (rest, bits) = BitString::from_der(bytes).ok()?;
293 if !rest.is_empty()
294 || bits.data.is_empty()
295 || bits.data.len() > 2
296 || bits.unused_bits > 7
297 || bits.data.last()? & ((1u8 << bits.unused_bits) - 1) != 0
298 || (bits.data.len() == 2 && (bits.data[1] & 0x7f != 0 || bits.unused_bits != 7))
299 {
300 return None;
301 }
302 let (_, k) = backend::KeyUsage::from_der(bytes).ok()?;
303 Some(ExtensionDetails::KeyUsage(KeyUsage {
304 digital_signature: k.digital_signature(),
305 content_commitment: k.non_repudiation(),
306 key_encipherment: k.key_encipherment(),
307 data_encipherment: k.data_encipherment(),
308 key_agreement: k.key_agreement(),
309 key_cert_sign: k.key_cert_sign(),
310 crl_sign: k.crl_sign(),
311 encipher_only: k.encipher_only(),
312 decipher_only: k.decipher_only(),
313 }))
314 }
315 "2.5.29.37" => {
316 let (rest, oids) = Vec::<Oid<'_>>::from_der(bytes).ok()?;
319 if !rest.is_empty() || oids.is_empty() {
320 return None;
321 }
322 Some(ExtensionDetails::ExtendedKeyUsage(
323 oids.iter()
324 .map(|oid| {
325 let oid = oid.to_id_string();
326 let name = names.get(&oid).map(str::to_owned);
327 KeyPurpose { oid, name }
328 })
329 .collect(),
330 ))
331 }
332 "2.5.29.14" => {
333 let (rest, key) = backend::KeyIdentifier::from_der(bytes).ok()?;
334 if !rest.is_empty() {
335 return None;
336 }
337 Some(ExtensionDetails::SubjectKeyIdentifier(hex::encode(key.0)))
338 }
339 "2.5.29.35" => {
340 x509_cert::ext::pkix::AuthorityKeyIdentifier::from_der(bytes).ok()?;
343 let (rest, aki) = backend::AuthorityKeyIdentifier::from_der(bytes).ok()?;
344 if !rest.is_empty() {
345 return None;
346 }
347 Some(ExtensionDetails::AuthorityKeyIdentifier(
348 AuthorityKeyIdentifier {
349 key_identifier_hex: aki.key_identifier.as_ref().map(|k| hex::encode(k.0)),
350 authority_cert_issuer: aki
351 .authority_cert_issuer
352 .as_ref()
353 .map(|list| list.iter().map(|n| general_name(n, names)).collect()),
354 authority_cert_serial_hex: aki.authority_cert_serial.map(hex::encode),
355 },
356 ))
357 }
358 "1.3.6.1.5.5.7.1.1" => {
359 x509_cert::ext::pkix::AuthorityInfoAccessSyntax::from_der(bytes).ok()?;
360 let (rest, aia) = backend::AuthorityInfoAccess::from_der(bytes).ok()?;
361 if !rest.is_empty() || aia.accessdescs.is_empty() {
362 return None;
363 }
364 Some(ExtensionDetails::AuthorityInfoAccess(
365 crate::extensions::locations::access(&aia.accessdescs, names),
366 ))
367 }
368 "1.3.6.1.5.5.7.1.11" => {
369 x509_cert::ext::pkix::SubjectInfoAccessSyntax::from_der(bytes).ok()?;
370 let (rest, sia) = backend::SubjectInfoAccess::from_der(bytes).ok()?;
371 if !rest.is_empty() || sia.accessdescs.is_empty() {
372 return None;
373 }
374 Some(ExtensionDetails::SubjectInfoAccess(
375 crate::extensions::locations::access(&sia.accessdescs, names),
376 ))
377 }
378 "2.5.29.31" | "2.5.29.46" => {
379 let points = crate::extensions::locations::distribution(bytes, names)?;
380 Some(if oid == "2.5.29.31" {
381 ExtensionDetails::CrlDistributionPoints(points)
382 } else {
383 ExtensionDetails::FreshestCrl(points)
384 })
385 }
386 "2.5.29.32" => Some(ExtensionDetails::CertificatePolicies(
387 crate::extensions::policies::decode(bytes, names)?,
388 )),
389 _ => crate::extensions::additional::decode(oid, bytes, names),
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn known_extensions_reject_trailing_truncated_and_wrong_inner_types() {
399 let examples: &[(&str, &[u8])] = &[
400 ("2.5.29.19", &[0x30, 0]),
401 ("2.5.29.15", &[3, 2, 7, 0x80]),
402 ("2.5.29.37", &[0x30, 5, 6, 3, 42, 3, 5]),
403 ("2.5.29.17", &[0x30, 3, 0x82, 1, b'a']),
404 ("2.5.29.18", &[0x30, 3, 0x82, 1, b'a']),
405 ("2.5.29.14", &[4, 1, 42]),
406 ("2.5.29.32", &[0x30, 5, 0x30, 3, 6, 1, 42]),
407 ("2.5.29.35", &[0x30, 3, 0x80, 1, 42]),
408 (
409 "1.3.6.1.5.5.7.1.1",
410 &[
411 0x30, 15, 0x30, 13, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 0x86, 1, b'a',
412 ],
413 ),
414 (
415 "1.3.6.1.5.5.7.1.11",
416 &[
417 0x30, 15, 0x30, 13, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 0x86, 1, b'a',
418 ],
419 ),
420 (
421 "2.5.29.31",
422 &[0x30, 9, 0x30, 7, 0xa0, 5, 0xa0, 3, 0x86, 1, b'a'],
423 ),
424 (
425 "2.5.29.46",
426 &[0x30, 9, 0x30, 7, 0xa0, 5, 0xa0, 3, 0x86, 1, b'a'],
427 ),
428 ];
429 for (oid, bytes) in examples {
430 assert_ne!(decode(oid, bytes), ExtensionDetails::Malformed);
431 for n in 0..bytes.len() {
432 assert_eq!(
433 decode(oid, &bytes[..n]),
434 ExtensionDetails::Malformed,
435 "{oid} prefix {n}"
436 );
437 }
438 assert_eq!(
439 decode(oid, &[*bytes, &[0]].concat()),
440 ExtensionDetails::Malformed
441 );
442 assert_eq!(decode(oid, &[5, 0]), ExtensionDetails::Malformed);
443 }
444 assert_eq!(decode("1.2.3.4", &[0xff]), ExtensionDetails::Unsupported);
445 }
446
447 #[test]
448 fn optional_fields_and_nested_sequences_do_not_hide_extra_objects() {
449 let cases: &[(&str, &[u8])] = &[
450 ("2.5.29.35", &[0x30, 2, 5, 0]),
451 ("2.5.29.31", &[0x30, 4, 0x30, 2, 5, 0]),
452 (
453 "1.3.6.1.5.5.7.1.1",
454 &[
455 0x30, 17, 0x30, 15, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 0x86, 1, b'a', 5, 0,
456 ],
457 ),
458 (
459 "1.3.6.1.5.5.7.1.11",
460 &[
461 0x30, 17, 0x30, 15, 6, 8, 43, 6, 1, 5, 5, 7, 48, 1, 0x86, 1, b'a', 5, 0,
462 ],
463 ),
464 ];
465 for (oid, bytes) in cases {
466 assert_eq!(decode(oid, bytes), ExtensionDetails::Malformed, "{oid}");
467 }
468 }
469
470 #[test]
471 fn key_usage_and_general_names_do_not_silently_drop_invalid_data() {
472 for invalid in [
473 &[3, 1, 0][..],
474 &[3, 2, 8, 0x80],
475 &[3, 2, 7, 0x81],
476 &[3, 3, 6, 0, 0x40],
477 &[3, 4, 0, 0, 0, 1],
478 ] {
479 assert_eq!(decode("2.5.29.15", invalid), ExtensionDetails::Malformed);
480 }
481 let ExtensionDetails::KeyUsage(ku) = decode("2.5.29.15", &[3, 3, 7, 0x08, 0x80]) else {
482 panic!("expected KU")
483 };
484 assert!(ku.key_agreement && ku.decipher_only);
485 let ExtensionDetails::SubjectAlternativeName(names) =
486 decode("2.5.29.17", &[0x30, 5, 0x87, 3, 1, 2, 3])
487 else {
488 panic!("expected SAN")
489 };
490 assert_eq!(names, [GeneralName::Malformed(7)]);
491 let ExtensionDetails::SubjectAlternativeName(names) =
492 decode("2.5.29.17", &[0x30, 2, 0xa3, 0])
493 else {
494 panic!("expected SAN")
495 };
496 assert_eq!(
497 names,
498 [GeneralName::X400Address {
499 constructed: true,
500 content_hex: String::new()
501 }]
502 );
503 }
504
505 #[test]
506 fn opaque_names_preserve_backend_data_and_caller_labels() {
507 let entries = {
508 let bytes = vec![
511 0x30, 25, 0xa0, 10, 6, 3, 42, 3, 4, 0xa0, 3, 12, 1, b'a', 0xa3, 2, 0x30, 0, 0xa5,
512 0, 0xa0, 5, 6, 3, 42, 3, 4,
513 ];
514 let mut names = OidNames::default();
515 names.insert("1.2.3.4", "Private identity").unwrap();
516 let ExtensionDetails::SubjectAlternativeName(entries) =
517 decode_with_names("2.5.29.17", &bytes, &names)
518 else {
519 panic!("expected SAN")
520 };
521 entries
522 }; assert_eq!(
524 entries,
525 vec![
526 GeneralName::OtherName {
527 oid: "1.2.3.4".into(),
528 name: Some("Private identity".into()),
529 value_der_hex: "a0030c0161".into(),
530 },
531 GeneralName::X400Address {
532 constructed: true,
533 content_hex: "3000".into()
534 },
535 GeneralName::EdiPartyName {
536 constructed: true,
537 content_hex: String::new()
538 },
539 GeneralName::OtherName {
540 oid: "1.2.3.4".into(),
541 name: Some("Private identity".into()),
542 value_der_hex: String::new(),
543 },
544 ]
545 );
546 #[cfg(feature = "serde")]
547 {
548 let json = serde_json::to_value(&entries).unwrap();
549 assert_eq!(
550 json,
551 serde_json::json!([
552 {"kind":"other_name","value":{"oid":"1.2.3.4","name":"Private identity","value_der_hex":"a0030c0161"}},
553 {"kind":"x400_address","value":{"constructed":true,"content_hex":"3000"}},
554 {"kind":"edi_party_name","value":{"constructed":true,"content_hex":""}},
555 {"kind":"other_name","value":{"oid":"1.2.3.4","name":"Private identity","value_der_hex":""}}
556 ])
557 );
558 let mut cbor = Vec::new();
559 ciborium::into_writer(&entries, &mut cbor).unwrap();
560 let decoded: serde_json::Value = ciborium::from_reader(cbor.as_slice()).unwrap();
561 assert_eq!(decoded, json);
562 }
563 }
564
565 #[test]
566 fn eku_retains_repeated_unknown_purposes_in_order() {
567 let ExtensionDetails::ExtendedKeyUsage(purposes) =
568 decode("2.5.29.37", &[0x30, 10, 6, 3, 42, 3, 5, 6, 3, 42, 3, 5])
569 else {
570 panic!("expected EKU")
571 };
572 assert_eq!(purposes.len(), 2);
573 assert_eq!(purposes[0], purposes[1]);
574 assert_eq!(purposes[0].name, None);
575 }
576}