1use std::{
4 collections::HashSet,
5 fmt::Display,
6 num::{NonZeroU8, NonZeroUsize},
7};
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12 Error,
13 common::{ordered_set, set_to_vec},
14 config::technology::openpgp::{DomainName, OpenpgpFingerprint},
15 file::ConfigWebOfTrustMode,
16};
17
18const DEFAULT_ROOT_AMOUNT: NonZeroU8 = NonZeroU8::new(120).expect("120 is greater than 0");
20
21const DEFAULT_PARTIAL_AMOUNT: NonZeroU8 = NonZeroU8::new(40).expect("40 is greater than 0");
23
24const DEFAULT_FLOW_AMOUNT: NonZeroUsize = NonZeroUsize::new(120).expect("120 is greater than 0");
26
27pub(crate) const TRUST_AMOUNT_MAX: u8 = 120;
29
30#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
32pub struct TrustAmountFlow(NonZeroUsize);
33
34impl TrustAmountFlow {
35 pub fn new(num: NonZeroUsize) -> Self {
37 Self(num)
38 }
39
40 pub fn get(&self) -> NonZeroUsize {
42 self.0
43 }
44}
45
46impl Default for TrustAmountFlow {
47 fn default() -> Self {
48 Self(DEFAULT_FLOW_AMOUNT)
49 }
50}
51
52impl Display for TrustAmountFlow {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.0)
55 }
56}
57
58#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
60#[serde(try_from = "NonZeroU8")]
61pub struct TrustAmountRoot(NonZeroU8);
62
63impl TrustAmountRoot {
64 pub fn new(num: NonZeroU8) -> Result<Self, Error> {
70 if num.get() > TRUST_AMOUNT_MAX {
71 return Err(
72 crate::config::technology::openpgp::Error::InvalidTrustAmount {
73 amount: num.get().into(),
74 }
75 .into(),
76 );
77 }
78
79 Ok(Self(num))
80 }
81
82 pub fn get(&self) -> NonZeroU8 {
84 self.0
85 }
86}
87
88impl Default for TrustAmountRoot {
89 fn default() -> Self {
90 Self(DEFAULT_ROOT_AMOUNT)
91 }
92}
93
94impl Display for TrustAmountRoot {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(f, "{}", self.0)
97 }
98}
99
100impl TryFrom<NonZeroU8> for TrustAmountRoot {
101 type Error = Error;
102
103 fn try_from(value: NonZeroU8) -> Result<Self, Self::Error> {
104 Self::new(value)
105 }
106}
107
108#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
110#[serde(try_from = "NonZeroU8")]
111pub struct TrustAmountPartial(NonZeroU8);
112
113impl TrustAmountPartial {
114 pub fn new(num: NonZeroU8) -> Result<Self, Error> {
120 if num.get() > TRUST_AMOUNT_MAX {
121 return Err(
122 crate::config::technology::openpgp::Error::InvalidTrustAmount {
123 amount: num.get().into(),
124 }
125 .into(),
126 );
127 }
128
129 Ok(Self(num))
130 }
131
132 pub fn get(&self) -> NonZeroU8 {
134 self.0
135 }
136}
137
138impl Default for TrustAmountPartial {
139 fn default() -> Self {
140 Self(DEFAULT_PARTIAL_AMOUNT)
141 }
142}
143
144impl Display for TrustAmountPartial {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}", self.0)
147 }
148}
149
150impl TryFrom<NonZeroU8> for TrustAmountPartial {
151 type Error = Error;
152
153 fn try_from(value: NonZeroU8) -> Result<Self, Self::Error> {
154 Self::new(value)
155 }
156}
157
158#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
160pub struct WebOfTrustRoot {
161 fingerprint: OpenpgpFingerprint,
163
164 amount: TrustAmountRoot,
166}
167
168impl WebOfTrustRoot {
169 pub fn new(fingerprint: OpenpgpFingerprint, amount: TrustAmountRoot) -> Self {
174 Self {
175 fingerprint,
176 amount,
177 }
178 }
179
180 pub fn fingerprint(&self) -> &OpenpgpFingerprint {
182 &self.fingerprint
183 }
184
185 pub fn amount(&self) -> TrustAmountRoot {
187 self.amount
188 }
189}
190
191#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
193pub struct WebOfTrustMode {
194 flow_amount: TrustAmountFlow,
196
197 partial_amount: TrustAmountPartial,
199
200 #[serde(serialize_with = "ordered_set")]
212 roots: HashSet<WebOfTrustRoot>,
213
214 #[serde(serialize_with = "ordered_set")]
216 artifact_verifier_identity_domain_matches: HashSet<DomainName>,
217}
218
219impl WebOfTrustMode {
220 pub fn new(
222 flow_amount: TrustAmountFlow,
223 partial_amount: TrustAmountPartial,
224 roots: HashSet<WebOfTrustRoot>,
225 artifact_verifier_identity_domain_matches: HashSet<DomainName>,
226 ) -> Self {
227 Self {
228 flow_amount,
229 partial_amount,
230 roots,
231 artifact_verifier_identity_domain_matches,
232 }
233 }
234
235 pub(crate) fn from_config_with_defaults(
243 config: &ConfigWebOfTrustMode,
244 defaults: &WebOfTrustMode,
245 ) -> Self {
246 let flow_amount = config.flow_amount().unwrap_or(defaults.flow_amount());
247 let partial_amount = config.partial_amount().unwrap_or(defaults.partial_amount());
248 let root = if let Some(root) = config.roots() {
249 root.iter()
250 .map(|root| {
251 let amount = root.amount().unwrap_or_default();
252 WebOfTrustRoot::new(root.fingerprint().clone(), amount)
253 })
254 .collect::<HashSet<_>>()
255 } else {
256 defaults.roots().clone()
257 };
258 let artifact_verifier_identity_domain_matches = config
259 .artifact_verifier_identity_domain_matches()
260 .unwrap_or(defaults.artifact_verifier_identity_domain_matches())
261 .clone();
262
263 Self::new(
264 flow_amount,
265 partial_amount,
266 root,
267 artifact_verifier_identity_domain_matches,
268 )
269 }
270
271 pub fn flow_amount(&self) -> TrustAmountFlow {
273 self.flow_amount
274 }
275
276 pub fn partial_amount(&self) -> TrustAmountPartial {
278 self.partial_amount
279 }
280
281 pub fn roots(&self) -> &HashSet<WebOfTrustRoot> {
283 &self.roots
284 }
285
286 pub fn root_trust_amount(&self, fingerprint: &OpenpgpFingerprint) -> Option<TrustAmountRoot> {
288 self.roots
289 .iter()
290 .filter_map(|root| {
291 if root.fingerprint() == fingerprint {
292 Some(root.amount())
293 } else {
294 None
295 }
296 })
297 .next()
298 }
299
300 pub fn artifact_verifier_identity_domain_matches(&self) -> &HashSet<DomainName> {
302 &self.artifact_verifier_identity_domain_matches
303 }
304}
305
306impl Default for WebOfTrustMode {
307 fn default() -> Self {
308 Self::new(
309 TrustAmountFlow::default(),
310 TrustAmountPartial::default(),
311 HashSet::new(),
312 HashSet::new(),
313 )
314 }
315}
316
317impl Display for WebOfTrustMode {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 writeln!(
320 f,
321 "✅ Each artifact is verified using the \"web of trust\" verification method.\n"
322 )?;
323 writeln!(
324 f,
325 "↔️ Each partially trusted introducer has a trust amount of {}.\n",
326 self.partial_amount()
327 )?;
328
329 if !self.artifact_verifier_identity_domain_matches().is_empty() {
330 writeln!(
331 f,
332 "📧 A valid certificate must have a valid User ID that uses one of the following domains and reaches a flow amount of {} for the certificate to be considered as artifact verifier:",
333 self.flow_amount()
334 )?;
335 for domain_name in set_to_vec(self.artifact_verifier_identity_domain_matches()).iter() {
336 writeln!(f, "⤷ {domain_name}")?;
337 }
338 writeln!(f)?;
339 } else {
340 writeln!(
341 f,
342 "📧 A valid certificate is not required to use a specific domain in any of its User IDs, but one of them must reach a flow amount of {} for the certificate to be considered as artifact verifier.\n",
343 self.flow_amount()
344 )?;
345 }
346
347 if !self.roots().is_empty() {
348 writeln!(
349 f,
350 "🐾 Only a valid certificate that matches one of the following OpenPGP fingerprints is used as trust root with the respective trust amount:"
351 )?;
352 for trust_root in set_to_vec(self.roots()).iter() {
353 writeln!(
354 f,
355 "⤷ {} ({})",
356 trust_root.fingerprint(),
357 trust_root.amount()
358 )?;
359 }
360 } else {
361 writeln!(
362 f,
363 "🐾 Each valid certificate is considered as trust root with a default trust amount of {}.",
364 TrustAmountRoot::default()
365 )?;
366 }
367
368 Ok(())
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use rstest::rstest;
375 use testresult::TestResult;
376
377 use super::*;
378 use crate::file::ConfigWebOfTrustRoot;
379
380 #[rstest]
382 #[case::all_defaults(
383 ConfigWebOfTrustMode::default(),
384 WebOfTrustMode::default(),
385 WebOfTrustMode::default()
386 )]
387 #[rstest]
388 #[case::all_defaults(
389 ConfigWebOfTrustMode::default(),
390 WebOfTrustMode::default(),
391 WebOfTrustMode::default()
392 )]
393 #[case::default_config_and_custom_defaults(
394 ConfigWebOfTrustMode::default(),
395 WebOfTrustMode::new(
396 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
397 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
398 HashSet::from_iter([
399 WebOfTrustRoot::new(
400 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
401 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
402 ),
403 WebOfTrustRoot::new(
404 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
405 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
406 ),
407 WebOfTrustRoot::new(
408 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
409 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
410 ),
411 ]),
412 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
413 ),
414 WebOfTrustMode::new(
415 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
416 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
417 HashSet::from_iter([
418 WebOfTrustRoot::new(
419 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
420 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
421 ),
422 WebOfTrustRoot::new(
423 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
424 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
425 ),
426 WebOfTrustRoot::new(
427 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
428 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
429 ),
430 ]),
431 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
432 ),
433 )]
434 #[case::empty_list_config_custom_defaults(
435 ConfigWebOfTrustMode::new(
436 Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
437 Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
438 Some(HashSet::new()),
439 Some(HashSet::new()),
440 ),
441 WebOfTrustMode::new(
442 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
443 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
444 HashSet::from_iter([
445 WebOfTrustRoot::new(
446 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
447 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
448 ),
449 WebOfTrustRoot::new(
450 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
451 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
452 ),
453 WebOfTrustRoot::new(
454 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
455 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
456 ),
457 ]),
458 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
459 ),
460 WebOfTrustMode::new(
461 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
462 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
463 HashSet::new(),
464 HashSet::new(),
465 ),
466 )]
467 #[case::fully_custom_config_with_default_defaults(
468 ConfigWebOfTrustMode::new(
469 Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
470 Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
471 Some(HashSet::from_iter([
472 ConfigWebOfTrustRoot::new(
473 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
474 Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
475 ),
476 ConfigWebOfTrustRoot::new(
477 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
478 Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
479 ),
480 ConfigWebOfTrustRoot::new(
481 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
482 Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
483 ),
484 ])),
485 Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])),
486 ),
487 WebOfTrustMode::default(),
488 WebOfTrustMode::new(
489 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
490 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
491 HashSet::from_iter([
492 WebOfTrustRoot::new(
493 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
494 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
495 ),
496 WebOfTrustRoot::new(
497 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
498 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
499 ),
500 WebOfTrustRoot::new(
501 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
502 TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
503 ),
504 ]),
505 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
506 ),
507 )]
508 #[case::mixed_custom_config_with_custom_defaults(
509 ConfigWebOfTrustMode::new(
510 None,
511 None,
512 Some(HashSet::from_iter([
513 ConfigWebOfTrustRoot::new(
514 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
515 None,
516 ),
517 ConfigWebOfTrustRoot::new(
518 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
519 None,
520 ),
521 ConfigWebOfTrustRoot::new(
522 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
523 None,
524 ),
525 ])),
526 Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])),
527 ),
528 WebOfTrustMode::new(
529 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
530 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
531 HashSet::from_iter([
532 WebOfTrustRoot::new(
533 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
534 TrustAmountRoot::default(),
535 ),
536 WebOfTrustRoot::new(
537 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
538 TrustAmountRoot::default(),
539 ),
540 WebOfTrustRoot::new(
541 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
542 TrustAmountRoot::default(),
543 ),
544 ]),
545 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
546 ),
547 WebOfTrustMode::new(
548 TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
549 TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
550 HashSet::from_iter([
551 WebOfTrustRoot::new(
552 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
553 TrustAmountRoot::default(),
554 ),
555 WebOfTrustRoot::new(
556 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
557 TrustAmountRoot::default(),
558 ),
559 WebOfTrustRoot::new(
560 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
561 TrustAmountRoot::default(),
562 ),
563 ]),
564 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
565 )
566 )]
567 fn web_of_trust_mode_from_config_with_defaults(
568 #[case] config: ConfigWebOfTrustMode,
569 #[case] defaults: WebOfTrustMode,
570 #[case] output: WebOfTrustMode,
571 ) -> TestResult {
572 assert_eq!(
573 WebOfTrustMode::from_config_with_defaults(&config, &defaults),
574 output
575 );
576 Ok(())
577 }
578
579 #[test]
580 fn trust_amount_flow_get() {
581 assert_eq!(TrustAmountFlow::default().get().get(), 120);
582 }
583
584 #[test]
585 fn trust_amount_partial_get() {
586 assert_eq!(TrustAmountPartial::default().get().get(), 40);
587 }
588
589 #[test]
590 fn trust_amount_root_get() {
591 assert_eq!(TrustAmountRoot::default().get().get(), 120);
592 }
593
594 #[test]
595 fn web_of_trust_root_fingerprint() -> TestResult {
596 let fingerprint: OpenpgpFingerprint = "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?;
597 assert_eq!(
598 WebOfTrustRoot::new(fingerprint.clone(), TrustAmountRoot::default()).fingerprint(),
599 &fingerprint
600 );
601
602 Ok(())
603 }
604
605 #[test]
606 fn web_of_trust_root_amount() -> TestResult {
607 assert_eq!(
608 WebOfTrustRoot::new(
609 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
610 TrustAmountRoot::default()
611 )
612 .amount(),
613 TrustAmountRoot::default()
614 );
615
616 Ok(())
617 }
618
619 #[test]
620 fn web_of_trust_mode_root() -> TestResult {
621 let root = HashSet::from_iter([
622 WebOfTrustRoot::new(
623 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
624 TrustAmountRoot::default(),
625 ),
626 WebOfTrustRoot::new(
627 "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
628 TrustAmountRoot::default(),
629 ),
630 WebOfTrustRoot::new(
631 "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
632 TrustAmountRoot::default(),
633 ),
634 ]);
635 let mode = WebOfTrustMode::new(
636 TrustAmountFlow::default(),
637 TrustAmountPartial::default(),
638 root.clone(),
639 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
640 );
641
642 assert_eq!(mode.roots(), &root);
643
644 Ok(())
645 }
646
647 #[rstest]
648 #[case::matches("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?, Some(TrustAmountRoot::default()))]
649 #[case::does_not_match("e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?, None)]
650 fn web_of_trust_mode_root_trust_amount(
651 #[case] fingerprint: OpenpgpFingerprint,
652 #[case] amount: Option<TrustAmountRoot>,
653 ) -> TestResult {
654 let mode = WebOfTrustMode::new(
655 TrustAmountFlow::default(),
656 TrustAmountPartial::default(),
657 HashSet::from_iter([WebOfTrustRoot::new(
658 "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
659 TrustAmountRoot::default(),
660 )]),
661 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
662 );
663
664 assert_eq!(mode.root_trust_amount(&fingerprint), amount);
665
666 Ok(())
667 }
668
669 #[test]
670 fn web_of_trust_mode_artifact_verifier_identity_domain_matches() -> TestResult {
671 let domain_matches =
672 HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]);
673 let mode = WebOfTrustMode::new(
674 TrustAmountFlow::default(),
675 TrustAmountPartial::default(),
676 HashSet::new(),
677 domain_matches.clone(),
678 );
679
680 assert_eq!(
681 mode.artifact_verifier_identity_domain_matches(),
682 &domain_matches
683 );
684
685 Ok(())
686 }
687
688 #[test]
689 fn trust_amount_partial_new_fails_on_trust_amount_too_large() {
690 assert!(
691 TrustAmountPartial::new(NonZeroU8::new(121).expect("121 is larger than 0")).is_err()
692 )
693 }
694
695 #[test]
696 fn trust_amount_root_new_fails_on_trust_amount_too_large() {
697 assert!(TrustAmountRoot::new(NonZeroU8::new(121).expect("121 is larger than 0")).is_err())
698 }
699}