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

1//! The "plain" OpenPGP verification method.
2
3use std::{collections::HashSet, fmt::Display};
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    common::{ordered_set, set_to_vec},
9    config::technology::openpgp::{DomainName, OpenpgpFingerprint},
10    file::ConfigPlainMode,
11};
12
13/// The configuration for the "plain" verification method.
14#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
15pub struct PlainMode {
16    /// The identity of an artifact verifier must match one of the domains.
17    #[serde(serialize_with = "ordered_set", default)]
18    identity_domain_matches: HashSet<DomainName>,
19
20    /// The fingerprint of an artifact verifier must match one of the fingerprints.
21    #[serde(serialize_with = "ordered_set", default)]
22    fingerprint_matches: HashSet<OpenpgpFingerprint>,
23}
24
25impl PlainMode {
26    /// Creates a new [`PlainMode`].
27    pub fn new(
28        identity_domain_matches: HashSet<DomainName>,
29        fingerprint_matches: HashSet<OpenpgpFingerprint>,
30    ) -> Self {
31        Self {
32            identity_domain_matches,
33            fingerprint_matches,
34        }
35    }
36
37    /// Creates a new [`PlainMode`] from a [`ConfigPlainMode`] and defaults.
38    pub(crate) fn from_config_with_defaults(
39        config: &ConfigPlainMode,
40        defaults: &PlainMode,
41    ) -> Self {
42        Self::new(
43            config
44                .identity_domain_matches()
45                .unwrap_or(defaults.identity_domain_matches())
46                .clone(),
47            config
48                .fingerprint_matches()
49                .unwrap_or(defaults.fingerprint_matches())
50                .clone(),
51        )
52    }
53
54    /// Returns a reference to the list of [`DomainName`] entries.
55    pub fn identity_domain_matches(&self) -> &HashSet<DomainName> {
56        &self.identity_domain_matches
57    }
58
59    /// Returns a reference to the list of [`OpenpgpFingerprint`] entries.
60    pub fn fingerprint_matches(&self) -> &HashSet<OpenpgpFingerprint> {
61        &self.fingerprint_matches
62    }
63}
64
65impl Display for PlainMode {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        writeln!(
68            f,
69            "✅ Each artifact is verified using the \"plain\" verification method.\n"
70        )?;
71
72        if !self.identity_domain_matches().is_empty() {
73            writeln!(
74                f,
75                "📧 A valid certificate must have a valid User ID that uses one of the following domains to be considered as artifact verifier:"
76            )?;
77            for domain_name in set_to_vec(self.identity_domain_matches()).iter() {
78                writeln!(f, "⤷ {domain_name}")?;
79            }
80            writeln!(f)?;
81        } else {
82            writeln!(
83                f,
84                "📧 A valid certificate is not required to use a specific domain in any of its User IDs to be considered as artifact verifier.\n"
85            )?;
86        }
87
88        if !self.fingerprint_matches().is_empty() {
89            writeln!(
90                f,
91                "🐾 A certificate must match one of the following OpenPGP fingerprints to be considered as artifact verifier:"
92            )?;
93            for fingerprint in set_to_vec(self.fingerprint_matches()).iter() {
94                writeln!(f, "⤷ {fingerprint}")?;
95            }
96        } else {
97            writeln!(
98                f,
99                "🐾 A certificate is not required to match a specific OpenPGP fingerprint to be considered as artifact verifier."
100            )?;
101        }
102
103        Ok(())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use rstest::rstest;
110    use testresult::TestResult;
111
112    use super::*;
113
114    #[rstest]
115    #[case::some_domains_and_fingerprints(
116        HashSet::from_iter(["example.org".parse()?]),
117        HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
118    )]
119    #[case::no_domains_and_fingerprints(HashSet::new(), HashSet::new())]
120    fn plain_mode_new(
121        #[case] domain_names: HashSet<DomainName>,
122        #[case] fingerprints: HashSet<OpenpgpFingerprint>,
123    ) -> TestResult {
124        let mode = PlainMode::new(domain_names.clone(), fingerprints.clone());
125
126        assert_eq!(mode.identity_domain_matches(), &domain_names);
127        assert_eq!(mode.fingerprint_matches(), &fingerprints);
128
129        Ok(())
130    }
131
132    #[rstest]
133    #[case::all_defaults(ConfigPlainMode::default(), PlainMode::default(), PlainMode::default())]
134    #[case::config_empty_lists_vs_default_defaults(
135        ConfigPlainMode::new(Some(HashSet::new()), Some(HashSet::new())),
136        PlainMode::default(),
137        PlainMode::default()
138    )]
139    #[case::default_config_vs_custom_defaults(
140        ConfigPlainMode::default(),
141        PlainMode::new(
142            HashSet::from_iter(["example.org".parse()?]),
143            HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
144        ),
145        PlainMode::new(
146            HashSet::from_iter(["example.org".parse()?]),
147            HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
148        )
149    )]
150    #[case::config_empty_lists_vs_custom_defaults(
151        ConfigPlainMode::new(Some(HashSet::new()), Some(HashSet::new())),
152        PlainMode::new(
153            HashSet::from_iter(["example.org".parse()?]),
154            HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
155        ),
156        PlainMode::new(
157            HashSet::new(),
158            HashSet::new(),
159        )
160    )]
161    #[case::custom_config_vs_custom_defaults(
162        ConfigPlainMode::new(
163            Some(HashSet::from_iter(["other.org".parse()?])),
164            Some(HashSet::from_iter(["e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?]))
165        ),
166        PlainMode::new(
167            HashSet::from_iter(["example.org".parse()?]),
168            HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
169        ),
170        PlainMode::new(
171            HashSet::from_iter(["other.org".parse()?]),
172            HashSet::from_iter(["e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?])
173        )
174    )]
175    fn plain_mode_from_config_with_defaults(
176        #[case] config: ConfigPlainMode,
177        #[case] defaults: PlainMode,
178        #[case] expected: PlainMode,
179    ) -> TestResult {
180        let mode = PlainMode::from_config_with_defaults(&config, &defaults);
181
182        assert_eq!(mode, expected);
183
184        Ok(())
185    }
186}