1
//! Service discovery client key providers.
2

            
3
use crate::config::restricted_discovery::HsClientNickname;
4
use crate::internal_prelude::*;
5
use derive_more::From;
6

            
7
use std::collections::BTreeMap;
8
use std::fs::DirEntry;
9

            
10
use derive_more::{AsRef, Into};
11
use fs_mistrust::{CheckedDir, Mistrust, MistrustBuilder};
12

            
13
use amplify::Getters;
14
use serde_with::DisplayFromStr;
15

            
16
use tor_config::define_list_builder_helper;
17
use tor_config::derive::prelude::*;
18
use tor_config::extend_builder::extend_with_replace;
19
use tor_config::mistrust::BuilderExt as _;
20
use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver};
21
use tor_error::warn_report;
22
use tor_hscrypto::pk::HsClientDescEncKeyParseError;
23
use 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)]
29
pub struct StaticKeyProvider(
30
    #[serde_as(as = "BTreeMap<DisplayFromStr, DisplayFromStr>")]
31
    BTreeMap<HsClientNickname, HsClientDescEncKey>,
32
);
33

            
34
define_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
172
    item_build: |value| Ok(value.clone());
42
    #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")]
43
}
44

            
45
impl TryFrom<StaticKeyProvider> for StaticKeyProviderBuilder {
46
    type Error = ConfigBuildError;
47

            
48
37
    fn try_from(value: StaticKeyProvider) -> Result<Self, Self::Error> {
49
37
        let mut list_builder = StaticKeyProviderBuilder::default();
50
111
        for (nickname, key) in value.0 {
51
74
            list_builder.access().push((nickname, key));
52
74
        }
53
37
        Ok(list_builder)
54
37
    }
55
}
56

            
57
impl From<StaticKeyProviderBuilder> for StaticKeyProvider {
58
    /// Convert our Builder representation of a set of static keys into the
59
    /// format that serde will serialize.
60
    ///
61
    /// Note: This is a potentially lossy conversion, since the serialized format
62
    /// can't represent a collection of keys with duplicate nicknames.
63
    fn from(value: StaticKeyProviderBuilder) -> Self {
64
        let mut map = BTreeMap::new();
65
        for (nickname, key) in value.keys.into_iter().flatten() {
66
            map.insert(nickname, key);
67
        }
68
        Self(map)
69
    }
70
}
71

            
72
/// Helper for building a [`StaticKeyProvider`] out of a list of client keys.
73
///
74
/// Returns an error if the list contains duplicate keys
75
1330
fn build_static(
76
1330
    keys: Vec<(HsClientNickname, HsClientDescEncKey)>,
77
1330
) -> Result<StaticKeyProvider, ConfigBuildError> {
78
1330
    let mut key_map = BTreeMap::new();
79

            
80
1330
    for (nickname, key) in keys.into_iter() {
81
172
        if key_map.insert(nickname.clone(), key).is_some() {
82
            return Err(ConfigBuildError::Invalid {
83
                field: "keys".into(),
84
                problem: format!("Multiple client keys for nickname {nickname}"),
85
            });
86
172
        };
87
    }
88

            
89
1330
    Ok(StaticKeyProvider(key_map))
90
1330
}
91

            
92
/// A directory containing the client keys, each in the
93
/// `descriptor:x25519:<base32-encoded-x25519-public-key>` format.
94
///
95
/// Each file in this directory must have a file name of the form `<nickname>.auth`,
96
/// where `<nickname>` is a valid [`HsClientNickname`].
97
#[derive(Debug, Clone, Deftly, Eq, PartialEq, Getters)]
98
#[derive_deftly(TorConfig)]
99
#[deftly(tor_config(no_default_trait))]
100
pub struct DirectoryKeyProvider {
101
    /// The path.
102
    #[deftly(tor_config(no_default))]
103
    path: CfgPath,
104

            
105
    /// Configuration about which permissions we want to enforce on our files.
106
    #[deftly(tor_config(
107
        sub_builder(build_fn = "build_for_arti"),
108
        extend_with = "extend_with_replace"
109
    ))]
110
    permissions: Mistrust,
111
}
112

            
113
/// The serialized format of a [`DirectoryKeyProviderListBuilder`]:
114
pub type DirectoryKeyProviderList = Vec<DirectoryKeyProvider>;
115

            
116
define_list_builder_helper! {
117
    pub struct DirectoryKeyProviderListBuilder {
118
        key_dirs: [DirectoryKeyProviderBuilder],
119
    }
120
    built: DirectoryKeyProviderList = key_dirs;
121
    default = vec![];
122
}
123

            
124
impl DirectoryKeyProvider {
125
    /// Read the client service discovery keys from the specified directory.
126
12
    pub(super) fn read_keys(
127
12
        &self,
128
12
        path_resolver: &CfgPathResolver,
129
12
    ) -> Result<Vec<(HsClientNickname, HsClientDescEncKey)>, DirectoryKeyProviderError> {
130
12
        let dir_path = self.path.path(path_resolver).map_err(|err| {
131
            DirectoryKeyProviderError::PathExpansionFailed {
132
                path: self.path.clone(),
133
                err,
134
            }
135
        })?;
136

            
137
12
        let checked_dir = self
138
12
            .permissions
139
12
            .verifier()
140
12
            .secure_dir(&dir_path)
141
12
            .map_err(|err| DirectoryKeyProviderError::FsMistrust {
142
                path: dir_path.clone(),
143
                err,
144
            })?;
145

            
146
        // TODO: should this be a method on CheckedDir?
147
12
        Ok(fs::read_dir(checked_dir.as_path())
148
12
            .map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))?
149
46
            .flat_map(|entry| match read_key_file(&checked_dir, entry) {
150
36
                Ok((client_nickname, key)) => Some((client_nickname, key)),
151
4
                Err(e) => {
152
4
                    warn_report!(e, "Failed to read client discovery key",);
153
4
                    None
154
                }
155
40
            })
156
12
            .collect_vec())
157
12
    }
158
}
159

            
160
/// Read the client key at  `path`.
161
40
fn read_key_file(
162
40
    checked_dir: &CheckedDir,
163
40
    entry: io::Result<DirEntry>,
164
40
) -> Result<(HsClientNickname, HsClientDescEncKey), DirectoryKeyProviderError> {
165
    /// The extension the client key files are expected to have.
166
    const KEY_EXTENSION: &str = "auth";
167

            
168
40
    let entry = entry.map_err(|e| DirectoryKeyProviderError::IoError(Arc::new(e)))?;
169

            
170
40
    if entry.path().is_dir() {
171
        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
172
            path: entry.path(),
173
            problem: "entry is a directory".into(),
174
        });
175
40
    }
176

            
177
40
    let file_name = entry.file_name();
178
40
    let file_name: &Path = file_name.as_ref();
179
60
    let extension = file_name.extension().and_then(|e| e.to_str());
180
40
    if extension != Some(KEY_EXTENSION) {
181
2
        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
182
2
            path: file_name.into(),
183
2
            problem: "invalid extension (file must end in .auth)".into(),
184
2
        });
185
38
    }
186

            
187
    // We unwrap_or_default() instead of returning an error if the file stem is None,
188
    // because empty slugs handled by HsClientNickname::from_str (they are rejected).
189
38
    let client_nickname = file_name
190
38
        .file_stem()
191
57
        .and_then(|e| e.to_str())
192
38
        .unwrap_or_default();
193
38
    let client_nickname = HsClientNickname::from_str(client_nickname)?;
194

            
195
38
    let key = checked_dir.read_to_string(file_name).map_err(|err| {
196
        DirectoryKeyProviderError::FsMistrust {
197
            path: entry.path(),
198
            err,
199
        }
200
    })?;
201

            
202
39
    let parsed_key = HsClientDescEncKey::from_str(key.trim()).map_err(|err| {
203
2
        DirectoryKeyProviderError::KeyParse {
204
2
            path: entry.path(),
205
2
            err,
206
2
        }
207
3
    })?;
208

            
209
36
    Ok((client_nickname, parsed_key))
210
40
}
211

            
212
/// Error type representing an invalid [`DirectoryKeyProvider`].
213
#[derive(Debug, Clone, thiserror::Error)]
214
pub(super) enum DirectoryKeyProviderError {
215
    /// Encountered an inaccessible path or invalid permissions.
216
    #[error("Inaccessible path or bad permissions on {path}")]
217
    FsMistrust {
218
        /// The path of the key we were trying to read.
219
        path: PathBuf,
220
        /// The underlying error.
221
        #[source]
222
        err: fs_mistrust::Error,
223
    },
224

            
225
    /// Encountered an error while reading the keys from disk.
226
    #[error("IO error while reading discovery keys")]
227
    IoError(#[source] Arc<io::Error>),
228

            
229
    /// We couldn't expand a path.
230
    #[error("Failed to expand path {path}")]
231
    PathExpansionFailed {
232
        /// The offending path.
233
        path: CfgPath,
234
        /// The error encountered.
235
        #[source]
236
        err: CfgPathError,
237
    },
238

            
239
    /// Found an invalid key entry.
240
    #[error("{path} is not a valid key entry: {problem}")]
241
    InvalidKeyDirectoryEntry {
242
        /// The path of the key we were trying to read.
243
        path: PathBuf,
244
        /// The problem we encountered.
245
        problem: String,
246
    },
247

            
248
    /// Failed to parse a client nickname.
249
    #[error("Invalid client nickname")]
250
    ClientNicknameParse(#[from] BadSlug),
251

            
252
    /// Failed to parse a key.
253
    #[error("Failed to parse key at {path}")]
254
    KeyParse {
255
        /// The path of the key we were trying to parse.
256
        path: PathBuf,
257
        /// The underlying error.
258
        #[source]
259
        err: HsClientDescEncKeyParseError,
260
    },
261
}