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}