voa_config/config/technology/openpgp/verification/
trust_anchor.rs1use 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
15const DEFAULT_REQUIRED_CERTIFICATIONS: NonZeroUsize =
17 NonZeroUsize::new(3).expect("3 is greater than 0");
18
19#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
21pub struct NumCertifications(NonZeroUsize);
22
23impl NumCertifications {
24 pub fn new(num: NonZeroUsize) -> Self {
26 Self(num)
27 }
28
29 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
47fn 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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
77pub struct TrustAnchorMode {
78 #[garde(custom(validate_required_certifications(&self.trust_anchor_fingerprint_matches)))]
81 required_certifications: NumCertifications,
82
83 #[serde(serialize_with = "ordered_set")]
85 #[garde(skip)]
86 artifact_verifier_identity_domain_matches: HashSet<DomainName>,
87
88 #[serde(serialize_with = "ordered_set")]
90 #[garde(skip)]
91 trust_anchor_fingerprint_matches: HashSet<OpenpgpFingerprint>,
92}
93
94impl TrustAnchorMode {
95 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 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 pub fn required_certifications(&self) -> NumCertifications {
147 self.required_certifications
148 }
149
150 pub fn artifact_verifier_identity_domain_matches(&self) -> &HashSet<DomainName> {
152 &self.artifact_verifier_identity_domain_matches
153 }
154
155 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 #[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 #[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}