voa_config/config/technology/openpgp/
settings.rs

1//! The top-level settings object for OpenPGP related settings.
2
3use std::{fmt::Display, num::NonZeroUsize};
4
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9    Error,
10    config::technology::openpgp::{PlainMode, TrustAnchorMode, WebOfTrustMode},
11    file::{ConfigOpenpgpSettings, ConfigVerificationMethod},
12};
13
14/// Default required amount of signatures for an artifact.
15const DEFAULT_REQUIRED_SIGNATURES: NonZeroUsize =
16    NonZeroUsize::new(1).expect("1 is greater than 0");
17
18/// The required number of data signatures for an artifact.
19#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
20pub struct NumDataSignatures(NonZeroUsize);
21
22impl NumDataSignatures {
23    /// Creates a new [`NumDataSignatures`] from a [`NonZeroUsize`].
24    pub fn new(num: NonZeroUsize) -> Self {
25        Self(num)
26    }
27
28    /// Returns the inner [`NonZeroUsize`].
29    pub fn get(&self) -> NonZeroUsize {
30        self.0
31    }
32}
33
34impl Default for NumDataSignatures {
35    fn default() -> Self {
36        NumDataSignatures(DEFAULT_REQUIRED_SIGNATURES)
37    }
38}
39
40impl Display for NumDataSignatures {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        write!(f, "{}", self.0)
43    }
44}
45
46/// The OpenPGP verification method to use for OpenPGP verifiers.
47#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
48#[serde(rename_all = "snake_case")]
49pub enum VerificationMethod {
50    /// Only use artifact verifiers, ignore trust-anchors.
51    Plain(#[garde(skip)] PlainMode),
52
53    /// Always use trust-anchors for artifact verifiers.
54    TrustAnchor(#[garde(dive)] TrustAnchorMode),
55
56    /// Use the "Web of Trust" model.
57    WebOfTrust(#[garde(skip)] WebOfTrustMode),
58}
59
60impl Default for VerificationMethod {
61    fn default() -> Self {
62        Self::TrustAnchor(TrustAnchorMode::default())
63    }
64}
65
66impl Display for VerificationMethod {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Plain(mode) => write!(f, "{mode}"),
70            Self::TrustAnchor(mode) => write!(f, "{mode}"),
71            Self::WebOfTrust(mode) => write!(f, "{mode}"),
72        }
73    }
74}
75
76/// Validates that the required number of data signatures matches the verification method.
77///
78/// # Errors
79///
80/// Returns an error, if the `verification_method` is [`PlainMode`] and its number of
81/// `fingerprint_matches` is fewer than that of `num_data_signatures`.
82fn validate_num_data_signatures(
83    verification_method: &VerificationMethod,
84) -> impl FnOnce(&NumDataSignatures, &()) -> garde::Result + '_ {
85    move |num_data_signatures, _| {
86        if let VerificationMethod::Plain(mode) = verification_method {
87            let num_fingerprints = mode.fingerprint_matches().len();
88            if num_fingerprints > 0 && num_data_signatures.get().get() > num_fingerprints {
89                return Err(garde::Error::new(format!(
90                    "must be <= {num_fingerprints}, as the plain verification mode pins {num_fingerprints} artifact verifier fingerprint(s)"
91                )));
92            }
93        }
94        Ok(())
95    }
96}
97
98/// OpenPGP settings.
99#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
100pub struct OpenpgpSettings {
101    /// The number of signatures required for an artifact to be considered valid.
102    #[garde(custom(validate_num_data_signatures(&self.verification_method)))]
103    num_data_signatures: NumDataSignatures,
104
105    /// The verification method to use.
106    #[garde(dive)]
107    pub(crate) verification_method: VerificationMethod,
108}
109
110impl OpenpgpSettings {
111    /// Creates a new [`OpenpgpSettings`].
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if validation for the created [`OpenpgpSettings`] fails:
116    /// The `verification_method` uses [`PlainMode`] and `num_data_signatures` is larger than the
117    /// length of the verification mode's `fingerprint_matches`,
118    pub fn new(
119        num_data_signatures: NumDataSignatures,
120        verification_method: VerificationMethod,
121    ) -> Result<Self, Error> {
122        let settings = Self {
123            num_data_signatures,
124            verification_method,
125        };
126
127        settings.validate().map_err(|source| Error::Validation {
128            context: "creating an OpenPGP settings object".to_string(),
129            source,
130        })?;
131
132        Ok(settings)
133    }
134
135    /// Creates a new [`OpenpgpSettings`] from a [`ConfigOpenpgpSettings`] and explicit defaults.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if [`TrustAnchorMode::from_config_with_defaults`] fails.
140    pub(crate) fn from_config_with_defaults(
141        config_openpgp_settings: &ConfigOpenpgpSettings,
142        defaults: &OpenpgpSettings,
143    ) -> Result<Self, Error> {
144        let num_data_signatures = config_openpgp_settings
145            .num_data_signatures()
146            .unwrap_or(defaults.num_data_signatures());
147        let verification_method = match (
148            config_openpgp_settings.config_verification_method(),
149            defaults.verification_method(),
150        ) {
151            // Replace unset fields on the `ConfigPlainMode` with the specific fields of a
152            // `TrustAnchorMode`.
153            (
154                ConfigVerificationMethod::Plain(config_mode),
155                VerificationMethod::Plain(default_mode),
156            ) => VerificationMethod::Plain(PlainMode::from_config_with_defaults(
157                config_mode,
158                default_mode,
159            )),
160            // Replace unset fields on the `ConfigPlainMode` with inherent defaults.
161            (ConfigVerificationMethod::Plain(config_mode), _) => VerificationMethod::Plain(
162                PlainMode::from_config_with_defaults(config_mode, &PlainMode::default()),
163            ),
164            // Replace unset fields on the `ConfigTrustAnchorMode` with the specific fields of a
165            // `TrustAnchorMode`.
166            (
167                ConfigVerificationMethod::TrustAnchor(config_mode),
168                VerificationMethod::TrustAnchor(defaults_mode),
169            ) => VerificationMethod::TrustAnchor(TrustAnchorMode::from_config_with_defaults(
170                config_mode,
171                defaults_mode,
172            )?),
173            // Replace unset fields on the `ConfigTrustAnchorMode` with inherent defaults.
174            (ConfigVerificationMethod::TrustAnchor(config_mode), _) => {
175                VerificationMethod::TrustAnchor(TrustAnchorMode::from_config_with_defaults(
176                    config_mode,
177                    &TrustAnchorMode::default(),
178                )?)
179            }
180            // Replace unset fields on the `ConfigWebOfTrustMode` with the specific fields of a
181            // `WebOfTrustMode`.
182            (
183                ConfigVerificationMethod::WebOfTrust(config_mode),
184                VerificationMethod::WebOfTrust(defaults_mode),
185            ) => VerificationMethod::WebOfTrust(WebOfTrustMode::from_config_with_defaults(
186                config_mode,
187                defaults_mode,
188            )),
189            // Replace unset fields on the `ConfigWebOfTrustMode` with inherent defaults.
190            (ConfigVerificationMethod::WebOfTrust(config_mode), _) => {
191                VerificationMethod::WebOfTrust(WebOfTrustMode::from_config_with_defaults(
192                    config_mode,
193                    &WebOfTrustMode::default(),
194                ))
195            }
196        };
197
198        Self::new(num_data_signatures, verification_method)
199    }
200
201    /// Returns the required number of data signatures required for an artifact to be considered
202    /// valid.
203    pub fn num_data_signatures(&self) -> NumDataSignatures {
204        self.num_data_signatures
205    }
206
207    /// Returns a reference to the OpenPGP verification method.
208    pub fn verification_method(&self) -> &VerificationMethod {
209        &self.verification_method
210    }
211}
212
213impl Display for OpenpgpSettings {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        writeln!(f, "OpenPGP settings\n")?;
216        writeln!(
217            f,
218            "🔏 Each artifact requires {} valid data signature(s) from artifact verifiers to be successfully verified.\n",
219            self.num_data_signatures
220        )?;
221
222        write!(f, "{}", self.verification_method())
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use std::{collections::HashSet, num::NonZeroU8};
229
230    use rstest::rstest;
231    use testresult::TestResult;
232
233    use super::*;
234    use crate::{
235        config::technology::openpgp::{
236            NumCertifications,
237            TrustAmountFlow,
238            TrustAmountPartial,
239            TrustAmountRoot,
240            WebOfTrustRoot,
241        },
242        file::{ConfigWebOfTrustMode, ConfigWebOfTrustRoot},
243    };
244
245    /// Ensures that [`OpenpgpSettings::from_config_with_defaults`] behaves consistently.
246    #[rstest]
247    #[case::all_defaults(
248        ConfigOpenpgpSettings::default(),
249        OpenpgpSettings::default(),
250        OpenpgpSettings::default()
251    )]
252    #[case::default_config_and_custom_defaults(
253        ConfigOpenpgpSettings::default(),
254        OpenpgpSettings::new(
255            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
256            VerificationMethod::Plain(PlainMode::new(
257                HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
258                HashSet::from_iter([
259                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
260                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?
261                ])
262            ))
263        )?,
264        OpenpgpSettings::new(
265            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
266            VerificationMethod::TrustAnchor(TrustAnchorMode::default())
267        )?
268    )]
269    #[case::custom_config_and_default_defaults(
270        ConfigOpenpgpSettings::new(
271            Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
272            ConfigVerificationMethod::WebOfTrust(
273                ConfigWebOfTrustMode::new(
274                    Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
275                    Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
276                    Some(HashSet::from_iter([
277                        ConfigWebOfTrustRoot::new(
278                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
279                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
280                        ),
281                        ConfigWebOfTrustRoot::new(
282                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
283                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
284                        ),
285                    ])),
286                    Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]))
287                )
288            )
289        )?,
290        OpenpgpSettings::default(),
291        OpenpgpSettings::new(
292            NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0")),
293            VerificationMethod::WebOfTrust(
294                WebOfTrustMode::new(
295                    TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
296                    TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
297                    HashSet::from_iter([
298                        WebOfTrustRoot::new(
299                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
300                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
301                        ),
302                        WebOfTrustRoot::new(
303                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
304                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
305                        ),
306                    ]),
307                    HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])
308                )
309            )
310        )?
311    )]
312    #[case::custom_config_and_custom_defaults(
313        ConfigOpenpgpSettings::new(
314            Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
315            ConfigVerificationMethod::WebOfTrust(
316                ConfigWebOfTrustMode::new(
317                    Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
318                    Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
319                    Some(HashSet::from_iter([
320                        ConfigWebOfTrustRoot::new(
321                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
322                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
323                        ),
324                        ConfigWebOfTrustRoot::new(
325                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
326                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
327                        ),
328                    ])),
329                    Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]))
330                )
331            )
332        )?,
333        OpenpgpSettings::new(
334            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
335            VerificationMethod::Plain(PlainMode::new(
336                HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
337                HashSet::from_iter([
338                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
339                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?
340                ])
341            ))
342        )?,
343        OpenpgpSettings::new(
344            NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0")),
345            VerificationMethod::WebOfTrust(
346                WebOfTrustMode::new(
347                    TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
348                    TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
349                    HashSet::from_iter([
350                        WebOfTrustRoot::new(
351                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
352                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
353                        ),
354                        WebOfTrustRoot::new(
355                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
356                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
357                        ),
358                    ]),
359                    HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])
360                )
361            )
362        )?
363    )]
364    fn openpgp_settings_from_config_with_defaults(
365        #[case] config: ConfigOpenpgpSettings,
366        #[case] defaults: OpenpgpSettings,
367        #[case] expected_output: OpenpgpSettings,
368    ) -> TestResult {
369        let config = OpenpgpSettings::from_config_with_defaults(&config, &defaults)?;
370        assert_eq!(config, expected_output);
371        Ok(())
372    }
373
374    /// Ensures that [`OpenpgpSettings::validate`] succeeds if the `num_data_signatures` is equal to
375    /// or smaller than the number of `fingerprint_matches` in a [`PlainMode`] verification method.
376    #[test]
377    fn openpgp_settings_validate_succeeds_with_plain_mode() -> TestResult {
378        let result = OpenpgpSettings::new(
379            NumDataSignatures::new(NonZeroUsize::new(1).expect("2 is larger than 0")),
380            VerificationMethod::Plain(PlainMode::new(
381                HashSet::new(),
382                HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
383            )),
384        );
385
386        match result {
387            Ok(settings) => {
388                eprintln!("{}", settings.num_data_signatures());
389            }
390            Err(error) => {
391                panic!("Should have succeeded but failed instead: {error}")
392            }
393        }
394
395        Ok(())
396    }
397
398    /// Ensures that [`OpenpgpSettings::validate`] fails if the `num_data_signatures` is larger than
399    /// the number of `fingerprint_matches` in a [`PlainMode`].
400    #[test]
401    fn openpgp_settings_validate_fails_on_num_data_signature_mismatch() -> TestResult {
402        let result = OpenpgpSettings::new(
403            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
404            VerificationMethod::Plain(PlainMode::new(
405                HashSet::new(),
406                HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
407            )),
408        );
409
410        match result {
411            Err(Error::Validation { .. }) => {}
412            Err(error) => panic!(
413                "Should have failed with an Error::Validation but failed differently: {error}"
414            ),
415            Ok(settings) => {
416                panic!(
417                    "Should have failed with a garde::error::Error but succeeded instead: {settings:?}"
418                )
419            }
420        }
421
422        Ok(())
423    }
424
425    /// Ensures that [`OpenpgpSettings::validate`] succeeds if the `required_certifications` is
426    /// equal to or smaller than the number of `trust_anchor_fingerprint_matches` in a
427    /// [`TrustAnchorMode`] verification method.
428    #[test]
429    fn openpgp_settings_validate_succeeds_with_trust_anchor_mode() -> TestResult {
430        let result = OpenpgpSettings::new(
431            NumDataSignatures::new(NonZeroUsize::new(1).expect("2 is larger than 0")),
432            VerificationMethod::TrustAnchor(TrustAnchorMode::new(
433                NumCertifications::new(NonZeroUsize::new(3).expect("3 is larger than 0")),
434                HashSet::new(),
435                HashSet::from_iter([
436                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
437                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
438                    "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
439                ]),
440            )?),
441        );
442
443        match result {
444            Ok(_settings) => {}
445            Err(error) => {
446                panic!("Should have succeeded but failed instead: {error}")
447            }
448        }
449
450        Ok(())
451    }
452}