1use crate::access::{self, DistributionPoint, PointName};
4use crate::error::{Error, ErrorCode, Located, Result};
5use crate::extension::{self, Extension};
6use crate::general_name::GeneralName;
7use crate::name::{self, Name, RelativeName};
8use crate::{Certificate, Version, oid};
9use structured_asn1::BitString;
10use structured_asn1::memory::Memory;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Reasons(u16);
16
17impl Reasons {
18 pub const ALL: Self = Self(0x1fe);
20 pub const NONE: Self = Self(0);
22 #[must_use]
24 pub const fn union(self, other: Self) -> Self {
25 Self(self.0 | other.0)
26 }
27 #[must_use]
29 pub const fn is_complete(self) -> bool {
30 self.0 == Self::ALL.0
31 }
32 #[must_use]
34 pub const fn is_empty(self) -> bool {
35 self.0 == 0
36 }
37 #[must_use]
39 pub const fn bits(self) -> u16 {
40 self.0
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ScopeFailure {
47 Issuer,
49 IndirectRequired,
51 DistributionName,
53 CertificateKind,
55 CertificateVersion,
57 NoReasons,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ScopeOutcome {
64 Applicable(Reasons),
66 NotApplicable(ScopeFailure),
68}
69
70impl super::Authenticated<'_, '_> {
71 pub fn scope(&self, target: &Certificate<'_>, execution: &Memory) -> Result<ScopeOutcome> {
76 const FIELD: &str = "certificateList.scope";
77 let crl = self.crl();
78 execution
79 .charge_work(1)
80 .map_err(|reason| Error::resource(reason, FIELD, crl.span().start))?;
81 let idp = crl
82 .tbs()
83 .extensions()
84 .get(oid::ISSUING_DISTRIBUTION_POINT, execution)?
85 .map(|extension| IssuingDistributionPoint::read(extension, execution))
86 .transpose()?;
87 if let Some(idp) = &idp {
88 match idp.kind {
89 CertificateKind::Attribute => {
90 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::CertificateKind));
91 }
92 CertificateKind::EndEntity | CertificateKind::Authority => {
93 if target.tbs().version() != Version::V3 {
98 return Ok(ScopeOutcome::NotApplicable(
99 ScopeFailure::CertificateVersion,
100 ));
101 }
102 let ca = target
103 .tbs()
104 .extensions()
105 .basic_constraints(execution)?
106 .is_some_and(crate::constraints::BasicConstraints::is_authority);
107 if ca != (idp.kind == CertificateKind::Authority) {
108 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::CertificateKind));
109 }
110 }
111 CertificateKind::All => (),
112 }
113 }
114 if let Some(outcome) = specified_scope(crl.tbs().issuer(), idp.as_ref(), target, execution)?
115 {
116 return Ok(outcome);
117 }
118 if !crl
122 .tbs()
123 .issuer()
124 .matches(target.tbs().issuer(), execution)?
125 {
126 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
127 }
128 if let Some(idp) = &idp
129 && idp.name.is_some()
130 {
131 let mut matched = point_name_matches_directory(
132 idp,
133 crl.tbs().issuer(),
134 target.tbs().issuer(),
135 execution,
136 )?;
137 if !matched && let Some(names) = target.tbs().extensions().issuer_alt_name(execution)? {
138 matched =
139 point_name_matches_names::<false>(idp, crl.tbs().issuer(), &names, execution)?;
140 }
141 if !matched {
142 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::DistributionName));
143 }
144 }
145 Ok(ScopeOutcome::Applicable(reason_mask(
146 idp.as_ref().and_then(|idp| idp.reasons),
147 execution,
148 crl.span().start,
149 )?))
150 }
151}
152
153fn specified_scope(
154 issuer: &Name<'_>,
155 idp: Option<&IssuingDistributionPoint<'_>>,
156 target: &Certificate<'_>,
157 execution: &Memory,
158) -> Result<Option<ScopeOutcome>> {
159 let mut coverage = Reasons::NONE;
160 let mut failure = None;
161 if let Some(points) = target
162 .tbs()
163 .extensions()
164 .crl_distribution_points(execution)?
165 {
166 for point in points.points() {
167 execution.charge_work(1).map_err(|reason| {
168 Error::resource(reason, "certificateList.scope", target.span().start)
169 })?;
170 match point_scope(issuer, idp, target, point, execution)? {
171 ScopeOutcome::Applicable(reasons) => coverage = coverage.union(reasons),
172 ScopeOutcome::NotApplicable(ScopeFailure::NoReasons) => {
173 failure = Some(ScopeFailure::NoReasons);
174 }
175 ScopeOutcome::NotApplicable(ScopeFailure::DistributionName)
176 if failure.is_none()
177 && idp.is_some_and(|idp| idp.name.is_some())
178 && (!point.full_name().is_empty()
179 || point.name_relative_to_issuer().is_some()) =>
180 {
181 failure = Some(ScopeFailure::DistributionName);
185 }
186 ScopeOutcome::NotApplicable(_) => (),
187 }
188 }
189 }
190 if !coverage.is_empty() {
191 return Ok(Some(ScopeOutcome::Applicable(coverage)));
192 }
193 Ok(failure.map(ScopeOutcome::NotApplicable))
194}
195
196fn reason_mask(flags: Option<BitString<'_>>, execution: &Memory, at: u64) -> Result<Reasons> {
197 execution
198 .charge_work(9)
199 .map_err(|reason| Error::resource(reason, "certificateList.scope", at))?;
200 let Some(flags) = flags else {
201 return Ok(Reasons::ALL);
202 };
203 let mut mask = 0;
204 for bit in 1..=8 {
205 if flags.bit(bit) {
206 mask |= 1 << bit;
207 }
208 }
209 Ok(Reasons(mask))
210}
211
212fn point_scope(
213 issuer: &Name<'_>,
214 idp: Option<&IssuingDistributionPoint<'_>>,
215 target: &Certificate<'_>,
216 point: &DistributionPoint<'_>,
217 execution: &Memory,
218) -> Result<ScopeOutcome> {
219 if !point.issuer().is_empty() {
220 if !idp.is_some_and(IssuingDistributionPoint::is_indirect) {
221 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::IndirectRequired));
222 }
223 let mut matches = false;
224 for name in point.issuer() {
225 execution.charge_work(1).map_err(|reason| {
226 Error::resource(reason, "certificateList.scope", target.span().start)
227 })?;
228 if let GeneralName::Directory(name) = name
229 && issuer_encoding_matches(issuer, name, execution)?
230 {
231 matches = true;
232 break;
233 }
234 }
235 if !matches {
236 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
237 }
238 } else if !issuer.matches(target.tbs().issuer(), execution)? {
239 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::Issuer));
240 }
241 if let Some(idp) = idp
242 && idp.name.is_some()
243 {
244 let matched = if let Some(relative) = point.name_relative_to_issuer() {
245 let base = if point.issuer().is_empty() {
246 target.tbs().issuer()
247 } else {
248 match &point.issuer()[0] {
249 GeneralName::Directory(name) => name,
250 _ => unreachable!("DP reader requires directory issuers"),
251 }
252 };
253 point_name_matches_composed::<true>(idp, issuer, base, Some(relative), execution)?
254 } else if !point.full_name().is_empty() {
255 point_name_matches_names::<true>(idp, issuer, point.full_name(), execution)?
260 } else {
261 point_name_matches_names::<false>(idp, issuer, point.issuer(), execution)?
262 };
263 if !matched {
264 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::DistributionName));
265 }
266 }
267 let left = reason_mask(
268 idp.and_then(|idp| idp.reasons),
269 execution,
270 target.span().start,
271 )?;
272 let right = reason_mask(point.reasons(), execution, target.span().start)?;
273 let reasons = Reasons(left.0 & right.0);
274 if reasons.is_empty() {
275 return Ok(ScopeOutcome::NotApplicable(ScopeFailure::NoReasons));
276 }
277 Ok(ScopeOutcome::Applicable(reasons))
278}
279
280fn issuer_encoding_matches(
281 issuer: &Name<'_>,
282 named: &Name<'_>,
283 execution: &Memory,
284) -> Result<bool> {
285 let left = issuer.as_bytes();
289 let right = named.as_bytes();
290 let work = u64::try_from(left.len().max(right.len()))
291 .ok()
292 .and_then(|n| n.checked_mul(2))
293 .and_then(|n| n.checked_add(1))
294 .ok_or_else(|| named.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
295 execution
296 .charge_work(work)
297 .map_err(|reason| named.resource(reason))?;
298 Ok(left == right)
299}
300
301fn point_name_matches_directory(
302 idp: &IssuingDistributionPoint<'_>,
303 issuer: &Name<'_>,
304 directory: &Name<'_>,
305 execution: &Memory,
306) -> Result<bool> {
307 point_name_matches_composed::<false>(idp, issuer, directory, None, execution)
308}
309
310fn point_name_matches_composed<const ENCODED: bool>(
311 idp: &IssuingDistributionPoint<'_>,
312 issuer: &Name<'_>,
313 base: &Name<'_>,
314 relative: Option<&RelativeName<'_>>,
315 execution: &Memory,
316) -> Result<bool> {
317 match &idp.name {
318 Some(PointName::Relative(suffix)) => {
319 if ENCODED && relative.is_none() {
320 return Ok(false);
321 }
322 name::matches_composed::<ENCODED>(issuer, Some(suffix), base, relative, execution)
323 }
324 Some(PointName::Full(names)) => {
325 if ENCODED && relative.is_some() {
326 return Ok(false);
327 }
328 for name in names {
329 execution
330 .charge_work(1)
331 .map_err(|reason| base.resource(reason))?;
332 if let GeneralName::Directory(directory) = name
333 && name::matches_composed::<ENCODED>(
334 directory, None, base, relative, execution,
335 )?
336 {
337 return Ok(true);
338 }
339 }
340 Ok(false)
341 }
342 None => Ok(true),
343 }
344}
345
346fn point_name_matches_names<const ENCODED: bool>(
347 idp: &IssuingDistributionPoint<'_>,
348 issuer: &Name<'_>,
349 names: &[GeneralName<'_>],
350 execution: &Memory,
351) -> Result<bool> {
352 for right in names {
353 execution
354 .charge_work(1)
355 .map_err(|reason| issuer.resource(reason))?;
356 match &idp.name {
357 Some(PointName::Relative(relative)) => {
358 if ENCODED {
359 return Ok(false);
360 }
361 if let GeneralName::Directory(directory) = right
362 && name::matches_composed::<ENCODED>(
363 issuer,
364 Some(relative),
365 directory,
366 None,
367 execution,
368 )?
369 {
370 return Ok(true);
371 }
372 }
373 Some(PointName::Full(left)) => {
374 for left in left {
375 execution
376 .charge_work(1)
377 .map_err(|reason| issuer.resource(reason))?;
378 if names_match::<ENCODED>(left, right, issuer, execution)? {
379 return Ok(true);
380 }
381 }
382 }
383 None => return Ok(true),
384 }
385 }
386 Ok(false)
387}
388
389fn names_match<const ENCODED: bool>(
390 left: &GeneralName<'_>,
391 right: &GeneralName<'_>,
392 source: &Name<'_>,
393 execution: &Memory,
394) -> Result<bool> {
395 let (left, right) = match (left, right) {
396 (GeneralName::Directory(left), GeneralName::Directory(right)) => {
397 return name::matches_composed::<ENCODED>(left, None, right, None, execution);
398 }
399 (GeneralName::Uri(left), GeneralName::Uri(right)) if !ENCODED => {
400 return super::uri::matches(left, right, execution, source.offset());
401 }
402 (GeneralName::Dns(left), GeneralName::Dns(right)) if !ENCODED => {
403 admit_text(left, right, source, execution)?;
404 return Ok(left.eq_ignore_ascii_case(right));
408 }
409 (GeneralName::Email(left), GeneralName::Email(right)) if !ENCODED => {
410 admit_text(left, right, source, execution)?;
411 let (Some((left_local, left_host)), Some((right_local, right_host))) =
415 (mail_parts(left), mail_parts(right))
416 else {
417 return Ok(false);
418 };
419 return Ok(left_local == right_local && left_host.eq_ignore_ascii_case(right_host));
420 }
421 (GeneralName::Uri(left), GeneralName::Uri(right))
422 | (GeneralName::Dns(left), GeneralName::Dns(right))
423 | (GeneralName::Email(left), GeneralName::Email(right)) => {
424 (left.as_bytes(), right.as_bytes())
425 }
426 (GeneralName::X400(left), GeneralName::X400(right))
427 | (GeneralName::EdiParty(left), GeneralName::EdiParty(right))
428 | (GeneralName::IpAddress(left), GeneralName::IpAddress(right)) => (*left, *right),
429 (GeneralName::RegisteredId(left), GeneralName::RegisteredId(right)) => {
430 (left.as_bytes(), right.as_bytes())
431 }
432 (
433 GeneralName::Other {
434 type_id: left_kind,
435 value: left,
436 },
437 GeneralName::Other {
438 type_id: right_kind,
439 value: right,
440 },
441 ) => {
442 let work = (left_kind.as_bytes().len().max(right_kind.as_bytes().len()) as u64)
443 .checked_mul(2)
444 .ok_or_else(|| {
445 source.resource(structured_asn1::memory::MemoryError::WorkExceeded)
446 })?;
447 execution
448 .charge_work(work)
449 .map_err(|reason| source.resource(reason))?;
450 if left_kind != right_kind {
451 return Ok(false);
452 }
453 (*left, *right)
454 }
455 _ => return Ok(false),
456 };
457 let work = (left.len().max(right.len()) as u64)
458 .checked_mul(2)
459 .and_then(|n| n.checked_add(1))
460 .ok_or_else(|| source.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
461 execution
462 .charge_work(work)
463 .map_err(|reason| source.resource(reason))?;
464 Ok(left == right)
465}
466
467fn admit_text(left: &str, right: &str, source: &Name<'_>, execution: &Memory) -> Result<()> {
468 let work = u64::try_from(left.len().max(right.len()))
469 .ok()
470 .and_then(|length| length.checked_mul(4))
471 .and_then(|work| work.checked_add(1))
472 .ok_or_else(|| source.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
473 execution
474 .charge_work(work)
475 .map_err(|reason| source.resource(reason))
476}
477
478fn mail_parts(text: &str) -> Option<(&str, &str)> {
479 let (mut quoted, mut escaped) = (false, false);
480 for (index, byte) in text.bytes().enumerate() {
481 if escaped {
482 escaped = false;
483 continue;
484 }
485 if quoted && byte == b'\\' {
486 escaped = true;
487 } else if byte == b'"' {
488 quoted = !quoted;
489 } else if !quoted && byte == b'@' {
490 return Some((&text[..index], &text[index + 1..]));
491 }
492 }
493 None
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub enum CertificateKind {
499 All,
501 EndEntity,
503 Authority,
505 Attribute,
507}
508
509#[derive(Debug)]
511pub struct IssuingDistributionPoint<'a> {
512 name: Option<PointName<'a>>,
513 kind: CertificateKind,
514 reasons: Option<BitString<'a>>,
515 indirect: bool,
516}
517
518impl<'a> IssuingDistributionPoint<'a> {
519 #[must_use]
521 pub fn full_name(&self) -> &[GeneralName<'a>] {
522 match &self.name {
523 Some(PointName::Full(names)) => names,
524 _ => &[],
525 }
526 }
527
528 #[must_use]
530 pub fn name_relative_to_issuer(&self) -> Option<&RelativeName<'a>> {
531 match &self.name {
532 Some(PointName::Relative(name)) => Some(name),
533 _ => None,
534 }
535 }
536
537 #[must_use]
539 pub fn certificate_kind(&self) -> CertificateKind {
540 self.kind
541 }
542
543 #[must_use]
545 pub fn reasons(&self) -> Option<BitString<'a>> {
546 self.reasons
547 }
548
549 #[must_use]
551 pub fn is_indirect(&self) -> bool {
552 self.indirect
553 }
554
555 pub fn read(extension: &Extension<'a>, execution: &Memory) -> Result<Self> {
557 const FIELD: &str = "extensions.issuingDistributionPoint";
558 let fail = |code, at| Error::new(code, FIELD, at);
559 let mut reader = extension::sequence(extension, FIELD, execution)?;
560 let mut result = Self {
561 name: None,
562 kind: CertificateKind::All,
563 reasons: None,
564 indirect: false,
565 };
566 let mut last = None;
567 while let Some(element) = reader.next_element().field(FIELD)? {
568 let (number, constructed) = extension::context_of(element.tag())
569 .ok_or_else(|| fail(ErrorCode::UnexpectedField, element.span().start))?;
570 if last.is_some_and(|previous| number <= previous) {
571 return Err(fail(ErrorCode::UnexpectedField, element.span().start));
572 }
573 last = Some(number);
574 match (number, constructed) {
575 (0, true) => {
576 result.name = Some(access::read_distribution_name(&reader, &element, FIELD)?);
577 }
578 (3, false) => {
579 let reasons = reader.bit_string(&element).field(FIELD)?;
580 reader.charge(9).field(FIELD)?;
581 if reasons.len() > 9
585 || !(1..=8).any(|bit| reasons.bit(bit))
586 || !reasons.bit(reasons.len() - 1)
587 {
588 return Err(fail(ErrorCode::MalformedValue, element.span().start));
589 }
590 result.reasons = Some(reasons);
591 }
592 (1 | 2 | 4 | 5, false) => {
593 if !reader.boolean(&element).field(FIELD)? {
595 return Err(fail(ErrorCode::MalformedValue, element.span().start));
596 }
597 if number == 4 {
598 result.indirect = true;
599 continue;
600 }
601 if result.kind != CertificateKind::All {
602 return Err(fail(ErrorCode::MalformedValue, element.span().start));
603 }
604 result.kind = match number {
605 1 => CertificateKind::EndEntity,
606 2 => CertificateKind::Authority,
607 _ => CertificateKind::Attribute,
608 };
609 }
610 _ => return Err(fail(ErrorCode::UnexpectedField, element.span().start)),
611 }
612 }
613 if result.name.is_none()
616 && result.kind == CertificateKind::All
617 && result.reasons.is_none()
618 && !result.indirect
619 {
620 return Err(fail(ErrorCode::EmptySequence, extension.span().start));
621 }
622 Ok(result)
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use crate::general_name::GeneralName::{Dns, Email};
629 #[test]
630 fn dns_and_mailbox_scope_follow_their_separate_case_rules() {
631 let bytes = crate::fixtures::CertificateParts::default().build();
634 let parser = structured_asn1::Parser::der(crate::fixtures::memory());
635 let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
636 let execution = crate::fixtures::memory();
637 for (left, right, expected) in [
638 (Dns("EXAMPLE.com"), Dns("example.COM"), true),
639 (Email("user@EXAMPLE.com"), Email("user@example.COM"), true),
640 (Email("User@example.com"), Email("user@example.com"), false),
641 (
642 Email("\"a@b\"@EXAMPLE.com"),
643 Email("\"a@b\"@example.com"),
644 true,
645 ),
646 ] {
647 assert_eq!(
648 super::names_match::<false>(&left, &right, source.tbs().issuer(), &execution)
649 .unwrap(),
650 expected
651 );
652 }
653 }
654
655 #[test]
656 fn uri_scope_compares_normalized_identity_without_folding_path_or_query() {
657 let bytes = crate::fixtures::CertificateParts::default().build();
660 let parser = structured_asn1::Parser::der(crate::fixtures::memory());
661 let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
662 let execution = crate::fixtures::memory();
663 for (left, right, expected) in [
664 (
665 "HTTP://Example.com:080/a/../%7e",
666 "http://example.com/~",
667 true,
668 ),
669 ("https://example.com:443", "https://example.com/", true),
670 (
671 "ldap://example.com/",
672 "ldap://EXAMPLE.com:389/??BASE?(objectClass=*)",
673 true,
674 ),
675 ("ftp://example.com:21", "ftp://example.com/", true),
676 (
677 "http://example.com/A?q=A",
678 "http://example.com/a?q=A",
679 false,
680 ),
681 (
682 "http://example.com/a?q=A",
683 "http://example.com/a?q=a",
684 false,
685 ),
686 ("http://example.com/%2f", "http://example.com//", false),
687 ("http://example.com/?", "http://example.com/", false),
688 (
689 "ftp://example.com/a;type=a",
690 "ftp://example.com/a;type=i",
691 false,
692 ),
693 ] {
694 assert_eq!(
695 super::names_match::<false>(
696 &crate::general_name::GeneralName::Uri(left),
697 &crate::general_name::GeneralName::Uri(right),
698 source.tbs().issuer(),
699 &execution
700 )
701 .unwrap(),
702 expected,
703 "{left} versus {right}"
704 );
705 }
706 }
707}