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;
18
use crate::{
19
    ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result, UnknownKeyTypeError,
20
    UnrecognizedEntry, 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
1926
    pub fn from_path_and_mistrust(
74
1926
        keystore_dir: impl AsRef<Path>,
75
1926
        mistrust: &Mistrust,
76
1926
    ) -> Result<Self> {
77
1926
        let keystore_dir = mistrust
78
1926
            .verifier()
79
1926
            .check_content()
80
1926
            .make_secure_dir(&keystore_dir)
81
1926
            .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
1926
            .map_err(ArtiNativeKeystoreError::Filesystem)?;
87

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

            
93
    /// The path on disk of the key with the specified identity and type, relative to
94
    /// `keystore_dir`.
95
27924
    fn rel_path(
96
27924
        &self,
97
27924
        key_spec: &dyn KeySpecifier,
98
27924
        item_type: &KeystoreItemType,
99
27924
    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
100
27924
        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
101
27924
    }
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
10964
    fn id(&self) -> &KeystoreId {
123
10964
        &self.id
124
10964
    }
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
22984
    fn get(
156
22984
        &self,
157
22984
        key_spec: &dyn KeySpecifier,
158
22984
        item_type: &KeystoreItemType,
159
22984
    ) -> Result<Option<ErasedKey>> {
160
22984
        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
161

            
162
22984
        let inner = match checked_op!(read, path) {
163
3408
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
164
19576
            res => res
165
19576
                .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
19576
                .map_err(ArtiNativeKeystoreError::Filesystem)?,
171
        };
172

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

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

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

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

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

            
265
1050
        match checked_op!(remove_file, rel_path) {
266
1044
            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
1050
    }
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
1076
    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
298
1076
        WalkDir::new(self.keystore_dir.as_path())
299
1076
            .into_iter()
300
9180
            .map(|entry| {
301
9136
                let entry = entry
302
9136
                    .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
9136
                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
314

            
315
9136
                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
9136
                if entry.file_type().is_dir() {
321
3892
                    return Ok(None);
322
5244
                }
323

            
324
5244
                let path = path
325
5244
                    .strip_prefix(self.keystore_dir.as_path())
326
5244
                    .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
5244
                if let Some(parent) = path.parent() {
336
                    // Check the properties of the parent directory by attempting to list its
337
                    // contents.
338
5244
                    self.keystore_dir
339
5244
                        .read_directory(parent)
340
5244
                        .map_err(|e| FilesystemError::FsMistrust {
341
2
                            action: FilesystemAction::Read,
342
2
                            path: parent.into(),
343
2
                            err: e.into(),
344
2
                        })
345
5244
                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
346
                }
347

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

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

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

            
369
4836
                let item_type = KeystoreItemType::from(extension);
370
                // Strip away the file extension
371
4836
                let p = path.with_extension("");
372
                // Construct slugs in platform-independent way
373
4836
                let slugs = p
374
4836
                    .components()
375
18384
                    .map(|component| component.as_os_str().to_string_lossy())
376
4836
                    .collect::<Vec<_>>()
377
4836
                    .join(&arti_path::PATH_SEP.to_string());
378
4836
                let opt = match ArtiPath::new(slugs) {
379
4836
                    Ok(arti_path) => {
380
4836
                        let raw_id = RawEntryId::Path(path.to_owned());
381
4836
                        Some(Ok(KeystoreEntry::new(
382
4836
                            arti_path.into(),
383
4836
                            item_type,
384
4836
                            self.id(),
385
4836
                            raw_id,
386
4836
                        )))
387
                    }
388
                    Err(e) => {
389
                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
390
                    }
391
                };
392
4836
                Ok(opt)
393
9136
            })
394
1076
            .flatten_ok()
395
1076
            .collect()
396
1076
    }
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
    #![allow(clippy::string_slice)] // See arti#2571
414
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
415
    use super::*;
416
    use crate::KeyPath;
417
    use crate::UnrecognizedEntry;
418
    use crate::test_utils::TEST_SPECIFIER_PATH;
419
    use crate::test_utils::ssh_keys::*;
420
    use crate::test_utils::sshkeygen_ed25519_strings;
421
    use crate::test_utils::{TestSpecifier, assert_found};
422
    use std::cmp::Ordering;
423
    use std::fs;
424
    use std::path::PathBuf;
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
    use web_time_compat::{Duration, SystemTime};
431

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

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

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

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

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

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

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

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

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

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

            
478
        (key_store, keystore_dir)
479
    }
480

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
813
        // Remove valid entry
814
        let raw_id = entries
815
            .iter()
816
            .find_map(|res| {
817
                let Ok(entry) = res else {
818
                    return None;
819
                };
820
                match entry.key_path() {
821
                    KeyPath::Arti(a) => {
822
                        let mut path_str = a.to_string();
823
                        path_str.push('.');
824
                        path_str.push_str(&entry.key_type().arti_extension());
825
                        Some(RawEntryId::Path(PathBuf::from(&path_str)))
826
                    }
827
                    _ => {
828
                        panic!("Unexpected KeyPath variant encountered")
829
                    }
830
                }
831
            })
832
            .unwrap();
833
        key_store.remove_unchecked(&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::builder()
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
}