voa_openpgp/
lib.rs

1#![doc = include_str!("../README.md")]
2
3mod cert;
4mod error;
5mod voa;
6
7pub mod import;
8pub mod lookup;
9mod signature;
10
11use std::{fs::read, path::Path};
12
13pub use error::Error;
14pub use import::OpenPgpImport;
15use log::{debug, error, info, warn};
16use pgp::types::KeyDetails;
17
18use crate::lookup::VerifierLookup;
19pub use crate::{
20    cert::OpenpgpCert,
21    signature::{OpenpgpSignature, OpenpgpSignatureCheck, SignerInfo},
22    voa::VoaOpenpgp,
23};
24
25/// File name suffix for verifiers in the VOA "openpgp" technology
26pub(crate) const FILE_SUFFIX: &str = ".openpgp";
27
28/// Creates a representative string for logging from an [`OpenpgpSignature`].
29///
30/// The returned string is of the form "fingerprint (file)" where "fingerprint" is the
31/// list of issuer fingerprints of the signature and "file" is the optional path of a file from
32/// which the OpenPGP signature has been read.
33fn log_sig_data(signature: &OpenpgpSignature) -> String {
34    format!(
35        "{}{}",
36        signature
37            .detached
38            .signature
39            .issuer_fingerprint()
40            .iter()
41            .map(|fingerprint| fingerprint.to_string())
42            .collect::<Vec<String>>()
43            .join(", "),
44        if let Some(path) = signature.source() {
45            format!(" ({})", path.to_string_lossy())
46        } else {
47            "".to_string()
48        }
49    )
50}
51
52/// Creates a representative string for logging from an [`OpenpgpCert`].
53///
54/// The returned string is of the form "fingerprint (verifier)" where "fingerprint" is the
55/// primary key fingerprint of the certificate and "verifier" is a list of one or more verifier
56/// locations from which the certificate has been assembled.
57fn log_cert_data(cert: &OpenpgpCert) -> String {
58    format!(
59        "{} ({})",
60        cert.fingerprint(),
61        cert.sources
62            .iter()
63            .map(|verifier| verifier.canonicalized().to_string_lossy().to_string())
64            .collect::<Vec<_>>()
65            .join(",")
66    )
67}
68
69/// Verifies one or more `signatures` for a `signed_file` with one or more `certs`.
70///
71/// All `signatures` are used to check the data in `signed_file`.
72/// For each signature, all `certs` are considered for finding a matching component key for signing.
73///
74/// The return value is a list of tuples, one entry for each signature (in order).
75/// Signatures that could not be positively verified have a `None` entry.
76/// Successfully verified signatures have a `Some` entry with a tuple of a signature, certificate
77/// and the fingerprint of the component key which verified the data in `signed_file`.
78///
79/// # Note
80///
81/// Does not consider OpenPGP signatures with an unset creation time and instead emits a warning for
82/// them.
83///
84/// # Errors
85///
86/// Returns an error, if the `signed_file` cannot be read.
87pub fn verify_from_file<'a>(
88    signed_file: impl AsRef<Path>,
89    lookup: &'a VerifierLookup,
90    signatures: &'a [OpenpgpSignature],
91) -> Result<Vec<OpenpgpSignatureCheck<'a>>, Error> {
92    let signed_file = signed_file.as_ref();
93    let mut result = Vec::new();
94
95    // We load the signed payload into memory (for now)
96    let data = read(signed_file).map_err(|source| Error::IoPath {
97        path: signed_file.into(),
98        source,
99        context: "reading signed_file",
100    })?;
101
102    'sigs: for signature in signatures {
103        let log_sig_data = log_sig_data(signature);
104        info!("Verifying OpenPGP signature ({log_sig_data}) for file {signed_file:?}");
105
106        let verifiers = lookup.get_matching_verifiers(signature);
107        debug!("Found {} candidate verifiers", verifiers.len());
108
109        for (verifier, cert) in &verifiers {
110            let component_fp = format!("{:02x?}", verifier.as_componentkey().fingerprint());
111            let log_cert_data = log_cert_data(cert);
112
113            // Cryptographic signature check of the payload against the verifier public key material
114            if let Err(error) = verifier.verify(&signature.detached.signature, &data) {
115                // A signature that doesn't verify as cryptographically correct with a verifier
116                // that `VerifierLookup::get_matching_verifiers` selected is a very irregular
117                // situation. So we log it at the "error" level.
118                //
119                // Ideally, a human should investigate what's going on there.
120                error!(
121                    "Signature verification error for {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}: {error}"
122                );
123                continue; // try next verifier, if any
124            }
125
126            info!(
127                "Successfully verified OpenPGP signature {log_sig_data} for file {signed_file:?} with component key {component_fp} in OpenPGP certificate {log_cert_data}"
128            );
129
130            result.push(OpenpgpSignatureCheck::new(
131                signature,
132                Some(SignerInfo::new(cert, component_fp)),
133            ));
134
135            continue 'sigs; // We're done with this signature and can move to the next one
136        }
137
138        warn!(
139            "Signature {log_sig_data} and file {signed_file:?} could not be verified (tried {} candidate verifiers)",
140            verifiers.len()
141        );
142        result.push(OpenpgpSignatureCheck::new(signature, None));
143    }
144
145    Ok(result)
146}