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, CTorServicePath, KeyPath, KeystoreEntry, KeystoreEntryResult, Result,
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
/// [`CTorServicePath::PrivateKey`] (with [`KeyType::Ed25519ExpandedKeypair`])
41
/// and [`CTorServicePath::PublicKey`] (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 [`Service`](CTorPath::Service),
51
/// or if the [`HsNickname`] specified in the `CTorPath` does not match the nickname of this store,
52
/// the key will be declared not found.
53
/// If the requested `CTorPath` is [`Service`](CTorPath::Service),
54
/// but the `ItemType` and [`CTorServicePath`] are mismatched,
55
/// an error is returned.
56
pub struct CTorServiceKeystore {
57
    /// The underlying keystore
58
    keystore: CTorKeystore,
59
    /// The nickname of the service this keystore is meant for
60
    nickname: HsNickname,
61
}
62

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

            
77
484
        Ok(Self { keystore, nickname })
78
484
    }
79
}
80

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

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

            
95
        // This keystore only deals with service keys...
96
        let CTorPath::Service { path, nickname } = ctor_path else {
97
            return $ret;
98
        };
99

            
100
        // ...more specifically, it has the service keys of a *particular* service
101
        // (identified by nickname).
102
        if nickname != $self.nickname {
103
            return $ret;
104
        };
105

            
106
        let relpath = $self.keystore.rel_path(PathBuf::from(path.to_string()));
107
        match ($item_type, &path) {
108
            (Key(KeyType::Ed25519ExpandedKeypair), CTorServicePath::PrivateKey)
109
            | (Key(KeyType::Ed25519PublicKey), CTorServicePath::PublicKey) => Ok(()),
110
            _ => Err(CTorKeystoreError::InvalidKeystoreItemType {
111
                item_type: $item_type.clone(),
112
                item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
113
            }),
114
        }?;
115

            
116
        relpath
117
    }};
118
}
119

            
120
impl Keystore for CTorServiceKeystore {
121
2606
    fn id(&self) -> &KeystoreId {
122
2606
        &self.keystore.id
123
2606
    }
124

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

            
128
4
        let meta = match checked_op!(metadata, path) {
129
4
            Ok(meta) => meta,
130
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
131
            Err(e) => {
132
                return Err(FilesystemError::FsMistrust {
133
                    action: FilesystemAction::Read,
134
                    path: path.rel_path_unchecked().into(),
135
                    err: e.into(),
136
                })
137
                .map_err(|e| CTorKeystoreError::Filesystem(e).into());
138
            }
139
        };
140

            
141
        // The path exists, now check that it's actually a file and not a directory or symlink.
142
4
        if meta.is_file() {
143
4
            Ok(true)
144
        } else {
145
            Err(
146
                CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
147
                    path.rel_path_unchecked().into(),
148
                ))
149
                .into(),
150
            )
151
        }
152
4
    }
153

            
154
408
    fn get(
155
408
        &self,
156
408
        key_spec: &dyn KeySpecifier,
157
408
        item_type: &KeystoreItemType,
158
408
    ) -> Result<Option<ErasedKey>> {
159
        use KeystoreItemType::*;
160

            
161
408
        let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
162

            
163
404
        let key = match checked_op!(read, path) {
164
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
165
404
            res => res
166
404
                .map_err(|err| FilesystemError::FsMistrust {
167
                    action: FilesystemAction::Read,
168
                    path: path.rel_path_unchecked().into(),
169
                    err: err.into(),
170
                })
171
404
                .map_err(CTorKeystoreError::Filesystem)?,
172
        };
173

            
174
404
        let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
175
            path: path.rel_path_unchecked().into(),
176
            err: err.into(),
177
        };
178

            
179
404
        let parsed_key: ErasedKey = match item_type {
180
302
            Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
181
302
                .map_err(parse_err)
182
302
                .map(Box::new)?,
183
102
            Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
184
102
                .map_err(parse_err)
185
102
                .map(Box::new)?,
186
            _ => {
187
                return Err(
188
                    internal!("item type was not validated by rel_path_if_supported?!").into(),
189
                );
190
            }
191
        };
192

            
193
404
        Ok(Some(parsed_key))
194
408
    }
195

            
196
    #[cfg(feature = "onion-service-cli-extra")]
197
    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
198
        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
199
    }
200

            
201
2
    fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
202
2
        Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
203
2
    }
204

            
205
2
    fn remove(
206
2
        &self,
207
2
        _key_spec: &dyn KeySpecifier,
208
2
        _item_type: &KeystoreItemType,
209
2
    ) -> Result<Option<()>> {
210
2
        Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
211
2
    }
212

            
213
    #[cfg(feature = "onion-service-cli-extra")]
214
    fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
215
        Err(CTorKeystoreError::NotSupported {
216
            action: "remove_unchecked",
217
        }
218
        .into())
219
    }
220

            
221
452
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
222
        use crate::CTorServicePath::*;
223

            
224
        // This keystore can contain at most 2 keys (the public and private
225
        // keys of the service)
226
452
        let all_keys = [
227
452
            (
228
452
                CTorPath::Service {
229
452
                    nickname: self.nickname.clone(),
230
452
                    path: PublicKey,
231
452
                },
232
452
                KeyType::Ed25519PublicKey,
233
452
            ),
234
452
            (
235
452
                CTorPath::Service {
236
452
                    nickname: self.nickname.clone(),
237
452
                    path: PrivateKey,
238
452
                },
239
452
                KeyType::Ed25519ExpandedKeypair,
240
452
            ),
241
452
        ];
242

            
243
452
        let valid_rel_paths = all_keys
244
452
            .into_iter()
245
914
            .map(|(ctor_path, key_type)| {
246
904
                let path = rel_path_if_supported!(
247
                    self,
248
904
                    ctor_path,
249
                    Err(internal!("Failed to build {ctor_path:?} path?!").into()),
250
                    KeystoreItemType::Key(key_type.clone())
251
                );
252

            
253
904
                Ok((ctor_path, key_type, path))
254
904
            })
255
452
            .collect::<Result<Vec<_>>>()?;
256

            
257
452
        let keystore_path = self.keystore.keystore_dir.as_path();
258

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

            
278
1958
                let path = entry.path();
279

            
280
                // Skip over directories as they won't be valid ctor-paths
281
1958
                if entry.file_type().is_dir() {
282
452
                    return Ok(None);
283
1506
                }
284

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

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

            
308
                // Check if path is one of the valid C Tor service key paths
309
1506
                let maybe_path =
310
1506
                    valid_rel_paths
311
1506
                        .iter()
312
2560
                        .find_map(|(ctor_path, key_type, rel_path)| {
313
2560
                            (path == rel_path.rel_path_unchecked())
314
2560
                                .then_some((ctor_path, key_type, rel_path))
315
2560
                        });
316

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

            
337
1506
                Ok(Some(res))
338
1958
            })
339
452
            .flatten_ok()
340
452
            .collect()
341
452
    }
342
}
343

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

            
349
        if $key.len() != expected_len {
350
            return Err(MalformedServiceKeyError::InvalidKeyLen {
351
                len: $key.len(),
352
                expected_len,
353
            });
354
        }
355

            
356
        let (tag, key) = $key.split_at($tag.len());
357

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

            
365
        ($parse_fn)(key)
366
    }};
367
}
368

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

            
376
102
    parse_ed25519!(
377
102
        key,
378
102
        |key| ed25519::PublicKey::try_from(key)
379
102
            .map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
380
        PUBKEY_TAG,
381
        PUBKEY_LEN
382
    )
383
102
}
384

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

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

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

            
424
    use super::*;
425
    use std::fs;
426
    use tempfile::{TempDir, tempdir};
427

            
428
    use crate::CTorServicePath;
429
    use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
430

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

            
434
    #[cfg(unix)]
435
    use std::os::unix::fs::PermissionsExt;
436

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

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

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

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

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

            
462
        (keystore, keystore_dir)
463
    }
464

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

            
469
        let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
470
        let path = CTorPath::Service {
471
            nickname: unk_nickname.clone(),
472
            path: CTorServicePath::PublicKey,
473
        };
474

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

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

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

            
508
    #[test]
509
    fn unsupported_operation() {
510
        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
511
        let path = CTorPath::Service {
512
            nickname: keystore.nickname.clone(),
513
            path: CTorServicePath::PublicKey,
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::Service {
537
            nickname: keystore.nickname.clone(),
538
            path: CTorServicePath::PublicKey,
539
        };
540

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

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

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

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

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

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

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

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

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