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;
11
use std::result::Result as StdResult;
12
use std::str::FromStr;
13

            
14
use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
15
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
16
use crate::{
17
    arti_path, ArtiPath, ArtiPathUnavailableError, KeyPath, KeystoreId, Result, UnknownKeyTypeError,
18
};
19
use certs::UnparsedCert;
20
use err::ArtiNativeKeystoreError;
21
use ssh::UnparsedOpenSshKey;
22

            
23
use fs_mistrust::{CheckedDir, Mistrust};
24
use itertools::Itertools;
25
use tor_error::internal;
26
use walkdir::WalkDir;
27

            
28
use tor_basic_utils::PathExt as _;
29
use tor_key_forge::{CertData, KeystoreItem, KeystoreItemType};
30

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

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

            
83
        // TODO: load the keystore ID from config.
84
352
        let id = KeystoreId::from_str("arti")?;
85
352
        Ok(Self { keystore_dir, id })
86
354
    }
87

            
88
    /// The path on disk of the key with the specified identity and type, relative to
89
    /// `keystore_dir`.
90
20926
    fn rel_path(
91
20926
        &self,
92
20926
        key_spec: &dyn KeySpecifier,
93
20926
        item_type: &KeystoreItemType,
94
20926
    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
95
20926
        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
96
20926
    }
97
}
98

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

            
108
        match $res {
109
            Ok(path) => path,
110
            Err(ArtiPathUnavailable) => return $ret,
111
            Err(e) => return Err(tor_error::internal!("invalid ArtiPath: {e}").into()),
112
        }
113
    }};
114
}
115

            
116
impl Keystore for ArtiNativeKeystore {
117
4572
    fn id(&self) -> &KeystoreId {
118
4572
        &self.id
119
4572
    }
120

            
121
1166
    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
122
1166
        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(false));
123

            
124
1166
        let meta = match checked_op!(metadata, path) {
125
14
            Ok(meta) => meta,
126
1152
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
127
            Err(e) => {
128
                return Err(FilesystemError::FsMistrust {
129
                    action: FilesystemAction::Read,
130
                    path: path.rel_path_unchecked().into(),
131
                    err: e.into(),
132
                })
133
                .map_err(|e| ArtiNativeKeystoreError::Filesystem(e).into());
134
            }
135
        };
136

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

            
150
17124
    fn get(
151
17124
        &self,
152
17124
        key_spec: &dyn KeySpecifier,
153
17124
        item_type: &KeystoreItemType,
154
17124
    ) -> Result<Option<ErasedKey>> {
155
17124
        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
156

            
157
17124
        let inner = match checked_op!(read, path) {
158
2348
            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
159
14776
            res => res
160
14777
                .map_err(|err| FilesystemError::FsMistrust {
161
2
                    action: FilesystemAction::Read,
162
2
                    path: path.rel_path_unchecked().into(),
163
2
                    err: err.into(),
164
14777
                })
165
14776
                .map_err(ArtiNativeKeystoreError::Filesystem)?,
166
        };
167

            
168
14774
        let abs_path = path
169
14774
            .checked_path()
170
14774
            .map_err(ArtiNativeKeystoreError::Filesystem)?;
171

            
172
14774
        match item_type {
173
14772
            KeystoreItemType::Key(key_type) => {
174
14772
                let inner = String::from_utf8(inner).map_err(|_| {
175
                    let err = io::Error::new(
176
                        io::ErrorKind::InvalidData,
177
                        "OpenSSH key is not valid UTF-8".to_string(),
178
                    );
179

            
180
                    ArtiNativeKeystoreError::Filesystem(FilesystemError::Io {
181
                        action: FilesystemAction::Read,
182
                        path: abs_path.clone(),
183
                        err: err.into(),
184
                    })
185
14772
                })?;
186

            
187
14772
                UnparsedOpenSshKey::new(inner, abs_path)
188
14772
                    .parse_ssh_format_erased(key_type)
189
14772
                    .map(Some)
190
            }
191
2
            KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
192
2
                .parse_certificate_erased(cert_type)
193
2
                .map(Some),
194
            KeystoreItemType::Unknown { arti_extension } => Err(
195
                ArtiNativeKeystoreError::UnknownKeyType(UnknownKeyTypeError {
196
                    arti_extension: arti_extension.clone(),
197
                })
198
                .into(),
199
            ),
200
            _ => Err(internal!("unknown item type {item_type:?}").into()),
201
        }
202
17124
    }
203

            
204
1992
    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
205
1992
        let keystore_item = key.as_keystore_item()?;
206
1992
        let item_type = keystore_item.item_type()?;
207
1992
        let path = self
208
1992
            .rel_path(key_spec, &item_type)
209
1992
            .map_err(|e| tor_error::internal!("{e}"))?;
210
1992
        let unchecked_path = path.rel_path_unchecked();
211

            
212
        // Create the parent directories as needed
213
1992
        if let Some(parent) = unchecked_path.parent() {
214
1992
            self.keystore_dir
215
1992
                .make_directory(parent)
216
1992
                .map_err(|err| FilesystemError::FsMistrust {
217
                    action: FilesystemAction::Write,
218
                    path: parent.to_path_buf(),
219
                    err: err.into(),
220
1992
                })
221
1992
                .map_err(ArtiNativeKeystoreError::Filesystem)?;
222
        }
223

            
224
1992
        let item_bytes: Vec<u8> = match keystore_item {
225
1990
            KeystoreItem::Key(key) => {
226
1990
                // TODO (#1095): decide what information, if any, to put in the comment
227
1990
                let comment = "";
228
1990
                key.to_openssh_string(comment)?.into_bytes()
229
            }
230
2
            KeystoreItem::Cert(cert) => match cert {
231
2
                CertData::TorEd25519Cert(cert) => cert.into(),
232
                _ => return Err(internal!("unknown cert type {item_type:?}").into()),
233
            },
234
            _ => return Err(internal!("unknown item type {item_type:?}").into()),
235
        };
236

            
237
1992
        Ok(checked_op!(write_and_replace, path, item_bytes)
238
1992
            .map_err(|err| FilesystemError::FsMistrust {
239
                action: FilesystemAction::Write,
240
                path: unchecked_path.into(),
241
                err: err.into(),
242
1992
            })
243
1992
            .map_err(ArtiNativeKeystoreError::Filesystem)?)
244
1992
    }
245

            
246
622
    fn remove(
247
622
        &self,
248
622
        key_spec: &dyn KeySpecifier,
249
622
        item_type: &KeystoreItemType,
250
622
    ) -> Result<Option<()>> {
251
622
        let rel_path = self
252
622
            .rel_path(key_spec, item_type)
253
622
            .map_err(|e| tor_error::internal!("{e}"))?;
254

            
255
622
        match checked_op!(remove_file, rel_path) {
256
616
            Ok(()) => Ok(Some(())),
257
4
            Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
258
2
            Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
259
2
                FilesystemError::FsMistrust {
260
2
                    action: FilesystemAction::Remove,
261
2
                    path: rel_path.rel_path_unchecked().into(),
262
2
                    err: e.into(),
263
2
                },
264
2
            ))?,
265
        }
266
622
    }
267

            
268
460
    fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
269
460
        WalkDir::new(self.keystore_dir.as_path())
270
460
            .into_iter()
271
5596
            .map(|entry| {
272
5570
                let entry = entry
273
5570
                    .map_err(|e| {
274
                        let msg = e.to_string();
275
                        FilesystemError::Io {
276
                            action: FilesystemAction::Read,
277
                            path: self.keystore_dir.as_path().into(),
278
                            err: e
279
                                .into_io_error()
280
                                .unwrap_or_else(|| io::Error::other(msg.to_string()))
281
                                .into(),
282
                        }
283
5570
                    })
284
5570
                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
285

            
286
5570
                let path = entry.path();
287
5570

            
288
5570
                // Skip over directories as they won't be valid arti-paths
289
5570
                //
290
5570
                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
291
5570
                if entry.file_type().is_dir() {
292
2020
                    return Ok(None);
293
3550
                }
294

            
295
3550
                let path = path
296
3550
                    .strip_prefix(self.keystore_dir.as_path())
297
3550
                    .map_err(|_| {
298
                        /* This error should be impossible. */
299
                        tor_error::internal!(
300
                            "found key {} outside of keystore_dir {}?!",
301
                            path.display_lossy(),
302
                            self.keystore_dir.as_path().display_lossy()
303
                        )
304
3550
                    })?;
305

            
306
3550
                if let Some(parent) = path.parent() {
307
                    // Check the properties of the parent directory by attempting to list its
308
                    // contents.
309
3550
                    self.keystore_dir
310
3550
                        .read_directory(parent)
311
3550
                        .map_err(|e| FilesystemError::FsMistrust {
312
2
                            action: FilesystemAction::Read,
313
2
                            path: parent.into(),
314
2
                            err: e.into(),
315
3550
                        })
316
3550
                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
317
                }
318

            
319
3548
                let malformed_err = |path: &Path, err| ArtiNativeKeystoreError::MalformedPath {
320
                    path: path.into(),
321
                    err,
322
                };
323

            
324
3548
                let extension = path
325
3548
                    .extension()
326
3548
                    .ok_or_else(|| malformed_err(path, err::MalformedPathError::NoExtension))?
327
3548
                    .to_str()
328
3548
                    .ok_or_else(|| malformed_err(path, err::MalformedPathError::Utf8))?;
329

            
330
3548
                let item_type = KeystoreItemType::from(extension);
331
3548
                // Strip away the file extension
332
3548
                let path = path.with_extension("");
333
3548
                // Construct slugs in platform-independent way
334
3548
                let slugs = path
335
3548
                    .components()
336
14192
                    .map(|component| component.as_os_str().to_string_lossy())
337
3548
                    .collect::<Vec<_>>()
338
3548
                    .join(&arti_path::PATH_SEP.to_string());
339
3548
                ArtiPath::new(slugs)
340
3548
                    .map(|path| Some((path.into(), item_type)))
341
3548
                    .map_err(|e| {
342
                        malformed_err(&path, err::MalformedPathError::InvalidArtiPath(e)).into()
343
3548
                    })
344
5596
            })
345
460
            .flatten_ok()
346
460
            .collect()
347
460
    }
348
}
349

            
350
#[cfg(test)]
351
mod tests {
352
    // @@ begin test lint list maintained by maint/add_warning @@
353
    #![allow(clippy::bool_assert_comparison)]
354
    #![allow(clippy::clone_on_copy)]
355
    #![allow(clippy::dbg_macro)]
356
    #![allow(clippy::mixed_attributes_style)]
357
    #![allow(clippy::print_stderr)]
358
    #![allow(clippy::print_stdout)]
359
    #![allow(clippy::single_char_pattern)]
360
    #![allow(clippy::unwrap_used)]
361
    #![allow(clippy::unchecked_duration_subtraction)]
362
    #![allow(clippy::useless_vec)]
363
    #![allow(clippy::needless_pass_by_value)]
364
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
365
    use super::*;
366
    use crate::test_utils::ssh_keys::*;
367
    use crate::test_utils::sshkeygen_ed25519_strings;
368
    use crate::test_utils::{assert_found, TestSpecifier};
369
    use crate::KeyPath;
370
    use std::cmp::Ordering;
371
    use std::fs;
372
    use std::path::PathBuf;
373
    use std::time::{Duration, SystemTime};
374
    use tempfile::{tempdir, TempDir};
375
    use tor_cert::{CertifiedKey, Ed25519Cert};
376
    use tor_checkable::{SelfSigned, Timebound};
377
    use tor_key_forge::{CertType, KeyType, ParsedEd25519Cert};
378
    use tor_llcrypto::pk::ed25519::{self, Ed25519PublicKey as _};
379

            
380
    #[cfg(unix)]
381
    use std::os::unix::fs::PermissionsExt;
382

            
383
    impl Ord for KeyPath {
384
        fn cmp(&self, other: &Self) -> Ordering {
385
            match (self, other) {
386
                (KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
387
                _ => unimplemented!("not supported"),
388
            }
389
        }
390
    }
391

            
392
    impl PartialOrd for KeyPath {
393
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
394
            Some(self.cmp(other))
395
        }
396
    }
397

            
398
    fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
399
        let rel_key_path = key_store
400
            .rel_path(&TestSpecifier::default(), &key_type.clone().into())
401
            .unwrap();
402

            
403
        rel_key_path.checked_path().unwrap()
404
    }
405

            
406
    fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
407
        let keystore_dir = tempdir().unwrap();
408

            
409
        #[cfg(unix)]
410
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
411

            
412
        let key_store =
413
            ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
414
                .unwrap();
415

            
416
        if gen_keys {
417
            let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
418
            let parent = key_path.parent().unwrap();
419
            fs::create_dir_all(parent).unwrap();
420
            #[cfg(unix)]
421
            fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
422

            
423
            fs::write(key_path, ED25519_OPENSSH).unwrap();
424
        }
425

            
426
        (key_store, keystore_dir)
427
    }
428

            
429
    /// Checks if the `expected` list of `ArtiPath`s is the same as the specified `list`.
430
    macro_rules! assert_contains_arti_paths {
431
        ($expected:expr, $list:expr) => {{
432
            let mut expected = Vec::from_iter($expected.iter().cloned().map(KeyPath::Arti));
433
            expected.sort();
434

            
435
            let mut sorted_list = $list
436
                .iter()
437
                .map(|(path, _)| path.clone())
438
                .collect::<Vec<_>>();
439
            sorted_list.sort();
440

            
441
            assert_eq!(expected, sorted_list);
442
        }};
443
    }
444

            
445
    #[test]
446
    #[cfg(unix)]
447
    fn init_failure_perms() {
448
        use std::os::unix::fs::PermissionsExt;
449

            
450
        let keystore_dir = tempdir().unwrap();
451

            
452
        // Too permissive
453
        let mode = 0o777;
454

            
455
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
456
        let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
457
            .expect_err(&format!("expected failure (perms = {mode:o})"));
458

            
459
        assert_eq!(
460
            err.to_string(),
461
            format!(
462
                "Inaccessible path or bad permissions on {} while attempting to Init",
463
                keystore_dir.path().display_lossy()
464
            ),
465
            "expected keystore init failure (perms = {:o})",
466
            mode
467
        );
468
    }
469

            
470
    #[test]
471
    fn key_path_repr() {
472
        let (key_store, _) = init_keystore(false);
473

            
474
        assert_eq!(
475
            key_store
476
                .rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
477
                .unwrap()
478
                .rel_path_unchecked(),
479
            PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
480
        );
481

            
482
        assert_eq!(
483
            key_store
484
                .rel_path(
485
                    &TestSpecifier::default(),
486
                    &KeyType::X25519StaticKeypair.into()
487
                )
488
                .unwrap()
489
                .rel_path_unchecked(),
490
            PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
491
        );
492
    }
493

            
494
    #[cfg(unix)]
495
    #[test]
496
    fn get_and_rm_bad_perms() {
497
        use std::os::unix::fs::PermissionsExt;
498

            
499
        let (key_store, _keystore_dir) = init_keystore(true);
500

            
501
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
502

            
503
        // Make the permissions of the test key too permissive
504
        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
505
        assert!(key_store
506
            .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
507
            .is_err());
508

            
509
        // Make the permissions of the parent directory too lax
510
        fs::set_permissions(
511
            key_path.parent().unwrap(),
512
            fs::Permissions::from_mode(0o777),
513
        )
514
        .unwrap();
515

            
516
        assert!(key_store.list().is_err());
517

            
518
        let key_spec = TestSpecifier::default();
519
        let ed_key_type = &KeyType::Ed25519Keypair.into();
520
        assert_eq!(
521
            key_store
522
                .remove(&key_spec, ed_key_type)
523
                .unwrap_err()
524
                .to_string(),
525
            format!(
526
                "Inaccessible path or bad permissions on {} while attempting to Remove",
527
                key_store
528
                    .rel_path(&key_spec, ed_key_type)
529
                    .unwrap()
530
                    .rel_path_unchecked()
531
                    .display_lossy()
532
            ),
533
        );
534
    }
535

            
536
    #[test]
537
    fn get() {
538
        // Initialize an empty key store
539
        let (key_store, _keystore_dir) = init_keystore(false);
540

            
541
        let mut expected_arti_paths = Vec::new();
542

            
543
        // Not found
544
        assert_found!(
545
            key_store,
546
            &TestSpecifier::default(),
547
            &KeyType::Ed25519Keypair,
548
            false
549
        );
550
        assert!(key_store.list().unwrap().is_empty());
551

            
552
        // Initialize a key store with some test keys
553
        let (key_store, _keystore_dir) = init_keystore(true);
554

            
555
        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
556

            
557
        // Found!
558
        assert_found!(
559
            key_store,
560
            &TestSpecifier::default(),
561
            &KeyType::Ed25519Keypair,
562
            true
563
        );
564

            
565
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
566
    }
567

            
568
    #[test]
569
    fn insert() {
570
        // Initialize an empty key store
571
        let (key_store, keystore_dir) = init_keystore(false);
572

            
573
        let mut expected_arti_paths = Vec::new();
574

            
575
        // Not found
576
        assert_found!(
577
            key_store,
578
            &TestSpecifier::default(),
579
            &KeyType::Ed25519Keypair,
580
            false
581
        );
582
        assert!(key_store.list().unwrap().is_empty());
583

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

            
586
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
587
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
588
        }
589

            
590
        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
591
            // Insert the keys
592
            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
593
            let erased_kp = key
594
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
595
                .unwrap();
596

            
597
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
598
                panic!("failed to downcast key to ed25519::Keypair")
599
            };
600

            
601
            let path = keystore_dir.as_ref().join(
602
                key_store
603
                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
604
                    .unwrap()
605
                    .rel_path_unchecked(),
606
            );
607

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

            
612
            assert!(key_store.insert(&*key, key_spec).is_ok());
613

            
614
            // Update expected_arti_paths after inserting key
615
            expected_arti_paths.push(key_spec.arti_path().unwrap());
616

            
617
            // insert() is supposed to create the missing directories
618
            assert!(path.parent().unwrap().try_exists().unwrap());
619

            
620
            // Found!
621
            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
622

            
623
            // Check keystore list
624
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
625
        }
626
    }
627

            
628
    #[test]
629
    fn remove() {
630
        // Initialize the key store
631
        let (key_store, _keystore_dir) = init_keystore(true);
632

            
633
        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
634
        let mut specs = vec![TestSpecifier::default()];
635

            
636
        assert_found!(
637
            key_store,
638
            &TestSpecifier::default(),
639
            &KeyType::Ed25519Keypair,
640
            true
641
        );
642

            
643
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
644
            // Insert ssh-keygen key
645
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
646
            let erased_kp = key
647
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
648
                .unwrap();
649

            
650
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
651
                panic!("failed to downcast key to ed25519::Keypair")
652
            };
653

            
654
            let key_spec = TestSpecifier::new("-sshkeygen");
655

            
656
            assert!(key_store.insert(&*key, &key_spec).is_ok());
657

            
658
            expected_arti_paths.push(key_spec.arti_path().unwrap());
659
            specs.push(key_spec);
660
        }
661

            
662
        let ed_key_type = &KeyType::Ed25519Keypair.into();
663

            
664
        for spec in specs {
665
            // Found!
666
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
667

            
668
            // Check keystore list before removing key
669
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
670

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

            
674
            // Remove the current key_spec's ArtiPath from expected_arti_paths
675
            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
676

            
677
            // Can't find it anymore!
678
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
679

            
680
            // Check keystore list after removing key
681
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
682

            
683
            // remove() returns Ok(None) now.
684
            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
685
        }
686

            
687
        assert!(key_store.list().unwrap().is_empty());
688
    }
689

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

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

            
697
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
698

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

            
702
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
703
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
704
        }
705

            
706
        // Insert more keys
707
        for (key, key_spec) in keys_and_specs {
708
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
709
            let erased_kp = key
710
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
711
                .unwrap();
712

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

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

            
719
            expected_arti_paths.push(key_spec.arti_path().unwrap());
720

            
721
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
722
        }
723
    }
724

            
725
    #[test]
726
    fn key_path_not_regular_file() {
727
        let (key_store, _keystore_dir) = init_keystore(false);
728

            
729
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
730
        // The key is a directory, not a regular file
731
        fs::create_dir_all(&key_path).unwrap();
732
        assert!(key_path.try_exists().unwrap());
733
        let parent = key_path.parent().unwrap();
734
        #[cfg(unix)]
735
        fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
736

            
737
        let err = key_store
738
            .contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
739
            .unwrap_err();
740
        assert!(err.to_string().contains("not a regular file"), "{err}");
741
    }
742

            
743
    #[test]
744
    fn certs() {
745
        let (key_store, _keystore_dir) = init_keystore(false);
746

            
747
        let mut rng = rand::rng();
748
        let subject_key = ed25519::Keypair::generate(&mut rng);
749
        let signing_key = ed25519::Keypair::generate(&mut rng);
750

            
751
        // Note: the cert constructor rounds the expiration forward to the nearest hour
752
        // after the epoch.
753
        let cert_exp = SystemTime::UNIX_EPOCH + Duration::from_secs(60 * 60);
754

            
755
        let encoded_cert = Ed25519Cert::constructor()
756
            .cert_type(tor_cert::CertType::IDENTITY_V_SIGNING)
757
            .expiration(cert_exp)
758
            .signing_key(signing_key.public_key().into())
759
            .cert_key(CertifiedKey::Ed25519(subject_key.public_key().into()))
760
            .encode_and_sign(&signing_key)
761
            .unwrap();
762

            
763
        // The specifier doesn't really matter.
764
        let cert_spec = TestSpecifier::default();
765
        assert!(key_store.insert(&encoded_cert, &cert_spec).is_ok());
766

            
767
        let erased_cert = key_store
768
            .get(&cert_spec, &CertType::Ed25519TorCert.into())
769
            .unwrap()
770
            .unwrap();
771
        let Ok(found_cert) = erased_cert.downcast::<ParsedEd25519Cert>() else {
772
            panic!("failed to downcast cert to KewUnknownCert")
773
        };
774

            
775
        let found_cert = found_cert
776
            .should_be_signed_with(&signing_key.public_key().into())
777
            .unwrap()
778
            .dangerously_assume_wellsigned()
779
            .dangerously_assume_timely();
780

            
781
        assert_eq!(
782
            found_cert.as_ref().cert_type(),
783
            tor_cert::CertType::IDENTITY_V_SIGNING
784
        );
785
        assert_eq!(found_cert.as_ref().expiry(), cert_exp);
786
        assert_eq!(
787
            found_cert.as_ref().signing_key(),
788
            Some(&signing_key.public_key().into())
789
        );
790
        assert_eq!(
791
            found_cert.subject_key().unwrap(),
792
            &subject_key.public_key().into()
793
        );
794
    }
795
}