voa_config/file/technology/
openpgp.rs

1//! Configuration file handling for OpenPGP related settings.
2//!
3//! All types in this module are used solely in the _configuration file_ representation.
4//! They have pendants in [`crate::config::technology::openpgp`] which are used in _configuration
5//! object_ representation. The difference between _configuration file_ and _configuration object_
6//! representation is usually, that in the former it is allowed to have optional fields, whereas in
7//! the latter all fields must be specific.
8
9use std::collections::HashSet;
10
11use garde::Validate;
12use serde::{Deserialize, Serialize};
13
14use crate::{
15    Error,
16    common::ordered_option_set,
17    config::technology::openpgp::{
18        DomainName,
19        NumCertifications,
20        NumDataSignatures,
21        OpenpgpFingerprint,
22        TrustAmountFlow,
23        TrustAmountPartial,
24        TrustAmountRoot,
25    },
26};
27
28/// The configuration for the "plain" verification method.
29#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
30pub struct ConfigPlainMode {
31    /// The identity of an artifact verifier must match one of the domains.
32    #[serde(
33        skip_serializing_if = "Option::is_none",
34        serialize_with = "ordered_option_set",
35        default
36    )]
37    identity_domain_matches: Option<HashSet<DomainName>>,
38
39    /// The fingerprint of an artifact verifier must match one of the fingerprints.
40    #[serde(
41        skip_serializing_if = "Option::is_none",
42        serialize_with = "ordered_option_set",
43        default
44    )]
45    fingerprint_matches: Option<HashSet<OpenpgpFingerprint>>,
46}
47
48impl ConfigPlainMode {
49    /// Creates a new [`ConfigPlainMode`].
50    // NOTE: Used in tests.
51    #[allow(dead_code)]
52    pub(crate) fn new(
53        identity_domain_matches: Option<HashSet<DomainName>>,
54        fingerprint_matches: Option<HashSet<OpenpgpFingerprint>>,
55    ) -> Self {
56        Self {
57            identity_domain_matches,
58            fingerprint_matches,
59        }
60    }
61
62    /// Returns a reference to the optional set of [`DomainName`] entries.
63    pub fn identity_domain_matches(&self) -> Option<&HashSet<DomainName>> {
64        self.identity_domain_matches.as_ref()
65    }
66
67    /// Returns a reference to the optional set of [`OpenpgpFingerprint`] entries.
68    pub fn fingerprint_matches(&self) -> Option<&HashSet<OpenpgpFingerprint>> {
69        self.fingerprint_matches.as_ref()
70    }
71}
72
73/// Validates that the required number of certifications matches the amount of pinned trust anchors.
74///
75/// # Errors
76///
77/// Returns an error, if the `num_certifications` of a [`ConfigTrustAnchorMode`] is set it is
78/// larger than the number of `trust_anchor_fingerprint_matches`.
79fn validate_required_certifications(
80    trust_anchor_fingerprint_matches: Option<&HashSet<OpenpgpFingerprint>>,
81) -> impl FnOnce(&Option<NumCertifications>, &()) -> garde::Result + '_ {
82    move |num_certifications, _| {
83        let num_certifications = if let Some(num_certifications) = num_certifications {
84            num_certifications.get().get()
85        } else {
86            return Ok(());
87        };
88        let fingerprints =
89            if let Some(trust_anchor_fingerprint_matches) = trust_anchor_fingerprint_matches {
90                trust_anchor_fingerprint_matches
91            } else {
92                return Ok(());
93            };
94        let num_fingerprints = fingerprints.len();
95
96        if num_fingerprints > 0 && num_certifications > num_fingerprints {
97            return Err(garde::Error::new(format!(
98                "is {num_certifications}, but must be <= {num_fingerprints}, as the \"trust anchor\" verification mode only pins {num_fingerprints} trust anchor fingerprint(s): {}",
99                fingerprints
100                    .iter()
101                    .map(ToString::to_string)
102                    .collect::<Vec<_>>()
103                    .join(", ")
104            )));
105        }
106
107        Ok(())
108    }
109}
110
111/// Settings for the trust-anchor mode.
112///
113/// # Note
114///
115/// This struct _must_ be validated after creation.
116#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
117pub struct ConfigTrustAnchorMode {
118    /// The number of certifications from a trust-anchor required to exist for an artifact
119    /// verifier.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    #[garde(custom(validate_required_certifications(self.trust_anchor_fingerprint_matches.as_ref())))]
122    required_certifications: Option<NumCertifications>,
123
124    /// The identity of an artifact verifier must match one of the domains.
125    #[serde(
126        skip_serializing_if = "Option::is_none",
127        serialize_with = "ordered_option_set",
128        default
129    )]
130    #[garde(skip)]
131    artifact_verifier_identity_domain_matches: Option<HashSet<DomainName>>,
132
133    /// The fingerprint of an trust anchor must match one of the fingerprints.
134    #[serde(
135        skip_serializing_if = "Option::is_none",
136        serialize_with = "ordered_option_set",
137        default
138    )]
139    #[garde(skip)]
140    trust_anchor_fingerprint_matches: Option<HashSet<OpenpgpFingerprint>>,
141}
142
143impl ConfigTrustAnchorMode {
144    /// Creates a new [`ConfigTrustAnchorMode`].
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if `required_certifications` is provided, but it is larger than the number
149    /// of pinned trust anchor fingerprints in `trust_anchor_fingerprint_matches`.
150    // NOTE: Used in tests.
151    #[allow(dead_code)]
152    pub fn new(
153        required_certifications: Option<NumCertifications>,
154        artifact_verifier_identity_domain_matches: Option<HashSet<DomainName>>,
155        trust_anchor_fingerprint_matches: Option<HashSet<OpenpgpFingerprint>>,
156    ) -> Result<Self, Error> {
157        let mode = Self {
158            required_certifications,
159            artifact_verifier_identity_domain_matches,
160            trust_anchor_fingerprint_matches,
161        };
162        mode.validate().map_err(|source| Error::Validation {
163            context: "creating a trust anchor verification".to_string(),
164            source,
165        })?;
166
167        Ok(mode)
168    }
169
170    /// Returns the optional [`NumCertifications`].
171    pub fn required_certifications(&self) -> Option<NumCertifications> {
172        self.required_certifications
173    }
174
175    /// Returns a reference to the optional set of artifact verifier [`DomainName`] entries.
176    pub fn artifact_verifier_identity_domain_matches(&self) -> Option<&HashSet<DomainName>> {
177        self.artifact_verifier_identity_domain_matches.as_ref()
178    }
179
180    /// Returns a reference to the optional set of trust anchor [`OpenpgpFingerprint`] entries.
181    pub fn trust_anchor_fingerprint_matches(&self) -> Option<&HashSet<OpenpgpFingerprint>> {
182        self.trust_anchor_fingerprint_matches.as_ref()
183    }
184}
185
186/// A root for the "Web of Trust".
187#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
188pub(crate) struct ConfigWebOfTrustRoot {
189    /// The fingerprint of the trust anchor that acts as "Web of Trust" root.
190    fingerprint: OpenpgpFingerprint,
191
192    /// The trust amount for the root.
193    amount: Option<TrustAmountRoot>,
194}
195
196impl ConfigWebOfTrustRoot {
197    /// Creates a new [`ConfigWebOfTrustRoot`].
198    // NOTE: This method is used in unit tests.
199    #[allow(dead_code)]
200    pub(crate) fn new(fingerprint: OpenpgpFingerprint, amount: Option<TrustAmountRoot>) -> Self {
201        Self {
202            fingerprint,
203            amount,
204        }
205    }
206
207    /// Returns a reference to the [`OpenpgpFingerprint`].
208    pub fn fingerprint(&self) -> &OpenpgpFingerprint {
209        &self.fingerprint
210    }
211
212    /// Returns a reference to the optional [`TrustAmountRoot`].
213    pub fn amount(&self) -> Option<TrustAmountRoot> {
214        self.amount
215    }
216}
217
218/// Settings for the "Web of Trust" model.
219#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
220pub struct ConfigWebOfTrustMode {
221    /// The required flow amount for authentication for a target identity.
222    flow_amount: Option<TrustAmountFlow>,
223
224    /// The numerical trust amount of a partially trusted introducer.
225    partial_amount: Option<TrustAmountPartial>,
226
227    /// The trust roots for the "Web of Trust".
228    ///
229    /// If a trust root defined in this list cannot be found in the set of trust anchors, a
230    /// warning is emitted.
231    ///
232    /// # Note
233    ///
234    /// If this list is empty, all trust-anchors found for a specific context are considered to be
235    /// trust roots.
236    /// If this list is empty and no trust-anchors are found, no artifact
237    /// verifiers will be trusted.
238    #[serde(
239        skip_serializing_if = "Option::is_none",
240        serialize_with = "ordered_option_set",
241        default
242    )]
243    roots: Option<HashSet<ConfigWebOfTrustRoot>>,
244
245    /// The identity of an artifact verifier must match one of the domains.
246    #[serde(
247        skip_serializing_if = "Option::is_none",
248        serialize_with = "ordered_option_set",
249        default
250    )]
251    artifact_verifier_identity_domain_matches: Option<HashSet<DomainName>>,
252}
253
254impl ConfigWebOfTrustMode {
255    /// Creates a new [`ConfigWebOfTrustMode`].
256    pub(crate) fn new(
257        flow_amount: Option<TrustAmountFlow>,
258        partial_amount: Option<TrustAmountPartial>,
259        roots: Option<HashSet<ConfigWebOfTrustRoot>>,
260        artifact_verifier_identity_domain_matches: Option<HashSet<DomainName>>,
261    ) -> Self {
262        Self {
263            flow_amount,
264            partial_amount,
265            roots,
266            artifact_verifier_identity_domain_matches,
267        }
268    }
269
270    /// Returns the optional [`TrustAmountFlow`].
271    pub fn flow_amount(&self) -> Option<TrustAmountFlow> {
272        self.flow_amount
273    }
274
275    /// Returns the optional [`TrustAmountPartial`].
276    pub fn partial_amount(&self) -> Option<TrustAmountPartial> {
277        self.partial_amount
278    }
279
280    /// Returns a reference to the list of trust roots.
281    pub(crate) fn roots(&self) -> Option<&HashSet<ConfigWebOfTrustRoot>> {
282        self.roots.as_ref()
283    }
284
285    /// Returns a reference to the list of [`DomainName`] entries.
286    pub fn artifact_verifier_identity_domain_matches(&self) -> Option<&HashSet<DomainName>> {
287        self.artifact_verifier_identity_domain_matches.as_ref()
288    }
289}
290
291impl Default for ConfigWebOfTrustMode {
292    fn default() -> Self {
293        Self::new(None, None, None, None)
294    }
295}
296
297/// The OpenPGP verification method to use for OpenPGP verifiers.
298///
299/// # Note
300///
301/// This enum _must_ be validated after creation.
302#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
303#[serde(rename_all = "snake_case")]
304pub(crate) enum ConfigVerificationMethod {
305    /// Only use artifact verifiers, ignore trust-anchors.
306    Plain(#[garde(skip)] ConfigPlainMode),
307
308    /// Always use trust-anchors for artifact verifiers.
309    TrustAnchor(#[garde(dive)] ConfigTrustAnchorMode),
310
311    /// Use the "Web of Trust" model.
312    WebOfTrust(#[garde(skip)] ConfigWebOfTrustMode),
313}
314
315impl Default for ConfigVerificationMethod {
316    fn default() -> Self {
317        Self::TrustAnchor(ConfigTrustAnchorMode::default())
318    }
319}
320
321/// Validates that the required number of data signatures matches the verification method.
322///
323/// # Errors
324///
325/// Returns an error, if the `verification_method` is [`ConfigPlainMode`] and its number of
326/// `fingerprint_matches` is fewer than that of `num_data_signatures`.
327fn validate_num_data_signatures(
328    verification_method: &ConfigVerificationMethod,
329) -> impl FnOnce(&Option<NumDataSignatures>, &()) -> garde::Result + '_ {
330    move |num_data_signatures, _| {
331        let num_data_signatures = if let Some(num_data_signatures) = num_data_signatures {
332            num_data_signatures.get().get()
333        } else {
334            return Ok(());
335        };
336
337        if let ConfigVerificationMethod::Plain(mode) = verification_method {
338            let Some(fingerprint_matches) = mode.fingerprint_matches() else {
339                return Ok(());
340            };
341            let num_fingerprints = fingerprint_matches.len();
342            if num_fingerprints > 0 && num_data_signatures > num_fingerprints {
343                return Err(garde::Error::new(format!(
344                    "is {num_data_signatures}, but must be <= {num_fingerprints}, as the plain verification mode only pins {num_fingerprints} artifact verifier fingerprint(s): {}",
345                    fingerprint_matches
346                        .iter()
347                        .map(ToString::to_string)
348                        .collect::<Vec<_>>()
349                        .join(", ")
350                )));
351            }
352        }
353        Ok(())
354    }
355}
356
357/// OpenPGP settings.
358///
359/// # Note
360///
361/// This struct _must_ be validated after creation.
362#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
363pub(crate) struct ConfigOpenpgpSettings {
364    /// The number of signatures required for an artifact to be considered valid.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    #[garde(custom(validate_num_data_signatures(&self.verification_method)))]
367    num_data_signatures: Option<NumDataSignatures>,
368
369    /// The verification method to use.
370    #[garde(dive)]
371    verification_method: ConfigVerificationMethod,
372}
373
374impl ConfigOpenpgpSettings {
375    /// Creates a new [`ConfigOpenpgpSettings`].
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if the validation of the created [`ConfigOpenpgpSettings`] fails.
380    // NOTE: This is used in tests.
381    #[allow(dead_code)]
382    pub(crate) fn new(
383        num_data_signatures: Option<NumDataSignatures>,
384        verification_method: ConfigVerificationMethod,
385    ) -> Result<Self, Error> {
386        let settings = Self {
387            num_data_signatures,
388            verification_method,
389        };
390
391        settings.validate().map_err(|source| Error::Validation {
392            context: "creating OpenPGP settings from file".to_string(),
393            source,
394        })?;
395
396        Ok(settings)
397    }
398
399    /// Returns the optional [`NumDataSignatures`].
400    pub fn num_data_signatures(&self) -> Option<NumDataSignatures> {
401        self.num_data_signatures
402    }
403
404    /// Returns a reference to the [`ConfigVerificationMethod`].
405    pub fn config_verification_method(&self) -> &ConfigVerificationMethod {
406        &self.verification_method
407    }
408}