1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Reasons(u16);
17
18impl Reasons {
19 pub const ALL: Self = Self(0x1fe);
21 pub const NONE: Self = Self(0);
23 #[must_use]
25 pub const fn union(self, other: Self) -> Self {
26 Self(self.0 | other.0)
27 }
28 #[must_use]
30 pub const fn is_complete(self) -> bool {
31 self.0 == Self::ALL.0
32 }
33 #[must_use]
35 pub const fn is_empty(self) -> bool {
36 self.0 == 0
37 }
38 #[must_use]
40 pub const fn bits(self) -> u16 {
41 self.0
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ScopeFailure {
48 NoRevAvail,
50 Issuer,
52 IndirectRequired,
54 DistributionName,
56 CertificateKind,
58 CertificateVersion,
60 NoReasons,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ScopeOutcome {
67 Applicable(Reasons),
69 NotApplicable(ScopeFailure),
71}
72
73impl super::Authenticated<'_, '_> {
74 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum CertificateKind {
490 All,
492 EndEntity,
494 Authority,
496 Attribute,
498}
499
500#[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 #[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 #[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 #[must_use]
530 pub fn certificate_kind(&self) -> CertificateKind {
531 self.kind
532 }
533
534 #[must_use]
536 pub fn reasons(&self) -> Option<BitString<'a>> {
537 self.reasons
538 }
539
540 #[must_use]
542 pub fn is_indirect(&self) -> bool {
543 self.indirect
544 }
545
546 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 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 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 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 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 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}