1use std::collections::BTreeSet;
60
61use async_trait::async_trait;
62use regex::Regex;
63use tracing::info;
64
65use super::policy::{Check, StageSet, Verdict};
66use super::{
67 IdentifierContext, ListVerdict, SUBJECT_ONLY_TYPES, check_lists, compile_matchers,
68 default_identifier_types,
69};
70
71#[derive(Debug, Clone)]
73pub struct Settings {
74 pub allowed_types: Vec<String>,
75 pub allow: Vec<String>,
76 pub deny: Vec<String>,
77 pub allow_regex: Vec<String>,
78 pub deny_regex: Vec<String>,
79 pub allow_wildcards: bool,
80}
81
82impl Default for Settings {
83 fn default() -> Self {
84 Self {
85 allowed_types: default_identifier_types(),
86 allow: Vec::new(),
87 deny: Vec::new(),
88 allow_regex: Vec::new(),
89 deny_regex: Vec::new(),
90 allow_wildcards: false,
91 }
92 }
93}
94
95#[derive(Debug)]
97pub struct IdentifierList {
98 allowed_types: BTreeSet<String>,
99 allow: Vec<Regex>,
100 deny: Vec<Regex>,
101 allow_wildcards: bool,
102}
103
104impl IdentifierList {
105 pub fn from_settings(name: &str, settings: &Settings) -> anyhow::Result<Self> {
107 let check = Self {
108 allowed_types: settings
109 .allowed_types
110 .iter()
111 .map(|typ| typ.to_ascii_lowercase())
112 .collect(),
113 allow: compile_matchers(&settings.allow, &settings.allow_regex, name, "allow")?,
114 deny: compile_matchers(&settings.deny, &settings.deny_regex, name, "deny")?,
115 allow_wildcards: settings.allow_wildcards,
116 };
117 info!(
118 event = "filter_identifiers_loaded",
119 outcome = "success",
120 check = name,
121 allowed_types = ?settings.allowed_types,
122 allow = check.allow.len(),
123 deny = check.deny.len(),
124 allow_wildcards = settings.allow_wildcards,
125 );
126 Ok(check)
127 }
128
129 fn decide(&self, context: &IdentifierContext<'_>) -> Verdict {
130 let stage = context.stage.as_str();
131
132 for identifier in context.identifiers {
133 let typ = identifier.typ.to_ascii_lowercase();
134 if !self.allowed_types.contains(&typ) {
135 return Verdict::Fail(format!(
136 "{stage} requests a {} identifier, which is not permitted",
137 identifier.typ
138 ));
139 }
140
141 let value = &identifier.value;
142
143 if !self.allow_wildcards && typ == "dns" && value.starts_with("*.") {
149 return Verdict::Fail(format!(
150 "{stage} identifier {value} is a wildcard, which policy does not permit"
151 ));
152 }
153
154 let allow: &[Regex] = if SUBJECT_ONLY_TYPES.contains(&typ.as_str()) {
160 &[]
161 } else {
162 &self.allow
163 };
164
165 match check_lists(allow, &self.deny, |pattern| pattern.is_match(value)) {
166 ListVerdict::Permitted => {}
167 ListVerdict::Denied => {
168 return Verdict::Fail(format!(
169 "{stage} identifier {value} is denied by policy"
170 ));
171 }
172 ListVerdict::NotAllowed => {
173 return Verdict::Fail(format!(
174 "{stage} identifier {value} is not permitted by policy"
175 ));
176 }
177 }
178 }
179
180 Verdict::Pass
181 }
182}
183
184#[async_trait]
185impl Check for IdentifierList {
186 fn kind(&self) -> &'static str {
187 "identifiers"
188 }
189
190 fn stages(&self) -> StageSet {
191 StageSet::identifiers_only()
192 }
193
194 async fn check_identifiers(&self, context: &IdentifierContext<'_>) -> Verdict {
195 self.decide(context)
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::filter::IdentifierStage;
203 use crate::sqlite::order::Identifier;
204 use crate::testutil::identifiers as ids;
205
206 fn globs(allow: &[&str], deny: &[&str]) -> Settings {
207 Settings {
208 allow: allow.iter().map(std::string::ToString::to_string).collect(),
209 deny: deny.iter().map(std::string::ToString::to_string).collect(),
210 ..Settings::default()
211 }
212 }
213
214 fn regexes(allow: &[&str], deny: &[&str]) -> Settings {
215 Settings {
216 allow_regex: allow.iter().map(std::string::ToString::to_string).collect(),
217 deny_regex: deny.iter().map(std::string::ToString::to_string).collect(),
218 ..Settings::default()
219 }
220 }
221
222 fn built(settings: &Settings) -> IdentifierList {
223 IdentifierList::from_settings("names", settings).unwrap()
224 }
225
226 async fn verdict_for(
227 check: &IdentifierList,
228 stage: IdentifierStage,
229 identifiers: &[Identifier],
230 ) -> Verdict {
231 check
232 .check_identifiers(&IdentifierContext {
233 client_ip: None,
234 account_id: "acct-1",
235 stage,
236 identifiers,
237
238 eab: None,
239 })
240 .await
241 }
242
243 fn assert_failed(verdict: Verdict, needle: &str) {
244 match verdict {
245 Verdict::Fail(detail) => {
246 assert!(detail.contains(needle), "{detail:?} lacks {needle:?}");
247 }
248 other => panic!("expected Fail, got {other:?}"),
249 }
250 }
251
252 #[tokio::test]
257 async fn a_glob_star_matches_exactly_one_label() {
258 let check = built(&globs(&["*.example.com"], &[]));
259
260 assert_eq!(
261 verdict_for(
262 &check,
263 IdentifierStage::NewOrder,
264 &ids(&[("dns", "a.example.com")])
265 )
266 .await,
267 Verdict::Pass
268 );
269 assert_failed(
270 verdict_for(
271 &check,
272 IdentifierStage::NewOrder,
273 &ids(&[("dns", "a.b.example.com")]),
274 )
275 .await,
276 "not permitted by policy",
277 );
278 assert_failed(
280 verdict_for(
281 &check,
282 IdentifierStage::NewOrder,
283 &ids(&[("dns", "example.com")]),
284 )
285 .await,
286 "not permitted by policy",
287 );
288 }
289
290 #[tokio::test]
291 async fn listing_the_bare_name_beside_the_glob_covers_both() {
292 let check = built(&globs(&["*.example.com", "example.com"], &[]));
293 for name in ["a.example.com", "example.com"] {
294 assert_eq!(
295 verdict_for(&check, IdentifierStage::NewOrder, &ids(&[("dns", name)])).await,
296 Verdict::Pass,
297 "{name}"
298 );
299 }
300 }
301
302 #[tokio::test]
303 async fn a_glob_ignores_case_and_a_trailing_dot_is_not_special() {
304 let check = built(&globs(&["*.example.com"], &[]));
305 assert_eq!(
306 verdict_for(
307 &check,
308 IdentifierStage::NewOrder,
309 &ids(&[("dns", "A.Example.COM")])
310 )
311 .await,
312 Verdict::Pass
313 );
314 }
315
316 #[tokio::test]
319 async fn a_glob_escapes_every_other_metacharacter() {
320 let check = built(&globs(&["a.example.com"], &[]));
321 assert_failed(
323 verdict_for(
324 &check,
325 IdentifierStage::NewOrder,
326 &ids(&[("dns", "axexample.com")]),
327 )
328 .await,
329 "not permitted",
330 );
331 }
332
333 #[tokio::test]
334 async fn a_glob_deny_wins_over_a_glob_allow() {
335 let check = built(&globs(&["*.example.com"], &["secret.example.com"]));
336 assert_failed(
337 verdict_for(
338 &check,
339 IdentifierStage::NewOrder,
340 &ids(&[("dns", "secret.example.com")]),
341 )
342 .await,
343 "is denied by policy",
344 );
345 }
346
347 #[tokio::test]
350 async fn globs_and_regexes_are_unioned_on_both_sides() {
351 let settings = Settings {
352 allow: vec!["*.example.com".to_string()],
353 allow_regex: vec![r"host\d+\.internal".to_string()],
354 deny: vec!["secret.example.com".to_string()],
355 deny_regex: vec![r"host666\.internal".to_string()],
356 ..Settings::default()
357 };
358 let check = built(&settings);
359
360 for permitted in ["a.example.com", "host12.internal"] {
361 assert_eq!(
362 verdict_for(
363 &check,
364 IdentifierStage::NewOrder,
365 &ids(&[("dns", permitted)])
366 )
367 .await,
368 Verdict::Pass,
369 "{permitted}"
370 );
371 }
372 for refused in ["secret.example.com", "host666.internal"] {
373 assert_failed(
374 verdict_for(&check, IdentifierStage::NewOrder, &ids(&[("dns", refused)])).await,
375 "is denied by policy",
376 );
377 }
378 }
379
380 #[tokio::test]
381 async fn a_glob_allow_skips_the_common_name_but_a_glob_deny_reaches_it() {
382 let check = built(&globs(&["*.example.com"], &["secret.example.com"]));
383
384 assert_eq!(
386 verdict_for(
387 &check,
388 IdentifierStage::Csr,
389 &ids(&[("dns", "ok.example.com"), ("cn", "rcgen self signed cert")])
390 )
391 .await,
392 Verdict::Pass
393 );
394 assert_failed(
396 verdict_for(
397 &check,
398 IdentifierStage::Csr,
399 &ids(&[("dns", "ok.example.com"), ("cn", "secret.example.com")]),
400 )
401 .await,
402 "is denied by policy",
403 );
404 }
405
406 #[tokio::test]
409 async fn an_empty_allow_list_permits_any_allowed_type() {
410 let check = built(&Settings::default());
411 let identifiers = ids(&[("dns", "anything.example.com"), ("cn", "other.test")]);
412 assert_eq!(
413 verdict_for(&check, IdentifierStage::NewOrder, &identifiers).await,
414 Verdict::Pass
415 );
416 }
417
418 #[tokio::test]
419 async fn the_allow_list_refuses_everything_else() {
420 let check = built(®exes(&[r".*\.example\.com"], &[]));
421 assert_eq!(
422 verdict_for(
423 &check,
424 IdentifierStage::NewOrder,
425 &ids(&[("dns", "a.example.com")])
426 )
427 .await,
428 Verdict::Pass
429 );
430 assert_failed(
431 verdict_for(
432 &check,
433 IdentifierStage::NewOrder,
434 &ids(&[("dns", "a.evil.net")]),
435 )
436 .await,
437 "not permitted by policy",
438 );
439 }
440
441 #[tokio::test]
442 async fn deny_wins_over_allow() {
443 let check = built(®exes(&[r".*\.example\.com"], &[r"secret\..*"]));
444 assert_failed(
445 verdict_for(
446 &check,
447 IdentifierStage::NewOrder,
448 &ids(&[("dns", "secret.example.com")]),
449 )
450 .await,
451 "is denied by policy",
452 );
453 }
454
455 #[tokio::test]
456 async fn patterns_are_anchored_so_a_suffix_cannot_bypass_them() {
457 let check = built(®exes(&[r"example\.com"], &[]));
458 assert_failed(
460 verdict_for(
461 &check,
462 IdentifierStage::NewOrder,
463 &ids(&[("dns", "example.com.evil.net")]),
464 )
465 .await,
466 "not permitted",
467 );
468 }
469
470 #[tokio::test]
471 async fn matching_ignores_case() {
472 let check = built(®exes(&[], &[r"secret\.example\.com"]));
473 assert_failed(
474 verdict_for(
475 &check,
476 IdentifierStage::NewOrder,
477 &ids(&[("dns", "SECRET.Example.COM")]),
478 )
479 .await,
480 "is denied",
481 );
482 }
483
484 #[tokio::test]
485 async fn an_ip_san_is_refused_by_the_default_allowed_types() {
486 let check = built(&Settings::default());
487 assert_failed(
488 verdict_for(
489 &check,
490 IdentifierStage::Csr,
491 &ids(&[("dns", "ok.example.com"), ("ip", "10.0.0.1")]),
492 )
493 .await,
494 "requests a ip identifier",
495 );
496 }
497
498 #[tokio::test]
499 async fn email_and_uri_sans_are_refused_too() {
500 let check = built(&Settings::default());
501 for identifier in [("email", "a@example.com"), ("uri", "https://example.com")] {
502 assert_failed(
503 verdict_for(&check, IdentifierStage::Csr, &ids(&[identifier])).await,
504 "is not permitted",
505 );
506 }
507 }
508
509 #[tokio::test]
510 async fn an_operator_can_opt_into_ip_identifiers() {
511 let settings = Settings {
512 allowed_types: vec!["dns".to_string(), "ip".to_string()],
513 allow_regex: vec![r"10\..*".to_string(), r".*\.example\.com".to_string()],
514 ..Settings::default()
515 };
516 let check = built(&settings);
517 assert_eq!(
518 verdict_for(
519 &check,
520 IdentifierStage::Csr,
521 &ids(&[("ip", "10.0.0.1"), ("dns", "a.example.com")])
522 )
523 .await,
524 Verdict::Pass
525 );
526 }
527
528 #[tokio::test]
535 async fn a_wildcard_is_refused_by_default_even_when_the_lists_would_permit_it() {
536 let check = built(®exes(&[], &[r"secret\.example\.com"]));
537
538 assert_failed(
540 verdict_for(
541 &check,
542 IdentifierStage::NewOrder,
543 &ids(&[("dns", "*.example.com")]),
544 )
545 .await,
546 "is a wildcard",
547 );
548 assert_failed(
550 verdict_for(
551 &check,
552 IdentifierStage::NewOrder,
553 &ids(&[("dns", "secret.example.com")]),
554 )
555 .await,
556 "denied by policy",
557 );
558 }
559
560 #[tokio::test]
563 async fn an_operator_can_opt_into_wildcards() {
564 let settings = Settings {
565 allow_regex: vec![r"\*\.example\.com".to_string()],
566 deny_regex: vec![r"\*\.internal\.example".to_string()],
567 allow_wildcards: true,
568 ..Settings::default()
569 };
570 let check = built(&settings);
571
572 assert_eq!(
573 verdict_for(
574 &check,
575 IdentifierStage::NewOrder,
576 &ids(&[("dns", "*.example.com")])
577 )
578 .await,
579 Verdict::Pass
580 );
581 assert_failed(
582 verdict_for(
583 &check,
584 IdentifierStage::NewOrder,
585 &ids(&[("dns", "*.internal.example")]),
586 )
587 .await,
588 "denied by policy",
589 );
590 }
591
592 #[tokio::test]
595 async fn a_glob_can_name_the_wildcard_form_itself() {
596 let settings = Settings {
597 allow: vec!["*.example.com".to_string()],
598 allow_wildcards: true,
599 ..Settings::default()
600 };
601 let check = built(&settings);
602 assert_eq!(
604 verdict_for(
605 &check,
606 IdentifierStage::NewOrder,
607 &ids(&[("dns", "*.example.com")])
608 )
609 .await,
610 Verdict::Pass
611 );
612 }
613
614 #[tokio::test]
617 async fn the_wildcard_check_does_not_reach_the_common_name() {
618 let check = built(&Settings::default());
619 assert_eq!(
620 verdict_for(
621 &check,
622 IdentifierStage::Csr,
623 &ids(&[("dns", "example.com"), ("cn", "*.example.com")])
624 )
625 .await,
626 Verdict::Pass
627 );
628 }
629
630 #[tokio::test]
633 async fn deny_reaches_the_common_name() {
634 let check = built(®exes(&[], &[r"secret\.example\.com"]));
636 assert_failed(
637 verdict_for(
638 &check,
639 IdentifierStage::Csr,
640 &ids(&[("dns", "ok.example.com"), ("cn", "secret.example.com")]),
641 )
642 .await,
643 "is denied by policy",
644 );
645 }
646
647 #[tokio::test]
652 async fn allow_does_not_constrain_the_common_name() {
653 let check = built(®exes(&[r".*\.example\.com"], &[]));
654 assert_eq!(
655 verdict_for(
656 &check,
657 IdentifierStage::Csr,
658 &ids(&[("dns", "ok.example.com"), ("cn", "rcgen self signed cert")])
659 )
660 .await,
661 Verdict::Pass
662 );
663 }
664
665 #[tokio::test]
667 async fn allow_still_constrains_the_sans() {
668 let check = built(®exes(&[r".*\.example\.com"], &[]));
669 assert_failed(
670 verdict_for(
671 &check,
672 IdentifierStage::Csr,
673 &ids(&[("dns", "evil.net"), ("cn", "ok.example.com")]),
674 )
675 .await,
676 "evil.net is not permitted",
677 );
678 }
679
680 #[tokio::test]
682 async fn the_common_name_can_be_disallowed_by_type() {
683 let settings = Settings {
684 allowed_types: vec!["dns".to_string()],
685 ..Settings::default()
686 };
687 let check = built(&settings);
688 assert_failed(
689 verdict_for(&check, IdentifierStage::Csr, &ids(&[("cn", "anything")])).await,
690 "requests a cn identifier",
691 );
692 }
693
694 #[tokio::test]
695 async fn the_stage_appears_in_the_detail() {
696 let check = built(®exes(&[], &[r".*"]));
697 assert_failed(
698 verdict_for(
699 &check,
700 IdentifierStage::NewOrder,
701 &ids(&[("dns", "a.example.com")]),
702 )
703 .await,
704 "newOrder identifier",
705 );
706 assert_failed(
707 verdict_for(
708 &check,
709 IdentifierStage::Csr,
710 &ids(&[("dns", "a.example.com")]),
711 )
712 .await,
713 "CSR identifier",
714 );
715 }
716
717 #[tokio::test]
718 async fn an_empty_identifier_list_is_allowed() {
719 let check = built(®exes(&["nothing"], &[]));
720 assert_eq!(
721 verdict_for(&check, IdentifierStage::Csr, &[]).await,
722 Verdict::Pass
723 );
724 }
725
726 #[test]
729 fn a_bad_pattern_is_a_startup_error() {
730 let error = IdentifierList::from_settings("names", ®exes(&[], &["[unclosed"]))
731 .unwrap_err()
732 .to_string();
733 assert!(error.contains("filter.check.names.deny_regex"), "{error}");
734 }
735
736 #[test]
737 fn reports_its_type_and_stages() {
738 let check = built(&Settings::default());
739 assert_eq!(check.kind(), "identifiers");
740 assert_eq!(check.stages(), StageSet::identifiers_only());
741 }
742
743 #[test]
744 fn the_default_settings_permit_dns_names_and_common_names() {
745 assert_eq!(Settings::default().allowed_types, vec!["dns", "cn"]);
746 assert!(!Settings::default().allow_wildcards);
747 }
748}