1use 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
14const DEFAULT_REQUIRED_SIGNATURES: NonZeroUsize =
16 NonZeroUsize::new(1).expect("1 is greater than 0");
17
18#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
20pub struct NumDataSignatures(NonZeroUsize);
21
22impl NumDataSignatures {
23 pub fn new(num: NonZeroUsize) -> Self {
25 Self(num)
26 }
27
28 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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
48#[serde(rename_all = "snake_case")]
49pub enum VerificationMethod {
50 Plain(#[garde(skip)] PlainMode),
52
53 TrustAnchor(#[garde(dive)] TrustAnchorMode),
55
56 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
76fn 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#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
100pub struct OpenpgpSettings {
101 #[garde(custom(validate_num_data_signatures(&self.verification_method)))]
103 num_data_signatures: NumDataSignatures,
104
105 #[garde(dive)]
107 pub(crate) verification_method: VerificationMethod,
108}
109
110impl OpenpgpSettings {
111 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 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 (
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 (ConfigVerificationMethod::Plain(config_mode), _) => VerificationMethod::Plain(
162 PlainMode::from_config_with_defaults(config_mode, &PlainMode::default()),
163 ),
164 (
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 (ConfigVerificationMethod::TrustAnchor(config_mode), _) => {
175 VerificationMethod::TrustAnchor(TrustAnchorMode::from_config_with_defaults(
176 config_mode,
177 &TrustAnchorMode::default(),
178 )?)
179 }
180 (
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 (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 pub fn num_data_signatures(&self) -> NumDataSignatures {
204 self.num_data_signatures
205 }
206
207 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 #[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 #[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 #[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 #[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}