voa_openpgp/lookup.rs
1//! Helper functionality to find appropriate [`SignatureVerifier`]s from a set of [`OpenpgpCert`].
2
3use std::collections::HashMap;
4
5use log::{debug, trace, warn};
6use pgp::{
7 packet::Signature,
8 types::{Fingerprint, KeyDetails, KeyId},
9};
10use rpgpie::certificate::SignatureVerifier;
11
12use crate::{OpenpgpCert, OpenpgpSignature};
13
14/// Helper to efficiently look up [`OpenpgpCert`]s by [`Fingerprint`] or by [`KeyId`].
15///
16/// This struct keeps a lookup entry by the [`Fingerprint`] and [`KeyId`] of *each* component key
17/// of each indexed [`OpenpgpCert`] (including ones that don't have the data signing key flag, and
18/// invalid ones).
19///
20/// As a consequence, the resulting lookup configuration doesn't make any semantical guarantees
21/// about lookup results, beyond that one component key bears the queried [`Fingerprint`] or
22/// [`KeyId`].
23///
24/// Downstream users must thus perform additional checks if a certificate is actually
25/// appropriate to use for a particular purpose.
26#[derive(Debug)]
27struct CertLookup<'a> {
28 by_fingerprint: HashMap<Fingerprint, Vec<&'a OpenpgpCert>>,
29 by_key_id: HashMap<KeyId, Vec<&'a OpenpgpCert>>,
30}
31
32impl<'a> From<&'a [OpenpgpCert]> for CertLookup<'a> {
33 fn from(certs: &'a [OpenpgpCert]) -> Self {
34 let mut lookup = Self {
35 by_fingerprint: HashMap::new(),
36 by_key_id: HashMap::new(),
37 };
38
39 // Insert lookup entries for the Fingerprint and KeyId of each component key of each cert
40 certs.iter().for_each(|cert| lookup.insert_cert(cert));
41
42 lookup
43 }
44}
45
46impl<'a> CertLookup<'a> {
47 /// Inserts an entry for the [`Fingerprint`] and [`KeyId`] of _each_ component key of an
48 /// [`OpenpgpCert`].
49 fn insert_cert(&mut self, cert: &'a OpenpgpCert) {
50 let primary = &cert.certificate.as_signed_public_key().primary_key;
51 self.insert_by_fingerprint(cert, primary.fingerprint());
52 self.insert_by_key_id(cert, primary.key_id());
53
54 for subkey in &cert.certificate.as_signed_public_key().public_subkeys {
55 self.insert_by_fingerprint(cert, subkey.fingerprint());
56 self.insert_by_key_id(cert, subkey.key_id());
57 }
58 }
59
60 /// Inserts a lookup entry for an [`OpenpgpCert`] and an accompanying [`Fingerprint`].
61 fn insert_by_fingerprint(&mut self, cert: &'a OpenpgpCert, fingerprint: Fingerprint) {
62 self.by_fingerprint
63 .entry(fingerprint)
64 .or_default()
65 .push(cert);
66 }
67
68 /// Inserts a lookup entry for an [`OpenpgpCert`] and an accompanying [`KeyId`].
69 fn insert_by_key_id(&mut self, cert: &'a OpenpgpCert, key_id: KeyId) {
70 self.by_key_id.entry(key_id).or_default().push(cert);
71 }
72
73 /// Returns a list of [`OpenpgpCert`]s that match a provided [`Fingerprint`].
74 pub(crate) fn by_fingerprint(&self, fingerprint: &Fingerprint) -> &[&'a OpenpgpCert] {
75 self.by_fingerprint
76 .get(fingerprint)
77 .map_or(&[], |cert| cert)
78 }
79
80 /// Returns a list of [`OpenpgpCert`]s that match a provided [`KeyId`].
81 pub(crate) fn by_key_id(&self, key_id: &KeyId) -> &[&'a OpenpgpCert] {
82 self.by_key_id.get(key_id).map_or(&[], |cert| cert)
83 }
84}
85
86/// An abstraction over a set of [`OpenpgpCert`], to facilitate signature verification.
87///
88/// It can efficiently look up component keys that are appropriate for attempting validation of
89/// specific signatures.
90#[derive(Debug)]
91pub struct VerifierLookup<'a> {
92 index: CertLookup<'a>,
93}
94
95impl<'a> VerifierLookup<'a> {
96 /// Creates a new [`VerifierLookup`] from a list of [`OpenpgpCert`]s.
97 pub fn new(certs: &'a [OpenpgpCert]) -> Self {
98 Self {
99 index: certs.into(),
100 }
101 }
102
103 /// Returns a list of [`SignatureVerifier`] and [`OpenpgpCert`] tuples, that are reasonable
104 /// candidates for attempting to verify an [`OpenpgpSignature`].
105 ///
106 /// Looks up all component keys that match the [IssuerFingerprint] and/or [Issuer] subpackets in
107 /// `signature`, and returns them as [`SignatureVerifier`] objects.
108 /// For informational purposes, each [`SignatureVerifier`] is accompanied by a reference to the
109 /// [`OpenpgpCert`] that contains it.
110 ///
111 /// Callers of this function will usually want to validate `signature` with the returned
112 /// [`SignatureVerifier`]s.
113 ///
114 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
115 /// [Issuer]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
116 pub(crate) fn get_matching_verifiers(
117 &self,
118 signature: &OpenpgpSignature,
119 ) -> Vec<(SignatureVerifier, &'a OpenpgpCert)> {
120 let Some(created) = signature.creation_time() else {
121 warn!("Skipping signature without creation time");
122 return Vec::new();
123 };
124
125 let mut verifiers = Vec::new();
126
127 // Iterate over all candidate certificates that contain component keys that match
128 // an issuer identity that `signature` lists.
129 for cert in self.candidate_certs(&signature.detached.signature) {
130 // OpenPGP semantics: enumerate all component keys that are valid for data signing
131 // purposes at the creation time of this signature.
132 for verifier in cert
133 .certificate
134 .valid_signing_capable_component_keys_at(&created.into())
135 {
136 // This set of valid component keys may contain some that do not match the issuer
137 // hints in the signature.
138 // Here, we filter the valid verifiers by the signature's issuer hints again.
139 if Self::matches_issuer(signature, &verifier) {
140 verifiers.push((verifier, cert))
141 }
142 }
143 }
144
145 debug!(
146 "get_matching_verifiers for {:?}: {}",
147 &signature.source,
148 verifiers
149 .iter()
150 .map(|(verifier, cert)| format!(
151 "{}/{}",
152 verifier.as_componentkey().fingerprint(),
153 cert.fingerprint()
154 ))
155 .collect::<Vec<_>>()
156 .join(", ")
157 );
158
159 verifiers
160 }
161
162 /// Returns a list of [`OpenpgpCert`]s that match either the
163 /// [IssuerFingerprint] or [Issuer] subpackets in a [`Signature`].
164 ///
165 /// All component keys of each [`OpenpgpCert`] are considered when matching against the
166 /// [IssuerFingerprint] and [Issuer] subpackets of the `signature`. The returned
167 /// [`OpenpgpCert`]s are deduplicated by their primary [`Fingerprint`].
168 ///
169 /// ## Note
170 ///
171 /// The lookup does not enforce any semantics constraints. It does not guarantee
172 /// validity of certificates or component keys for any particular purpose. Semantics checks
173 /// must be performed separately, after this lookup.
174 ///
175 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
176 /// [Issuer]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
177 fn candidate_certs(&self, signature: &Signature) -> Vec<&'a OpenpgpCert> {
178 let mut candidates = HashMap::new();
179
180 // Try to find the signer by Issuer Fingerprint subpacket
181 for fp in signature.issuer_fingerprint() {
182 for &cert in self.index.by_fingerprint(fp) {
183 candidates.insert(cert.fingerprint(), cert);
184 }
185 }
186
187 // Try to find the signer by Issuer KeyId subpacket
188 for key_id in signature.issuer() {
189 for &cert in self.index.by_key_id(key_id) {
190 candidates.insert(cert.fingerprint(), cert);
191 }
192 }
193
194 trace!(
195 "candidate_certs for {signature:#?}: {:?}",
196 candidates.keys().cloned().collect::<Vec<_>>().join(", ")
197 );
198
199 candidates.into_values().collect()
200 }
201
202 /// Checks if a [`SignatureVerifier`] matches any [IssuerFingerprint] or [Issuer] subpacket in
203 /// an [`OpenpgpSignature`].
204 ///
205 /// [IssuerFingerprint]: https://www.rfc-editor.org/rfc/rfc9580.html#issuer-fingerprint-subpacket
206 /// [Issuer]: https://www.rfc-editor.org/rfc/rfc9580.html#name-issuer-key-id
207 fn matches_issuer(signature: &OpenpgpSignature, verifier: &SignatureVerifier) -> bool {
208 let issuer_fingerprint = signature.detached.signature.issuer_fingerprint();
209 let issuer_key_id = signature.detached.signature.issuer();
210
211 issuer_fingerprint.contains(&&verifier.as_componentkey().fingerprint())
212 || issuer_key_id.contains(&&verifier.as_componentkey().key_id())
213 }
214}