voa_openpgp/trust/
model.rs

1use std::{collections::HashSet, fs::read, path::Path, time::SystemTime};
2
3use log::{debug, error, info, warn};
4use pgp::types::KeyDetails;
5use voa_config::openpgp::{NumDataSignatures, OpenpgpSettings, VerificationMethod};
6
7use crate::{
8    Error,
9    OpenpgpCert,
10    OpenpgpSignature,
11    OpenpgpSignatureCheck,
12    SignerInfo,
13    lookup::VerifierLookup,
14    trust::{
15        anchor::TrustAnchorModel,
16        plain::PlainTrustModel,
17        util::{log_cert_data, log_sig_data},
18        wot::WebOfTrustModel,
19    },
20};
21
22/// The trust model used for verification.
23#[derive(Debug)]
24enum TrustModel<'a> {
25    Plain(PlainTrustModel<'a>),
26    TrustAnchor(TrustAnchorModel<'a>),
27    WebOfTrust(WebOfTrustModel<'a>),
28}
29
30/// Performs signature verification based on a trust model configuration and sets of verifiers.
31#[derive(Debug)]
32pub struct ModelBasedVerifier<'a> {
33    /// Number of distinct valid data signatures that we expect for a data file to be
34    /// considered valid
35    num_data_signatures: NumDataSignatures,
36
37    /// The trust model used by this verifier object
38    trust_model: TrustModel<'a>,
39}
40
41impl<'a> ModelBasedVerifier<'a> {
42    /// Create a new signature verifier object based on an [`OpenpgpSettings`] configuration,
43    /// a set of artifact verifiers and trust anchors.
44    ///
45    /// For [`VerificationMethod::Plain`], the set of anchors must be empty.
46    pub fn new(
47        config: &'a OpenpgpSettings,
48        artifact_verifiers: &'a [OpenpgpCert],
49        trust_anchors: &'a [OpenpgpCert],
50    ) -> Self {
51        let trust_model = match config.verification_method() {
52            VerificationMethod::Plain(plain) => {
53                if !trust_anchors.is_empty() {
54                    warn!(
55                        "VerificationMethod is 'Plain', but {} trust_anchors exist",
56                        trust_anchors.len()
57                    );
58                }
59
60                TrustModel::Plain(PlainTrustModel::new(plain, artifact_verifiers))
61            }
62
63            VerificationMethod::TrustAnchor(anchor) => TrustModel::TrustAnchor(
64                TrustAnchorModel::new(anchor, artifact_verifiers, trust_anchors),
65            ),
66
67            VerificationMethod::WebOfTrust(wot) => {
68                TrustModel::WebOfTrust(WebOfTrustModel::new(wot, artifact_verifiers, trust_anchors))
69            }
70        };
71
72        Self {
73            num_data_signatures: config.num_data_signatures(),
74            trust_model,
75        }
76    }
77
78    /// Verifies one or more `signatures` for a `signed_file`.
79    ///
80    /// Success of the verification is decided according to the trust model configuration of this
81    /// [`ModelBasedVerifier`]. Signature verifiers are obtained from the trust model.
82    ///
83    /// The return value is a boolean value as am immediate judgment about validity of the signature
84    /// set, under the configured trust model.
85    ///
86    /// In addition, a `Vec` of [`OpenpgpSignatureCheck`] is returned, with more detailed
87    /// information about each checked signature.
88    /// This links each signature to a signer certificate that signed it, if the signature has been
89    /// found valid under the current trust model.
90    ///
91    /// # Note
92    ///
93    /// Does not consider OpenPGP signatures with an unset creation time and instead emits a warning
94    /// for them.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error, if the `signed_file` cannot be read.
99    pub fn verify_file_with_signatures(
100        &'a self,
101        signed_file: impl AsRef<Path>,
102        signatures: &'a [OpenpgpSignature],
103    ) -> Result<Vec<OpenpgpSignatureCheck<'a>>, Error> {
104        let signed_file = signed_file.as_ref();
105
106        // Verify the signatures against the file using the trust model
107        let checks = match &self.trust_model {
108            TrustModel::Plain(plain) => verify(plain, signatures, signed_file),
109            TrustModel::TrustAnchor(anchor) => verify(anchor, signatures, signed_file),
110            TrustModel::WebOfTrust(_wot) => {
111                return Err(Error::UnimplementedVerificationMethod {
112                    verification_method: "OpenPGP Web of Trust".to_string(),
113                });
114            }
115        }?;
116
117        // How many valid data signatures have we found?
118        //
119        // Specifically, we check the number of distinct primary fingerprints of issuing
120        // certificates that have produced valid data signatures.
121        //
122        // Counting primary fingerprints ensures that we don't count multiple signatures from the
123        // same issuing certificate as independent evidence.
124        let signer_fingerprints: HashSet<_> = checks
125            .iter()
126            .filter_map(|check| check.signer_info())
127            .map(|si| si.certificate().certificate.fingerprint())
128            .collect();
129
130        // We only consider the verification successful if the threshold of required valid data
131        // signatures has been met.
132        let num_required_signatures = self.num_data_signatures.get().get();
133        let num_successful_signatures = signer_fingerprints.len();
134        if num_successful_signatures < num_required_signatures {
135            let failed_signatures = checks
136                .iter()
137                .filter_map(|check| {
138                    if check.signer_info().is_none() {
139                        Some(check.signature().clone())
140                    } else {
141                        None
142                    }
143                })
144                .collect::<Vec<_>>();
145
146            return Err(Error::FileVerificationFailed {
147                path: signed_file.to_path_buf(),
148                num_required_signatures,
149                num_successful_signatures,
150                failed_signatures,
151            });
152        };
153
154        Ok(checks)
155    }
156}
157
158/// An interface into an OpenPGP trust model that determines if an artifact verifier is accepted, at
159/// reference time.
160///
161/// Objects that implement this trait hold a set of verifiers, and a trust model configuration that
162/// specifies which verifiers are valid in which context.
163pub(crate) trait ArtifactVerifierValidity {
164    /// Check if `cert` is acceptable as an artifact signer at `reference_time`, according to
165    /// the trust model
166    fn check(&self, cert: &OpenpgpCert, reference_time: SystemTime) -> bool;
167
168    /// Borrow a `VerifierLookup` over the artifact verifiers in the trust model
169    fn lookup(&self) -> &VerifierLookup<'_>;
170}
171
172/// Verifies the content of `signed_file` against a set of `signatures`, and returns validity
173/// information for each signature.
174fn verify<'b>(
175    artifact_verifiers: &'b impl ArtifactVerifierValidity,
176    signatures: &'b [OpenpgpSignature],
177    signed_file: &Path,
178) -> Result<Vec<OpenpgpSignatureCheck<'b>>, Error> {
179    // We load the signed payload into memory (for now)
180    let data = read(signed_file).map_err(|source| Error::IoPath {
181        path: signed_file.into(),
182        source,
183        context: "reading a signed file",
184    })?;
185
186    let mut checks = Vec::new();
187
188    'sigs: for signature in signatures {
189        let log_sig_data = log_sig_data(signature);
190        info!("Verifying OpenPGP signature ({log_sig_data}) for file {signed_file:?}");
191
192        let Some(sig_created) = signature.creation_time() else {
193            warn!("Skipping signature without creation time");
194            checks.push(OpenpgpSignatureCheck::new(signature, None));
195            continue;
196        };
197
198        let verifiers = artifact_verifiers
199            .lookup()
200            .get_matching_verifiers(signature);
201        debug!("Found {} candidate verifiers", verifiers.len());
202
203        for (verifier, cert) in &verifiers {
204            if !artifact_verifiers.check(cert, sig_created) {
205                // We can't use this cert
206                warn!(
207                    "We can't use cert {} to verify, under the current policy, at {:?}",
208                    cert.certificate.fingerprint(),
209                    sig_created
210                );
211
212                continue;
213            }
214
215            let log_cert_data = log_cert_data(cert);
216            let component_fp = verifier.as_componentkey().fingerprint().to_string();
217
218            // Cryptographic signature check of the payload against the verifier
219            if let Err(error) = verifier.verify(&signature.detached.signature, &data) {
220                // This signature didn't verify as cryptographically correct.
221                // However, the verifier was considered appropriate to use by
222                // `VerifierLookup::get_matching_verifiers` (based on issuer subpackets in the
223                // signature).
224                //
225                // It's very irregular that validation would fail under these circumstances,
226                // so we log this at the "error" level.
227                // Ideally, a human should investigate what's going on there.
228                error!(
229                    "Signature verification error for {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}: {error}"
230                );
231
232                // try next verifier, if any
233                continue;
234            }
235
236            info!(
237                "Successfully verified OpenPGP signature {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}"
238            );
239
240            checks.push(OpenpgpSignatureCheck::new(
241                signature,
242                Some(SignerInfo::new(cert, component_fp)),
243            ));
244
245            // We're done with this signature and can move to the next one
246            continue 'sigs;
247        }
248
249        warn!(
250            "Signature {log_sig_data} and file {signed_file:?} could not be verified (tried {} candidate verifiers)",
251            verifiers.len()
252        );
253        checks.push(OpenpgpSignatureCheck::new(signature, None));
254    }
255
256    Ok(checks)
257}