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}