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::mistrust::BuilderExt as _;
18
use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver};
19
use tor_error::warn_report;
20
use tor_hscrypto::pk::HsClientDescEncKeyParseError;
21
use tor_persist::slug::BadSlug;
22

            
23
/// A static mapping from [`HsClientNickname`] to client discovery keys.
24
#[serde_with::serde_as]
25
#[derive(Default, Debug, Clone, Eq, PartialEq)] //
26
#[derive(Into, From, AsRef, Serialize, Deserialize)]
27
pub struct StaticKeyProvider(
28
    #[serde_as(as = "BTreeMap<DisplayFromStr, DisplayFromStr>")]
29
    BTreeMap<HsClientNickname, HsClientDescEncKey>,
30
);
31

            
32
define_list_builder_helper! {
33
    #[derive(Eq, PartialEq)]
34
    pub struct StaticKeyProviderBuilder {
35
        keys : [(HsClientNickname, HsClientDescEncKey)],
36
    }
37
    built: StaticKeyProvider = build_static(keys)?;
38
    default = vec![];
39
168
    item_build: |value| Ok(value.clone());
40
    #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")]
41
}
42

            
43
impl TryFrom<StaticKeyProvider> for StaticKeyProviderBuilder {
44
    type Error = ConfigBuildError;
45

            
46
36
    fn try_from(value: StaticKeyProvider) -> Result<Self, Self::Error> {
47
36
        let mut list_builder = StaticKeyProviderBuilder::default();
48
108
        for (nickname, key) in value.0 {
49
72
            list_builder.access().push((nickname, key));
50
72
        }
51
36
        Ok(list_builder)
52
36
    }
53
}
54

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

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

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

            
87
1292
    Ok(StaticKeyProvider(key_map))
88
1292
}
89

            
90
/// A directory containing the client keys, each in the
91
/// `descriptor:x25519:<base32-encoded-x25519-public-key>` format.
92
///
93
/// Each file in this directory must have a file name of the form `<nickname>.auth`,
94
/// where `<nickname>` is a valid [`HsClientNickname`].
95
#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters)]
96
#[builder(derive(Serialize, Deserialize, Debug))]
97
#[builder(build_fn(error = "ConfigBuildError"))]
98
pub struct DirectoryKeyProvider {
99
    /// The path.
100
    path: CfgPath,
101

            
102
    /// Configuration about which permissions we want to enforce on our files.
103
    #[builder(sub_builder(fn_name = "build_for_arti"))]
104
    #[builder_field_attr(serde(default))]
105
    permissions: Mistrust,
106
}
107

            
108
/// The serialized format of a [`DirectoryKeyProviderListBuilder`]:
109
pub type DirectoryKeyProviderList = Vec<DirectoryKeyProvider>;
110

            
111
define_list_builder_helper! {
112
    pub struct DirectoryKeyProviderListBuilder {
113
        key_dirs: [DirectoryKeyProviderBuilder],
114
    }
115
    built: DirectoryKeyProviderList = key_dirs;
116
    default = vec![];
117
}
118

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

            
132
12
        let checked_dir = self
133
12
            .permissions
134
12
            .verifier()
135
12
            .secure_dir(&dir_path)
136
12
            .map_err(|err| DirectoryKeyProviderError::FsMistrust {
137
                path: dir_path.clone(),
138
                err,
139
            })?;
140

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

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

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

            
165
40
    if entry.path().is_dir() {
166
        return Err(DirectoryKeyProviderError::InvalidKeyDirectoryEntry {
167
            path: entry.path(),
168
            problem: "entry is a directory".into(),
169
        });
170
40
    }
171

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

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

            
190
38
    let key = checked_dir.read_to_string(file_name).map_err(|err| {
191
        DirectoryKeyProviderError::FsMistrust {
192
            path: entry.path(),
193
            err,
194
        }
195
    })?;
196

            
197
39
    let parsed_key = HsClientDescEncKey::from_str(key.trim()).map_err(|err| {
198
2
        DirectoryKeyProviderError::KeyParse {
199
2
            path: entry.path(),
200
2
            err,
201
2
        }
202
3
    })?;
203

            
204
36
    Ok((client_nickname, parsed_key))
205
40
}
206

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

            
220
    /// Encountered an error while reading the keys from disk.
221
    #[error("IO error while reading discovery keys")]
222
    IoError(#[source] Arc<io::Error>),
223

            
224
    /// We couldn't expand a path.
225
    #[error("Failed to expand path {path}")]
226
    PathExpansionFailed {
227
        /// The offending path.
228
        path: CfgPath,
229
        /// The error encountered.
230
        #[source]
231
        err: CfgPathError,
232
    },
233

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

            
243
    /// Failed to parse a client nickname.
244
    #[error("Invalid client nickname")]
245
    ClientNicknameParse(#[from] BadSlug),
246

            
247
    /// Failed to parse a key.
248
    #[error("Failed to parse key at {path}")]
249
    KeyParse {
250
        /// The path of the key we were trying to parse.
251
        path: PathBuf,
252
        /// The underlying error.
253
        #[source]
254
        err: HsClientDescEncKeyParseError,
255
    },
256
}