voa-core 0.4.1

File Hierarchy for the Verification of OS Artifacts (VOA)
Documentation
//! Verifier path handling and canonicalization

use std::{
    fmt::Debug,
    fs::File,
    path::{Path, PathBuf},
};

use crate::{
    Error,
    identifiers::{Context, Os, Purpose, SegmentPath, Technology},
    load_path::LoadPath,
    util::symlinks::{PathType, ResolvedSymlink, resolve_symlink},
};

/// A [`Verifier`] points to a signature verifier in the file system.
///
/// It consists of the [`VoaLocation`] via which the verifier was obtained, and a canonicalized
/// path to the actual verifier file.
///
/// Depending on the verifier [`Technology`], a [`Verifier`] instance may represent, e.g.:
///
/// - an individual, standalone signature verifier,
/// - an individual verifier that acts as a trust anchor,
/// - a certificate complete with its trust chain,
/// - a set of individual verifiers in one shared data structure.
#[derive(Clone, Debug)]
pub struct Verifier {
    /// The logical VOA location via which the verifier was found
    voa_location: VoaLocation,

    /// Canonicalized path of the verifier file
    canonicalized: PathBuf,
}

impl Verifier {
    /// Creates a new [`Verifier`].
    ///
    /// # Note
    ///
    /// Callers of this constructor must ensure that `canonicalized` contains a fully canonicalized
    /// filename, that the file exists, and that the naming and any potential symlinks involved
    /// conform to the constraints defined in the VOA specification.
    pub(crate) fn new(voa_location: VoaLocation, canonicalized: PathBuf) -> Self {
        Self {
            voa_location,
            canonicalized,
        }
    }

    /// The [`VoaLocation`] that this verifier file was found through
    pub fn voa_location(&self) -> &VoaLocation {
        &self.voa_location
    }

    /// Returns a reference to the canonicalized path for the file that this [`Verifier`]
    /// represents.
    pub fn canonicalized(&self) -> &Path {
        &self.canonicalized
    }

    /// Returns the optional file name part of the canonicalized path of this [`Verifier`].
    pub(crate) fn filename(&self) -> Option<&std::ffi::OsStr> {
        self.canonicalized.file_name()
    }

    /// Opens the file this [`Verifier`] represents as a [`File`] in read-only mode.
    ///
    /// # Errors
    ///
    /// Returns an error if the file (see [`Verifier::canonicalized`]) cannot be opened for reading.
    pub fn open(&self) -> Result<File, Error> {
        File::open(&self.canonicalized).map_err(|source| Error::IoPath {
            path: self.canonicalized.clone(),
            context: "opening the file for reading",
            source,
        })
    }
}

/// A [`VoaLocation`] combines a load path and a full set of identifier parameters.
/// It represents a logical (not canonicalized) location in a VOA filesystem hierarchy.
///
/// A [`VoaLocation`] points to a "leaf directory" in the VOA structure.
/// Signature verifier files are situated in a [`VoaLocation`].
#[derive(Clone, Debug, PartialEq)]
pub struct VoaLocation {
    load_path: LoadPath,
    os: Os,
    purpose: Purpose,
    context: Context,
    technology: Technology,
}

impl VoaLocation {
    /// Creates a new [`VoaLocation`].
    pub(crate) fn new(
        load_path: LoadPath,
        os: Os,
        purpose: Purpose,
        context: Context,
        technology: Technology,
    ) -> Self {
        Self {
            load_path,
            os,
            purpose,
            context,
            technology,
        }
    }

    /// The load path of the [`VoaLocation`].
    pub fn load_path(&self) -> &LoadPath {
        &self.load_path
    }

    /// The [`Os`] of the [`VoaLocation`].
    pub fn os(&self) -> &Os {
        &self.os
    }

    /// The [`Purpose`] of the [`VoaLocation`].
    pub fn purpose(&self) -> &Purpose {
        &self.purpose
    }

    /// The [`Context`] of the [`VoaLocation`].
    pub fn context(&self) -> &Context {
        &self.context
    }

    /// The [`Technology`] of the [`VoaLocation`].
    pub fn technology(&self) -> &Technology {
        &self.technology
    }

    /// Canonicalize a [`VoaLocation`] and check that its identifiers conform to VOA
    /// restrictions.
    ///
    /// Ensures that the provided [`VoaLocation`] points to a legal path in the local
    /// filesystem, and that any involved symlinks conform to the VOA symlink restrictions.
    ///
    /// Checks the legality of symlinks (if any) in the VOA path structure, and
    /// returns the canonicalized path to the target directory.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - the load path of this [`VoaLocation`] can't be canonicalized,
    /// - any intermediate symlink doesn't conform to VOA symlinking rules,
    ///   e.g. by escaping from `legal_symlink_paths`.
    ///   (also see <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking>),
    /// - if there are cycles in any symlink chain,
    /// - if the target (or any intermediate) path is not a directory.
    pub(crate) fn check_and_canonicalize(
        &self,
        legal_symlink_paths: &[&LoadPath],
    ) -> Result<PathBuf, Error> {
        // Canonicalized base load path
        // (any potential internal symlinks of this top level "load_path" are not checked)
        let base_path = self
            .load_path()
            .path()
            .canonicalize()
            .map_err(|source| Error::IoPath {
                path: self.load_path.path.clone(),
                context: "canonicalizing",
                source,
            })?;

        let mut path = Self::append(&base_path, &self.os().path_segment()?, legal_symlink_paths)?;
        path = Self::append(&path, &self.purpose().path_segment()?, legal_symlink_paths)?;
        path = Self::append(&path, &self.context().path_segment()?, legal_symlink_paths)?;
        path = Self::append(
            &path,
            &self.technology().path_segment()?,
            legal_symlink_paths,
        )?;

        Ok(path)
    }

    /// Append a segment to a path and ensure that the resulting path conforms
    /// to the VOA symlink constraints.
    ///
    /// # Errors
    ///
    /// Returns an error if
    ///
    /// - any intermediate symlink doesn't conform to VOA symlinking rules,
    ///   e.g. by escaping from `legal_symlink_paths`.
    ///   (also see <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#symlinking>),
    /// - if there are cycles in any symlink chain,
    /// - if the target path is not a directory.
    fn append(
        current_path: &Path,
        segment: &SegmentPath,
        legal_symlink_paths: &[&LoadPath],
    ) -> Result<PathBuf, Error> {
        let mut buf = current_path.join(segment);

        if buf.is_symlink() {
            buf = match resolve_symlink(&buf, legal_symlink_paths, PathType::Dir)? {
                ResolvedSymlink::Dir(dir) => dir,
                ResolvedSymlink::File(path) => {
                    return Err(Error::IllegalSymlink {
                        path,
                        context: "Unexpected file",
                    });
                }
                ResolvedSymlink::Masked => {
                    // VOA must not consider masking symlinks for directories
                    return Err(Error::IllegalSymlink {
                        path: buf,
                        context: "Illegal masking symlink from directory",
                    });
                }
            };
        }

        if buf.is_dir() {
            Ok(buf)
        } else {
            Err(Error::ExpectedDirectory { path: buf })
        }
    }
}