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::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
16
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
17
use crate::raw::{RawEntryId, RawKeystoreEntry};
18
use crate::{
19
    ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result, UnknownKeyTypeError,
20
    UnrecognizedEntryError, arti_path,
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
1790
    pub fn from_path_and_mistrust(
74
1790
        keystore_dir: impl AsRef<Path>,
75
1790
        mistrust: &Mistrust,
76
1790
    ) -> Result<Self> {
77
1790
        let keystore_dir = mistrust
78
1790
            .verifier()
79
1790
            .check_content()
80
1790
            .make_secure_dir(&keystore_dir)
81
1790
            .map_err(|e| FilesystemError::FsMistrust {
82
2
                action: FilesystemAction::Init,
83
2
                path: keystore_dir.as_ref().into(),
84
2
                err: e.into(),
85
2
            })
86
1790
            .map_err(ArtiNativeKeystoreError::Filesystem)?;
87

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

            
93
    /// The path on disk of the key with the specified identity and type, relative to
94
    /// `keystore_dir`.
95
35860
    fn rel_path(
96
35860
        &self,
97
35860
        key_spec: &dyn KeySpecifier,
98
35860
        item_type: &KeystoreItemType,
99
35860
    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
100
35860
        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
101
35860
    }
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
13514
    fn id(&self) -> &KeystoreId {
123
13514
        &self.id
124
13514
    }
125

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

            
129
1678
        let meta = match checked_op!(metadata, path) {
130
14
            Ok(meta) => meta,
131
1664
            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
1678
    }
154

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

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

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

            
177
25026
        match item_type {
178
25024
            KeystoreItemType::Key(key_type) => {
179
25024
                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
                })?;
191

            
192
25024
                UnparsedOpenSshKey::new(inner, abs_path)
193
25024
                    .parse_ssh_format_erased(key_type)
194
25024
                    .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
29456
    }
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
3340
    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
215
3340
        let keystore_item = key.as_keystore_item()?;
216
3340
        let item_type = keystore_item.item_type()?;
217
3340
        let path = self
218
3340
            .rel_path(key_spec, &item_type)
219
3340
            .map_err(|e| tor_error::internal!("{e}"))?;
220
3340
        let unchecked_path = path.rel_path_unchecked();
221

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

            
234
3340
        let item_bytes: Vec<u8> = match keystore_item {
235
3338
            KeystoreItem::Key(key) => {
236
                // TODO (#1095): decide what information, if any, to put in the comment
237
3338
                let comment = "";
238
3338
                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
3340
        Ok(checked_op!(write_and_replace, path, item_bytes)
248
3340
            .map_err(|err| FilesystemError::FsMistrust {
249
                action: FilesystemAction::Write,
250
                path: unchecked_path.into(),
251
                err: err.into(),
252
            })
253
3340
            .map_err(ArtiNativeKeystoreError::Filesystem)?)
254
3340
    }
255

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

            
265
1362
        match checked_op!(remove_file, rel_path) {
266
1356
            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
1362
    }
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
3
                })?;
289
            }
290
            _other => {
291
                return Err(ArtiNativeKeystoreError::UnsupportedRawEntry(raw_id.clone()).into());
292
            }
293
        }
294
4
        Ok(())
295
6
    }
296

            
297
1232
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
298
1232
        WalkDir::new(self.keystore_dir.as_path())
299
1232
            .into_iter()
300
10773
            .map(|entry| {
301
10732
                let entry = entry
302
10732
                    .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.clone()))
310
                                .into(),
311
                        }
312
                    })
313
10732
                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
314

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

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

            
324
6388
                let path = path
325
6388
                    .strip_prefix(self.keystore_dir.as_path())
326
6388
                    .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
                    })?;
334

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

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

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

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

            
369
5964
                let item_type = KeystoreItemType::from(extension);
370
                // Strip away the file extension
371
5964
                let p = path.with_extension("");
372
                // Construct slugs in platform-independent way
373
5964
                let slugs = p
374
5964
                    .components()
375
23024
                    .map(|component| component.as_os_str().to_string_lossy())
376
5964
                    .collect::<Vec<_>>()
377
5964
                    .join(&arti_path::PATH_SEP.to_string());
378
5964
                let opt = match ArtiPath::new(slugs) {
379
5964
                    Ok(arti_path) => {
380
5964
                        let raw_id = RawEntryId::Path(path.to_owned());
381
5964
                        Some(Ok(KeystoreEntry::new(
382
5964
                            arti_path.into(),
383
5964
                            item_type,
384
5964
                            self.id(),
385
5964
                            raw_id,
386
5964
                        )))
387
                    }
388
                    Err(e) => {
389
                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
390
                    }
391
                };
392
5964
                Ok(opt)
393
10732
            })
394
1232
            .flatten_ok()
395
1232
            .collect()
396
1232
    }
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_time_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::KeyPath;
416
    use crate::UnrecognizedEntry;
417
    use crate::test_utils::TEST_SPECIFIER_PATH;
418
    use crate::test_utils::ssh_keys::*;
419
    use crate::test_utils::sshkeygen_ed25519_strings;
420
    use crate::test_utils::{TestSpecifier, assert_found};
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!(
563
            key_store
564
                .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
565
                .is_err()
566
        );
567

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

            
575
        assert!(key_store.list().is_err());
576

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

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

            
600
        let mut expected_arti_paths = Vec::new();
601

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

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

            
614
        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
615

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

            
624
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
625
    }
626

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

            
632
        let mut expected_arti_paths = Vec::new();
633

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

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

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

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

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

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

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

            
671
            assert!(key_store.insert(&*key, key_spec).is_ok());
672

            
673
            // Update expected_arti_paths after inserting key
674
            expected_arti_paths.push(key_spec.arti_path().unwrap());
675

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

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

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

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

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

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

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

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

            
713
            let key_spec = TestSpecifier::new("-sshkeygen");
714

            
715
            assert!(key_store.insert(&*key, &key_spec).is_ok());
716

            
717
            expected_arti_paths.push(key_spec.arti_path().unwrap());
718
            specs.push(key_spec);
719
        }
720

            
721
        let ed_key_type = &KeyType::Ed25519Keypair.into();
722

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

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

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

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

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

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

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

            
746
        assert!(key_store.list().unwrap().is_empty());
747
    }
748

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

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

            
756
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
757

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

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

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

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

            
776
            assert!(key_store.insert(&*key, &key_spec).is_ok());
777

            
778
            expected_arti_paths.push(key_spec.arti_path().unwrap());
779

            
780
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
781
        }
782

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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