voa_config/config/technology/openpgp/verification/
wot.rs

1//! The "web of trust" OpenPGP verification method.
2
3use 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
18/// Default numerical trust amount for a root in "Web of Trust".
19const DEFAULT_ROOT_AMOUNT: NonZeroU8 = NonZeroU8::new(120).expect("120 is greater than 0");
20
21/// Default numerical trust amount for a partially trusted introducer in "Web of Trust".
22const DEFAULT_PARTIAL_AMOUNT: NonZeroU8 = NonZeroU8::new(40).expect("40 is greater than 0");
23
24/// Default required flow amount for authentication in "Web of Trust".
25const DEFAULT_FLOW_AMOUNT: NonZeroUsize = NonZeroUsize::new(120).expect("120 is greater than 0");
26
27/// The maximum trust amount for partially trusted introducers or trust roots.
28pub(crate) const TRUST_AMOUNT_MAX: u8 = 120;
29
30/// The trust amount required for a flow in a "Web of Trust" network to be considered fully trusted.
31#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
32pub struct TrustAmountFlow(NonZeroUsize);
33
34impl TrustAmountFlow {
35    /// Creates a new [`TrustAmountFlow`] from a [`NonZeroUsize`].
36    pub fn new(num: NonZeroUsize) -> Self {
37        Self(num)
38    }
39
40    /// Returns the inner [`NonZeroUsize`].
41    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/// The trust amount for a root in a "Web of Trust" network.
59#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
60#[serde(try_from = "NonZeroU8")]
61pub struct TrustAmountRoot(NonZeroU8);
62
63impl TrustAmountRoot {
64    /// Creates a new [`TrustAmountRoot`] from a [`NonZeroU8`].
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if `num` is outside the allowed range of 1 - 120.
69    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    /// Returns the inner [`NonZeroU8`].
83    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/// The trust amount for a partially trusted introducer in a "Web of Trust" network.
109#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
110#[serde(try_from = "NonZeroU8")]
111pub struct TrustAmountPartial(NonZeroU8);
112
113impl TrustAmountPartial {
114    /// Creates a new [`TrustAmountPartial`] from a [`NonZeroU8`].
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if `num` is outside the allowed range of 1 - 120.
119    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    /// Returns the inner [`NonZeroU8`].
133    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/// A root for the "Web of Trust".
159#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
160pub struct WebOfTrustRoot {
161    /// The fingerprint of the trust anchor that acts as "Web of Trust" root.
162    fingerprint: OpenpgpFingerprint,
163
164    /// The trust amount for the root.
165    amount: TrustAmountRoot,
166}
167
168impl WebOfTrustRoot {
169    /// Creates a new [`WebOfTrustRoot`].
170    ///
171    /// Takes a [`OpenpgpFingerprint`], which defines the OpenPGP fingerprint of the root and a
172    /// [`TrustAmountRoot`] which sets the trust amount for the root.
173    pub fn new(fingerprint: OpenpgpFingerprint, amount: TrustAmountRoot) -> Self {
174        Self {
175            fingerprint,
176            amount,
177        }
178    }
179
180    /// Returns a reference to the [`OpenpgpFingerprint`].
181    pub fn fingerprint(&self) -> &OpenpgpFingerprint {
182        &self.fingerprint
183    }
184
185    /// Returns a reference to the [`TrustAmountRoot`].
186    pub fn amount(&self) -> TrustAmountRoot {
187        self.amount
188    }
189}
190
191/// Settings for the "Web of Trust" verification mode.
192#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
193pub struct WebOfTrustMode {
194    /// The required flow amount for authentication for a target identity.
195    flow_amount: TrustAmountFlow,
196
197    /// The numerical trust amount of a partially trusted introducer.
198    partial_amount: TrustAmountPartial,
199
200    /// The trust roots for the "Web of Trust".
201    ///
202    /// If a trust root defined in this list cannot be found in the set of trust anchors, a
203    /// warning is emitted.
204    ///
205    /// # Note
206    ///
207    /// If this list is empty, all trust-anchors found for a specific context are considered to
208    /// be trust roots.
209    /// If this list is empty and no trust-anchors are found, no artifact
210    /// verifiers will be trusted.
211    #[serde(serialize_with = "ordered_set")]
212    roots: HashSet<WebOfTrustRoot>,
213
214    /// The identity of an artifact verifier must match one of the domains.
215    #[serde(serialize_with = "ordered_set")]
216    artifact_verifier_identity_domain_matches: HashSet<DomainName>,
217}
218
219impl WebOfTrustMode {
220    /// Creates a new [`WebOfTrustMode`].
221    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    /// Creates a new [`WebOfTrustMode`] from a [`ConfigWebOfTrustMode`] and explicit defaults.
236    ///
237    /// # Note
238    ///
239    /// If found, the trust amount of each [`WebOfTrustRoot`] created from the `config` is
240    /// derived from the `defaults` as well.
241    /// Otherwise, the implicit default is used instead.
242    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    /// Returns the [`TrustAmountFlow`]
272    pub fn flow_amount(&self) -> TrustAmountFlow {
273        self.flow_amount
274    }
275
276    /// Returns the [`TrustAmountPartial`]
277    pub fn partial_amount(&self) -> TrustAmountPartial {
278        self.partial_amount
279    }
280
281    /// Returns a reference to the list of [`WebOfTrustRoot`] entries.
282    pub fn roots(&self) -> &HashSet<WebOfTrustRoot> {
283        &self.roots
284    }
285
286    /// Returns the [`TrustAmountRoot`] for a `root` if it matches the provided `fingerprint`.
287    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    /// Returns a reference to the list of [`DomainName`] entries.
301    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    /// Ensures that [`WebOfTrustMode::from_config_with_defaults`] behaves consistently.
381    #[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}