voa_openpgp/lookup.rs
1//! Helper functionality to find appropriate [`SignatureVerifier`]s from a set of [`OpenpgpCert`].
2
3use std::{
4 collections::{BTreeMap, HashMap},
5 ops::Add,
6 time::SystemTime,
7};
8
9use log::{debug, info, trace, warn};
10use pgp::{
11 packet::{Signature, SignatureType, UserId},
12 types::{Fingerprint, KeyDetails, KeyId, Tag},
13};
14use rpgpie::{certificate::SignatureVerifier, signature::signature_acceptable};
15
16use crate::{OpenpgpCert, OpenpgpSignature};
17
18/// Helper to efficiently look up [`OpenpgpCert`]s by [`Fingerprint`] or by [`KeyId`].
19///
20/// This struct keeps a lookup entry by the [`Fingerprint`] and [`KeyId`] of *each* component key
21/// of each indexed [`OpenpgpCert`] (including ones that don't have the data signing key flag, and
22/// invalid ones).
23///
24/// As a consequence, the resulting lookup configuration doesn't make any semantical guarantees
25/// about lookup results, beyond that one component key bears the queried [`Fingerprint`] or
26/// [`KeyId`].
27///
28/// Downstream users must thus perform additional checks if a certificate is actually
29/// appropriate to use for a particular purpose.
30#[derive(Debug)]
31struct CertLookup<'a> {
32 by_fingerprint: HashMap<Fingerprint, Vec<&'a OpenpgpCert>>,
33 by_key_id: HashMap<KeyId, Vec<&'a OpenpgpCert>>,
34}
35
36impl<'a> From<&'a [OpenpgpCert]> for CertLookup<'a> {
37 fn from(certs: &'a [OpenpgpCert]) -> Self {
38 let mut lookup = Self {
39 by_fingerprint: HashMap::new(),
40 by_key_id: HashMap::new(),
41 };
42
43 // Insert lookup entries for the Fingerprint and KeyId of each component key of each cert
44 certs.iter().for_each(|cert| lookup.insert_cert(cert));
45
46 lookup
47 }
48}
49
50impl<'a> CertLookup<'a> {
51 /// Inserts an entry for the [`Fingerprint`] and [`KeyId`] of _each_ component key of an
52 /// [`OpenpgpCert`].
53 fn insert_cert(&mut self, cert: &'a OpenpgpCert) {
54 let primary = &cert.certificate.as_signed_public_key().primary_key;
55 self.insert_by_fingerprint(cert, primary.fingerprint());
56 self.insert_by_key_id(cert, primary.key_id());
57
58 for subkey in &cert.certificate.as_signed_public_key().public_subkeys {
59 self.insert_by_fingerprint(cert, subkey.fingerprint());
60 self.insert_by_key_id(cert, subkey.key_id());
61 }
62 }
63
64 /// Inserts a lookup entry for an [`OpenpgpCert`] and an accompanying [`Fingerprint`].
65 fn insert_by_fingerprint(&mut self, cert: &'a OpenpgpCert, fingerprint: Fingerprint) {
66 self.by_fingerprint
67 .entry(fingerprint)
68 .or_default()
69 .push(cert);
70 }
71
72 /// Inserts a lookup entry for an [`OpenpgpCert`] and an accompanying [`KeyId`].
73 fn insert_by_key_id(&mut self, cert: &'a OpenpgpCert, key_id: KeyId) {
74 self.by_key_id.entry(key_id).or_default().push(cert);
75 }
76
77 /// Returns a list of [`OpenpgpCert`]s that match a provided [`Fingerprint`].
78 pub(crate) fn by_fingerprint(&self, fingerprint: &Fingerprint) -> &[&'a OpenpgpCert] {
79 self.by_fingerprint
80 .get(fingerprint)
81 .map_or(&[], |cert| cert)
82 }
83
84 /// Returns a list of [`OpenpgpCert`]s that match a provided [`KeyId`].
85 pub(crate) fn by_key_id(&self, key_id: &KeyId) -> &[&'a OpenpgpCert] {
86 self.by_key_id.get(key_id).map_or(&[], |cert| cert)
87 }
88}
89
90/// An abstraction over a set of [`OpenpgpCert`], to facilitate signature verification.
91///
92/// It can efficiently look up component keys that are appropriate for attempting validation of
93/// specific signatures.
94#[derive(Debug)]
95pub struct VerifierLookup<'a> {
96 index: CertLookup<'a>,
97}
98
99impl<'a> FromIterator<&'a OpenpgpCert> for VerifierLookup<'a> {
100 fn from_iter<T: IntoIterator<Item = &'a OpenpgpCert>>(iter: T) -> Self {
101 let mut index = CertLookup {
102 by_fingerprint: HashMap::new(),
103 by_key_id: HashMap::new(),
104 };
105
106 for c in iter.into_iter() {
107 index.insert_cert(c);
108 }
109
110 Self { index }
111 }
112}
113
114impl<'a> VerifierLookup<'a> {
115 /// The signature types that we consider meaningful for certifications on identities.
116 ///
117 /// Note that the set excludes `SignatureType::CertPersona` which is specified to not carry any
118 /// meaningful information.
119 const CERTIFICATION_SIGNATURE_TYPES: &'static [SignatureType] = &[
120 SignatureType::CertPositive,
121 SignatureType::CertGeneric,
122 SignatureType::CertCasual,
123 SignatureType::CertRevocation,
124 ];
125
126 /// Creates a new [`VerifierLookup`] from a list of [`OpenpgpCert`]s.
127 pub fn new(certs: &'a [OpenpgpCert]) -> Self {
128 Self {
129 index: certs.into(),
130 }
131 }
132
133 /// Returns a list of [`SignatureVerifier`] and [`OpenpgpCert`] tuples, that are reasonable
134 /// candidates for attempting to verify an [`OpenpgpSignature`].
135 ///
136 /// Looks up all component keys that match the [IssuerFingerprint] and/or [Issuer] subpackets in
137 /// `signature`, and returns them as [`SignatureVerifier`] objects.
138 /// For informational purposes, each [`SignatureVerifier`] is accompanied by a reference to the
139 /// [`OpenpgpCert`] that contains it.
140 ///
141 /// Callers of this function will usually want to validate `signature` with the returned
142 /// [`SignatureVerifier`]s.
143 ///
144 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
145 /// [Issuer]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
146 pub(crate) fn get_matching_verifiers(
147 &self,
148 signature: &OpenpgpSignature,
149 ) -> Vec<(SignatureVerifier, &'a OpenpgpCert)> {
150 let Some(created) = signature.creation_time() else {
151 warn!("Skipping signature without creation time");
152 return Vec::new();
153 };
154
155 let mut verifiers = Vec::new();
156
157 // Iterate over all candidate certificates that contain component keys that match
158 // an issuer identity that `signature` lists.
159 // (This also performs rpgpie policy checks on the signature.)
160 for cert in self.candidate_certs(&signature.detached.signature) {
161 // OpenPGP semantics: enumerate all component keys that are valid for data signing
162 // purposes at the creation time of this signature.
163 for verifier in cert
164 .certificate
165 .valid_signing_capable_component_keys_at(&created.into())
166 {
167 // This set of valid component keys may contain some that do not match the issuer
168 // hints in the signature.
169 // Here, we filter the valid verifiers by the signature's issuer hints again.
170 if Self::matches_issuer(signature, &verifier) {
171 verifiers.push((verifier, cert))
172 }
173 }
174 }
175
176 debug!(
177 "get_matching_verifiers for {:?}: {}",
178 &signature.source,
179 verifiers
180 .iter()
181 .map(|(verifier, cert)| format!(
182 "{}/{}",
183 verifier.as_componentkey().fingerprint(),
184 cert.certificate.fingerprint()
185 ))
186 .collect::<Vec<_>>()
187 .join(", ")
188 );
189
190 verifiers
191 }
192
193 /// Returns a list of [`OpenpgpCert`]s that match either the
194 /// [IssuerFingerprint] or [IssuerKeyId] subpackets in a [`Signature`].
195 ///
196 /// All component keys of each [`OpenpgpCert`] are considered when matching against the
197 /// [IssuerFingerprint] and [IssuerKeyId] subpackets of the `signature`. The returned
198 /// [`OpenpgpCert`]s are deduplicated by their primary [`Fingerprint`].
199 ///
200 /// ## Note
201 ///
202 /// The lookup does not enforce any semantics constraints. It does not guarantee
203 /// validity of certificates or component keys for any particular purpose. Semantics checks
204 /// must be performed separately, after this lookup.
205 ///
206 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
207 /// [IssuerKeyId]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
208 fn candidate_certs(&self, signature: &Signature) -> Vec<&'a OpenpgpCert> {
209 // We use a map with the key as a string-representation of the fingerprint.
210 // The fingerprint is used both to ensure unique results and a stable ordering.
211 let mut candidates = BTreeMap::new();
212
213 // Try to find the signer by Issuer Fingerprint subpacket
214 for fp in signature.issuer_fingerprint() {
215 for &cert in self.index.by_fingerprint(fp) {
216 candidates.insert(cert.certificate.fingerprint().to_string(), cert);
217 }
218 }
219
220 // Try to find the signer by Issuer KeyId subpacket
221 for key_id in signature.issuer() {
222 for &cert in self.index.by_key_id(key_id) {
223 candidates.insert(cert.certificate.fingerprint().to_string(), cert);
224 }
225 }
226
227 trace!(
228 "candidate_certs for {signature:#?}: {:?}",
229 candidates
230 .keys()
231 .map(ToString::to_string)
232 .collect::<Vec<_>>()
233 .join(", ")
234 );
235
236 candidates.into_values().collect()
237 }
238
239 /// Checks if a [`SignatureVerifier`] matches any [IssuerFingerprint] or [Issuer] subpacket in
240 /// an [`OpenpgpSignature`].
241 ///
242 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
243 /// [Issuer]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
244 fn matches_issuer(signature: &OpenpgpSignature, verifier: &SignatureVerifier) -> bool {
245 let issuer_fingerprint = signature.detached.signature.issuer_fingerprint();
246 let issuer_key_id = signature.detached.signature.issuer();
247
248 issuer_fingerprint.contains(&&verifier.as_componentkey().fingerprint())
249 || issuer_key_id.contains(&&verifier.as_componentkey().key_id())
250 }
251
252 /// Returns pairs of [`OpenpgpCert`] and [`Signature`]s for third-party UserId certifications.
253 ///
254 /// Filters `signatures` (a slice of third-party certifications over `target` and
255 /// `target_user`) by policy, as well as temporal and cryptographic validity (at
256 /// `reference_time`).
257 /// The validated signatures are grouped by signer certificate.
258 ///
259 /// ## Notes
260 ///
261 /// - A certifying signature must pass [`rpgpie`]'s policy checks (i.e. cryptographic mechanisms
262 /// that are considered weak at signature creation time are rejected).
263 /// - If a certifying signature has a "signature expiration time" that is after the reference
264 /// time, that certifying signature is ignored (except for certification revocation
265 /// signatures, which may not expire).
266 /// - The certifying signature *may* be younger than the data signature that is authenticated.
267 /// - The certifying certificate *may* be younger than the data signature that is authenticated.
268 pub fn valid_userid_certifications(
269 &self,
270 signatures: &[&'a Signature],
271 target: &OpenpgpCert,
272 target_user: &UserId,
273 reference_time: SystemTime,
274 ) -> Vec<(&'a OpenpgpCert, Vec<&'a Signature>)> {
275 let mut map = HashMap::new();
276
277 for &sig in signatures {
278 if let Some(typ) = sig.typ()
279 && !Self::CERTIFICATION_SIGNATURE_TYPES.contains(&typ)
280 {
281 continue;
282 }
283
284 // Policy check - is the signature using acceptable algorithms?
285 if !signature_acceptable(sig) {
286 continue;
287 }
288
289 let Some(sig_created) = sig.created() else {
290 continue;
291 };
292
293 if sig.typ() != Some(SignatureType::CertRevocation) {
294 // Filter out certifications that expire before reference_time.
295 // (However, we don't accept signature expiration in revocations)
296 if let Some(exp) = sig.signature_expiration_time()
297 && !exp.is_zero()
298 {
299 let expires = sig_created.add(*exp);
300
301 if SystemTime::from(expires) <= reference_time {
302 trace!("skipping expired sig {:?}", sig);
303 continue;
304 }
305 }
306 }
307
308 // Find the signer that has made this signature, if any.
309 // This includes a check for cryptographic validity of the signature.
310 let Some(signer) = self.lookup_third_party_certifier(sig, target, target_user) else {
311 // We found no signer, go to the next signature
312 continue;
313 };
314
315 trace!(" found signer: {:?}", signer.certificate.fingerprint());
316
317 // Ignore certification if signer is not valid at its creation time.
318 //
319 // (However, we do allow certifiers that were not valid "yet" at the *data signature*'s
320 // creation time.)
321 if !matches!(signer.certificate.primary_valid_at(sig_created), Ok(true)) {
322 continue;
323 }
324
325 // Check that the signer's primary has key flag `0x01`
326 // that allows issuing third-party certifications
327 if let Some(self_sig) = signer
328 .certificate
329 .active_certificate_self_signature_at(sig_created)
330 {
331 if !self_sig.key_flags().certify() {
332 info!(
333 " skipping signer {:?} because it's missing the 'certifications' key flag",
334 signer.certificate.fingerprint()
335 );
336 continue;
337 }
338 } else {
339 info!(
340 " skipping signer {:?} because we found no active self-signature",
341 signer.certificate.fingerprint()
342 );
343 continue;
344 }
345
346 // Store this certifying signature in map, using the signer certificate's fingerprint
347 // as the map key
348 let (_, sigs) = map
349 .entry(signer.certificate.fingerprint())
350 .or_insert((signer, Vec::new()));
351 sigs.push(sig);
352 }
353
354 map.into_values().collect()
355 }
356
357 /// Returns a matching [`OpenpgpCert`] for a third-party certification over a [`UserId`].
358 ///
359 /// The considered certificate must be cryptographically valid and must have issued `sig` as a
360 /// (third-party) certification over `target` and `target_user`.
361 ///
362 /// If a signer is found, a reference to it is returned, otherwise [`None`].
363 fn lookup_third_party_certifier(
364 &self,
365 sig: &Signature,
366 target: &OpenpgpCert,
367 target_user: &UserId,
368 ) -> Option<&'a OpenpgpCert> {
369 self.candidate_certs(sig).into_iter().find(|&candidate| {
370 sig.verify_third_party_certification(
371 &target.certificate.as_signed_public_key().primary_key,
372 &candidate.certificate.as_signed_public_key().primary_key,
373 Tag::UserId,
374 target_user,
375 )
376 .is_ok()
377 })
378 }
379}