Skip to main content

x509_info/extensions/
mod.rs

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/// An owned GeneralName. Values are decoded, not validated as identities or URLs.
19#[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 text as encoded; no wildcard or hostname matching is performed.
29    Dns(String),
30    /// RFC822 mailbox text; no mailbox validation is performed.
31    Email(String),
32    /// URI text; no resource is fetched.
33    Uri(String),
34    /// IPv4 or IPv6 in standard textual notation.
35    Ip(String),
36    /// Directory name with ordered RDN groups.
37    Directory(Vec<Vec<NameAttribute>>),
38    /// Registered identifier in dotted-decimal notation.
39    RegisteredId(String),
40    /// OtherName type identifier and opaque value; no OID-specific decoder runs.
41    OtherName {
42        /// Dotted-decimal type-id OID.
43        oid: String,
44        /// Presentation label from the caller's registry, when available.
45        name: Option<String>,
46        /// Lowercase hex of bytes following type-id, including the expected `[0]`
47        /// explicit value wrapper. The backend does not validate that wrapper
48        /// or the inner value; these bytes may be empty or malformed.
49        value_der_hex: String,
50    },
51    /// Opaque X.400 address. The backend does not decode the ORAddress fields.
52    X400Address {
53        /// Constructed bit of the context-specific `[3]` header as encoded.
54        constructed: bool,
55        /// Lowercase hex of content octets, excluding the outer tag and length.
56        content_hex: String,
57    },
58    /// Opaque EDI party name. The backend does not decode its inner fields.
59    EdiPartyName {
60        /// Constructed bit of the context-specific `[5]` header as encoded.
61        constructed: bool,
62        /// Lowercase hex of content octets, excluding the outer tag and length.
63        content_hex: String,
64    },
65    /// Reserved unsupported choice, identified by its context-specific tag.
66    /// Retained for compatibility; current backend choices have dedicated variants.
67    /// Its bytes remain in the containing extension's value_der.
68    Unsupported(u32),
69    /// Malformed GeneralName choice, including invalid IP lengths or text encodings.
70    /// Its bytes remain in the containing extension's value_der.
71    Malformed(u32),
72}
73
74/// Decoded Key Usage bits. These describe assertions, not enforced permissions.
75#[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    /// Digital signatures other than certificate/CRL signing.
81    pub digital_signature: bool,
82    /// The nonRepudiation/contentCommitment bit.
83    pub content_commitment: bool,
84    /// Key transport.
85    pub key_encipherment: bool,
86    /// Direct data encryption.
87    pub data_encipherment: bool,
88    /// Key agreement.
89    pub key_agreement: bool,
90    /// Certificate signing.
91    pub key_cert_sign: bool,
92    /// CRL signing.
93    pub crl_sign: bool,
94    /// Encipher-only key agreement.
95    pub encipher_only: bool,
96    /// Decipher-only key agreement.
97    pub decipher_only: bool,
98}
99
100/// An Extended Key Usage purpose, preserving OIDs, order and repetitions.
101#[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    /// Dotted-decimal key-purpose OID.
107    pub oid: String,
108    /// Stable common label, or None for an unknown purpose.
109    pub name: Option<String>,
110}
111
112/// Application-facing interpretation of a certificate extension.
113/// Malformed/unsupported values and duplicate extensions never disappear silently.
114#[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    /// Subject Alternative Name entries in encoded order.
124    SubjectAlternativeName(Vec<GeneralName>),
125    /// Key Usage flags.
126    KeyUsage(KeyUsage),
127    /// Extended Key Usage purposes.
128    ExtendedKeyUsage(Vec<KeyPurpose>),
129    /// Basic Constraints; no trust or path policy is enforced.
130    BasicConstraints {
131        /// Encoded CA assertion (default false).
132        ca: bool,
133        /// Encoded path-length constraint, when present.
134        path_len_constraint: Option<u32>,
135    },
136    /// Subject Key Identifier octets in lowercase hex.
137    SubjectKeyIdentifier(String),
138    /// Authority Key Identifier fields; no issuer matching is performed.
139    AuthorityKeyIdentifier(AuthorityKeyIdentifier),
140    /// Authority Information Access entries, including OCSP/CA issuer locations.
141    AuthorityInfoAccess(Vec<AccessDescription>),
142    /// Subject Information Access entries, including CA repository locations.
143    SubjectInfoAccess(Vec<AccessDescription>),
144    /// Issuer Alternative Names, in encoded order.
145    IssuerAlternativeName(Vec<GeneralName>),
146    /// CRL Distribution Points; no CRL retrieval or revocation checks occur.
147    CrlDistributionPoints(Vec<DistributionPoint>),
148    /// Freshest CRL locations for delta CRLs; no CRL processing occurs.
149    FreshestCrl(Vec<DistributionPoint>),
150    /// Certificate policy OIDs and qualifiers; no policy evaluation is performed.
151    CertificatePolicies(Vec<crate::CertificatePolicy>),
152    /// Permitted/excluded name subtrees; no name matching or policy evaluation.
153    NameConstraints(crate::NameConstraints),
154    /// Explicit-policy and policy-mapping counters.
155    PolicyConstraints(crate::PolicyConstraints),
156    /// Issuer/subject policy pairs in encoded order, including duplicates.
157    PolicyMappings(Vec<crate::PolicyMapping>),
158    /// Number of additional certificates before anyPolicy is inhibited.
159    InhibitAnyPolicy(u32),
160    /// Private-key usage bounds, independent of certificate validity.
161    PrivateKeyUsagePeriod(crate::PrivateKeyUsagePeriod),
162    /// Multi-valued subject directory attributes with complete value encodings.
163    SubjectDirectoryAttributes(Vec<crate::DirectoryAttribute>),
164    /// RFC 7633 TLS feature numbers in encoded order, including unknowns/duplicates.
165    /// 5 is status_request and 17 is status_request_v2; no TLS policy is enforced.
166    TlsFeatures(Vec<u16>),
167    /// OCSP no-check marker; does not disable validation in this library or callers.
168    OcspNoCheck,
169    /// Certificate Transparency precertificate poison marker; not an acceptance verdict.
170    CtPoison,
171    /// Embedded SCT entries; signatures and log membership are not verified.
172    SignedCertificateTimestamps(Vec<crate::SctEntry>),
173    /// Legacy Netscape certificate-type flags.
174    NetscapeCertificateType(crate::NetscapeCertificateType),
175    /// Legacy Netscape IA5 comment text, not sanitized markup.
176    NetscapeComment(String),
177    /// FIDO AAGUID OCTET STRING in lowercase hex. No length/profile/device matching.
178    FidoAaguid(String),
179    /// FIDO U2F transport bits, including unknown set bits.
180    FidoTransports(crate::FidoTransports),
181    /// Microsoft legacy template name, decoded from BMPString.
182    MicrosoftTemplateName(String),
183    /// Microsoft certificate-template OID and optional version INTEGERs.
184    MicrosoftCertificateTemplate(crate::CertificateTemplate),
185    /// This library does not interpret this OID. Raw bytes remain available.
186    Unsupported,
187    /// A supported extension could not be decoded within this library's limits.
188    /// This is a finding, not a certificate-level parse failure.
189    Malformed,
190}
191
192/// An extension and its decoded information, retained in certificate order.
193#[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    /// Dotted-decimal extension OID, including unknown OIDs.
199    pub oid: String,
200    /// Encoded critical flag; this library does not enforce critical extensions.
201    pub critical: bool,
202    /// True on every occurrence of an OID that appears more than once.
203    pub duplicate: bool,
204    /// Inner extnValue octets, without the outer OCTET STRING wrapper.
205    pub value_der: Vec<u8>,
206    /// Decoded information or an explicit unsupported/malformed state.
207    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            // Bound the backend's u16 flag representation before conversion; reject
291            // bits beyond the nine named usages rather than silently truncating them.
292            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            // Decode the sequence directly using the backend ASN.1 types so order,
317            // duplicate purposes and unknown OIDs survive its boolean convenience model.
318            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            // The inspection backend accepts unconsumed optional-field content.
341            // Check the complete schema first, then retain its raw INTEGER octets.
342            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            // SAN: OtherName 1.2.3.4 with explicit UTF8 "a", opaque X.400,
509            // opaque EDI, and an OtherName whose value wrapper is absent.
510            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        }; // The input and caller registry no longer exist.
523        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}