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}