//! Service discovery client key providers. use crate::config::restricted_discovery::HsClientNickname; use crate::internal_prelude::*; use derive_more::From; use std::collections::BTreeMap; use std::fs::DirEntry; use derive_more::{AsRef, Into}; use fs_mistrust::{CheckedDir, Mistrust, MistrustBuilder}; use amplify::Getters; use serde_with::DisplayFromStr; use tor_config::define_list_builder_helper; use tor_config::derive::prelude::*; use tor_config::extend_builder::extend_with_replace; use tor_config::mistrust::BuilderExt as _; use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver}; use tor_error::warn_report; use tor_hscrypto::pk::HsClientDescEncKeyParseError; use tor_persist::slug::BadSlug; /// A static mapping from [`HsClientNickname`] to client discovery keys. #[serde_with::serde_as] #[derive(Default, Debug, Clone, Eq, PartialEq)] // #[derive(Into, From, AsRef, Serialize, Deserialize)] pub struct StaticKeyProvider( #[serde_as(as = "BTreeMap")] BTreeMap, ); define_list_builder_helper! { #[derive(Eq, PartialEq)] pub struct StaticKeyProviderBuilder { keys : [(HsClientNickname, HsClientDescEncKey)], } built: StaticKeyProvider = build_static(keys)?; default = vec![]; item_build: |value| Ok(value.clone()); item_apply_defaults: |_| Ok::<_, tor_config::ConfigBuildError>(()); #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")] } impl TryFrom for StaticKeyProviderBuilder { type Error = ConfigBuildError; fn try_from(value: StaticKeyProvider) -> Result { let mut list_builder = StaticKeyProviderBuilder::default(); for (nickname, key) in value.0 { list_builder.access().push((nickname, key)); } Ok(list_builder) } } impl From for StaticKeyProvider { /// Convert our Builder representation of a set of static keys into the /// format that serde will serialize. /// /// Note: This is a potentially lossy conversion, since the serialized format /// can't represent a collection of keys with duplicate nicknames. fn from(value: StaticKeyProviderBuilder) -> Self { let mut map = BTreeMap::new(); for (nickname, key) in value.keys.into_iter().flatten() { map.insert(nickname, key); } Self(map) } } /// Helper for building a [`StaticKeyProvider`] out of a list of client keys. /// /// Returns an error if the list contains duplicate keys fn build_static( keys: Vec<(HsClientNickname, HsClientDescEncKey)>, ) -> Result { let mut key_map = BTreeMap::new(); for (nickname, key) in keys.into_iter() { if key_map.insert(nickname.clone(), key).is_some() { return Err(ConfigBuildError::Invalid { field: "keys".into(), problem: format!("Multiple client keys for nickname {nickname}"), }); }; } Ok(StaticKeyProvider(key_map)) } /// A directory containing the client keys, each in the /// `descriptor:x25519:` format. /// /// Each file in this directory must have a file name of the form `.auth`, /// where `` is a valid [`HsClientNickname`]. #[derive(Debug, Clone, Deftly, Eq, PartialEq, Getters)] #[derive_deftly(TorConfig)] #[deftly(tor_config(no_default_trait))] pub struct DirectoryKeyProvider { /// The path. #[deftly(tor_config(no_default))] path: CfgPath, /// Configuration about which permissions we want to enforce on our files. #[deftly(tor_config( sub_builder(build_fn = "build_for_arti"), extend_with = "extend_with_replace" ))] permissions: Mistrust, } /// The serialized format of a [`DirectoryKeyProviderListBuilder`]: pub type DirectoryKeyProviderList = Vec; define_list_builder_helper! { pub struct DirectoryKeyProviderListBuilder { key_dirs: [DirectoryKeyProviderBuilder], } built: DirectoryKeyProviderList = key_dirs; default = vec![]; } impl DirectoryKeyProvider { /// Read the client service discovery keys from the specified directory. pub(super) fn read_keys( &self, path_resolver: &CfgPathResolver, ) -> Result, DirectoryKeyProviderError> { let dir_path = self.path.path(path_resolver).map_err(|err| { DirectoryKeyProviderError::PathExpansionFailed { path: self.path.clone(), err, } })?; let checked_dir = self .permissions .verifier() .secure_dir(&dir_path) .map_err(|err| DirectoryKeyProviderError::FsMistrust { path: dir_path.clone(), err, })?; // TODO: should this be a method on CheckedDir? Ok(fs::read_dir(checked_dir.as_path()) .map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))? .flat_map(|entry| match read_key_file(&checked_dir, entry) { Ok((client_nickname, key)) => Some((client_nickname, key)), Err(e) => { warn_report!(e, "Failed to read client discovery key",); None } }) .collect_vec()) } } /// Read the client key at `path`. fn read_key_file( checked_dir: &CheckedDir, entry: io::Result, ) -> Result<(HsClientNickname, HsClientDescEncKey), DirectoryKeyProviderError> { /// The extension the client key files are expected to have. const KEY_EXTENSION: &str = "auth"; let entry = entry.map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))?; if entry.path().is_dir() { return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry { path: entry.path(), problem: "entry is a directory".into(), }); } let file_name = entry.file_name(); let file_name: &Path = file_name.as_ref(); let extension = file_name.extension().and_then(|e| e.to_str()); if extension != Some(KEY_EXTENSION) { return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry { path: file_name.into(), problem: "invalid extension (file must end in .auth)".into(), }); } // We unwrap_or_default() instead of returning an error if the file stem is None, // because empty slugs handled by HsClientNickname::from_str (they are rejected). let client_nickname = file_name .file_stem() .and_then(|e| e.to_str()) .unwrap_or_default(); let client_nickname = HsClientNickname::from_str(client_nickname)?; let key = checked_dir.read_to_string(file_name).map_err(|err| { DirectoryKeyProviderError::FsMistrust { path: entry.path(), err, } })?; let parsed_key = HsClientDescEncKey::from_str(key.trim()).map_err(|err| { DirectoryKeyProviderError::KeyParse { path: entry.path(), err, } })?; Ok((client_nickname, parsed_key)) } /// Error type representing an invalid [`DirectoryKeyProvider`]. #[derive(Debug, Clone, thiserror::Error)] pub(super) enum DirectoryKeyProviderError { /// Encountered an inaccessible path or invalid permissions. #[error("Inaccessible path or bad permissions on {path}")] FsMistrust { /// The path of the key we were trying to read. path: PathBuf, /// The underlying error. #[source] err: fs_mistrust::Error, }, /// Encountered an error while reading the keys from disk. #[error("IO error while reading discovery keys")] IoError(#[source] Arc), /// We couldn't expand a path. #[error("Failed to expand path {path}")] PathExpansionFailed { /// The offending path. path: CfgPath, /// The error encountered. #[source] err: CfgPathError, }, /// Found an invalid key entry. #[error("{path} is not a valid key entry: {problem}")] InvalidKeyDirectoryEntry { /// The path of the key we were trying to read. path: PathBuf, /// The problem we encountered. problem: String, }, /// Failed to parse a client nickname. #[error("Invalid client nickname")] ClientNicknameParse(#[from] BadSlug), /// Failed to parse a key. #[error("Failed to parse key at {path}")] KeyParse { /// The path of the key we were trying to parse. path: PathBuf, /// The underlying error. #[source] err: HsClientDescEncKeyParseError, }, }