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}