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