voa/
commands.rs

1//! Commands to interact with VOA.
2
3use std::{
4    collections::{BTreeMap, HashSet},
5    fmt::Debug,
6    io::stdin,
7    path::{Path, PathBuf},
8};
9
10use log::info;
11use voa_core::{
12    LoadPathList,
13    Verifier,
14    VerifierWriter,
15    Voa,
16    identifiers::{Context, Os, Purpose, Technology},
17};
18use voa_openpgp::{
19    OpenPgpImport,
20    OpenpgpCert,
21    OpenpgpSignature,
22    VoaOpenpgp,
23    import::destructured::load_from_dir,
24    verify_from_file,
25};
26
27use crate::{
28    Error,
29    utils::{DirOrFile, DirOrFileType, RegularFile},
30};
31
32/// Returns a writable VOA load path.
33///
34/// Gathers the list of writable VOA load paths for the calling user and returns the first from the
35/// list.
36/// If `runtime` is `true`, the ephemeral load path of the calling user is selected instead.
37///
38/// # Errors
39///
40/// Returns an error if no [`LoadPath`][voa_core::LoadPath] can be found.
41///
42/// # Examples
43///
44/// ```
45/// use voa::commands::get_writable_load_path;
46///
47/// # fn main() -> Result<(), voa::Error> {
48/// let config_dir = get_writable_load_path(false)?;
49/// let runtime_dir = get_writable_load_path(true)?;
50/// # Ok(())
51/// # }
52/// ```
53pub fn get_writable_load_path(runtime: bool) -> Result<PathBuf, Error> {
54    let load_path_list = LoadPathList::from_effective_user();
55
56    let filter = voa_core::LoadPathFilter {
57        ephemeral: runtime,
58        writable: true,
59    };
60    let load_path = load_path_list
61        .filter(&filter)
62        .first()
63        .cloned()
64        .ok_or(Error::NoLoadPath)?;
65
66    Ok(load_path.path.clone())
67}
68
69/// Returns an implementation of [`VerifierWriter`] from an input.
70///
71/// Depending on `technology`, attempts to load a verifier from file or directory if `input` is a
72/// [`DirOrFile`]. Attempts to load a verifier from [`stdin`] if `input` is [`None`].
73///
74/// # Note
75///
76/// Currently only supports [`Technology::Openpgp`].
77///
78/// # Errors
79///
80/// Returns an error if
81///
82/// - a verifier cannot be loaded from file/directory or stdin,
83/// - an unsupported `technology` is provided.
84///
85/// # Examples
86///
87/// ```
88/// use std::io::Write;
89///
90/// use tempfile::{NamedTempFile, tempdir};
91/// use voa::commands::load_verifier;
92///
93/// # fn main() -> testresult::TestResult {
94/// // Write a generic OpenPGP certificate to a temporary file.
95/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
96///
97/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
98/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
99/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
100/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
101/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
102/// Xwo=
103/// =Pkv9
104/// -----END PGP PUBLIC KEY BLOCK-----"#;
105/// let mut temp_file = NamedTempFile::new()?;
106/// write!(temp_file, "{cert}")?;
107/// let input_path = temp_file.path();
108///
109/// // Load an OpenPGP verifier from file.
110/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
111///
112/// // Loading a verifier from file for an unknown technology will fail.
113/// assert!(load_verifier(Some(input_path.try_into()?), "foo".parse()?).is_err());
114/// # Ok(())
115/// # }
116/// ```
117pub fn load_verifier(
118    input: Option<DirOrFile>,
119    technology: Technology,
120) -> Result<impl VerifierWriter + Debug, Error> {
121    match technology {
122        Technology::Openpgp => Ok(if let Some(path) = input {
123            match path.typ {
124                DirOrFileType::Dir => load_from_dir(&path)?,
125                DirOrFileType::File => OpenPgpImport::from_file(&path)?,
126            }
127        } else {
128            OpenPgpImport::from_reader(stdin())?
129        }),
130        technology => Err(Error::UnsupportedTechnology { technology }),
131    }
132}
133
134/// Writes a `verifier` to a VOA hierarchy in a directory.
135///
136/// The [`VerifierWriter`] implementation writes its on-disk representation to a specific location
137/// in a VOA hierarchy in a VOA base path directory based on the `os`, `purpose` and optional
138/// `context` identifier.
139///
140/// # Errors
141///
142/// Returns an error if [`VerifierWriter::write_to_hierarchy`] fails.
143///
144/// # Examples
145///
146/// ```
147/// use std::io::Write;
148///
149/// use tempfile::{NamedTempFile, tempdir};
150/// use voa::commands::{load_verifier, write_verifier_to_hierarchy};
151///
152/// # fn main() -> testresult::TestResult {
153/// // Write a generic OpenPGP certificate to a temporary file.
154/// let cert = r#"-----BEGIN PGP PUBLIC KEY BLOCK-----
155///
156/// xjMEaNBDAhYJKwYBBAHaRw8BAQdAzjzrpQ/AEteCmzjd1xTdXGaHV0VKSm4HLy6l
157/// HVcmWT3NH0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLm9yZz7CmgQQFggAQgUC
158/// aNBDAhYhBEauMg3lOimFWKbyoPtSEBy0DfYKAhsDAh4BBAsJCAcGFQ4KCQwIARYN
159/// JwkCCAIHAgkBCAEHAQIZAQAKCRD7UhActA32CkhIAP9bhoLJeZRCAc+q1kFEkstT
160/// uXBPlzHagF6ghuUfToMmVQD+KaakONKSekglKR4rJxzhleQJ4qsptt1gjXX13QgF
161/// Xwo=
162/// =Pkv9
163/// -----END PGP PUBLIC KEY BLOCK-----"#;
164/// let mut temp_file = NamedTempFile::new()?;
165/// write!(temp_file, "{cert}")?;
166/// let input_path = temp_file.path();
167/// // Load an OpenPGP verifier from file.
168/// let verifier = load_verifier(Some(input_path.try_into()?), "openpgp".parse()?)?;
169/// // Prepare a temporary output directory.
170/// let temp_dir = tempdir()?;
171///
172/// // Write a verifier to a location in a temporary VOA hierarchy.
173/// write_verifier_to_hierarchy(verifier, temp_dir, "os".parse()?, "packages".parse()?, None)?;
174/// # Ok(())
175/// # }
176/// ```
177pub fn write_verifier_to_hierarchy(
178    verifier: impl VerifierWriter,
179    base_path: impl AsRef<Path>,
180    os: Os,
181    purpose: Purpose,
182    context: Option<Context>,
183) -> Result<(), Error> {
184    let base_path = base_path.as_ref();
185    info!("Writing verifier to VOA base path: {base_path:?}");
186    verifier.write_to_hierarchy(base_path, os, purpose, context)?;
187    Ok(())
188}
189
190/// Searches for all verifiers matching a query of VOA identifiers.
191pub fn search_verifiers(
192    os: Os,
193    purpose: Purpose,
194    context: Option<Context>,
195    technology: Option<Technology>,
196) -> Result<BTreeMap<PathBuf, Vec<Verifier>>, Error> {
197    let context = if let Some(context) = context {
198        context
199    } else {
200        Context::Default
201    };
202    let technology = if let Some(technology) = technology {
203        technology
204    } else {
205        Technology::Openpgp
206    };
207
208    let voa = Voa::new();
209    let verifiers = voa.lookup(os, purpose, context, technology);
210
211    Ok(verifiers)
212}
213
214/// Verifies that a file can be verified using one or more cryptographic signatures.
215///
216/// # Errors
217///
218/// Returns an error if
219///
220/// - a technology other than [`Technology::Openpgp`] is provided,
221/// - one of the `signatures` cannot be read,
222/// - or [`voa_openpgp::verify_from_file`] fails.
223#[allow(clippy::type_complexity)]
224pub fn verify(
225    os: Os,
226    purpose: Purpose,
227    context: Context,
228    technology: Technology,
229    file: &RegularFile,
230    signatures: HashSet<&RegularFile>,
231) -> Result<Vec<(OpenpgpSignature, Option<(OpenpgpCert, String)>)>, Error> {
232    if technology != Technology::Openpgp {
233        return Err(Error::UnsupportedTechnology { technology });
234    }
235
236    let voa = VoaOpenpgp::new();
237
238    // Lookup all relevant certificates.
239    let certs = voa.lookup(os, purpose, context);
240    let cert_ref: Vec<&OpenpgpCert> = certs.iter().collect();
241
242    let openpgp_sigs = {
243        let mut openpgp_sigs = Vec::new();
244        for path in &signatures {
245            openpgp_sigs.push(OpenpgpSignature::from_file(path).map_err(Error::VoaOpenPgp)?)
246        }
247        openpgp_sigs
248    };
249    let openpgp_sigs_ref: Vec<&OpenpgpSignature> = openpgp_sigs.iter().collect();
250
251    let verification = verify_from_file(file.as_ref(), &cert_ref, &openpgp_sigs_ref)?
252        .into_iter()
253        .map(|(signature, value)| {
254            (
255                signature.clone(),
256                value.map(|(cert, fingerprint)| (cert.clone(), fingerprint)),
257            )
258        })
259        .collect();
260
261    Ok(verification)
262}
263
264#[cfg(test)]
265mod tests {
266    use libc::geteuid;
267    use rstest::rstest;
268    use testresult::TestResult;
269
270    use super::*;
271
272    #[rstest]
273    #[case::runtime_dir(true)]
274    #[case::config_dir(false)]
275    fn get_writable_load_path_succeeds(#[case] runtime: bool) -> TestResult {
276        let load_path = get_writable_load_path(runtime)?;
277
278        let euid = unsafe { geteuid() };
279
280        eprintln!("Load path: {load_path:?}");
281        if runtime {
282            assert!(load_path.starts_with("/run"))
283        } else if euid < 1000 {
284            assert_eq!(load_path, PathBuf::from("/etc/voa"))
285        } else {
286            assert!(load_path.ends_with(".config/voa"))
287        }
288
289        Ok(())
290    }
291
292    #[test]
293    fn load_verifier_fails_on_unsupported_technology() -> TestResult {
294        let result = load_verifier(None, Technology::Custom("foo".parse()?));
295        match result {
296            Err(Error::UnsupportedTechnology { .. }) => {}
297            Err(error) => panic!("Did not raise Error::UnsupportedTechnology but {error}"),
298            Ok(verifier) => {
299                panic!("Is expected to fail, but succeeded to load verifier: {verifier:?}")
300            }
301        }
302
303        Ok(())
304    }
305}