1
//! Read-only C Tor client key store implementation
2
//!
3
//! See [`CTorClientKeystore`] for more details.
4

            
5
use std::fs;
6
use std::path::{Path, PathBuf};
7
use std::result::Result as StdResult;
8
use std::str::FromStr as _;
9
use std::sync::Arc;
10

            
11
use crate::keystore::ctor::CTorKeystore;
12
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
13
use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
14
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
15
use crate::raw::{RawEntryId, RawKeystoreEntry};
16
use crate::{
17
    CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId, Result,
18
    UnrecognizedEntryError,
19
};
20

            
21
use fs_mistrust::Mistrust;
22
use itertools::Itertools as _;
23
use tor_basic_utils::PathExt;
24
use tor_error::debug_report;
25
use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
26
use tor_key_forge::{KeyType, KeystoreItemType};
27
use tor_llcrypto::pk::curve25519;
28
use tracing::debug;
29

            
30
/// A read-only C Tor client keystore.
31
///
32
/// This keystore provides read-only access to the client restricted discovery keys
33
/// rooted at a given `ClientOnionAuthDir` directory (see `ClientOnionAuthDir` in `tor(1)`).
34
///
35
/// The key files must be in the
36
/// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>` format
37
/// and have the `.auth_private` extension.
38
/// Invalid keys, and keys that don't have the expected extension, will be ignored.
39
///
40
/// The only supported [`Keystore`] operations are [`contains`](Keystore::contains),
41
/// [`get`](Keystore::get), and [`list`](Keystore::list). All other keystore operations
42
/// will return an error.
43
///
44
/// This keystore implementation uses the [`CTorPath`] of the requested [`KeySpecifier`]
45
/// and the [`KeystoreItemType`] to identify the appropriate restricted discovery keypair.
46
/// If the requested `CTorPath` is not [`ClientHsDescEncKey`](CTorPath::ClientHsDescEncKey),
47
/// the keystore will declare the key not found.
48
/// If the requested `CTorPath` is [`ClientHsDescEncKey`](CTorPath::ClientHsDescEncKey),
49
/// but the `KeystoreItemType` is not [`X25519StaticKeypair`](KeyType::X25519StaticKeypair),
50
/// an error is returned.
51
pub struct CTorClientKeystore(CTorKeystore);
52

            
53
impl CTorClientKeystore {
54
    /// Create a new `CTorKeystore` rooted at the specified `keystore_dir` directory.
55
    ///
56
    /// This function returns an error if `keystore_dir` is not a directory,
57
    /// or if it does not conform to the requirements of the specified `Mistrust`.
58
8
    pub fn from_path_and_mistrust(
59
8
        keystore_dir: impl AsRef<Path>,
60
8
        mistrust: &Mistrust,
61
8
        id: KeystoreId,
62
8
    ) -> Result<Self> {
63
8
        CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
64
8
    }
65
}
66

            
67
/// Extract the HsId from `spec, or return `res`.
68
macro_rules! hsid_if_supported {
69
    ($spec:expr, $ret:expr, $key_type:expr) => {{
70
        // If the key specifier doesn't have a CTorPath,
71
        // we can't possibly handle this key.
72
        let Some(ctor_path) = $spec.ctor_path() else {
73
            return $ret;
74
        };
75

            
76
        // This keystore only deals with service keys...
77
        let CTorPath::ClientHsDescEncKey(hsid) = ctor_path else {
78
            return $ret;
79
        };
80

            
81
        if *$key_type != KeyType::X25519StaticKeypair.into() {
82
            return Err(CTorKeystoreError::InvalidKeystoreItemType {
83
                item_type: $key_type.clone(),
84
                item: "client restricted discovery key".into(),
85
            }
86
            .into());
87
        }
88

            
89
        hsid
90
    }};
91
}
92

            
93
impl CTorClientKeystore {
94
    /// List all the key entries in the keystore_dir.
95
14
    fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
96
14
        let entries = checked_op!(read_directory, dir)
97
14
            .map_err(|e| FilesystemError::FsMistrust {
98
                action: FilesystemAction::Read,
99
                path: dir.rel_path_unchecked().into(),
100
                err: e.into(),
101
            })
102
14
            .map_err(CTorKeystoreError::Filesystem)?;
103

            
104
14
        Ok(entries)
105
14
    }
106
}
107

            
108
/// The extension of the client keys stored in this store.
109
const KEY_EXTENSION: &str = "auth_private";
110

            
111
impl CTorClientKeystore {
112
    /// Read the contents of the specified key.
113
    ///
114
    /// Returns `Ok(None)` if the file doesn't exist.
115
52
    fn read_key(&self, key_path: &Path) -> StdResult<Option<String>, CTorKeystoreError> {
116
52
        let key_path = self.0.rel_path(key_path.into());
117

            
118
        // TODO: read and parse the key, see if it matches the specified hsid
119
52
        let content = match checked_op!(read_to_string, key_path) {
120
            Err(fs_mistrust::Error::NotFound(_)) => {
121
                // Someone removed the file between the time we read the directory and now.
122
                return Ok(None);
123
            }
124
52
            res => res
125
52
                .map_err(|err| FilesystemError::FsMistrust {
126
                    action: FilesystemAction::Read,
127
                    path: key_path.rel_path_unchecked().into(),
128
                    err: err.into(),
129
                })
130
52
                .map_err(CTorKeystoreError::Filesystem)?,
131
        };
132

            
133
52
        Ok(Some(content))
134
52
    }
135

            
136
    /// List all entries in this store
137
    ///
138
    /// Returns a list of results, where `Ok` signifies a recognized entry,
139
    /// and [`Err(CTorKeystoreError)`](crate::keystore::ctor::CTorKeystoreError)
140
    /// an unrecognized one.
141
    /// A key is said to be recognized if its file name ends with `.auth_private`,
142
    /// and it presents this format:
143
    /// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>`
144
14
    fn list_keys(
145
14
        &self,
146
14
    ) -> Result<
147
14
        impl Iterator<Item = StdResult<(HsId, HsClientDescEncKeypair), CTorKeystoreError>> + '_,
148
14
    > {
149
        use CTorKeystoreError::*;
150

            
151
14
        let dir = self.0.rel_path(PathBuf::from("."));
152
73
        Ok(self.list_entries(&dir)?.filter_map(|entry| {
153
66
            let entry = entry
154
66
                .map_err(|e| {
155
                    // NOTE: can't use debug_report here, because debug_report
156
                    // expects the ErrorKind (returned by e.kind()) to be
157
                    // tor_error::ErrorKind (which has a is_always_a_warning() function
158
                    // used by the macro).
159
                    //
160
                    // We have an io::Error here, which has an io::ErrorKind,
161
                    // and thus can't be used with debug_report.
162
                    debug!("cannot access key entry: {e}");
163
                })
164
66
                .ok()?;
165

            
166
66
            let file_name = entry.file_name();
167
66
            let path: &Path = file_name.as_ref();
168
66
            let Some(KEY_EXTENSION) = path.extension().and_then(|e| e.to_str()) else {
169
14
                return Some(Err(MalformedKey {
170
14
                    path: entry.path(),
171
14
                    err: MalformedClientKeyError::InvalidFormat.into(),
172
14
                }));
173
            };
174

            
175
52
            let content = match self.read_key(path) {
176
52
                Ok(c) => c,
177
                Err(e) => {
178
                    debug_report!(&e, "failed to read {}", path.display_lossy());
179
                    return Some(Err(e));
180
                }
181
            }?;
182
            Some(
183
52
                parse_client_keypair(content.trim()).map_err(|e| MalformedKey {
184
28
                    path: path.into(),
185
28
                    err: e.into(),
186
28
                }),
187
            )
188
66
        }))
189
14
    }
190
}
191

            
192
/// Parse a client restricted discovery keypair,
193
/// returning the [`HsId`] of the service the key is meant for,
194
/// and the corresponding [`HsClientDescEncKeypair`].
195
///
196
/// `key` is expected to be in the
197
/// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>`
198
/// format.
199
///
200
/// TODO: we might want to move this to tor-hscrypto at some point,
201
/// but for now, we don't actually *need* to expose this publicly.
202
52
fn parse_client_keypair(
203
52
    key: impl AsRef<str>,
204
52
) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
205
52
    let key = key.as_ref();
206
52
    let (hsid, auth_type, key_type, encoded_key) = key
207
52
        .split(':')
208
52
        .collect_tuple()
209
52
        .ok_or(MalformedClientKeyError::InvalidFormat)?;
210

            
211
52
    if auth_type != "descriptor" {
212
14
        return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
213
38
    }
214

            
215
38
    if key_type != "x25519" {
216
        return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
217
38
    }
218

            
219
    // Note: Tor's base32 decoder is case-insensitive, so we can't assume the input
220
    // is all uppercase.
221
    //
222
    // TODO: consider using `data_encoding_macro::new_encoding` to create a new Encoding
223
    // with an alphabet that includes lowercase letters instead of to_uppercase()ing the string.
224
38
    let encoded_key = encoded_key.to_uppercase();
225
38
    let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
226
24
    let x25519_sk: [u8; 32] = x25519_sk
227
24
        .try_into()
228
24
        .map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
229

            
230
24
    let secret = curve25519::StaticSecret::from(x25519_sk);
231
24
    let public = (&secret).into();
232
24
    let x25519_keypair = curve25519::StaticKeypair { secret, public };
233
24
    let hsid = HsId::from_str(&format!("{hsid}.onion"))?;
234

            
235
24
    Ok((hsid, x25519_keypair.into()))
236
52
}
237

            
238
impl Keystore for CTorClientKeystore {
239
20
    fn id(&self) -> &KeystoreId {
240
20
        &self.0.id
241
20
    }
242

            
243
4
    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
244
6
        self.get(key_spec, item_type).map(|k| k.is_some())
245
4
    }
246

            
247
12
    fn get(
248
12
        &self,
249
12
        key_spec: &dyn KeySpecifier,
250
12
        item_type: &KeystoreItemType,
251
12
    ) -> Result<Option<ErasedKey>> {
252
12
        let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
253
10
        Ok(self
254
10
            .list_keys()?
255
51
            .find_map(|entry| {
256
46
                if let Ok((hsid, key)) = entry {
257
16
                    (hsid == want_hsid).then(|| key.into())
258
                } else {
259
30
                    None
260
                }
261
46
            })
262
14
            .map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
263
12
    }
264

            
265
    #[cfg(feature = "onion-service-cli-extra")]
266
    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
267
        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
268
    }
269

            
270
2
    fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
271
2
        Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
272
2
    }
273

            
274
2
    fn remove(
275
2
        &self,
276
2
        _key_spec: &dyn KeySpecifier,
277
2
        _item_type: &KeystoreItemType,
278
2
    ) -> Result<Option<()>> {
279
2
        Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
280
2
    }
281

            
282
    #[cfg(feature = "onion-service-cli-extra")]
283
    fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
284
        Err(CTorKeystoreError::NotSupported {
285
            action: "remove_unchecked",
286
        }
287
        .into())
288
    }
289

            
290
4
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
291
        use CTorKeystoreError::*;
292

            
293
4
        let keys = self
294
4
            .list_keys()?
295
22
            .filter_map(|entry| match entry {
296
8
                Ok((hsid, _)) => {
297
8
                    let key_path: KeyPath = CTorPath::ClientHsDescEncKey(hsid).into();
298
8
                    let key_type: KeystoreItemType = KeyType::X25519StaticKeypair.into();
299
8
                    let raw_id = RawEntryId::Path(key_path.ctor()?.to_string().into());
300
8
                    Some(Ok(KeystoreEntry::new(
301
8
                        key_path,
302
8
                        key_type,
303
8
                        self.id(),
304
8
                        raw_id,
305
8
                    )))
306
                }
307
12
                Err(e) => match e {
308
12
                    MalformedKey { ref path, err: _ } => {
309
12
                        let raw_id = RawEntryId::Path(path.clone());
310
12
                        let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
311
12
                        Some(Err(UnrecognizedEntryError::new(entry, Arc::new(e))))
312
                    }
313
                    // `InvalidKeystoreItemType` variant is filtered out because it can't
314
                    // be returned by [`CTorClientKeystore::list_keys`].
315
                    InvalidKeystoreItemType { .. } => None,
316
                    // The following variants are irrelevant at this level because they
317
                    // cannot represent an unrecognized key.
318
                    Filesystem(_) => None,
319
                    NotSupported { .. } => None,
320
                    Bug(_) => None,
321
                },
322
20
            })
323
4
            .collect();
324

            
325
4
        Ok(keys)
326
4
    }
327
}
328

            
329
#[cfg(test)]
330
mod tests {
331
    // @@ begin test lint list maintained by maint/add_warning @@
332
    #![allow(clippy::bool_assert_comparison)]
333
    #![allow(clippy::clone_on_copy)]
334
    #![allow(clippy::dbg_macro)]
335
    #![allow(clippy::mixed_attributes_style)]
336
    #![allow(clippy::print_stderr)]
337
    #![allow(clippy::print_stdout)]
338
    #![allow(clippy::single_char_pattern)]
339
    #![allow(clippy::unwrap_used)]
340
    #![allow(clippy::unchecked_time_subtraction)]
341
    #![allow(clippy::useless_vec)]
342
    #![allow(clippy::needless_pass_by_value)]
343
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
344
    use super::*;
345
    use std::fs;
346
    use tempfile::{TempDir, tempdir};
347

            
348
    use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
349

            
350
    #[cfg(unix)]
351
    use std::os::unix::fs::PermissionsExt;
352

            
353
    /// A valid client restricted discovery key.
354
    const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");
355

            
356
    /// An invalid client restricted discovery key.
357
    const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");
358

            
359
    /// A valid client restricted discovery key.
360
    const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");
361

            
362
    /// A valid client restricted discovery key.
363
    const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");
364

            
365
    // An .onion addr we don't have a client key for.
366
    const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";
367

            
368
    fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
369
        let keystore_dir = tempdir().unwrap();
370

            
371
        #[cfg(unix)]
372
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
373

            
374
        let id = KeystoreId::from_str(id).unwrap();
375
        let keystore =
376
            CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
377
                .unwrap();
378

            
379
        let keys: &[(&str, &str)] = &[
380
            ("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
381
            // A couple of malformed key, added to check that our impl doesn't trip over them
382
            ("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
383
            (
384
                "alice-truncated.auth_private",
385
                &ALICE_AUTH_PRIVATE_VALID[..100],
386
            ),
387
            // A valid key, but with the wrong extension (so it should be ignored)
388
            ("carol.auth", CAROL_AUTH_PRIVATE_VALID),
389
            ("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
390
        ];
391

            
392
        for (name, key) in keys {
393
            fs::write(keystore_dir.path().join(name), key).unwrap();
394
        }
395

            
396
        (keystore, keystore_dir)
397
    }
398

            
399
    #[test]
400
    fn get() {
401
        let (keystore, _keystore_dir) = init_keystore("foo");
402
        let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
403

            
404
        // Not found!
405
        assert_found!(
406
            keystore,
407
            &TestCTorSpecifier(path.clone()),
408
            &KeyType::X25519StaticKeypair,
409
            false
410
        );
411

            
412
        for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
413
            // Extract the HsId associated with this key.
414
            let onion = hsid.split(":").next().unwrap();
415
            let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
416
            let path = CTorPath::ClientHsDescEncKey(hsid.clone());
417

            
418
            // Found!
419
            assert_found!(
420
                keystore,
421
                &TestCTorSpecifier(path.clone()),
422
                &KeyType::X25519StaticKeypair,
423
                true
424
            );
425
        }
426

            
427
        let keys: Vec<_> = keystore
428
            .list()
429
            .unwrap()
430
            .into_iter()
431
            .filter(|e| e.is_ok())
432
            .collect();
433

            
434
        assert_eq!(keys.len(), 2);
435
        assert!(keys.iter().all(|entry| {
436
            entry.as_ref().unwrap().key_type() == &KeyType::X25519StaticKeypair.into()
437
        }));
438
    }
439

            
440
    #[test]
441
    fn unsupported_operation() {
442
        let (keystore, _keystore_dir) = init_keystore("foo");
443
        let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
444

            
445
        let err = keystore
446
            .remove(
447
                &TestCTorSpecifier(path.clone()),
448
                &KeyType::X25519StaticKeypair.into(),
449
            )
450
            .unwrap_err();
451

            
452
        assert_eq!(err.to_string(), "Operation not supported: remove");
453

            
454
        let err = keystore
455
            .insert(&DummyKey, &TestCTorSpecifier(path))
456
            .unwrap_err();
457

            
458
        assert_eq!(err.to_string(), "Operation not supported: insert");
459
    }
460

            
461
    #[test]
462
    fn wrong_keytype() {
463
        let (keystore, _keystore_dir) = init_keystore("foo");
464
        let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
465

            
466
        let err = keystore
467
            .get(
468
                &TestCTorSpecifier(path.clone()),
469
                &KeyType::Ed25519PublicKey.into(),
470
            )
471
            .map(|_| ())
472
            .unwrap_err();
473

            
474
        assert_eq!(
475
            err.to_string(),
476
            "Invalid item type Ed25519PublicKey for client restricted discovery key"
477
        );
478
    }
479

            
480
    #[test]
481
    fn list() {
482
        let (keystore, _keystore_dir) = init_keystore("foo");
483
        // The keystore contains two recognized entries and three
484
        // unrecognized entries.
485
        let mut recognized = 0;
486
        let mut unrecognized = 0;
487
        for e in keystore.list().unwrap() {
488
            if e.is_ok() {
489
                recognized += 1;
490
            } else {
491
                unrecognized += 1;
492
            }
493
        }
494
        assert_eq!(recognized, 2);
495
        assert_eq!(unrecognized, 3);
496
    }
497
}