voa_openpgp/voa.rs
1//! VOA technology implementation for OpenPGP
2
3use std::{
4 collections::{HashMap, hash_map::Entry},
5 fs::File,
6};
7
8use log::{debug, info, warn};
9use rpgpie::certificate::Certificate;
10use voa_core::{
11 Voa,
12 identifiers::{Context, Os, Purpose, Technology},
13};
14
15use crate::{FILE_SUFFIX, OpenpgpCert, cert::UncheckedOpenpgpCert};
16
17/// An OpenPGP-specific view of a VOA hierarchy
18#[derive(Debug)]
19pub struct VoaOpenpgp(Voa);
20
21impl Default for VoaOpenpgp {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl VoaOpenpgp {
28 /// Initializes a new [`VoaOpenpgp`] instance.
29 ///
30 /// # Note
31 ///
32 /// Depending on the effective user ID of the calling process, [`VoaOpenpgp::lookup`] later
33 /// happens either in system or in user mode.
34 pub fn new() -> VoaOpenpgp {
35 info!("Initializing VOA OpenPGP instance");
36
37 Self(Voa::new())
38 }
39
40 /// Retrieves OpenPGP verifiers from available VOA hierarchies based on a query.
41 ///
42 /// Verifiers are returned as OpenPGP certificates.
43 /// Valid verifiers are considered, if they match the VOA identifiers `os`, `purpose` and
44 /// `context`. Invalid verifiers are skipped.
45 ///
46 /// # Note
47 ///
48 /// Skips OpenPGP verifiers under certain conditions and emits warning messages instead:
49 ///
50 /// - an OpenPGP verifier file name is not valid UTF-8,
51 /// - an OpenPGP verifier file cannot be opened for reading,
52 /// - an OpenPGP certificate cannot be read from an OpenPGP verifier file,
53 /// - an OpenPGP verifier file contains more than one OpenPGP certificate,
54 /// - or an OpenPGP verifier file name does not match the primary fingerprint of the OpenPGP
55 /// certificate contained in it.
56 pub fn lookup(&self, os: Os, purpose: Purpose, context: Context) -> Vec<OpenpgpCert> {
57 let verifiers = self.0.lookup(os, purpose, context, Technology::Openpgp);
58
59 // Map of Fingerprint -> OpenPGPCert
60 let mut certs: HashMap<String, UncheckedOpenpgpCert> = HashMap::new();
61
62 for (canonicalized, verifiers) in verifiers {
63 debug!("Processing VOA file {canonicalized:?}");
64
65 // -- load cert from file --
66 let Ok(mut file) = File::open(&canonicalized) else {
67 warn!("Couldn't open verifier file {canonicalized:?}, skipping",);
68
69 continue;
70 };
71
72 let Ok(mut loaded) = Certificate::load(&mut file) else {
73 warn!("Failed to deserialize OpenPGP certificate {canonicalized:?}, skipping",);
74
75 continue;
76 };
77
78 if loaded.len() != 1 {
79 warn!(
80 "File {file:?} contained {} certificates, skipping",
81 loaded.len()
82 );
83
84 continue;
85 }
86
87 let certificate = loaded.remove(0);
88
89 // -- check that filename and certificate content align --
90
91 let Some(source_file_name) = canonicalized.file_name() else {
92 // The path of the verifier is not valid UTF-8.
93 warn!("The file name {canonicalized:?} is not valid UTF-8, skipping");
94 continue;
95 };
96
97 let fingerprint = format!("{:02x?}", certificate.fingerprint());
98 let expected_filename = format!("{fingerprint}{FILE_SUFFIX}");
99
100 if *source_file_name != *expected_filename {
101 warn!(
102 "Verifier filename {source_file_name:?} doesn't match expectation ({expected_filename}), skipping",
103 );
104
105 continue;
106 }
107
108 // -- merge if necessary, store in map --
109 match certs.entry(fingerprint) {
110 Entry::Vacant(ve) => {
111 // We don't have a certificate for that fingerprint yet, so we start a new entry
112 ve.insert(UncheckedOpenpgpCert {
113 certificate,
114 sources: verifiers,
115 });
116 }
117 Entry::Occupied(oe) => {
118 // We obtained data for a pre-existing fingerprint, merge in the new data
119 oe.into_mut().merge(certificate, verifiers);
120 }
121 }
122 }
123
124 // -- transform map into Vec of OpenPGPCert --
125 certs.into_values().map(Into::into).collect()
126 }
127}