voa_core/verifier.rs
1//! Verifier path handling and canonicalization
2
3use std::{
4 fmt::Debug,
5 fs::File,
6 path::{Path, PathBuf},
7};
8
9use crate::{
10 Error,
11 identifiers::{Context, Os, Purpose, Technology},
12 load_path::LoadPath,
13 util::symlinks::{ResolvedSymlink, resolve_symlink},
14};
15
16/// A [`Verifier`] points to a signature verifier in the file system.
17///
18/// It consists of the [`VoaLocation`] via which the verifier was obtained, and a canonicalized
19/// path to the actual verifier file.
20///
21/// Depending on the verifier [`Technology`], a [`Verifier`] instance may represent, e.g.:
22///
23/// - an individual, standalone signature verifier,
24/// - an individual verifier that acts as a trust anchor,
25/// - a certificate complete with its trust chain,
26/// - a set of individual verifiers in one shared data structure.
27#[derive(Clone, Debug)]
28pub struct Verifier {
29 /// The logical VOA location via which the verifier was found
30 voa_location: VoaLocation,
31
32 /// Canonicalized path of the verifier file
33 canonicalized: PathBuf,
34}
35
36impl Verifier {
37 /// Creates a new [`Verifier`].
38 ///
39 /// # Note
40 ///
41 /// Callers of this constructor must ensure that `canonicalized` contains a fully canonicalized
42 /// filename, that the file exists, and that the naming and any potential symlinks involved
43 /// conform to the constraints defined in the VOA specification.
44 pub(crate) fn new(voa_location: VoaLocation, canonicalized: PathBuf) -> Self {
45 Self {
46 voa_location,
47 canonicalized,
48 }
49 }
50
51 /// The [`VoaLocation`] that this verifier file was found through
52 pub fn voa_location(&self) -> &VoaLocation {
53 &self.voa_location
54 }
55
56 /// Returns a reference to the canonicalized path for the file that this [`Verifier`]
57 /// represents.
58 pub fn canonicalized(&self) -> &Path {
59 &self.canonicalized
60 }
61
62 /// Returns the optional file name part of the canonicalized path of this [`Verifier`].
63 pub(crate) fn filename(&self) -> Option<&std::ffi::OsStr> {
64 self.canonicalized.file_name()
65 }
66
67 /// Opens the file this [`Verifier`] represents as a [`File`] in read-only mode.
68 ///
69 /// # Errors
70 ///
71 /// Returns an error if the file (see [`Verifier::canonicalized`]) cannot be opened for reading.
72 pub fn open(&self) -> Result<File, Error> {
73 File::open(&self.canonicalized).map_err(|source| Error::IoPath {
74 path: self.canonicalized.clone(),
75 context: "opening the file for reading",
76 source,
77 })
78 }
79}
80
81/// A [`VoaLocation`] combines a load path and a full set of identifier parameters.
82/// It represents a logical (not canonicalized) location in a VOA filesystem hierarchy.
83///
84/// A [`VoaLocation`] points to a "leaf directory" in the VOA structure.
85/// Signature verifier files are situated in a [`VoaLocation`].
86#[derive(Clone, Debug, PartialEq)]
87pub struct VoaLocation {
88 load_path: LoadPath,
89 os: Os,
90 purpose: Purpose,
91 context: Context,
92 technology: Technology,
93}
94
95impl VoaLocation {
96 /// Creates a new [`VoaLocation`].
97 pub(crate) fn new(
98 load_path: LoadPath,
99 os: Os,
100 purpose: Purpose,
101 context: Context,
102 technology: Technology,
103 ) -> Self {
104 Self {
105 load_path,
106 os,
107 purpose,
108 context,
109 technology,
110 }
111 }
112
113 /// The load path of the [`VoaLocation`].
114 pub fn load_path(&self) -> &LoadPath {
115 &self.load_path
116 }
117
118 /// The [`Os`] of the [`VoaLocation`].
119 pub fn os(&self) -> &Os {
120 &self.os
121 }
122
123 /// The [`Purpose`] of the [`VoaLocation`].
124 pub fn purpose(&self) -> &Purpose {
125 &self.purpose
126 }
127
128 /// The [`Context`] of the [`VoaLocation`].
129 pub fn context(&self) -> &Context {
130 &self.context
131 }
132
133 /// The [`Technology`] of the [`VoaLocation`].
134 pub fn technology(&self) -> &Technology {
135 &self.technology
136 }
137
138 /// Canonicalize a [`VoaLocation`] and check that its identifiers conform to VOA
139 /// restrictions.
140 ///
141 /// Ensures that the provided [`VoaLocation`] points to a legal path in the local
142 /// filesystem, and that any involved symlinks conform to the VOA symlink restrictions.
143 ///
144 /// Checks the legality of symlinks (if any) in the VOA path structure, and
145 /// returns the canonicalized path to the target directory.
146 ///
147 /// # Errors
148 ///
149 /// Returns an error if
150 ///
151 /// - the load path of this [`VoaLocation`] can't be canonicalized,
152 /// - any intermediate symlink doesn't conform to VOA symlinking rules,
153 /// e.g. by escaping from `legal_symlink_paths`.
154 /// (also see <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking>),
155 /// - if there are cycles in any symlink chain,
156 /// - if the target (or any intermediate) path is not a directory.
157 pub(crate) fn check_and_canonicalize(
158 &self,
159 legal_symlink_paths: &[&LoadPath],
160 ) -> Result<PathBuf, Error> {
161 // Canonicalized base load path
162 // (any potential internal symlinks of this top level "load_path" are not checked)
163 let base_path = self
164 .load_path()
165 .path()
166 .canonicalize()
167 .map_err(|source| Error::IoPath {
168 path: self.load_path.path.clone(),
169 context: "canonicalizing",
170 source,
171 })?;
172
173 let mut path = Self::append(&base_path, &self.os().path_segment(), legal_symlink_paths)?;
174 path = Self::append(&path, &self.purpose().path_segment(), legal_symlink_paths)?;
175 path = Self::append(&path, &self.context().path_segment(), legal_symlink_paths)?;
176 path = Self::append(
177 &path,
178 &self.technology().path_segment(),
179 legal_symlink_paths,
180 )?;
181
182 Ok(path)
183 }
184
185 /// Append a segment to a path and ensure that the resulting path conforms
186 /// to the VOA symlink constraints.
187 ///
188 /// # Errors
189 ///
190 /// Returns an error if
191 ///
192 /// - any intermediate symlink doesn't conform to VOA symlinking rules,
193 /// e.g. by escaping from `legal_symlink_paths`.
194 /// (also see <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking>),
195 /// - if there are cycles in any symlink chain,
196 /// - if the target path is not a directory.
197 fn append(
198 current_path: &Path,
199 segment: &Path,
200 legal_symlink_paths: &[&LoadPath],
201 ) -> Result<PathBuf, Error> {
202 // Segments are not allowed to contain the `/` char.
203 // This prevents any `../` attacks to hijack the path.
204 let mut buf = current_path.join(segment);
205
206 if buf.is_symlink() {
207 buf = match resolve_symlink(&buf, legal_symlink_paths)? {
208 ResolvedSymlink::Dir(dir) => dir,
209 ResolvedSymlink::File(path) => {
210 return Err(Error::IllegalSymlink {
211 path,
212 context: "Unexpected file",
213 });
214 }
215 ResolvedSymlink::Masked => {
216 // VOA must not consider masking symlinks for directories
217 return Err(Error::IllegalSymlink {
218 path: buf,
219 context: "Illegal masking symlink from directory",
220 });
221 }
222 };
223 }
224
225 if buf.is_dir() {
226 Ok(buf)
227 } else {
228 Err(Error::ExpectedDirectory { path: buf })
229 }
230 }
231}