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}