1
//! The Arti key store.
2
//!
3
//! See the [`ArtiNativeKeystore`] docs for more details.
4

            
5
pub(crate) mod certs;
6
pub(crate) mod err;
7
pub(crate) mod ssh;
8

            
9
use std::io::{self};
10
use std::path::{Path, PathBuf};
11
use std::result::Result as StdResult;
12
use std::str::FromStr;
13
use std::sync::Arc;
14

            
15
use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
16
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
17
use crate::raw::{RawEntryId, RawKeystoreEntry};
18
use crate::{
19
    arti_path, ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result,
20
    UnknownKeyTypeError, UnrecognizedEntryError,
21
};
22
use certs::UnparsedCert;
23
use err::ArtiNativeKeystoreError;
24
use ssh::UnparsedOpenSshKey;
25

            
26
use fs_mistrust::{CheckedDir, Mistrust};
27
use itertools::Itertools;
28
use tor_error::internal;
29
use walkdir::WalkDir;
30

            
31
use tor_basic_utils::PathExt as _;
32
use tor_key_forge::{CertData, KeystoreItem, KeystoreItemType};
33

            
34
use super::KeystoreEntryResult;
35

            
36
/// The Arti key store.
37
///
38
/// This is a disk-based key store that encodes keys in OpenSSH format.
39
///
40
/// Some of the key types supported by the [`ArtiNativeKeystore`]
41
/// don't have a predefined SSH public key [algorithm name],
42
/// so we define several custom SSH algorithm names.
43
/// As per [RFC4251 § 6], our custom SSH algorithm names use the
44
/// `<something@subdomain.torproject.org>` format.
45
///
46
/// We have assigned the following custom algorithm names:
47
///   * `x25519@spec.torproject.org`, for x25519 keys
48
///   * `ed25519-expanded@spec.torproject.org`, for expanded ed25519 keys
49
///
50
/// See [SSH protocol extensions] for more details.
51
///
52
/// [algorithm name]: https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-19
53
/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
54
/// [SSH protocol extensions]: https://spec.torproject.org/ssh-protocols.html
55
#[derive(Debug)]
56
pub struct ArtiNativeKeystore {
57
    /// The root of the key store.
58
    ///
59
    /// All the keys are stored within this directory.
60
    keystore_dir: CheckedDir,
61
    /// The unique identifier of this instance.
62
    id: KeystoreId,
63
}
64

            
65
impl ArtiNativeKeystore {
66
    /// Create a new [`ArtiNativeKeystore`] rooted at the specified `keystore_dir` directory.
67
    ///
68
    /// The `keystore_dir` directory is created if it doesn't exist.
69
    ///
70
    /// This function returns an error if `keystore_dir` is not a directory, if it does not conform
71
    /// to the requirements of the specified `Mistrust`, or if there was a problem creating the
72
    /// directory.
73
382
    pub fn from_path_and_mistrust(
74
382
        keystore_dir: impl AsRef<Path>,
75
382
        mistrust: &Mistrust,
76
382
    ) -> Result<Self> {
77
382
        let keystore_dir = mistrust
78
382
            .verifier()
79
382
            .check_content()
80
382
            .make_secure_dir(&keystore_dir)
81
382
            .map_err(|e| FilesystemError::FsMistrust {
82
2
                action: FilesystemAction::Init,
83
2
                path: keystore_dir.as_ref().into(),
84
2
                err: e.into(),
85
382
            })
86
382
            .map_err(ArtiNativeKeystoreError::Filesystem)?;
87

            
88
        // TODO: load the keystore ID from config.
89
380
        let id = KeystoreId::from_str("arti")?;
90
380
        Ok(Self { keystore_dir, id })
91
382
    }
92

            
93
    /// The path on disk of the key with the specified identity and type, relative to
94
    /// `keystore_dir`.
95
5964
    fn rel_path(
96
5964
        &self,
97
5964
        key_spec: &dyn KeySpecifier,
98
5964
        item_type: &KeystoreItemType,
99
5964
    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
100
5964
        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
101
5964
    }
102
}
103

            
104
/// Extract the key path (relative to the keystore root) from the specified result `res`,
105
/// or return an error.
106
///
107
/// If the underlying error is `ArtiPathUnavailable` (i.e. the `KeySpecifier` cannot provide
108
/// an `ArtiPath`), return `ret`.
109
macro_rules! rel_path_if_supported {
110
    ($res:expr, $ret:expr) => {{
111
        use ArtiPathUnavailableError::*;
112

            
113
        match $res {
114
            Ok(path) => path,
115
            Err(ArtiPathUnavailable) => return $ret,
116
            Err(e) => return Err(tor_error::internal!("invalid ArtiPath: {e}").into()),
117
        }
118
    }};
119
}
120

            
121
impl Keystore for ArtiNativeKeystore {
122
5046
    fn id(&self) -> &KeystoreId {
123
5046
        &self.id
124
5046
    }
125

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

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

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

            
155
2544
    fn get(
156
2544
        &self,
157
2544
        key_spec: &dyn KeySpecifier,
158
2544
        item_type: &KeystoreItemType,
159
2544
    ) -> Result<Option<ErasedKey>> {
160
2544
        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
161

            
162
2544
        let inner = match checked_op!(read, path) {
163
1808
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
164
736
            res => res
165
737
                .map_err(|err| FilesystemError::FsMistrust {
166
2
                    action: FilesystemAction::Read,
167
2
                    path: path.rel_path_unchecked().into(),
168
2
                    err: err.into(),
169
737
                })
170
736
                .map_err(ArtiNativeKeystoreError::Filesystem)?,
171
        };
172

            
173
734
        let abs_path = path
174
734
            .checked_path()
175
734
            .map_err(ArtiNativeKeystoreError::Filesystem)?;
176

            
177
734
        match item_type {
178
732
            KeystoreItemType::Key(key_type) => {
179
732
                let inner = String::from_utf8(inner).map_err(|_| {
180
                    let err = io::Error::new(
181
                        io::ErrorKind::InvalidData,
182
                        "OpenSSH key is not valid UTF-8".to_string(),
183
                    );
184

            
185
                    ArtiNativeKeystoreError::Filesystem(FilesystemError::Io {
186
                        action: FilesystemAction::Read,
187
                        path: abs_path.clone(),
188
                        err: err.into(),
189
                    })
190
732
                })?;
191

            
192
732
                UnparsedOpenSshKey::new(inner, abs_path)
193
732
                    .parse_ssh_format_erased(key_type)
194
732
                    .map(Some)
195
            }
196
2
            KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
197
2
                .parse_certificate_erased(cert_type)
198
2
                .map(Some),
199
            KeystoreItemType::Unknown { arti_extension } => Err(
200
                ArtiNativeKeystoreError::UnknownKeyType(UnknownKeyTypeError {
201
                    arti_extension: arti_extension.clone(),
202
                })
203
                .into(),
204
            ),
205
            _ => Err(internal!("unknown item type {item_type:?}").into()),
206
        }
207
2544
    }
208

            
209
    #[cfg(feature = "onion-service-cli-extra")]
210
    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
211
        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
212
    }
213

            
214
1412
    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
215
1412
        let keystore_item = key.as_keystore_item()?;
216
1412
        let item_type = keystore_item.item_type()?;
217
1412
        let path = self
218
1412
            .rel_path(key_spec, &item_type)
219
1412
            .map_err(|e| tor_error::internal!("{e}"))?;
220
1412
        let unchecked_path = path.rel_path_unchecked();
221

            
222
        // Create the parent directories as needed
223
1412
        if let Some(parent) = unchecked_path.parent() {
224
1412
            self.keystore_dir
225
1412
                .make_directory(parent)
226
1412
                .map_err(|err| FilesystemError::FsMistrust {
227
                    action: FilesystemAction::Write,
228
                    path: parent.to_path_buf(),
229
                    err: err.into(),
230
1412
                })
231
1412
                .map_err(ArtiNativeKeystoreError::Filesystem)?;
232
        }
233

            
234
1412
        let item_bytes: Vec<u8> = match keystore_item {
235
1410
            KeystoreItem::Key(key) => {
236
1410
                // TODO (#1095): decide what information, if any, to put in the comment
237
1410
                let comment = "";
238
1410
                key.to_openssh_string(comment)?.into_bytes()
239
            }
240
2
            KeystoreItem::Cert(cert) => match cert {
241
2
                CertData::TorEd25519Cert(cert) => cert.into(),
242
                _ => return Err(internal!("unknown cert type {item_type:?}").into()),
243
            },
244
            _ => return Err(internal!("unknown item type {item_type:?}").into()),
245
        };
246

            
247
1412
        Ok(checked_op!(write_and_replace, path, item_bytes)
248
1412
            .map_err(|err| FilesystemError::FsMistrust {
249
                action: FilesystemAction::Write,
250
                path: unchecked_path.into(),
251
                err: err.into(),
252
1412
            })
253
1412
            .map_err(ArtiNativeKeystoreError::Filesystem)?)
254
1412
    }
255

            
256
690
    fn remove(
257
690
        &self,
258
690
        key_spec: &dyn KeySpecifier,
259
690
        item_type: &KeystoreItemType,
260
690
    ) -> Result<Option<()>> {
261
690
        let rel_path = self
262
690
            .rel_path(key_spec, item_type)
263
690
            .map_err(|e| tor_error::internal!("{e}"))?;
264

            
265
690
        match checked_op!(remove_file, rel_path) {
266
684
            Ok(()) => Ok(Some(())),
267
4
            Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
268
2
            Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
269
2
                FilesystemError::FsMistrust {
270
2
                    action: FilesystemAction::Remove,
271
2
                    path: rel_path.rel_path_unchecked().into(),
272
2
                    err: e.into(),
273
2
                },
274
2
            ))?,
275
        }
276
690
    }
277

            
278
    #[cfg(feature = "onion-service-cli-extra")]
279
6
    fn remove_unchecked(&self, raw_id: &RawEntryId) -> Result<()> {
280
6
        match raw_id {
281
6
            RawEntryId::Path(path) => {
282
7
                self.keystore_dir.remove_file(path).map_err(|e| {
283
2
                    ArtiNativeKeystoreError::Filesystem(FilesystemError::FsMistrust {
284
2
                        action: FilesystemAction::Remove,
285
2
                        path: path.clone(),
286
2
                        err: e.into(),
287
2
                    })
288
7
                })?;
289
            }
290
            _other => {
291
                return Err(ArtiNativeKeystoreError::UnsupportedRawEntry(raw_id.clone()).into());
292
            }
293
        }
294
4
        Ok(())
295
6
    }
296

            
297
516
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
298
516
        WalkDir::new(self.keystore_dir.as_path())
299
516
            .into_iter()
300
5646
            .map(|entry| {
301
5616
                let entry = entry
302
5616
                    .map_err(|e| {
303
                        let msg = e.to_string();
304
                        FilesystemError::Io {
305
                            action: FilesystemAction::Read,
306
                            path: self.keystore_dir.as_path().into(),
307
                            err: e
308
                                .into_io_error()
309
                                .unwrap_or_else(|| io::Error::other(msg.to_string()))
310
                                .into(),
311
                        }
312
5616
                    })
313
5616
                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
314

            
315
5616
                let path = entry.path();
316
5616

            
317
5616
                // Skip over directories as they won't be valid arti-paths
318
5616
                //
319
5616
                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
320
5616
                if entry.file_type().is_dir() {
321
2052
                    return Ok(None);
322
3564
                }
323

            
324
3564
                let path = path
325
3564
                    .strip_prefix(self.keystore_dir.as_path())
326
3564
                    .map_err(|_| {
327
                        /* This error should be impossible. */
328
                        tor_error::internal!(
329
                            "found key {} outside of keystore_dir {}?!",
330
                            path.display_lossy(),
331
                            self.keystore_dir.as_path().display_lossy()
332
                        )
333
3564
                    })?;
334

            
335
3564
                if let Some(parent) = path.parent() {
336
                    // Check the properties of the parent directory by attempting to list its
337
                    // contents.
338
3564
                    self.keystore_dir
339
3564
                        .read_directory(parent)
340
3564
                        .map_err(|e| FilesystemError::FsMistrust {
341
2
                            action: FilesystemAction::Read,
342
2
                            path: parent.into(),
343
2
                            err: e.into(),
344
3564
                        })
345
3564
                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
346
                }
347

            
348
3562
                let unrecognized_entry_err = |path: &Path, err| {
349
6
                    let error = ArtiNativeKeystoreError::MalformedPath {
350
6
                        path: path.into(),
351
6
                        err,
352
6
                    };
353
6
                    let raw_id = RawEntryId::Path(path.into());
354
6
                    let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
355
6
                    Some(Err(UnrecognizedEntryError::new(entry, Arc::new(error))))
356
6
                };
357

            
358
3562
                let Some(ext) = path.extension() else {
359
6
                    return Ok(unrecognized_entry_err(
360
6
                        path,
361
6
                        err::MalformedPathError::NoExtension,
362
6
                    ));
363
                };
364

            
365
3556
                let Some(extension) = ext.to_str() else {
366
                    return Ok(unrecognized_entry_err(path, err::MalformedPathError::Utf8));
367
                };
368

            
369
3556
                let item_type = KeystoreItemType::from(extension);
370
3556
                // Strip away the file extension
371
3556
                let p = path.with_extension("");
372
3556
                // Construct slugs in platform-independent way
373
3556
                let slugs = p
374
3556
                    .components()
375
14224
                    .map(|component| component.as_os_str().to_string_lossy())
376
3556
                    .collect::<Vec<_>>()
377
3556
                    .join(&arti_path::PATH_SEP.to_string());
378
3556
                let opt = match ArtiPath::new(slugs) {
379
3556
                    Ok(arti_path) => {
380
3556
                        let raw_id = RawEntryId::Path(path.to_owned());
381
3556
                        Some(Ok(KeystoreEntry::new(
382
3556
                            arti_path.into(),
383
3556
                            item_type,
384
3556
                            self.id(),
385
3556
                            raw_id,
386
3556
                        )))
387
                    }
388
                    Err(e) => {
389
                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
390
                    }
391
                };
392
3556
                Ok(opt)
393
5646
            })
394
516
            .flatten_ok()
395
516
            .collect()
396
516
    }
397
}
398

            
399
#[cfg(test)]
400
mod tests {
401
    // @@ begin test lint list maintained by maint/add_warning @@
402
    #![allow(clippy::bool_assert_comparison)]
403
    #![allow(clippy::clone_on_copy)]
404
    #![allow(clippy::dbg_macro)]
405
    #![allow(clippy::mixed_attributes_style)]
406
    #![allow(clippy::print_stderr)]
407
    #![allow(clippy::print_stdout)]
408
    #![allow(clippy::single_char_pattern)]
409
    #![allow(clippy::unwrap_used)]
410
    #![allow(clippy::unchecked_duration_subtraction)]
411
    #![allow(clippy::useless_vec)]
412
    #![allow(clippy::needless_pass_by_value)]
413
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
414
    use super::*;
415
    use crate::test_utils::ssh_keys::*;
416
    use crate::test_utils::sshkeygen_ed25519_strings;
417
    use crate::test_utils::TEST_SPECIFIER_PATH;
418
    use crate::test_utils::{assert_found, TestSpecifier};
419
    use crate::KeyPath;
420
    use crate::UnrecognizedEntry;
421
    use std::cmp::Ordering;
422
    use std::fs;
423
    use std::path::PathBuf;
424
    use std::time::{Duration, SystemTime};
425
    use tempfile::{tempdir, TempDir};
426
    use tor_cert::{CertifiedKey, Ed25519Cert};
427
    use tor_checkable::{SelfSigned, Timebound};
428
    use tor_key_forge::{CertType, KeyType, ParsedEd25519Cert};
429
    use tor_llcrypto::pk::ed25519::{self, Ed25519PublicKey as _};
430

            
431
    #[cfg(unix)]
432
    use std::os::unix::fs::PermissionsExt;
433

            
434
    impl Ord for KeyPath {
435
        fn cmp(&self, other: &Self) -> Ordering {
436
            match (self, other) {
437
                (KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
438
                _ => unimplemented!("not supported"),
439
            }
440
        }
441
    }
442

            
443
    impl PartialOrd for KeyPath {
444
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
445
            Some(self.cmp(other))
446
        }
447
    }
448

            
449
    fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
450
        let rel_key_path = key_store
451
            .rel_path(&TestSpecifier::default(), &key_type.clone().into())
452
            .unwrap();
453

            
454
        rel_key_path.checked_path().unwrap()
455
    }
456

            
457
    fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
458
        let keystore_dir = tempdir().unwrap();
459

            
460
        #[cfg(unix)]
461
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
462

            
463
        let key_store =
464
            ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
465
                .unwrap();
466

            
467
        if gen_keys {
468
            let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
469
            let parent = key_path.parent().unwrap();
470
            fs::create_dir_all(parent).unwrap();
471
            #[cfg(unix)]
472
            fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
473

            
474
            fs::write(key_path, ED25519_OPENSSH).unwrap();
475
        }
476

            
477
        (key_store, keystore_dir)
478
    }
479

            
480
    /// Checks if the `expected` list of `ArtiPath`s is the same as the specified `list`.
481
    macro_rules! assert_contains_arti_paths {
482
        ($expected:expr, $list:expr) => {{
483
            let mut expected = Vec::from_iter($expected.iter().cloned().map(KeyPath::Arti));
484
            expected.sort();
485

            
486
            let mut sorted_list = $list
487
                .iter()
488
                .filter_map(|entry| {
489
                    if let Ok(entry) = entry {
490
                        Some(entry.key_path().clone())
491
                    } else {
492
                        None
493
                    }
494
                })
495
                .collect::<Vec<_>>();
496
            sorted_list.sort();
497

            
498
            assert_eq!(expected, sorted_list);
499
        }};
500
    }
501

            
502
    #[test]
503
    #[cfg(unix)]
504
    fn init_failure_perms() {
505
        use std::os::unix::fs::PermissionsExt;
506

            
507
        let keystore_dir = tempdir().unwrap();
508

            
509
        // Too permissive
510
        let mode = 0o777;
511

            
512
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
513
        let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
514
            .expect_err(&format!("expected failure (perms = {mode:o})"));
515

            
516
        assert_eq!(
517
            err.to_string(),
518
            format!(
519
                "Inaccessible path or bad permissions on {} while attempting to Init",
520
                keystore_dir.path().display_lossy()
521
            ),
522
            "expected keystore init failure (perms = {:o})",
523
            mode
524
        );
525
    }
526

            
527
    #[test]
528
    fn key_path_repr() {
529
        let (key_store, _) = init_keystore(false);
530

            
531
        assert_eq!(
532
            key_store
533
                .rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
534
                .unwrap()
535
                .rel_path_unchecked(),
536
            PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
537
        );
538

            
539
        assert_eq!(
540
            key_store
541
                .rel_path(
542
                    &TestSpecifier::default(),
543
                    &KeyType::X25519StaticKeypair.into()
544
                )
545
                .unwrap()
546
                .rel_path_unchecked(),
547
            PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
548
        );
549
    }
550

            
551
    #[cfg(unix)]
552
    #[test]
553
    fn get_and_rm_bad_perms() {
554
        use std::os::unix::fs::PermissionsExt;
555

            
556
        let (key_store, _keystore_dir) = init_keystore(true);
557

            
558
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
559

            
560
        // Make the permissions of the test key too permissive
561
        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
562
        assert!(key_store
563
            .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
564
            .is_err());
565

            
566
        // Make the permissions of the parent directory too lax
567
        fs::set_permissions(
568
            key_path.parent().unwrap(),
569
            fs::Permissions::from_mode(0o777),
570
        )
571
        .unwrap();
572

            
573
        assert!(key_store.list().is_err());
574

            
575
        let key_spec = TestSpecifier::default();
576
        let ed_key_type = &KeyType::Ed25519Keypair.into();
577
        assert_eq!(
578
            key_store
579
                .remove(&key_spec, ed_key_type)
580
                .unwrap_err()
581
                .to_string(),
582
            format!(
583
                "Inaccessible path or bad permissions on {} while attempting to Remove",
584
                key_store
585
                    .rel_path(&key_spec, ed_key_type)
586
                    .unwrap()
587
                    .rel_path_unchecked()
588
                    .display_lossy()
589
            ),
590
        );
591
    }
592

            
593
    #[test]
594
    fn get() {
595
        // Initialize an empty key store
596
        let (key_store, _keystore_dir) = init_keystore(false);
597

            
598
        let mut expected_arti_paths = Vec::new();
599

            
600
        // Not found
601
        assert_found!(
602
            key_store,
603
            &TestSpecifier::default(),
604
            &KeyType::Ed25519Keypair,
605
            false
606
        );
607
        assert!(key_store.list().unwrap().is_empty());
608

            
609
        // Initialize a key store with some test keys
610
        let (key_store, _keystore_dir) = init_keystore(true);
611

            
612
        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
613

            
614
        // Found!
615
        assert_found!(
616
            key_store,
617
            &TestSpecifier::default(),
618
            &KeyType::Ed25519Keypair,
619
            true
620
        );
621

            
622
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
623
    }
624

            
625
    #[test]
626
    fn insert() {
627
        // Initialize an empty key store
628
        let (key_store, keystore_dir) = init_keystore(false);
629

            
630
        let mut expected_arti_paths = Vec::new();
631

            
632
        // Not found
633
        assert_found!(
634
            key_store,
635
            &TestSpecifier::default(),
636
            &KeyType::Ed25519Keypair,
637
            false
638
        );
639
        assert!(key_store.list().unwrap().is_empty());
640

            
641
        let mut keys_and_specs = vec![(ED25519_OPENSSH.into(), TestSpecifier::default())];
642

            
643
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
644
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
645
        }
646

            
647
        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
648
            // Insert the keys
649
            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
650
            let erased_kp = key
651
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
652
                .unwrap();
653

            
654
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
655
                panic!("failed to downcast key to ed25519::Keypair")
656
            };
657

            
658
            let path = keystore_dir.as_ref().join(
659
                key_store
660
                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
661
                    .unwrap()
662
                    .rel_path_unchecked(),
663
            );
664

            
665
            // The key and its parent directories don't exist for first key.
666
            // They are created after the first key is inserted.
667
            assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
668

            
669
            assert!(key_store.insert(&*key, key_spec).is_ok());
670

            
671
            // Update expected_arti_paths after inserting key
672
            expected_arti_paths.push(key_spec.arti_path().unwrap());
673

            
674
            // insert() is supposed to create the missing directories
675
            assert!(path.parent().unwrap().try_exists().unwrap());
676

            
677
            // Found!
678
            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
679

            
680
            // Check keystore list
681
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
682
        }
683
    }
684

            
685
    #[test]
686
    fn remove() {
687
        // Initialize the key store
688
        let (key_store, _keystore_dir) = init_keystore(true);
689

            
690
        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
691
        let mut specs = vec![TestSpecifier::default()];
692

            
693
        assert_found!(
694
            key_store,
695
            &TestSpecifier::default(),
696
            &KeyType::Ed25519Keypair,
697
            true
698
        );
699

            
700
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
701
            // Insert ssh-keygen key
702
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
703
            let erased_kp = key
704
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
705
                .unwrap();
706

            
707
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
708
                panic!("failed to downcast key to ed25519::Keypair")
709
            };
710

            
711
            let key_spec = TestSpecifier::new("-sshkeygen");
712

            
713
            assert!(key_store.insert(&*key, &key_spec).is_ok());
714

            
715
            expected_arti_paths.push(key_spec.arti_path().unwrap());
716
            specs.push(key_spec);
717
        }
718

            
719
        let ed_key_type = &KeyType::Ed25519Keypair.into();
720

            
721
        for spec in specs {
722
            // Found!
723
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
724

            
725
            // Check keystore list before removing key
726
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
727

            
728
            // Now remove the key... remove() should indicate success by returning Ok(Some(()))
729
            assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
730

            
731
            // Remove the current key_spec's ArtiPath from expected_arti_paths
732
            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
733

            
734
            // Can't find it anymore!
735
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
736

            
737
            // Check keystore list after removing key
738
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
739

            
740
            // remove() returns Ok(None) now.
741
            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
742
        }
743

            
744
        assert!(key_store.list().unwrap().is_empty());
745
    }
746

            
747
    #[test]
748
    fn list() {
749
        // Initialize the key store
750
        let (key_store, keystore_dir) = init_keystore(true);
751

            
752
        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
753

            
754
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
755

            
756
        let mut keys_and_specs =
757
            vec![(ED25519_OPENSSH.into(), TestSpecifier::new("-i-am-a-suffix"))];
758

            
759
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
760
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
761
        }
762

            
763
        // Insert more keys
764
        for (key, key_spec) in keys_and_specs {
765
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
766
            let erased_kp = key
767
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
768
                .unwrap();
769

            
770
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
771
                panic!("failed to downcast key to ed25519::Keypair")
772
            };
773

            
774
            assert!(key_store.insert(&*key, &key_spec).is_ok());
775

            
776
            expected_arti_paths.push(key_spec.arti_path().unwrap());
777

            
778
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
779
        }
780

            
781
        // Insert key with invalid ArtiPath
782
        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
783
        let entries = key_store.list().unwrap();
784
        let mut unrecognized_entries = entries.iter().filter_map(|e| {
785
            let Err(entry) = e else {
786
                return None;
787
            };
788
            Some(entry.entry())
789
        });
790
        let expected_entry = UnrecognizedEntry::from(RawKeystoreEntry::new(
791
            RawEntryId::Path(PathBuf::from(TEST_SPECIFIER_PATH)),
792
            key_store.id().clone(),
793
        ));
794
        assert_eq!(unrecognized_entries.next().unwrap(), &expected_entry);
795
        assert!(unrecognized_entries.next().is_none());
796
    }
797

            
798
    #[cfg(feature = "onion-service-cli-extra")]
799
    #[test]
800
    fn remove_unchecked() {
801
        // Initialize the key store
802
        let (key_store, keystore_dir) = init_keystore(true);
803

            
804
        // Insert key with invalid ArtiPath
805
        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
806

            
807
        // Keystore contains a valid entry and an unrecognized one
808
        let entries = key_store.list().unwrap();
809

            
810
        // Remove valid entry
811
        let vaild_spcifier = entries
812
            .iter()
813
            .find_map(|res| {
814
                let Ok(entry) = res else {
815
                    return None;
816
                };
817
                match entry.key_path() {
818
                    KeyPath::Arti(a) => {
819
                        let mut path_str = a.to_string();
820
                        path_str.push('.');
821
                        path_str.push_str(&entry.key_type().arti_extension());
822
                        let raw_id = RawEntryId::Path(PathBuf::from(&path_str));
823
                        Some(RawKeystoreEntry::new(raw_id, key_store.id().to_owned()))
824
                    }
825
                    _ => {
826
                        panic!("Unexpected KeyPath variant encountered")
827
                    }
828
                }
829
            })
830
            .unwrap();
831
        key_store.remove_unchecked(vaild_spcifier.raw_id()).unwrap();
832
        let entries = key_store.list().unwrap();
833
        // Assert no valid entries are encountered
834
        assert!(
835
            entries.iter().all(|res| res.is_err()),
836
            "the only valid entry should've been removed!"
837
        );
838

            
839
        // Remove unrecognized entry
840
        let unrecognized_raw = entries
841
            .iter()
842
            .find_map(|res| match res {
843
                Ok(_) => None,
844
                Err(e) => Some(e.entry()),
845
            })
846
            .unwrap();
847
        key_store
848
            .remove_unchecked(unrecognized_raw.raw_id())
849
            .unwrap();
850
        let entries = key_store.list().unwrap();
851
        // Assert the last entry (unrecognized) has been removed
852
        assert_eq!(entries.len(), 0);
853

            
854
        // Try to remove a non existing entry
855
        let _ = key_store
856
            .remove_unchecked(unrecognized_raw.raw_id())
857
            .unwrap_err();
858
    }
859

            
860
    #[test]
861
    fn key_path_not_regular_file() {
862
        let (key_store, _keystore_dir) = init_keystore(false);
863

            
864
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
865
        // The key is a directory, not a regular file
866
        fs::create_dir_all(&key_path).unwrap();
867
        assert!(key_path.try_exists().unwrap());
868
        let parent = key_path.parent().unwrap();
869
        #[cfg(unix)]
870
        fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
871

            
872
        let err = key_store
873
            .contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
874
            .unwrap_err();
875
        assert!(err.to_string().contains("not a regular file"), "{err}");
876
    }
877

            
878
    #[test]
879
    fn certs() {
880
        let (key_store, _keystore_dir) = init_keystore(false);
881

            
882
        let mut rng = rand::rng();
883
        let subject_key = ed25519::Keypair::generate(&mut rng);
884
        let signing_key = ed25519::Keypair::generate(&mut rng);
885

            
886
        // Note: the cert constructor rounds the expiration forward to the nearest hour
887
        // after the epoch.
888
        let cert_exp = SystemTime::UNIX_EPOCH + Duration::from_secs(60 * 60);
889

            
890
        let encoded_cert = Ed25519Cert::constructor()
891
            .cert_type(tor_cert::CertType::IDENTITY_V_SIGNING)
892
            .expiration(cert_exp)
893
            .signing_key(signing_key.public_key().into())
894
            .cert_key(CertifiedKey::Ed25519(subject_key.public_key().into()))
895
            .encode_and_sign(&signing_key)
896
            .unwrap();
897

            
898
        // The specifier doesn't really matter.
899
        let cert_spec = TestSpecifier::default();
900
        assert!(key_store.insert(&encoded_cert, &cert_spec).is_ok());
901

            
902
        let erased_cert = key_store
903
            .get(&cert_spec, &CertType::Ed25519TorCert.into())
904
            .unwrap()
905
            .unwrap();
906
        let Ok(found_cert) = erased_cert.downcast::<ParsedEd25519Cert>() else {
907
            panic!("failed to downcast cert to KewUnknownCert")
908
        };
909

            
910
        let found_cert = found_cert
911
            .should_be_signed_with(&signing_key.public_key().into())
912
            .unwrap()
913
            .dangerously_assume_wellsigned()
914
            .dangerously_assume_timely();
915

            
916
        assert_eq!(
917
            found_cert.as_ref().cert_type(),
918
            tor_cert::CertType::IDENTITY_V_SIGNING
919
        );
920
        assert_eq!(found_cert.as_ref().expiry(), cert_exp);
921
        assert_eq!(
922
            found_cert.as_ref().signing_key(),
923
            Some(&signing_key.public_key().into())
924
        );
925
        assert_eq!(
926
            found_cert.subject_key().unwrap(),
927
            &subject_key.public_key().into()
928
        );
929
    }
930
}