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;
10
use crate::{
11
    CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, Result, UnrecognizedEntry,
12
    UnrecognizedEntryError,
13
};
14

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

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

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

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

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

            
81
620
        Ok(Self { keystore, nickname })
82
620
    }
83
}
84

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

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

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

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

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

            
124
        relpath
125
    }};
126
}
127

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

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

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

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

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

            
169
328
        let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
170

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

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

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

            
201
324
        Ok(Some(parsed_key))
202
328
    }
203

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

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

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

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

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

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

            
257
964
                Ok((ctor_path, key_type, path))
258
964
            })
259
482
            .collect::<Result<Vec<_>>>()?;
260

            
261
482
        let keystore_path = self.keystore.keystore_dir.as_path();
262

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

            
282
2168
                let path = entry.path();
283

            
284
                // Skip over directories as they won't be valid ctor-paths
285
2168
                if entry.file_type().is_dir() {
286
482
                    return Ok(None);
287
1686
                }
288

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

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

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

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

            
341
1686
                Ok(Some(res))
342
2168
            })
343
482
            .flatten_ok()
344
482
            .collect()
345
482
    }
346
}
347

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

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

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

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

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

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

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

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

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

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

            
429
    use super::*;
430
    use std::fs;
431
    use tempfile::{TempDir, tempdir};
432

            
433
    use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
434

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

            
438
    #[cfg(unix)]
439
    use std::os::unix::fs::PermissionsExt;
440

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

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

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

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

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

            
466
        (keystore, keystore_dir)
467
    }
468

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

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

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

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

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

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

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

            
523
        assert_eq!(err.to_string(), "Operation not supported: remove");
524

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

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

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

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

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

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

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

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

            
561
        let keys: Vec<_> = keystore.list().unwrap();
562

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

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

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

            
580
        assert!(keys.iter().any(|entry| { entry.is_err() }));
581
    }
582
}