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

1//! The "simple trust anchor" OpenPGP verification method.
2
3use std::{collections::HashSet, fmt::Display, num::NonZeroUsize};
4
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    Error,
10    common::{ordered_set, set_to_vec},
11    config::technology::openpgp::{DomainName, OpenpgpFingerprint},
12    file::ConfigTrustAnchorMode,
13};
14
15/// Default minimum required amount of certifications for trust anchor mode.
16const DEFAULT_REQUIRED_CERTIFICATIONS: NonZeroUsize =
17    NonZeroUsize::new(3).expect("3 is greater than 0");
18
19/// The required number of certifications for an identity on an artifact verifier.
20#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
21pub struct NumCertifications(NonZeroUsize);
22
23impl NumCertifications {
24    /// Creates a new [`NumCertifications`] from a [`NonZeroUsize`].
25    pub fn new(num: NonZeroUsize) -> Self {
26        Self(num)
27    }
28
29    /// Returns the inner [`NonZeroUsize`].
30    pub fn get(&self) -> NonZeroUsize {
31        self.0
32    }
33}
34
35impl Default for NumCertifications {
36    fn default() -> Self {
37        Self(DEFAULT_REQUIRED_CERTIFICATIONS)
38    }
39}
40
41impl Display for NumCertifications {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47/// Validates that the required number of certifications matches the amount of pinned trust anchors.
48///
49/// # Errors
50///
51/// Returns an error, if the `num_certifications` of a [`ConfigTrustAnchorMode`] is larger than the
52/// number of `trust_anchor_fingerprint_matches`.
53fn validate_required_certifications(
54    trust_anchor_fingerprint_matches: &HashSet<OpenpgpFingerprint>,
55) -> impl FnOnce(&NumCertifications, &()) -> garde::Result + '_ {
56    move |num_certifications, _| {
57        let num_certifications = num_certifications.get().get();
58        let num_fingerprints = trust_anchor_fingerprint_matches.len();
59
60        if num_fingerprints > 0 && num_certifications > num_fingerprints {
61            return Err(garde::Error::new(format!(
62                "is {num_certifications}, but must be <= {num_fingerprints}, as the \"trust anchor\" verification mode only pins {num_fingerprints} trust anchor fingerprint(s): {}",
63                trust_anchor_fingerprint_matches
64                    .iter()
65                    .map(ToString::to_string)
66                    .collect::<Vec<_>>()
67                    .join(", ")
68            )));
69        }
70
71        Ok(())
72    }
73}
74
75/// Settings for the trust-anchor mode.
76#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
77pub struct TrustAnchorMode {
78    /// The number of certifications from a trust-anchor required to exist for an artifact
79    /// verifier.
80    #[garde(custom(validate_required_certifications(&self.trust_anchor_fingerprint_matches)))]
81    required_certifications: NumCertifications,
82
83    /// The identity of an artifact verifier must match one of the domains.
84    #[serde(serialize_with = "ordered_set")]
85    #[garde(skip)]
86    artifact_verifier_identity_domain_matches: HashSet<DomainName>,
87
88    /// The fingerprint of a trust anchor must match one of the fingerprints.
89    #[serde(serialize_with = "ordered_set")]
90    #[garde(skip)]
91    trust_anchor_fingerprint_matches: HashSet<OpenpgpFingerprint>,
92}
93
94impl TrustAnchorMode {
95    /// Creates a new [`TrustAnchorMode`].
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if `required_certifications` is larger than the length of
100    /// `trust_anchor_fingerprint_matches`.
101    pub fn new(
102        required_certifications: NumCertifications,
103        artifact_verifier_identity_domain_matches: HashSet<DomainName>,
104        trust_anchor_fingerprint_matches: HashSet<OpenpgpFingerprint>,
105    ) -> Result<Self, Error> {
106        let mode = Self {
107            required_certifications,
108            artifact_verifier_identity_domain_matches,
109            trust_anchor_fingerprint_matches,
110        };
111
112        mode.validate().map_err(|source| Error::Validation {
113            context: "creating a trust anchor verification mode object".to_string(),
114            source,
115        })?;
116
117        Ok(mode)
118    }
119
120    /// Creates a new [`TrustAnchorMode`] from a [`ConfigTrustAnchorMode`] and another
121    /// [`TrustAnchorMode`] for defaults.
122    pub(crate) fn from_config_with_defaults(
123        config: &ConfigTrustAnchorMode,
124        defaults: &TrustAnchorMode,
125    ) -> Result<Self, Error> {
126        let required_certifications = config
127            .required_certifications()
128            .unwrap_or(defaults.required_certifications());
129        let artifact_verifier_identity_domain_matches = config
130            .artifact_verifier_identity_domain_matches()
131            .unwrap_or(defaults.artifact_verifier_identity_domain_matches())
132            .clone();
133        let trust_anchor_fingerprint_matches = config
134            .trust_anchor_fingerprint_matches()
135            .unwrap_or(defaults.trust_anchor_fingerprint_matches())
136            .clone();
137
138        TrustAnchorMode::new(
139            required_certifications,
140            artifact_verifier_identity_domain_matches,
141            trust_anchor_fingerprint_matches,
142        )
143    }
144
145    /// Returns the required number of certifications from a trust-anchor on an artifact verifier.
146    pub fn required_certifications(&self) -> NumCertifications {
147        self.required_certifications
148    }
149
150    /// Returns a slice of the list of domains an artifact verifier must match at least one of.
151    pub fn artifact_verifier_identity_domain_matches(&self) -> &HashSet<DomainName> {
152        &self.artifact_verifier_identity_domain_matches
153    }
154
155    /// Returns a slice of the list of fingerprints a trust-anchor must match at least one of.
156    pub fn trust_anchor_fingerprint_matches(&self) -> &HashSet<OpenpgpFingerprint> {
157        &self.trust_anchor_fingerprint_matches
158    }
159}
160
161impl Display for TrustAnchorMode {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        writeln!(
164            f,
165            "✅ Each artifact is verified using the \"trust anchor\" verification method.\n"
166        )?;
167
168        if !self.artifact_verifier_identity_domain_matches().is_empty() {
169            writeln!(
170                f,
171                "📧 A valid certificate must have a valid User ID that uses one of the following domains and has {} certification(s) from individual trust anchors on it for the certificate to be considered as artifact verifier:",
172                self.required_certifications()
173            )?;
174            for domain_name in set_to_vec(self.artifact_verifier_identity_domain_matches()).iter() {
175                writeln!(f, "⤷ {domain_name}")?;
176            }
177            writeln!(f)?;
178        } else {
179            writeln!(
180                f,
181                "📧 A valid certificate is not required to use a specific domain in any of its User IDs, but needs {} certification(s) from individual trust anchors on one User ID for the certificate to be considered as artifact verifier.\n",
182                self.required_certifications()
183            )?;
184        }
185
186        if !self.trust_anchor_fingerprint_matches().is_empty() {
187            writeln!(
188                f,
189                "🐾 A valid certificate must match one of the following OpenPGP fingerprints to be considered as trust anchor:"
190            )?;
191            for fingerprint in set_to_vec(self.trust_anchor_fingerprint_matches()).iter() {
192                writeln!(f, "⤷ {fingerprint}")?;
193            }
194        } else {
195            writeln!(
196                f,
197                "🐾 A valid certificate is not required to match a specific OpenPGP fingerprint to be considered as trust anchor."
198            )?;
199        }
200
201        Ok(())
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use rstest::rstest;
208    use testresult::TestResult;
209
210    use super::*;
211
212    /// Ensures that [`TrustAnchorMode::new`] fails if the provided number of required
213    /// certifications is larger than the number of pinned trust anchors (if there are any).
214    #[test]
215    fn trust_anchor_mode_new_fails_on_too_many_certifications() -> TestResult {
216        let result = TrustAnchorMode::new(
217            NumCertifications::new(NonZeroUsize::new(5).expect("5 is larger than 0")),
218            HashSet::new(),
219            HashSet::from_iter([
220                "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
221                "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
222                "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
223            ]),
224        );
225
226        match result {
227            Err(Error::Validation { .. }) => {}
228            Ok(mode) => {
229                panic!("Should have failed with an Error::Validation but succeeded: {mode:?}")
230            }
231            Err(error) => panic!(
232                "Should have failed with an Error::Validation but failed with a different error: {error}"
233            ),
234        }
235        Ok(())
236    }
237
238    /// Ensures that [`TrustAnchorMode::from_config_with_defaults`] can create a
239    /// [`TrustAnchorMode`] from a [`ConfigTrustAnchorMode`] robustly.
240    #[rstest]
241    #[case::all_defaults(
242        ConfigTrustAnchorMode::default(),
243        TrustAnchorMode::default(),
244        TrustAnchorMode::default()
245    )]
246    #[case::config_and_defaults_with_empty_lists(
247        ConfigTrustAnchorMode::new(
248            Some(NumCertifications::new(NonZeroUsize::new(5).expect("5 is larger than 0"))),
249            Some(HashSet::new()),
250            Some(HashSet::new())
251        )?,
252        TrustAnchorMode::new(
253            NumCertifications::new(NonZeroUsize::new(6).expect("6 is larger than 0")),
254            HashSet::new(),
255            HashSet::new(),
256        )?,
257        TrustAnchorMode::new(
258            NumCertifications::new(NonZeroUsize::new(5).expect("5 is larger than 0")),
259            HashSet::new(),
260            HashSet::new(),
261        )?,
262    )]
263    #[case::config_and_defaults_with_filled_lists(
264        ConfigTrustAnchorMode::new(
265            None,
266            Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])),
267            Some(HashSet::from_iter([
268                "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
269                "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
270                "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
271            ])),
272        )?,
273        TrustAnchorMode::new(
274            NumCertifications::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
275            HashSet::from_iter(["other-example.org".parse()?, "sub.other-example.org".parse()?]),
276            HashSet::from_iter([
277                "d3b0f7c0b825ecbb0f0d7398072947e7b1537b6f".parse()?,
278                "b787a81c32997fd39a5f4c0188363902d3586e7b".parse()?,
279                "6132b58967cf1ebc05062492c17145e5ee9f82a8".parse()?,
280            ]),
281        )?,
282        TrustAnchorMode::new(
283            NumCertifications::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
284            HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
285            HashSet::from_iter([
286                "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
287                "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
288                "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
289            ]),
290        )?,
291    )]
292    #[case::config_with_empty_and_defaults_with_filled_lists(
293        ConfigTrustAnchorMode::new(
294            None,
295            Some(HashSet::new()),
296            Some(HashSet::new()),
297        )?,
298        TrustAnchorMode::new(
299            NumCertifications::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
300            HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
301            HashSet::from_iter([
302                "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
303                "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
304                "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
305            ]),
306        )?,
307        TrustAnchorMode::new(
308            NumCertifications::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
309            HashSet::new(),
310            HashSet::new(),
311        )?,
312    )]
313    fn trust_anchor_mode_from_config_with_defaults(
314        #[case] config: ConfigTrustAnchorMode,
315        #[case] defaults: TrustAnchorMode,
316        #[case] output: TrustAnchorMode,
317    ) -> TestResult {
318        let created = TrustAnchorMode::from_config_with_defaults(&config, &defaults)?;
319        eprintln!("{}", created.required_certifications());
320
321        assert_eq!(created, output);
322
323        Ok(())
324    }
325}