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::{PathType, 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            info!("Looking at location: {voa_location:?}");
119            info!("Legal symlink paths: {legal_symlink_paths:?}");
120
121            // Get the validated and canonicalized path for this VOA location
122            let canonicalized = match voa_location.check_and_canonicalize(&legal_symlink_paths) {
123                Ok(canonicalized) => {
124                    trace!(
125                        "VoaLocation::check_and_canonicalize canonicalized path: {canonicalized:?}"
126                    );
127                    canonicalized
128                }
129                Err(err) => {
130                    warn!(
131                        "Error while canonicalizing for load path {:?}: {err} (skipping)",
132                        load_path.path
133                    );
134                    continue;
135                }
136            };
137
138            // Get the entries of this verifier directory
139            trace!("Scanning verifiers in canonicalized VOA path {canonicalized:?}");
140            let dir = match read_dir(canonicalized) {
141                Ok(dir) => dir,
142                Err(err) => {
143                    // This should be unreachable, `check_and_canonicalize` only accepts directories
144                    warn!(
145                        "⤷ Inconsistent state: Canonicalized load path is not a directory {err:?} (skipping)"
146                    );
147                    continue; // try next load path
148                }
149            };
150
151            // Loop through (potential) verifier files
152            for res in dir {
153                let entry = match res {
154                    Ok(entry) => entry,
155                    Err(err) => {
156                        warn!("⤷ Invalid directory entry:\n{err} (skipping)");
157                        continue;
158                    }
159                };
160
161                let Ok(file_type) = entry.file_type() else {
162                    warn!("⤷ Cannot get file type of directory entry {entry:?} (skipping)");
163                    continue;
164                };
165
166                // Get the checked and canonicalized path for the verifier file behind this
167                // directory entry
168                let verifier = if file_type.is_file() {
169                    entry.path()
170                } else if file_type.is_symlink() {
171                    let resolved = match resolve_symlink(
172                        &entry.path(),
173                        &legal_symlink_paths,
174                        PathType::File,
175                    ) {
176                        Ok(resolved) => resolved,
177                        Err(err) => {
178                            warn!(
179                                "⤷ Symlink {:?} is invalid for use with VOA ({err:?}) (skipping)",
180                                &entry.path()
181                            );
182                            continue;
183                        }
184                    };
185
186                    match resolved {
187                        ResolvedSymlink::File(path) => path,
188                        ResolvedSymlink::Dir(d) => {
189                            warn!(
190                                "⤷ Symlink points to a directory {:?}: {d:?}  (skipping)",
191                                &entry.path()
192                            );
193                            continue;
194                        }
195                        ResolvedSymlink::Masked => {
196                            // Masking symlinks are only expected in writable load paths
197                            if !load_path.writable() {
198                                warn!(
199                                    "Masked file name {entry:?} is illegal in non-writable load path {load_path:?} (ignoring)"
200                                );
201                                continue;
202                            }
203
204                            // Store masked verifier name for filtering in the final output step
205                            masked_names.push(entry.file_name());
206
207                            continue;
208                        }
209                    }
210                } else {
211                    warn!("⤷ Unexpected file type {file_type:?} for entry {entry:?} (skipping)");
212                    continue;
213                };
214
215                if verifier.is_file() {
216                    trace!("⤷ Found verifier file {verifier:?}");
217                    verifiers.push(Verifier::new(voa_location.clone(), verifier));
218                } else {
219                    trace!("⤷ Verifier path {verifier:?} is not a file (ignoring)");
220                }
221            }
222        }
223
224        // Filter out masked verifiers ...
225        let filtered = filter_verifiers(verifiers, masked_names);
226
227        // ... and group the remaining verifiers as a map.
228        group_verifiers(filtered)
229    }
230}
231
232/// Filter out masked verifiers, and verifiers with non-UTF-8 filenames
233fn filter_verifiers(verifiers: Vec<Verifier>, masked_names: Vec<OsString>) -> Vec<Verifier> {
234    verifiers
235        .into_iter()
236        .filter(|verifier| {
237            if let Some(filename) = verifier.filename() {
238                // Filter out masked verifiers
239                !masked_names.contains(&filename.into())
240            } else {
241                // verifier doesn't have a filename, filter it out
242                false
243            }
244        })
245        .collect()
246}
247
248/// Build the return format: A map from "canonicalized path" to lists of Verifiers
249fn group_verifiers(verifiers: Vec<Verifier>) -> BTreeMap<PathBuf, Vec<Verifier>> {
250    let mut map: BTreeMap<PathBuf, Vec<Verifier>> = BTreeMap::new();
251
252    // Restructure the verifiers `Vec` into a map
253    verifiers.into_iter().for_each(|verifier| {
254        let canonicalized: PathBuf = verifier.canonicalized().into();
255        let e = map.entry(canonicalized);
256        match e {
257            Entry::Vacant(ve) => {
258                ve.insert(vec![verifier]);
259            }
260            Entry::Occupied(mut oe) => oe.get_mut().push(verifier),
261        }
262    });
263
264    map
265}