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
    item_apply_defaults: |_| Ok::<_, tor_config::ConfigBuildError>(());
43
    #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")]
44
}
45

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

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

            
58
impl 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
76
1478
fn build_static(
77
1478
    keys: Vec<(HsClientNickname, HsClientDescEncKey)>,
78
1478
) -> Result<StaticKeyProvider, ConfigBuildError> {
79
1478
    let mut key_map = BTreeMap::new();
80

            
81
1478
    for (nickname, key) in keys.into_iter() {
82
172
        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
172
        };
88
    }
89

            
90
1478
    Ok(StaticKeyProvider(key_map))
91
1478
}
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))]
101
pub 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`]:
115
pub type DirectoryKeyProviderList = Vec<DirectoryKeyProvider>;
116

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

            
125
impl DirectoryKeyProvider {
126
    /// Read the client service discovery keys from the specified directory.
127
12
    pub(super) fn read_keys(
128
12
        &self,
129
12
        path_resolver: &CfgPathResolver,
130
12
    ) -> Result<Vec<(HsClientNickname, HsClientDescEncKey)>, DirectoryKeyProviderError> {
131
12
        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
12
        let checked_dir = self
139
12
            .permissions
140
12
            .verifier()
141
12
            .secure_dir(&dir_path)
142
12
            .map_err(|err| DirectoryKeyProviderError::FsMistrust {
143
                path: dir_path.clone(),
144
                err,
145
            })?;
146

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

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

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

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

            
178
40
    let file_name = entry.file_name();
179
40
    let file_name: &Path = file_name.as_ref();
180
60
    let extension = file_name.extension().and_then(|e| e.to_str());
181
40
    if extension != Some(KEY_EXTENSION) {
182
2
        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
183
2
            path: file_name.into(),
184
2
            problem: "invalid extension (file must end in .auth)".into(),
185
2
        });
186
38
    }
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
38
    let client_nickname = file_name
191
38
        .file_stem()
192
57
        .and_then(|e| e.to_str())
193
38
        .unwrap_or_default();
194
38
    let client_nickname = HsClientNickname::from_str(client_nickname)?;
195

            
196
38
    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
39
    let parsed_key = HsClientDescEncKey::from_str(key.trim()).map_err(|err| {
204
2
        DirectoryKeyProviderError::KeyParse {
205
2
            path: entry.path(),
206
2
            err,
207
2
        }
208
3
    })?;
209

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

            
213
/// Error type representing an invalid [`DirectoryKeyProvider`].
214
#[derive(Debug, Clone, thiserror::Error)]
215
pub(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
}