voa_openpgp/
lib.rs

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