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 [`HsClientDescEncKeypair`](CTorPath::HsClientDescEncKeypair),
47
/// the keystore will declare the key not found.
48
/// If the requested `CTorPath` is [`HsClientDescEncKeypair`](CTorPath::HsClientDescEncKeypair),
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
212
    pub fn from_path_and_mistrust(
59
212
        keystore_dir: impl AsRef<Path>,
60
212
        mistrust: &Mistrust,
61
212
        id: KeystoreId,
62
212
    ) -> Result<Self> {
63
212
        CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
64
212
    }
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::HsClientDescEncKeypair { hs_id } = 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
        hs_id
90
    }};
91
}
92

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

            
104
638
        Ok(entries)
105
638
    }
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
1040
    fn read_key(&self, key_path: &Path) -> StdResult<Option<String>, CTorKeystoreError> {
116
1040
        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
1040
        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
1040
            res => res
125
1040
                .map_err(|err| FilesystemError::FsMistrust {
126
                    action: FilesystemAction::Read,
127
                    path: key_path.rel_path_unchecked().into(),
128
                    err: err.into(),
129
                })
130
1040
                .map_err(CTorKeystoreError::Filesystem)?,
131
        };
132

            
133
1040
        Ok(Some(content))
134
1040
    }
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
638
    fn list_keys(
145
638
        &self,
146
638
    ) -> Result<
147
638
        impl Iterator<Item = StdResult<(HsId, HsClientDescEncKeypair), CTorKeystoreError>> + '_,
148
638
    > {
149
        use CTorKeystoreError::*;
150

            
151
638
        let dir = self.0.rel_path(PathBuf::from("."));
152
1229
        Ok(self.list_entries(&dir)?.filter_map(|entry| {
153
1210
            let entry = entry
154
1210
                .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
1210
                .ok()?;
165

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

            
175
1040
            let content = match self.read_key(path) {
176
1040
                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
1040
                parse_client_keypair(content.trim()).map_err(|e| MalformedKey {
184
80
                    path: path.into(),
185
80
                    err: e.into(),
186
80
                }),
187
            )
188
1210
        }))
189
638
    }
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
1040
fn parse_client_keypair(
203
1040
    key: impl AsRef<str>,
204
1040
) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
205
1040
    let key = key.as_ref();
206
1040
    let (hsid, auth_type, key_type, encoded_key) = key
207
1040
        .split(':')
208
1040
        .collect_tuple()
209
1040
        .ok_or(MalformedClientKeyError::InvalidFormat)?;
210

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

            
215
974
    if key_type != "x25519" {
216
        return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
217
974
    }
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
974
    let encoded_key = encoded_key.to_uppercase();
225
974
    let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
226
960
    let x25519_sk: [u8; 32] = x25519_sk
227
960
        .try_into()
228
960
        .map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
229

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

            
235
960
    Ok((hsid, x25519_keypair.into()))
236
1040
}
237

            
238
impl Keystore for CTorClientKeystore {
239
1580
    fn id(&self) -> &KeystoreId {
240
1580
        &self.0.id
241
1580
    }
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
324
    fn get(
248
324
        &self,
249
324
        key_spec: &dyn KeySpecifier,
250
324
        item_type: &KeystoreItemType,
251
324
    ) -> Result<Option<ErasedKey>> {
252
324
        let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
253
322
        Ok(self
254
322
            .list_keys()?
255
577
            .find_map(|entry| {
256
566
                if let Ok((hsid, key)) = entry {
257
484
                    (hsid == want_hsid).then(|| key.into())
258
                } else {
259
82
                    None
260
                }
261
566
            })
262
332
            .map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
263
324
    }
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
316
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
291
        use CTorKeystoreError::*;
292

            
293
316
        let keys = self
294
316
            .list_keys()?
295
652
            .filter_map(|entry| match entry {
296
476
                Ok((hs_id, _)) => {
297
476
                    let key_path: KeyPath = CTorPath::HsClientDescEncKeypair { hs_id }.into();
298
476
                    let key_type: KeystoreItemType = KeyType::X25519StaticKeypair.into();
299
476
                    let raw_id = RawEntryId::Path(key_path.ctor()?.to_string().into());
300
476
                    Some(Ok(KeystoreEntry::new(
301
476
                        key_path,
302
476
                        key_type,
303
476
                        self.id(),
304
476
                        raw_id,
305
476
                    )))
306
                }
307
168
                Err(e) => match e {
308
168
                    MalformedKey { ref path, err: _ } => {
309
168
                        let raw_id = RawEntryId::Path(path.clone());
310
168
                        let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
311
168
                        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
644
            })
323
316
            .collect();
324

            
325
316
        Ok(keys)
326
316
    }
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::HsClientDescEncKeypair {
403
            hs_id: HsId::from_str(HSID).unwrap(),
404
        };
405

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

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

            
422
            // Found!
423
            assert_found!(
424
                keystore,
425
                &TestCTorSpecifier(path.clone()),
426
                &KeyType::X25519StaticKeypair,
427
                true
428
            );
429
        }
430

            
431
        let keys: Vec<_> = keystore
432
            .list()
433
            .unwrap()
434
            .into_iter()
435
            .filter(|e| e.is_ok())
436
            .collect();
437

            
438
        assert_eq!(keys.len(), 2);
439
        assert!(keys.iter().all(|entry| {
440
            entry.as_ref().unwrap().key_type() == &KeyType::X25519StaticKeypair.into()
441
        }));
442
    }
443

            
444
    #[test]
445
    fn unsupported_operation() {
446
        let (keystore, _keystore_dir) = init_keystore("foo");
447
        let path = CTorPath::HsClientDescEncKeypair {
448
            hs_id: HsId::from_str(HSID).unwrap(),
449
        };
450

            
451
        let err = keystore
452
            .remove(
453
                &TestCTorSpecifier(path.clone()),
454
                &KeyType::X25519StaticKeypair.into(),
455
            )
456
            .unwrap_err();
457

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

            
460
        let err = keystore
461
            .insert(&DummyKey, &TestCTorSpecifier(path))
462
            .unwrap_err();
463

            
464
        assert_eq!(err.to_string(), "Operation not supported: insert");
465
    }
466

            
467
    #[test]
468
    fn wrong_keytype() {
469
        let (keystore, _keystore_dir) = init_keystore("foo");
470
        let path = CTorPath::HsClientDescEncKeypair {
471
            hs_id: HsId::from_str(HSID).unwrap(),
472
        };
473

            
474
        let err = keystore
475
            .get(
476
                &TestCTorSpecifier(path.clone()),
477
                &KeyType::Ed25519PublicKey.into(),
478
            )
479
            .map(|_| ())
480
            .unwrap_err();
481

            
482
        assert_eq!(
483
            err.to_string(),
484
            "Invalid item type Ed25519PublicKey for client restricted discovery key"
485
        );
486
    }
487

            
488
    #[test]
489
    fn list() {
490
        let (keystore, _keystore_dir) = init_keystore("foo");
491
        // The keystore contains two recognized entries and three
492
        // unrecognized entries.
493
        let mut recognized = 0;
494
        let mut unrecognized = 0;
495
        for e in keystore.list().unwrap() {
496
            if e.is_ok() {
497
                recognized += 1;
498
            } else {
499
                unrecognized += 1;
500
            }
501
        }
502
        assert_eq!(recognized, 2);
503
        assert_eq!(unrecognized, 3);
504
    }
505
}