voa_core/
voa.rs

1use std::{
2    collections::{BTreeMap, btree_map::Entry},
3    ffi::OsString,
4    fs::read_dir,
5    path::PathBuf,
6};
7
8use log::{debug, info, trace, warn};
9
10use crate::{
11    identifiers::{Context, Os, Purpose, Technology},
12    load_path::LoadPathList,
13    util::symlinks::{ResolvedSymlink, resolve_symlink},
14    verifier::{Verifier, VoaLocation},
15};
16
17/// Access to the "File Hierarchy for the Verification of OS Artifacts (VOA)".
18///
19/// [`Voa`] provides lookup facilities for signature verifiers that are stored in a VOA hierarchy.
20/// Lookup of verifiers is agnostic to the cryptographic technology later using the verifiers.
21#[derive(Debug)]
22pub struct Voa(LoadPathList);
23
24impl Default for Voa {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl Voa {
31    /// Creates a new [`Voa`] instance.
32    ///
33    /// The VOA instance is initialized with a set of load paths, either in system mode or
34    /// user mode, based on the user id of the current process:
35    ///
36    /// - For user ids < 1000, the VOA instance is initialized in system mode. See [user mode].
37    /// - For user ids >= 1000, the VOA instance is initialized in user mode. See [system mode].
38    ///
39    /// [user mode]:
40    /// https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#user-mode
41    /// [system mode]:
42    /// https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#system-mode
43    pub fn new() -> Self {
44        info!("Initializing VOA instance");
45
46        Self(LoadPathList::from_effective_user())
47    }
48
49    /// Find applicable signature verifiers for a set of identifiers.
50    ///
51    /// Verifiers are found based on the provided [`Os`], [`Purpose`], [`Context`] and
52    /// [`Technology`] identifiers.
53    ///
54    /// This searches all VOA load paths that apply in this VOA instance.
55    ///
56    /// Warnings are emitted (via the Rust `log` mechanism) for all unusable files and directories
57    /// in the subset of the VOA hierarchy specified by the set of identifiers.
58    ///
59    /// Returns a map of "canonicalized path" to lists of [`Verifier`]s.
60    /// The same canonicalized verifier path can potentially be found via multiple load paths.
61    /// This return type gives callers full transparency into what has been found.
62    ///
63    /// # Note
64    ///
65    /// Many callers may find it sufficient to just use the `.keys()` of this result as a set
66    /// of verifier paths.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use voa_core::{
72    ///     Voa,
73    ///     identifiers::{Context, Mode, Os, Purpose, Role, Technology},
74    /// };
75    ///
76    /// # fn main() -> Result<(), voa_core::Error> {
77    /// let voa = Voa::new(); // Auto-detects System or User mode
78    ///
79    /// let verifiers = voa.lookup(
80    ///     Os::new("arch".parse()?, None, None, None, None),
81    ///     Purpose::new(Role::Packages, Mode::ArtifactVerifier),
82    ///     Context::Default,
83    ///     Technology::Openpgp,
84    /// );
85    ///
86    /// # Ok(())
87    /// # }
88    /// ```
89    pub fn lookup(
90        &self,
91        os: Os,
92        purpose: Purpose,
93        context: Context,
94        technology: Technology,
95    ) -> BTreeMap<PathBuf, Vec<Verifier>> {
96        // Collects all verifiers that we find for this set of search parameters
97        let mut verifiers = Vec::new();
98
99        // A set of filenames that we will mask out of `verifiers` in the end
100        let mut masked_names = Vec::new();
101
102        // Search in each load path
103        for load_path in self.0.paths() {
104            debug!("Looking for signature verifiers in the load path {load_path:?}");
105
106            // Load paths that symlinks from this load path may link into (or traverse through)
107            let legal_symlink_paths = self.0.legal_symlink_load_paths(load_path);
108
109            // The VOA leaf location implied by this `load_path`
110            let voa_location = VoaLocation::new(
111                load_path.clone(),
112                os.clone(),
113                purpose.clone(),
114                context.clone(),
115                technology.clone(),
116            );
117
118            // Get the validated and canonicalized path for this VOA location
119            let canonicalized = match voa_location.check_and_canonicalize(&legal_symlink_paths) {
120                Ok(canonicalized) => {
121                    trace!(
122                        "VoaLocation::check_and_canonicalize canonicalized path: {canonicalized:?}"
123                    );
124                    canonicalized
125                }
126                Err(err) => {
127                    warn!(
128                        "Error while canonicalizing for load path {:?}: {err:?} (skipping)",
129                        load_path.path
130                    );
131                    continue;
132                }
133            };
134
135            // Get the entries of this verifier directory
136            trace!("Scanning verifiers in canonicalized VOA path {canonicalized:?}");
137            let dir = match read_dir(canonicalized) {
138                Ok(dir) => dir,
139                Err(err) => {
140                    // This should be unreachable, `check_and_canonicalize` only accepts directories
141                    warn!(
142                        "⤷ Inconsistent state: Canonicalized load path is not a directory {err:?} (skipping)"
143                    );
144                    continue; // try next load path
145                }
146            };
147
148            // Loop through (potential) verifier files
149            for res in dir {
150                let entry = match res {
151                    Ok(entry) => entry,
152                    Err(err) => {
153                        warn!("⤷ Invalid directory entry:\n{err} (skipping)");
154                        continue;
155                    }
156                };
157
158                let Ok(file_type) = entry.file_type() else {
159                    warn!("⤷ Cannot get file type of directory entry {entry:?} (skipping)");
160                    continue;
161                };
162
163                // Get the checked and canonicalized path for the verifier file behind this
164                // directory entry
165                let verifier = if file_type.is_file() {
166                    entry.path()
167                } else if file_type.is_symlink() {
168                    let resolved = match resolve_symlink(&entry.path(), &legal_symlink_paths) {
169                        Ok(resolved) => resolved,
170                        Err(err) => {
171                            warn!(
172                                "⤷ Symlink {:?} is invalid for use with VOA ({err:?}) (skipping)",
173                                &entry.path()
174                            );
175                            continue;
176                        }
177                    };
178
179                    match resolved {
180                        ResolvedSymlink::File(path) => path,
181                        ResolvedSymlink::Dir(d) => {
182                            warn!(
183                                "⤷ Symlink points to a directory {:?}: {d:?}  (skipping)",
184                                &entry.path()
185                            );
186                            continue;
187                        }
188                        ResolvedSymlink::Masked => {
189                            // Masking symlinks are only expected in writable load paths
190                            if !load_path.writable() {
191                                warn!(
192                                    "Masked file name {entry:?} is illegal in non-writable load path {load_path:?} (ignoring)"
193                                );
194                                continue;
195                            }
196
197                            // Store masked verifier name for filtering in the final output step
198                            masked_names.push(entry.file_name());
199
200                            continue;
201                        }
202                    }
203                } else {
204                    warn!("⤷ Unexpected file type {file_type:?} for entry {entry:?} (skipping)");
205                    continue;
206                };
207
208                if verifier.is_file() {
209                    trace!("⤷ Found verifier file {verifier:?}");
210                    verifiers.push(Verifier::new(voa_location.clone(), verifier));
211                } else {
212                    trace!("⤷ Verifier path {verifier:?} is not a file (ignoring)");
213                }
214            }
215        }
216
217        // Filter out masked verifiers ...
218        let filtered = filter_verifiers(verifiers, masked_names);
219
220        // ... and group the remaining verifiers as a map.
221        group_verifiers(filtered)
222    }
223}
224
225/// Filter out masked verifiers, and verifiers with non-UTF-8 filenames
226fn filter_verifiers(verifiers: Vec<Verifier>, masked_names: Vec<OsString>) -> Vec<Verifier> {
227    verifiers
228        .into_iter()
229        .filter(|verifier| {
230            if let Some(filename) = verifier.filename() {
231                // Filter out masked verifiers
232                !masked_names.contains(&filename.into())
233            } else {
234                // verifier doesn't have a filename, filter it out
235                false
236            }
237        })
238        .collect()
239}
240
241/// Build the return format: A map from "canonicalized path" to lists of Verifiers
242fn group_verifiers(verifiers: Vec<Verifier>) -> BTreeMap<PathBuf, Vec<Verifier>> {
243    let mut map: BTreeMap<PathBuf, Vec<Verifier>> = BTreeMap::new();
244
245    // Restructure the verifiers `Vec` into a map
246    verifiers.into_iter().for_each(|verifier| {
247        let canonicalized: PathBuf = verifier.canonicalized().into();
248        let e = map.entry(canonicalized);
249        match e {
250            Entry::Vacant(ve) => {
251                ve.insert(vec![verifier]);
252            }
253            Entry::Occupied(mut oe) => oe.get_mut().push(verifier),
254        }
255    });
256
257    map
258}