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