Skip to main content

structured_pkix/crl/
scope.rs

1//! CRL issuing-distribution-point grammar shares certificate DP name parsing.
2
3use super::{RevocationScheme, RevocationTarget, TargetKind};
4use crate::access::{self, DistributionPoint, PointName};
5use crate::error::{Error, ErrorCode, Located, Result};
6use crate::extension::{self, Extension};
7use crate::general_name::GeneralName;
8use crate::name::{self, Name, RelativeName};
9use crate::{Version, oid};
10use structured_asn1::BitString;
11use structured_asn1::memory::Work;
12
13/// The eight revocation reasons in RFC 5280 §6.3.2, excluding unspecified
14/// and removeFromCRL. `ReasonFlags` unused(0) never contributes coverage.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Reasons(u16);
17
18impl Reasons {
19    /// Every reason required by the standard CRL-processing algorithm.
20    pub const ALL: Self = Self(0x1fe);
21    /// No authenticated reason coverage.
22    pub const NONE: Self = Self(0);
23    /// Union of already established coverage facts.
24    #[must_use]
25    pub const fn union(self, other: Self) -> Self {
26        Self(self.0 | other.0)
27    }
28    /// Whether the coverage includes all eight reasons.
29    #[must_use]
30    pub const fn is_complete(self) -> bool {
31        self.0 == Self::ALL.0
32    }
33    /// Whether coverage is empty.
34    #[must_use]
35    pub const fn is_empty(self) -> bool {
36        self.0 == 0
37    }
38    /// Original `ReasonFlags` positions (1 through 8), without unused(0).
39    #[must_use]
40    pub const fn bits(self) -> u16 {
41        self.0
42    }
43}
44
45/// Geometric issuer/name/category scope mismatch, not a signature verdict.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ScopeFailure {
48    /// The AC explicitly uses the `NoRevAvail` scheme, not CRL absence.
49    NoRevAvail,
50    /// Candidate CRL does not name the applicable issuer.
51    Issuer,
52    /// A DP naming a separate CRL issuer requires an indirect CRL.
53    IndirectRequired,
54    /// Expanded distribution-point names do not match.
55    DistributionName,
56    /// Category does not cover this target's certificate schema.
57    CertificateKind,
58    /// A user-only/CA-only IDP cannot cover a v1/v2 certificate.
59    CertificateVersion,
60    /// The reason intersection covers none of the legal reasons.
61    NoReasons,
62}
63
64/// Applicable scope says nothing about freshness or unknown critical demands.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ScopeOutcome {
67    /// Reason coverage for which this CRL's issuer and DP scope match.
68    Applicable(Reasons),
69    /// This source does not apply under the attempted distribution scope.
70    NotApplicable(ScopeFailure),
71}
72
73impl super::Authenticated<'_, '_> {
74    /// Derive applicability from the target's own signed DP extension. Relative
75    /// names are compared as borrowed composed RDN views, never encoded again.
76    /// If no explicit DP applies, also consider the RFC 5280 §6.3.3 issuer-name
77    /// fallback over supplied CRLs; this performs no fetching or discovery.
78    pub fn scope<'t, T: RevocationTarget<'t>, W: Work>(
79        &self,
80        target: &T,
81        execution: &W,
82    ) -> Result<ScopeOutcome> {
83        const FIELD: &str = "certificateList.scope";
84        let crl = self.crl();
85        let execution = &crl.tbs().signature().query_in(execution);
86        let target_execution = target.execution_in(execution);
87        target_execution
88            .charge_work(1)
89            .map_err(|e| Error::resource(e, FIELD, target.span().start))?;
90        if target.revocation_scheme() == RevocationScheme::NoRevAvail {
91            return Ok(ScopeOutcome::NotApplicable(ScopeFailure::NoRevAvail));
92        }
93        execution
94            .charge_work(1)
95            .map_err(|reason| Error::resource(reason, FIELD, crl.span().start))?;
96        let idp = crl
97            .tbs()
98            .extensions()
99            .get(oid::ISSUING_DISTRIBUTION_POINT, execution)?
100            .map(|extension| IssuingDistributionPoint::read(extension, execution))
101            .transpose()?;
102        if let Some(idp) = &idp {
103            match idp.kind {
104                CertificateKind::Attribute => {
105                    if target.kind() != TargetKind::Attribute {
106                        return Ok(ScopeOutcome::NotApplicable(ScopeFailure::CertificateKind));
107                    }
108                }
109                CertificateKind::EndEntity | CertificateKind::Authority => {
110                    // RFC5280 §5.2.5 excludes v1/v2 for BOTH user-only and
111                    // CA-only CRLs. The cA tests in §6.3.3(b)(2)(ii–iii) are
112                    // additional conditions, not an exception to that scope.
113                    // <https://www.rfc-editor.org/rfc/rfc5280#section-5.2.5>
114                    let TargetKind::PublicKey(version) = target.kind() else {
115                        return Ok(ScopeOutcome::NotApplicable(ScopeFailure::CertificateKind));
116                    };
117                    if version != Version::V3 {
118                        return Ok(ScopeOutcome::NotApplicable(
119                            ScopeFailure::CertificateVersion,
120                        ));
121                    }
122                    let ca = target
123                        .extensions()
124                        .map(|ext| ext.basic_constraints(&target_execution))
125                        .transpose()?
126                        .flatten()
127                        .is_some_and(crate::constraints::BasicConstraints::is_authority);
128                    if ca != (idp.kind == CertificateKind::Authority) {
129                        return Ok(ScopeOutcome::NotApplicable(ScopeFailure::CertificateKind));
130                    }
131                }
132                CertificateKind::All => (),
133            }
134        }
135        if let Some(outcome) = specified_scope(crl.tbs().issuer(), idp.as_ref(), target, execution)?
136        {
137            return Ok(outcome);
138        }
139        // RFC 5280 §6.3.3 final paragraph: synthetic DP names comprise the
140        // target issuer DN and issuerAltName, with no cRLIssuer or reasons.
141        // https://www.rfc-editor.org/rfc/rfc5280#section-6.3.3
142        if !target
143            .issuer()
144            .matches(crl.tbs().issuer(), &target_execution)?
145        {
146            return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
147        }
148        if let Some(idp) = &idp
149            && idp.name.is_some()
150        {
151            let mut matched = point_name_matches_directory(
152                idp,
153                crl.tbs().issuer(),
154                target.issuer(),
155                &target_execution,
156            )?;
157            if !matched
158                && matches!(target.kind(), TargetKind::PublicKey(_))
159                && let Some(names) = target
160                    .extensions()
161                    .map(|ext| ext.issuer_alt_name(&target_execution))
162                    .transpose()?
163                    .flatten()
164            {
165                matched = point_name_matches_names::<false>(
166                    idp,
167                    crl.tbs().issuer(),
168                    &names,
169                    &target_execution,
170                )?;
171            }
172            if !matched {
173                return Ok(ScopeOutcome::NotApplicable(ScopeFailure::DistributionName));
174            }
175        }
176        Ok(ScopeOutcome::Applicable(reason_mask(
177            idp.as_ref().and_then(|idp| idp.reasons),
178            execution,
179            crl.span().start,
180        )?))
181    }
182}
183
184fn specified_scope<'t, T: RevocationTarget<'t>, W: Work>(
185    issuer: &Name<'_>,
186    idp: Option<&IssuingDistributionPoint<'_>>,
187    target: &T,
188    execution: &W,
189) -> Result<Option<ScopeOutcome>> {
190    let mut coverage = Reasons::NONE;
191    let mut failure = None;
192    let target_execution = target.execution_in(execution);
193    if let Some(points) = target
194        .extensions()
195        .map(|ext| ext.crl_distribution_points(&target_execution))
196        .transpose()?
197        .flatten()
198    {
199        for point in points.points() {
200            target_execution.charge_work(1).map_err(|reason| {
201                Error::resource(reason, "certificateList.scope", target.span().start)
202            })?;
203            match point_scope(issuer, idp, target, point, &target_execution)? {
204                ScopeOutcome::Applicable(reasons) => coverage = coverage.union(reasons),
205                ScopeOutcome::NotApplicable(ScopeFailure::NoReasons) => {
206                    failure = Some(ScopeFailure::NoReasons);
207                }
208                ScopeOutcome::NotApplicable(ScopeFailure::DistributionName)
209                    if failure.is_none()
210                        && idp.is_some_and(|idp| idp.name.is_some())
211                        && (!point.full_name().is_empty()
212                            || point.name_relative_to_issuer().is_some()) =>
213                {
214                    // Both fields are present: §5.2.5 requires an identical
215                    // selected name. The §6.3.3 synthetic fallback cannot
216                    // discard this explicit field or its reason restriction.
217                    failure = Some(ScopeFailure::DistributionName);
218                }
219                ScopeOutcome::NotApplicable(_) => (),
220            }
221        }
222    }
223    if !coverage.is_empty() {
224        return Ok(Some(ScopeOutcome::Applicable(coverage)));
225    }
226    Ok(failure.map(ScopeOutcome::NotApplicable))
227}
228
229fn reason_mask(flags: Option<BitString<'_>>, execution: &impl Work, at: u64) -> Result<Reasons> {
230    execution
231        .charge_work(9)
232        .map_err(|reason| Error::resource(reason, "certificateList.scope", at))?;
233    let Some(flags) = flags else {
234        return Ok(Reasons::ALL);
235    };
236    let mut mask = 0;
237    for bit in 1..=8 {
238        if flags.bit(bit) {
239            mask |= 1 << bit;
240        }
241    }
242    Ok(Reasons(mask))
243}
244
245fn point_scope<'t, T: RevocationTarget<'t>>(
246    issuer: &Name<'_>,
247    idp: Option<&IssuingDistributionPoint<'_>>,
248    target: &T,
249    point: &DistributionPoint<'_>,
250    execution: &impl Work,
251) -> Result<ScopeOutcome> {
252    if !point.issuer().is_empty() {
253        if !idp.is_some_and(IssuingDistributionPoint::is_indirect) {
254            return Ok(ScopeOutcome::NotApplicable(ScopeFailure::IndirectRequired));
255        }
256        let mut matches = false;
257        for name in point.issuer() {
258            execution.charge_work(1).map_err(|reason| {
259                Error::resource(reason, "certificateList.scope", target.span().start)
260            })?;
261            if let GeneralName::Directory(name) = name
262                && issuer_encoding_matches(issuer, name, execution)?
263            {
264                matches = true;
265                break;
266            }
267        }
268        if !matches {
269            return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
270        }
271    } else if !issuer.matches(target.issuer(), execution)? {
272        return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
273    }
274    if let Some(idp) = idp
275        && idp.name.is_some()
276    {
277        let matched = if let Some(relative) = point.name_relative_to_issuer() {
278            let base = if point.issuer().is_empty() {
279                target.issuer()
280            } else {
281                match &point.issuer()[0] {
282                    GeneralName::Directory(name) => name,
283                    _ => unreachable!("DP reader requires directory issuers"),
284                }
285            };
286            point_name_matches_composed::<true>(idp, issuer, base, Some(relative), execution)?
287        } else if !point.full_name().is_empty() {
288            // RFC5280 5.2.5 requires identical selected name encoding when
289            // both DP fields are present; semantic name equality is used only
290            // for issuer-only/synthetic fields in 6.3.3(b)(2)(i).
291            // <https://www.rfc-editor.org/rfc/rfc5280#section-5.2.5>
292            point_name_matches_names::<true>(idp, issuer, point.full_name(), execution)?
293        } else {
294            point_name_matches_names::<false>(idp, issuer, point.issuer(), execution)?
295        };
296        if !matched {
297            return Ok(ScopeOutcome::NotApplicable(ScopeFailure::DistributionName));
298        }
299    }
300    let left = reason_mask(
301        idp.and_then(|idp| idp.reasons),
302        execution,
303        target.span().start,
304    )?;
305    let right = reason_mask(point.reasons(), execution, target.span().start)?;
306    let reasons = Reasons(left.0 & right.0);
307    if reasons.is_empty() {
308        return Ok(ScopeOutcome::NotApplicable(ScopeFailure::NoReasons));
309    }
310    Ok(ScopeOutcome::Applicable(reasons))
311}
312
313fn issuer_encoding_matches(
314    issuer: &Name<'_>,
315    named: &Name<'_>,
316    execution: &impl Work,
317) -> Result<bool> {
318    // RFC5280 §4.2.1.13 requires exactly the same encoding for explicit
319    // cRLIssuer; ordinary DN equality remains applicable to the default issuer.
320    // <https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13>
321    let left = issuer.as_bytes();
322    let right = named.as_bytes();
323    let work = u64::try_from(left.len().max(right.len()))
324        .ok()
325        .and_then(|n| n.checked_mul(2))
326        .and_then(|n| n.checked_add(1))
327        .ok_or_else(|| named.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
328    execution
329        .charge_work(work)
330        .map_err(|reason| named.resource(reason))?;
331    Ok(left == right)
332}
333
334fn point_name_matches_directory(
335    idp: &IssuingDistributionPoint<'_>,
336    issuer: &Name<'_>,
337    directory: &Name<'_>,
338    execution: &impl Work,
339) -> Result<bool> {
340    point_name_matches_composed::<false>(idp, issuer, directory, None, execution)
341}
342
343fn point_name_matches_composed<const ENCODED: bool>(
344    idp: &IssuingDistributionPoint<'_>,
345    issuer: &Name<'_>,
346    base: &Name<'_>,
347    relative: Option<&RelativeName<'_>>,
348    execution: &impl Work,
349) -> Result<bool> {
350    match &idp.name {
351        Some(PointName::Relative(suffix)) => {
352            if ENCODED && relative.is_none() {
353                return Ok(false);
354            }
355            name::matches_composed::<ENCODED>(issuer, Some(suffix), base, relative, execution)
356        }
357        Some(PointName::Full(names)) => {
358            if ENCODED && relative.is_some() {
359                return Ok(false);
360            }
361            for name in names {
362                execution
363                    .charge_work(1)
364                    .map_err(|reason| base.resource(reason))?;
365                if let GeneralName::Directory(directory) = name
366                    && name::matches_composed::<ENCODED>(
367                        directory, None, base, relative, execution,
368                    )?
369                {
370                    return Ok(true);
371                }
372            }
373            Ok(false)
374        }
375        None => Ok(true),
376    }
377}
378
379fn point_name_matches_names<const ENCODED: bool>(
380    idp: &IssuingDistributionPoint<'_>,
381    issuer: &Name<'_>,
382    names: &[GeneralName<'_>],
383    execution: &impl Work,
384) -> Result<bool> {
385    for right in names {
386        execution
387            .charge_work(1)
388            .map_err(|reason| issuer.resource(reason))?;
389        match &idp.name {
390            Some(PointName::Relative(relative)) => {
391                if ENCODED {
392                    return Ok(false);
393                }
394                if let GeneralName::Directory(directory) = right
395                    && name::matches_composed::<ENCODED>(
396                        issuer,
397                        Some(relative),
398                        directory,
399                        None,
400                        execution,
401                    )?
402                {
403                    return Ok(true);
404                }
405            }
406            Some(PointName::Full(left)) => {
407                for left in left {
408                    execution
409                        .charge_work(1)
410                        .map_err(|reason| issuer.resource(reason))?;
411                    if names_match::<ENCODED>(left, right, issuer, execution)? {
412                        return Ok(true);
413                    }
414                }
415            }
416            None => return Ok(true),
417        }
418    }
419    Ok(false)
420}
421
422fn names_match<const ENCODED: bool>(
423    left: &GeneralName<'_>,
424    right: &GeneralName<'_>,
425    source: &Name<'_>,
426    execution: &impl Work,
427) -> Result<bool> {
428    if !ENCODED {
429        return crate::general_name::matches_at(
430            left,
431            right,
432            execution,
433            source.offset(),
434            source.field(),
435        );
436    }
437    let (left, right) = match (left, right) {
438        (GeneralName::Directory(left), GeneralName::Directory(right)) => {
439            return name::matches_composed::<ENCODED>(left, None, right, None, execution);
440        }
441        (GeneralName::Uri(left), GeneralName::Uri(right))
442        | (GeneralName::Dns(left), GeneralName::Dns(right))
443        | (GeneralName::Email(left), GeneralName::Email(right)) => {
444            (left.as_bytes(), right.as_bytes())
445        }
446        (GeneralName::X400(left), GeneralName::X400(right))
447        | (GeneralName::EdiParty(left), GeneralName::EdiParty(right))
448        | (GeneralName::IpAddress(left), GeneralName::IpAddress(right)) => (*left, *right),
449        (GeneralName::RegisteredId(left), GeneralName::RegisteredId(right)) => {
450            (left.as_bytes(), right.as_bytes())
451        }
452        (
453            GeneralName::Other {
454                type_id: left_kind,
455                value: left,
456            },
457            GeneralName::Other {
458                type_id: right_kind,
459                value: right,
460            },
461        ) => {
462            let work = (left_kind.as_bytes().len().max(right_kind.as_bytes().len()) as u64)
463                .checked_mul(2)
464                .ok_or_else(|| {
465                    source.resource(structured_asn1::memory::MemoryError::WorkExceeded)
466                })?;
467            execution
468                .charge_work(work)
469                .map_err(|reason| source.resource(reason))?;
470            if left_kind != right_kind {
471                return Ok(false);
472            }
473            (*left, *right)
474        }
475        _ => return Ok(false),
476    };
477    let work = (left.len().max(right.len()) as u64)
478        .checked_mul(2)
479        .and_then(|n| n.checked_add(1))
480        .ok_or_else(|| source.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
481    execution
482        .charge_work(work)
483        .map_err(|reason| source.resource(reason))?;
484    Ok(left == right)
485}
486
487/// Which certificate category an issuing distribution point covers.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum CertificateKind {
490    /// No category restriction was encoded.
491    All,
492    /// End entity public-key certificates only.
493    EndEntity,
494    /// Certification authority certificates only.
495    Authority,
496    /// Attribute certificates only, preserved separately from public-key scope.
497    Attribute,
498}
499
500/// Parsed CRL scope, without claiming applicability to a target certificate.
501#[derive(Debug)]
502pub struct IssuingDistributionPoint<'a> {
503    name: Option<PointName<'a>>,
504    kind: CertificateKind,
505    reasons: Option<BitString<'a>>,
506    indirect: bool,
507}
508
509impl<'a> IssuingDistributionPoint<'a> {
510    /// Full distribution-point names, if that alternative was written.
511    #[must_use]
512    pub fn full_name(&self) -> &[GeneralName<'a>] {
513        match &self.name {
514            Some(PointName::Full(names)) => names,
515            _ => &[],
516        }
517    }
518
519    /// Relative RDN, if that alternative was written.
520    #[must_use]
521    pub fn name_relative_to_issuer(&self) -> Option<&RelativeName<'a>> {
522        match &self.name {
523            Some(PointName::Relative(name)) => Some(name),
524            _ => None,
525        }
526    }
527
528    /// Category restriction; it does not classify any supplied certificate.
529    #[must_use]
530    pub fn certificate_kind(&self) -> CertificateKind {
531        self.kind
532    }
533
534    /// Encoded `ReasonFlags`. Absent means all reasons, not an empty mask.
535    #[must_use]
536    pub fn reasons(&self) -> Option<BitString<'a>> {
537        self.reasons
538    }
539
540    /// Whether entries may belong to issuers other than this CRL's issuer.
541    #[must_use]
542    pub fn is_indirect(&self) -> bool {
543        self.indirect
544    }
545
546    /// Decode through the extension's original parser with current execution.
547    pub fn read<W: Work>(extension: &Extension<'a>, execution: &W) -> Result<Self> {
548        const FIELD: &str = "extensions.issuingDistributionPoint";
549        let fail = |code, at| Error::new(code, FIELD, at);
550        let mut reader = extension::sequence(extension, FIELD, execution)?;
551        let mut result = Self {
552            name: None,
553            kind: CertificateKind::All,
554            reasons: None,
555            indirect: false,
556        };
557        let mut last = None;
558        while let Some(element) = reader.next_element().field(FIELD)? {
559            let (number, constructed) = extension::context_of(element.tag())
560                .ok_or_else(|| fail(ErrorCode::UnexpectedField, element.span().start))?;
561            if last.is_some_and(|previous| number <= previous) {
562                return Err(fail(ErrorCode::UnexpectedField, element.span().start));
563            }
564            last = Some(number);
565            match (number, constructed) {
566                (0, true) => {
567                    result.name = Some(access::read_distribution_name(&reader, &element, FIELD)?);
568                }
569                (3, false) => {
570                    let reasons = reader.bit_string(&element).field(FIELD)?;
571                    reader.charge(9).field(FIELD)?;
572                    // RFC 5280 §5.2.5 references ReasonFlags of §4.2.1.13;
573                    // X.690 §11.2.2 trims a DER named bit list to its final one.
574                    // https://www.rfc-editor.org/rfc/rfc5280#section-5.2.5
575                    if reasons.len() > 9
576                        || !(1..=8).any(|bit| reasons.bit(bit))
577                        || !reasons.bit(reasons.len() - 1)
578                    {
579                        return Err(fail(ErrorCode::MalformedValue, element.span().start));
580                    }
581                    result.reasons = Some(reasons);
582                }
583                (1 | 2 | 4 | 5, false) => {
584                    // These BOOLEANs default to FALSE and DER omits defaults.
585                    if !reader.boolean(&element).field(FIELD)? {
586                        return Err(fail(ErrorCode::MalformedValue, element.span().start));
587                    }
588                    if number == 4 {
589                        result.indirect = true;
590                        continue;
591                    }
592                    if result.kind != CertificateKind::All {
593                        return Err(fail(ErrorCode::MalformedValue, element.span().start));
594                    }
595                    result.kind = match number {
596                        1 => CertificateKind::EndEntity,
597                        2 => CertificateKind::Authority,
598                        _ => CertificateKind::Attribute,
599                    };
600                }
601                _ => return Err(fail(ErrorCode::UnexpectedField, element.span().start)),
602            }
603        }
604        // RFC 5280 §5.2.5 disallows an entirely empty IDP. Attribute scope is
605        // kept as its actual category, never treated as public-key coverage.
606        if result.name.is_none()
607            && result.kind == CertificateKind::All
608            && result.reasons.is_none()
609            && !result.indirect
610        {
611            return Err(fail(ErrorCode::EmptySequence, extension.span().start));
612        }
613        Ok(result)
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use crate::general_name::GeneralName::{Dns, Email};
620    #[test]
621    fn dns_and_mailbox_scope_follow_their_separate_case_rules() {
622        // RFC5280 §§7.2/7.5 require DNS and mail host case-insensitive
623        // identity, while the mailbox local-part remains an exact match.
624        let bytes = crate::fixtures::CertificateParts::default().build();
625        let parser = structured_asn1::Parser::der(crate::fixtures::memory());
626        let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
627        let execution = crate::fixtures::memory();
628        for (left, right, expected) in [
629            (Dns("EXAMPLE.com"), Dns("example.COM"), true),
630            (Email("user@EXAMPLE.com"), Email("user@example.COM"), true),
631            (Email("User@example.com"), Email("user@example.com"), false),
632            (
633                Email("\"a@b\"@EXAMPLE.com"),
634                Email("\"a@b\"@example.com"),
635                true,
636            ),
637        ] {
638            assert_eq!(
639                super::names_match::<false>(&left, &right, source.tbs().issuer(), &execution)
640                    .unwrap(),
641                expected
642            );
643        }
644    }
645
646    #[test]
647    fn uri_scope_compares_normalized_identity_without_folding_path_or_query() {
648        // RFC5280 §7.4 requires scheme/host case, percent, path-segment
649        // and recognized scheme normalization before exact comparison.
650        let bytes = crate::fixtures::CertificateParts::default().build();
651        let parser = structured_asn1::Parser::der(crate::fixtures::memory());
652        let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
653        let execution = crate::fixtures::memory();
654        for (left, right, expected) in [
655            (
656                "HTTP://Example.com:080/a/../%7e",
657                "http://example.com/~",
658                true,
659            ),
660            ("https://example.com:443", "https://example.com/", true),
661            (
662                "ldap://example.com/",
663                "ldap://EXAMPLE.com:389/??BASE?(objectClass=*)",
664                true,
665            ),
666            ("ftp://example.com:21", "ftp://example.com/", true),
667            (
668                "http://example.com/A?q=A",
669                "http://example.com/a?q=A",
670                false,
671            ),
672            (
673                "http://example.com/a?q=A",
674                "http://example.com/a?q=a",
675                false,
676            ),
677            ("http://example.com/%2f", "http://example.com//", false),
678            ("http://example.com/?", "http://example.com/", false),
679            (
680                "ftp://example.com/a;type=a",
681                "ftp://example.com/a;type=i",
682                false,
683            ),
684        ] {
685            assert_eq!(
686                super::names_match::<false>(
687                    &crate::general_name::GeneralName::Uri(left),
688                    &crate::general_name::GeneralName::Uri(right),
689                    source.tbs().issuer(),
690                    &execution
691                )
692                .unwrap(),
693                expected,
694                "{left} versus {right}"
695            );
696        }
697    }
698}