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 if !ENCODED {
396 return crate::general_name::matches_at(
397 left,
398 right,
399 execution,
400 source.offset(),
401 source.field(),
402 );
403 }
404 let (left, right) = match (left, right) {
405 (GeneralName::Directory(left), GeneralName::Directory(right)) => {
406 return name::matches_composed::<ENCODED>(left, None, right, None, execution);
407 }
408 (GeneralName::Uri(left), GeneralName::Uri(right))
409 | (GeneralName::Dns(left), GeneralName::Dns(right))
410 | (GeneralName::Email(left), GeneralName::Email(right)) => {
411 (left.as_bytes(), right.as_bytes())
412 }
413 (GeneralName::X400(left), GeneralName::X400(right))
414 | (GeneralName::EdiParty(left), GeneralName::EdiParty(right))
415 | (GeneralName::IpAddress(left), GeneralName::IpAddress(right)) => (*left, *right),
416 (GeneralName::RegisteredId(left), GeneralName::RegisteredId(right)) => {
417 (left.as_bytes(), right.as_bytes())
418 }
419 (
420 GeneralName::Other {
421 type_id: left_kind,
422 value: left,
423 },
424 GeneralName::Other {
425 type_id: right_kind,
426 value: right,
427 },
428 ) => {
429 let work = (left_kind.as_bytes().len().max(right_kind.as_bytes().len()) as u64)
430 .checked_mul(2)
431 .ok_or_else(|| {
432 source.resource(structured_asn1::memory::MemoryError::WorkExceeded)
433 })?;
434 execution
435 .charge_work(work)
436 .map_err(|reason| source.resource(reason))?;
437 if left_kind != right_kind {
438 return Ok(false);
439 }
440 (*left, *right)
441 }
442 _ => return Ok(false),
443 };
444 let work = (left.len().max(right.len()) as u64)
445 .checked_mul(2)
446 .and_then(|n| n.checked_add(1))
447 .ok_or_else(|| source.resource(structured_asn1::memory::MemoryError::WorkExceeded))?;
448 execution
449 .charge_work(work)
450 .map_err(|reason| source.resource(reason))?;
451 Ok(left == right)
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq)]
456pub enum CertificateKind {
457 All,
459 EndEntity,
461 Authority,
463 Attribute,
465}
466
467#[derive(Debug)]
469pub struct IssuingDistributionPoint<'a> {
470 name: Option<PointName<'a>>,
471 kind: CertificateKind,
472 reasons: Option<BitString<'a>>,
473 indirect: bool,
474}
475
476impl<'a> IssuingDistributionPoint<'a> {
477 #[must_use]
479 pub fn full_name(&self) -> &[GeneralName<'a>] {
480 match &self.name {
481 Some(PointName::Full(names)) => names,
482 _ => &[],
483 }
484 }
485
486 #[must_use]
488 pub fn name_relative_to_issuer(&self) -> Option<&RelativeName<'a>> {
489 match &self.name {
490 Some(PointName::Relative(name)) => Some(name),
491 _ => None,
492 }
493 }
494
495 #[must_use]
497 pub fn certificate_kind(&self) -> CertificateKind {
498 self.kind
499 }
500
501 #[must_use]
503 pub fn reasons(&self) -> Option<BitString<'a>> {
504 self.reasons
505 }
506
507 #[must_use]
509 pub fn is_indirect(&self) -> bool {
510 self.indirect
511 }
512
513 pub fn read(extension: &Extension<'a>, execution: &Memory) -> Result<Self> {
515 const FIELD: &str = "extensions.issuingDistributionPoint";
516 let fail = |code, at| Error::new(code, FIELD, at);
517 let mut reader = extension::sequence(extension, FIELD, execution)?;
518 let mut result = Self {
519 name: None,
520 kind: CertificateKind::All,
521 reasons: None,
522 indirect: false,
523 };
524 let mut last = None;
525 while let Some(element) = reader.next_element().field(FIELD)? {
526 let (number, constructed) = extension::context_of(element.tag())
527 .ok_or_else(|| fail(ErrorCode::UnexpectedField, element.span().start))?;
528 if last.is_some_and(|previous| number <= previous) {
529 return Err(fail(ErrorCode::UnexpectedField, element.span().start));
530 }
531 last = Some(number);
532 match (number, constructed) {
533 (0, true) => {
534 result.name = Some(access::read_distribution_name(&reader, &element, FIELD)?);
535 }
536 (3, false) => {
537 let reasons = reader.bit_string(&element).field(FIELD)?;
538 reader.charge(9).field(FIELD)?;
539 if reasons.len() > 9
543 || !(1..=8).any(|bit| reasons.bit(bit))
544 || !reasons.bit(reasons.len() - 1)
545 {
546 return Err(fail(ErrorCode::MalformedValue, element.span().start));
547 }
548 result.reasons = Some(reasons);
549 }
550 (1 | 2 | 4 | 5, false) => {
551 if !reader.boolean(&element).field(FIELD)? {
553 return Err(fail(ErrorCode::MalformedValue, element.span().start));
554 }
555 if number == 4 {
556 result.indirect = true;
557 continue;
558 }
559 if result.kind != CertificateKind::All {
560 return Err(fail(ErrorCode::MalformedValue, element.span().start));
561 }
562 result.kind = match number {
563 1 => CertificateKind::EndEntity,
564 2 => CertificateKind::Authority,
565 _ => CertificateKind::Attribute,
566 };
567 }
568 _ => return Err(fail(ErrorCode::UnexpectedField, element.span().start)),
569 }
570 }
571 if result.name.is_none()
574 && result.kind == CertificateKind::All
575 && result.reasons.is_none()
576 && !result.indirect
577 {
578 return Err(fail(ErrorCode::EmptySequence, extension.span().start));
579 }
580 Ok(result)
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use crate::general_name::GeneralName::{Dns, Email};
587 #[test]
588 fn dns_and_mailbox_scope_follow_their_separate_case_rules() {
589 let bytes = crate::fixtures::CertificateParts::default().build();
592 let parser = structured_asn1::Parser::der(crate::fixtures::memory());
593 let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
594 let execution = crate::fixtures::memory();
595 for (left, right, expected) in [
596 (Dns("EXAMPLE.com"), Dns("example.COM"), true),
597 (Email("user@EXAMPLE.com"), Email("user@example.COM"), true),
598 (Email("User@example.com"), Email("user@example.com"), false),
599 (
600 Email("\"a@b\"@EXAMPLE.com"),
601 Email("\"a@b\"@example.com"),
602 true,
603 ),
604 ] {
605 assert_eq!(
606 super::names_match::<false>(&left, &right, source.tbs().issuer(), &execution)
607 .unwrap(),
608 expected
609 );
610 }
611 }
612
613 #[test]
614 fn uri_scope_compares_normalized_identity_without_folding_path_or_query() {
615 let bytes = crate::fixtures::CertificateParts::default().build();
618 let parser = structured_asn1::Parser::der(crate::fixtures::memory());
619 let source = crate::Certificate::read(&parser, &bytes, 100).unwrap();
620 let execution = crate::fixtures::memory();
621 for (left, right, expected) in [
622 (
623 "HTTP://Example.com:080/a/../%7e",
624 "http://example.com/~",
625 true,
626 ),
627 ("https://example.com:443", "https://example.com/", true),
628 (
629 "ldap://example.com/",
630 "ldap://EXAMPLE.com:389/??BASE?(objectClass=*)",
631 true,
632 ),
633 ("ftp://example.com:21", "ftp://example.com/", true),
634 (
635 "http://example.com/A?q=A",
636 "http://example.com/a?q=A",
637 false,
638 ),
639 (
640 "http://example.com/a?q=A",
641 "http://example.com/a?q=a",
642 false,
643 ),
644 ("http://example.com/%2f", "http://example.com//", false),
645 ("http://example.com/?", "http://example.com/", false),
646 (
647 "ftp://example.com/a;type=a",
648 "ftp://example.com/a;type=i",
649 false,
650 ),
651 ] {
652 assert_eq!(
653 super::names_match::<false>(
654 &crate::general_name::GeneralName::Uri(left),
655 &crate::general_name::GeneralName::Uri(right),
656 source.tbs().issuer(),
657 &execution
658 )
659 .unwrap(),
660 expected,
661 "{left} versus {right}"
662 );
663 }
664 }
665}