1
//! Service discovery client key providers.
2

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

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

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

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

            
15
use tor_config::define_list_builder_helper;
16
use tor_config::mistrust::BuilderExt as _;
17
use tor_config_path::{CfgPath, CfgPathError, CfgPathResolver};
18
use tor_error::warn_report;
19
use tor_hscrypto::pk::HsClientDescEncKeyParseError;
20
use 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)]
26
pub struct StaticKeyProvider(
27
    #[serde_as(as = "BTreeMap<DisplayFromStr, DisplayFromStr>")]
28
    BTreeMap<HsClientNickname, HsClientDescEncKey>,
29
);
30

            
31
define_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
144
    item_build: |value| Ok(value.clone());
39
    #[serde(try_from = "StaticKeyProvider", into = "StaticKeyProvider")]
40
}
41

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

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

            
54
impl 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
72
276
fn build_static(
73
276
    keys: Vec<(HsClientNickname, HsClientDescEncKey)>,
74
276
) -> Result<StaticKeyProvider, ConfigBuildError> {
75
276
    let mut key_map = BTreeMap::new();
76

            
77
276
    for (nickname, key) in keys.into_iter() {
78
144
        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
144
        };
84
    }
85

            
86
276
    Ok(StaticKeyProvider(key_map))
87
276
}
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
74
#[derive(Debug, Clone, Builder, Eq, PartialEq, Getters)]
95
#[builder(derive(Serialize, Deserialize, Debug))]
96
#[builder(build_fn(error = "ConfigBuildError"))]
97
pub 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`]:
108
pub type DirectoryKeyProviderList = Vec<DirectoryKeyProvider>;
109

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

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

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

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

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

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

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

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

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

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

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

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

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