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