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

            
5
use crate::keystore::ctor::CTorKeystore;
6
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedServiceKeyError};
7
use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, checked_op};
8
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore, KeystoreId};
9
use crate::raw::{RawEntryId, RawKeystoreEntry};
10
use crate::{
11
    CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, Result, UnrecognizedEntryError,
12
};
13

            
14
use fs_mistrust::Mistrust;
15
use tor_basic_utils::PathExt as _;
16
use tor_error::internal;
17
use tor_key_forge::{KeyType, KeystoreItemType};
18
use tor_llcrypto::pk::ed25519;
19
use tor_persist::hsnickname::HsNickname;
20

            
21
use std::io;
22
use std::path::{Path, PathBuf};
23
use std::result::Result as StdResult;
24
#[allow(unused_imports)]
25
use std::str::FromStr;
26
use std::sync::Arc;
27

            
28
use itertools::Itertools;
29
use walkdir::WalkDir;
30

            
31
/// A read-only C Tor service keystore.
32
///
33
/// This keystore provides read-only access to the hidden service keys
34
/// rooted at a given `HiddenServiceDirectory` directory
35
/// (see `HiddenServiceDirectory` in `tor(1)`).
36
///
37
/// This keystore can be used to read the `HiddenServiceDirectory/private_key`
38
/// and `HiddenServiceDirectory/public_key` C Tor keys, specified by
39
/// [`CTorPath::HsIdKeypair`] (with [`KeyType::Ed25519ExpandedKeypair`])
40
/// and [`CTorPath::HsIdPublicKey`] (with [`KeyType::Ed25519PublicKey`]),
41
/// respectively. Any other files stored in `HiddenServiceDirectory` will be ignored.
42
///
43
/// The only supported [`Keystore`] operations are [`contains`](Keystore::contains),
44
/// [`get`](Keystore::get), and [`list`](Keystore::list). All other keystore operations
45
/// will return an error.
46
///
47
/// This keystore implementation uses the [`CTorPath`] of the requested [`KeySpecifier`]
48
/// and the [`KeystoreItemType`] to identify the appropriate key.
49
/// If the requested `CTorPath` is not
50
/// [`HsIdPublicKey`](CTorPath::HsIdPublicKey) or
51
/// [`HsIdKeypair`](CTorPath::HsIdKeypair),
52
/// or if the [`HsNickname`] specified in the `CTorPath` does not match the nickname of this store,
53
/// the key will be declared not found.
54
/// If the requested `CTorPath` is
55
/// [`HsIdPublicKey`](CTorPath::HsIdPublicKey) or
56
/// [`HsIdKeypair`](CTorPath::HsIdKeypair),
57
/// but the `ItemType` and [`CTorPath`] are mismatched,
58
/// an error is returned.
59
pub struct CTorServiceKeystore {
60
    /// The underlying keystore
61
    keystore: CTorKeystore,
62
    /// The nickname of the service this keystore is meant for
63
    nickname: HsNickname,
64
}
65

            
66
impl CTorServiceKeystore {
67
    /// Create a new `CTorServiceKeystore`
68
    /// rooted at the specified `keystore_dir` directory.
69
    ///
70
    /// This function returns an error if `keystore_dir` is not a directory,
71
    /// or if it does not conform to the requirements of the specified `Mistrust`.
72
484
    pub fn from_path_and_mistrust(
73
484
        keystore_dir: impl AsRef<Path>,
74
484
        mistrust: &Mistrust,
75
484
        id: KeystoreId,
76
484
        nickname: HsNickname,
77
484
    ) -> Result<Self> {
78
484
        let keystore = CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id)?;
79

            
80
484
        Ok(Self { keystore, nickname })
81
484
    }
82
}
83

            
84
/// Extract the key path (relative to the keystore root) from the specified result `res`,
85
/// or return an error.
86
///
87
/// If `res` is `None`, return `ret`.
88
macro_rules! rel_path_if_supported {
89
    ($self:expr, $spec:expr, $ret:expr, $item_type:expr) => {{
90
        use CTorPath::*;
91
        use KeystoreItemType::*;
92

            
93
        // If the key specifier doesn't have a CTorPath,
94
        // we can't possibly handle this key.
95
        let Some(ctor_path) = $spec.ctor_path() else {
96
            return $ret;
97
        };
98

            
99
        // This keystore only deals with service keys...
100
        let nickname = match &ctor_path {
101
            HsClientDescEncKeypair { .. } => return $ret,
102
            HsIdKeypair { nickname } | HsIdPublicKey { nickname } => nickname,
103
        };
104

            
105
        // ...more specifically, it has the service keys of a *particular* service
106
        // (identified by nickname).
107
        if nickname != &$self.nickname {
108
            return $ret;
109
        };
110

            
111
        let relpath = $self
112
            .keystore
113
            .rel_path(PathBuf::from(ctor_path.to_string()));
114
        match ($item_type, &ctor_path) {
115
            (Key(KeyType::Ed25519ExpandedKeypair), HsIdKeypair { .. })
116
            | (Key(KeyType::Ed25519PublicKey), HsIdPublicKey { .. }) => Ok(()),
117
            _ => Err(CTorKeystoreError::InvalidKeystoreItemType {
118
                item_type: $item_type.clone(),
119
                item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
120
            }),
121
        }?;
122

            
123
        relpath
124
    }};
125
}
126

            
127
impl Keystore for CTorServiceKeystore {
128
2710
    fn id(&self) -> &KeystoreId {
129
2710
        &self.keystore.id
130
2710
    }
131

            
132
4
    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
133
4
        let path = rel_path_if_supported!(self, key_spec, Ok(false), item_type);
134

            
135
4
        let meta = match checked_op!(metadata, path) {
136
4
            Ok(meta) => meta,
137
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
138
            Err(e) => {
139
                return Err(FilesystemError::FsMistrust {
140
                    action: FilesystemAction::Read,
141
                    path: path.rel_path_unchecked().into(),
142
                    err: e.into(),
143
                })
144
                .map_err(|e| CTorKeystoreError::Filesystem(e).into());
145
            }
146
        };
147

            
148
        // The path exists, now check that it's actually a file and not a directory or symlink.
149
4
        if meta.is_file() {
150
4
            Ok(true)
151
        } else {
152
            Err(
153
                CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
154
                    path.rel_path_unchecked().into(),
155
                ))
156
                .into(),
157
            )
158
        }
159
4
    }
160

            
161
424
    fn get(
162
424
        &self,
163
424
        key_spec: &dyn KeySpecifier,
164
424
        item_type: &KeystoreItemType,
165
424
    ) -> Result<Option<ErasedKey>> {
166
        use KeystoreItemType::*;
167

            
168
424
        let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
169

            
170
420
        let key = match checked_op!(read, path) {
171
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
172
420
            res => res
173
420
                .map_err(|err| FilesystemError::FsMistrust {
174
                    action: FilesystemAction::Read,
175
                    path: path.rel_path_unchecked().into(),
176
                    err: err.into(),
177
                })
178
420
                .map_err(CTorKeystoreError::Filesystem)?,
179
        };
180

            
181
420
        let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
182
            path: path.rel_path_unchecked().into(),
183
            err: err.into(),
184
        };
185

            
186
420
        let parsed_key: ErasedKey = match item_type {
187
314
            Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
188
314
                .map_err(parse_err)
189
314
                .map(Box::new)?,
190
106
            Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
191
106
                .map_err(parse_err)
192
106
                .map(Box::new)?,
193
            _ => {
194
                return Err(
195
                    internal!("item type was not validated by rel_path_if_supported?!").into(),
196
                );
197
            }
198
        };
199

            
200
420
        Ok(Some(parsed_key))
201
424
    }
202

            
203
    #[cfg(feature = "onion-service-cli-extra")]
204
    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
205
        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
206
    }
207

            
208
2
    fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
209
2
        Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
210
2
    }
211

            
212
2
    fn remove(
213
2
        &self,
214
2
        _key_spec: &dyn KeySpecifier,
215
2
        _item_type: &KeystoreItemType,
216
2
    ) -> Result<Option<()>> {
217
2
        Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
218
2
    }
219

            
220
    #[cfg(feature = "onion-service-cli-extra")]
221
    fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
222
        Err(CTorKeystoreError::NotSupported {
223
            action: "remove_unchecked",
224
        }
225
        .into())
226
    }
227

            
228
470
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
229
        // This keystore can contain at most 2 keys (the public and private
230
        // keys of the service)
231
470
        let all_keys = [
232
470
            (
233
470
                CTorPath::HsIdPublicKey {
234
470
                    nickname: self.nickname.clone(),
235
470
                },
236
470
                KeyType::Ed25519PublicKey,
237
470
            ),
238
470
            (
239
470
                CTorPath::HsIdKeypair {
240
470
                    nickname: self.nickname.clone(),
241
470
                },
242
470
                KeyType::Ed25519ExpandedKeypair,
243
470
            ),
244
470
        ];
245

            
246
470
        let valid_rel_paths = all_keys
247
470
            .into_iter()
248
950
            .map(|(ctor_path, key_type)| {
249
940
                let path = rel_path_if_supported!(
250
                    self,
251
940
                    ctor_path,
252
                    Err(internal!("Failed to build {ctor_path:?} path?!").into()),
253
                    KeystoreItemType::Key(key_type.clone())
254
                );
255

            
256
940
                Ok((ctor_path, key_type, path))
257
940
            })
258
470
            .collect::<Result<Vec<_>>>()?;
259

            
260
470
        let keystore_path = self.keystore.keystore_dir.as_path();
261

            
262
        // TODO: this block presents duplication with the equivalent
263
        // [`ArtiNativeKeystore`](crate::ArtiNativeKeystore) implementation
264
470
        WalkDir::new(keystore_path)
265
470
            .into_iter()
266
2046
            .map(|entry| {
267
2036
                let entry = entry
268
2036
                    .map_err(|e| {
269
                        let msg = e.to_string();
270
                        FilesystemError::Io {
271
                            action: FilesystemAction::Read,
272
                            path: keystore_path.into(),
273
                            err: e
274
                                .into_io_error()
275
                                .unwrap_or_else(|| io::Error::other(msg.clone()))
276
                                .into(),
277
                        }
278
                    })
279
2036
                    .map_err(CTorKeystoreError::Filesystem)?;
280

            
281
2036
                let path = entry.path();
282

            
283
                // Skip over directories as they won't be valid ctor-paths
284
2036
                if entry.file_type().is_dir() {
285
470
                    return Ok(None);
286
1566
                }
287

            
288
1566
                let path = path.strip_prefix(keystore_path).map_err(|_| {
289
                    /* This error should be impossible. */
290
                    tor_error::internal!(
291
                        "found key {} outside of keystore_dir {}?!",
292
                        path.display_lossy(),
293
                        keystore_path.display_lossy()
294
                    )
295
                })?;
296

            
297
1566
                if let Some(parent) = path.parent() {
298
                    // Check the properties of the parent directory by attempting to list its
299
                    // contents.
300
1566
                    self.keystore
301
1566
                        .keystore_dir
302
1566
                        .read_directory(parent)
303
1566
                        .map_err(|e| FilesystemError::FsMistrust {
304
                            action: FilesystemAction::Read,
305
                            path: parent.into(),
306
                            err: e.into(),
307
                        })
308
1566
                        .map_err(CTorKeystoreError::Filesystem)?;
309
                }
310

            
311
                // Check if path is one of the valid C Tor service key paths
312
1566
                let maybe_path =
313
1566
                    valid_rel_paths
314
1566
                        .iter()
315
2662
                        .find_map(|(ctor_path, key_type, rel_path)| {
316
2662
                            (path == rel_path.rel_path_unchecked())
317
2662
                                .then_some((ctor_path, key_type, rel_path))
318
2662
                        });
319

            
320
1566
                let res = match maybe_path {
321
940
                    Some((ctor_path, key_type, rel_path)) => Ok(KeystoreEntry::new(
322
940
                        KeyPath::CTor(ctor_path.clone()),
323
940
                        KeystoreItemType::Key(key_type.clone()),
324
940
                        self.id(),
325
940
                        RawEntryId::Path(rel_path.rel_path_unchecked().to_owned()),
326
940
                    )),
327
                    None => {
328
626
                        let raw_id = RawEntryId::Path(path.into());
329
626
                        let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
330
626
                        Err(UnrecognizedEntryError::new(
331
626
                            entry,
332
626
                            Arc::new(CTorKeystoreError::MalformedKey {
333
626
                                path: path.into(),
334
626
                                err: MalformedServiceKeyError::NotAKey.into(),
335
626
                            }),
336
626
                        ))
337
                    }
338
                };
339

            
340
1566
                Ok(Some(res))
341
2036
            })
342
470
            .flatten_ok()
343
470
            .collect()
344
470
    }
345
}
346

            
347
/// Helper for parsing C Tor's ed25519 key format.
348
macro_rules! parse_ed25519 {
349
    ($key:expr, $parse_fn:expr, $tag:expr, $key_len:expr) => {{
350
        let expected_len = $tag.len() + $key_len;
351

            
352
        if $key.len() != expected_len {
353
            return Err(MalformedServiceKeyError::InvalidKeyLen {
354
                len: $key.len(),
355
                expected_len,
356
            });
357
        }
358

            
359
        let (tag, key) = $key.split_at($tag.len());
360

            
361
        if tag != $tag {
362
            return Err(MalformedServiceKeyError::InvalidTag {
363
                tag: tag.to_vec(),
364
                expected_tag: $tag.into(),
365
            });
366
        }
367

            
368
        ($parse_fn)(key)
369
    }};
370
}
371

            
372
/// Helper for parsing C Tor's ed25519 public key format.
373
106
fn parse_ed25519_public(key: &[u8]) -> StdResult<ed25519::PublicKey, MalformedServiceKeyError> {
374
    /// The tag C Tor ed25519 public keys are expected to begin with.
375
    const PUBKEY_TAG: &[u8] = b"== ed25519v1-public: type0 ==\0\0\0";
376
    /// The size of an ed25519 public key.
377
    const PUBKEY_LEN: usize = 32;
378

            
379
106
    parse_ed25519!(
380
106
        key,
381
106
        |key| ed25519::PublicKey::try_from(key)
382
106
            .map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
383
        PUBKEY_TAG,
384
        PUBKEY_LEN
385
    )
386
106
}
387

            
388
/// Helper for parsing C Tor's ed25519 keypair format.
389
314
fn parse_ed25519_keypair(
390
314
    key: &[u8],
391
314
) -> StdResult<ed25519::ExpandedKeypair, MalformedServiceKeyError> {
392
    /// The tag C Tor ed25519 keypairs are expected to begin with.
393
    const KEYPAIR_TAG: &[u8] = b"== ed25519v1-secret: type0 ==\0\0\0";
394
    /// The size of an ed25519 keypair.
395
    const KEYPAIR_LEN: usize = 64;
396

            
397
314
    parse_ed25519!(
398
314
        key,
399
314
        |key: &[u8]| {
400
314
            let key: [u8; 64] = key
401
314
                .try_into()
402
314
                .map_err(|_| internal!("bad length on expanded ed25519 secret key "))?;
403
314
            ed25519::ExpandedKeypair::from_secret_key_bytes(key)
404
314
                .ok_or(MalformedServiceKeyError::Ed25519Keypair)
405
314
        },
406
        KEYPAIR_TAG,
407
        KEYPAIR_LEN
408
    )
409
314
}
410

            
411
#[cfg(test)]
412
mod tests {
413
    // @@ begin test lint list maintained by maint/add_warning @@
414
    #![allow(clippy::bool_assert_comparison)]
415
    #![allow(clippy::clone_on_copy)]
416
    #![allow(clippy::dbg_macro)]
417
    #![allow(clippy::mixed_attributes_style)]
418
    #![allow(clippy::print_stderr)]
419
    #![allow(clippy::print_stdout)]
420
    #![allow(clippy::single_char_pattern)]
421
    #![allow(clippy::unwrap_used)]
422
    #![allow(clippy::unchecked_time_subtraction)]
423
    #![allow(clippy::useless_vec)]
424
    #![allow(clippy::needless_pass_by_value)]
425
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
426

            
427
    use super::*;
428
    use std::fs;
429
    use tempfile::{TempDir, tempdir};
430

            
431
    use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
432

            
433
    const PUBKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_public_key");
434
    const PRIVKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_secret_key");
435

            
436
    #[cfg(unix)]
437
    use std::os::unix::fs::PermissionsExt;
438

            
439
    fn init_keystore(id: &str, nickname: &str) -> (CTorServiceKeystore, TempDir) {
440
        let keystore_dir = tempdir().unwrap();
441

            
442
        #[cfg(unix)]
443
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
444

            
445
        let id = KeystoreId::from_str(id).unwrap();
446
        let nickname = HsNickname::from_str(nickname).unwrap();
447
        let keystore = CTorServiceKeystore::from_path_and_mistrust(
448
            &keystore_dir,
449
            &Mistrust::default(),
450
            id,
451
            nickname,
452
        )
453
        .unwrap();
454

            
455
        const KEYS: &[(&str, &[u8])] = &[
456
            ("hs_ed25519_public_key", PUBKEY),
457
            ("hs_ed25519_secret_key", PRIVKEY),
458
        ];
459

            
460
        for (name, key) in KEYS {
461
            fs::write(keystore_dir.path().join(name), key).unwrap();
462
        }
463

            
464
        (keystore, keystore_dir)
465
    }
466

            
467
    #[test]
468
    fn get() {
469
        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
470

            
471
        let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
472
        let path = CTorPath::HsIdPublicKey {
473
            nickname: unk_nickname.clone(),
474
        };
475

            
476
        // Not found!
477
        assert_found!(
478
            keystore,
479
            &TestCTorSpecifier(path.clone()),
480
            &KeyType::Ed25519PublicKey,
481
            false
482
        );
483

            
484
        // But if we use the right nickname (i.e. the one matching the keystore's nickname),
485
        // the key is found.
486
        let path = CTorPath::HsIdPublicKey {
487
            nickname: keystore.nickname.clone(),
488
        };
489
        assert_found!(
490
            keystore,
491
            &TestCTorSpecifier(path.clone()),
492
            &KeyType::Ed25519PublicKey,
493
            true
494
        );
495

            
496
        let path = CTorPath::HsIdKeypair {
497
            nickname: keystore.nickname.clone(),
498
        };
499
        assert_found!(
500
            keystore,
501
            &TestCTorSpecifier(path.clone()),
502
            &KeyType::Ed25519ExpandedKeypair,
503
            true
504
        );
505
    }
506

            
507
    #[test]
508
    fn unsupported_operation() {
509
        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
510
        let path = CTorPath::HsIdPublicKey {
511
            nickname: keystore.nickname.clone(),
512
        };
513

            
514
        let err = keystore
515
            .remove(
516
                &TestCTorSpecifier(path.clone()),
517
                &KeyType::Ed25519PublicKey.into(),
518
            )
519
            .unwrap_err();
520

            
521
        assert_eq!(err.to_string(), "Operation not supported: remove");
522

            
523
        let err = keystore
524
            .insert(&DummyKey, &TestCTorSpecifier(path.clone()))
525
            .unwrap_err();
526

            
527
        assert_eq!(err.to_string(), "Operation not supported: insert");
528
    }
529

            
530
    #[test]
531
    fn wrong_keytype() {
532
        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
533

            
534
        let path = CTorPath::HsIdPublicKey {
535
            nickname: keystore.nickname.clone(),
536
        };
537

            
538
        let err = keystore
539
            .get(
540
                &TestCTorSpecifier(path.clone()),
541
                &KeyType::X25519StaticKeypair.into(),
542
            )
543
            .map(|_| ())
544
            .unwrap_err();
545

            
546
        assert_eq!(
547
            err.to_string(),
548
            "Invalid item type X25519StaticKeypair for key hs_ed25519_public_key"
549
        );
550
    }
551

            
552
    #[test]
553
    fn list() {
554
        let (keystore, keystore_dir) = init_keystore("foo", "allium-cepa");
555

            
556
        // Insert unrecognized key
557
        let _ = fs::File::create(keystore_dir.path().join("unrecognized_key")).unwrap();
558

            
559
        let keys: Vec<_> = keystore.list().unwrap();
560

            
561
        // 2 recognized keys, 1 unrecognized key
562
        assert_eq!(keys.len(), 3);
563

            
564
        assert!(keys.iter().any(|entry| {
565
            if let Ok(e) = entry.as_ref() {
566
                return e.key_type() == &KeyType::Ed25519ExpandedKeypair.into();
567
            }
568
            false
569
        }));
570

            
571
        assert!(keys.iter().any(|entry| {
572
            if let Ok(e) = entry.as_ref() {
573
                return e.key_type() == &KeyType::Ed25519PublicKey.into();
574
            }
575
            false
576
        }));
577

            
578
        assert!(keys.iter().any(|entry| { entry.is_err() }));
579
    }
580
}