Skip to main content

tor_hsservice/config/restricted_discovery/
key_provider.rs

1//! Service discovery client key providers.
2
3use crate::config::restricted_discovery::HsClientNickname;
4use crate::internal_prelude::*;
5use derive_more::From;
6
7use std::collections::BTreeMap;
8use std::fs::DirEntry;
9
10use derive_more::{AsRef, Into};
11use fs_mistrust::{CheckedDir, Mistrust, MistrustBuilder};
12
13use amplify::Getters;
14use serde_with::DisplayFromStr;
15
16use tor_config::define_list_builder_helper;
17use tor_config::derive::prelude::*;
18use tor_config::extend_builder::extend_with_replace;
19use tor_config::mistrust::BuilderExt as _;
20use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver};
21use tor_error::warn_report;
22use tor_hscrypto::pk::HsClientDescEncKeyParseError;
23use tor_persist::slug::BadSlug;
24
25/// A static mapping from [`HsClientNickname`] to client discovery keys.
26#[serde_with::serde_as]
27#[derive(Default, Debug, Clone, Eq, PartialEq)] //
28#[derive(Into, From, AsRef, Serialize, Deserialize)]
29pub struct StaticKeyProvider(
30    #[serde_as(as = "BTreeMap<DisplayFromStr, DisplayFromStr>")]
31    BTreeMap<HsClientNickname, HsClientDescEncKey>,
32);
33
34define_list_builder_helper! {
35    #[derive(Eq, PartialEq)]
36    pub struct StaticKeyProviderBuilder {
37        keys : [(HsClientNickname, HsClientDescEncKey)],
38    }
39    built: StaticKeyProvider = build_static(keys)?;
40    default = vec![];
41    item_build: |value| Ok(value.clone());
42    item_apply_defaults: |_| Ok::<_, tor_config::ConfigBuildError>(());
43    #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")]
44}
45
46impl TryFrom<StaticKeyProvider> for StaticKeyProviderBuilder {
47    type Error = ConfigBuildError;
48
49    fn try_from(value: StaticKeyProvider) -> Result<Self, Self::Error> {
50        let mut list_builder = StaticKeyProviderBuilder::default();
51        for (nickname, key) in value.0 {
52            list_builder.access().push((nickname, key));
53        }
54        Ok(list_builder)
55    }
56}
57
58impl From<StaticKeyProviderBuilder> for StaticKeyProvider {
59    /// Convert our Builder representation of a set of static keys into the
60    /// format that serde will serialize.
61    ///
62    /// Note: This is a potentially lossy conversion, since the serialized format
63    /// can't represent a collection of keys with duplicate nicknames.
64    fn from(value: StaticKeyProviderBuilder) -> Self {
65        let mut map = BTreeMap::new();
66        for (nickname, key) in value.keys.into_iter().flatten() {
67            map.insert(nickname, key);
68        }
69        Self(map)
70    }
71}
72
73/// Helper for building a [`StaticKeyProvider`] out of a list of client keys.
74///
75/// Returns an error if the list contains duplicate keys
76fn build_static(
77    keys: Vec<(HsClientNickname, HsClientDescEncKey)>,
78) -> Result<StaticKeyProvider, ConfigBuildError> {
79    let mut key_map = BTreeMap::new();
80
81    for (nickname, key) in keys.into_iter() {
82        if key_map.insert(nickname.clone(), key).is_some() {
83            return Err(ConfigBuildError::Invalid {
84                field: "keys".into(),
85                problem: format!("Multiple client keys for nickname {nickname}"),
86            });
87        };
88    }
89
90    Ok(StaticKeyProvider(key_map))
91}
92
93/// A directory containing the client keys, each in the
94/// `descriptor:x25519:<base32-encoded-x25519-public-key>` format.
95///
96/// Each file in this directory must have a file name of the form `<nickname>.auth`,
97/// where `<nickname>` is a valid [`HsClientNickname`].
98#[derive(Debug, Clone, Deftly, Eq, PartialEq, Getters)]
99#[derive_deftly(TorConfig)]
100#[deftly(tor_config(no_default_trait))]
101pub struct DirectoryKeyProvider {
102    /// The path.
103    #[deftly(tor_config(no_default))]
104    path: CfgPath,
105
106    /// Configuration about which permissions we want to enforce on our files.
107    #[deftly(tor_config(
108        sub_builder(build_fn = "build_for_arti"),
109        extend_with = "extend_with_replace"
110    ))]
111    permissions: Mistrust,
112}
113
114/// The serialized format of a [`DirectoryKeyProviderListBuilder`]:
115pub type DirectoryKeyProviderList = Vec<DirectoryKeyProvider>;
116
117define_list_builder_helper! {
118    pub struct DirectoryKeyProviderListBuilder {
119        key_dirs: [DirectoryKeyProviderBuilder],
120    }
121    built: DirectoryKeyProviderList = key_dirs;
122    default = vec![];
123}
124
125impl DirectoryKeyProvider {
126    /// Read the client service discovery keys from the specified directory.
127    pub(super) fn read_keys(
128        &self,
129        path_resolver: &CfgPathResolver,
130    ) -> Result<Vec<(HsClientNickname, HsClientDescEncKey)>, DirectoryKeyProviderError> {
131        let dir_path = self.path.path(path_resolver).map_err(|err| {
132            DirectoryKeyProviderError::PathExpansionFailed {
133                path: self.path.clone(),
134                err,
135            }
136        })?;
137
138        let checked_dir = self
139            .permissions
140            .verifier()
141            .secure_dir(&dir_path)
142            .map_err(|err| DirectoryKeyProviderError::FsMistrust {
143                path: dir_path.clone(),
144                err,
145            })?;
146
147        // TODO: should this be a method on CheckedDir?
148        Ok(fs::read_dir(checked_dir.as_path())
149            .map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))?
150            .flat_map(|entry| match read_key_file(&checked_dir, entry) {
151                Ok((client_nickname, key)) => Some((client_nickname, key)),
152                Err(e) => {
153                    warn_report!(e, "Failed to read client discovery key",);
154                    None
155                }
156            })
157            .collect_vec())
158    }
159}
160
161/// Read the client key at  `path`.
162fn read_key_file(
163    checked_dir: &CheckedDir,
164    entry: io::Result<DirEntry>,
165) -> Result<(HsClientNickname, HsClientDescEncKey), DirectoryKeyProviderError> {
166    /// The extension the client key files are expected to have.
167    const KEY_EXTENSION: &str = "auth";
168
169    let entry = entry.map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))?;
170
171    if entry.path().is_dir() {
172        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
173            path: entry.path(),
174            problem: "entry is a directory".into(),
175        });
176    }
177
178    let file_name = entry.file_name();
179    let file_name: &Path = file_name.as_ref();
180    let extension = file_name.extension().and_then(|e| e.to_str());
181    if extension != Some(KEY_EXTENSION) {
182        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
183            path: file_name.into(),
184            problem: "invalid extension (file must end in .auth)".into(),
185        });
186    }
187
188    // We unwrap_or_default() instead of returning an error if the file stem is None,
189    // because empty slugs handled by HsClientNickname::from_str (they are rejected).
190    let client_nickname = file_name
191        .file_stem()
192        .and_then(|e| e.to_str())
193        .unwrap_or_default();
194    let client_nickname = HsClientNickname::from_str(client_nickname)?;
195
196    let key = checked_dir.read_to_string(file_name).map_err(|err| {
197        DirectoryKeyProviderError::FsMistrust {
198            path: entry.path(),
199            err,
200        }
201    })?;
202
203    let parsed_key = HsClientDescEncKey::from_str(key.trim()).map_err(|err| {
204        DirectoryKeyProviderError::KeyParse {
205            path: entry.path(),
206            err,
207        }
208    })?;
209
210    Ok((client_nickname, parsed_key))
211}
212
213/// Error type representing an invalid [`DirectoryKeyProvider`].
214#[derive(Debug, Clone, thiserror::Error)]
215pub(super) enum DirectoryKeyProviderError {
216    /// Encountered an inaccessible path or invalid permissions.
217    #[error("Inaccessible path or bad permissions on {path}")]
218    FsMistrust {
219        /// The path of the key we were trying to read.
220        path: PathBuf,
221        /// The underlying error.
222        #[source]
223        err: fs_mistrust::Error,
224    },
225
226    /// Encountered an error while reading the keys from disk.
227    #[error("IO error while reading discovery keys")]
228    IoError(#[source] Arc<io::Error>),
229
230    /// We couldn't expand a path.
231    #[error("Failed to expand path {path}")]
232    PathExpansionFailed {
233        /// The offending path.
234        path: CfgPath,
235        /// The error encountered.
236        #[source]
237        err: CfgPathError,
238    },
239
240    /// Found an invalid key entry.
241    #[error("{path} is not a valid key entry: {problem}")]
242    InvalidKeyDirectoryEntry {
243        /// The path of the key we were trying to read.
244        path: PathBuf,
245        /// The problem we encountered.
246        problem: String,
247    },
248
249    /// Failed to parse a client nickname.
250    #[error("Invalid client nickname")]
251    ClientNicknameParse(#[from] BadSlug),
252
253    /// Failed to parse a key.
254    #[error("Failed to parse key at {path}")]
255    KeyParse {
256        /// The path of the key we were trying to parse.
257        path: PathBuf,
258        /// The underlying error.
259        #[source]
260        err: HsClientDescEncKeyParseError,
261    },
262}