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
use std::sync::Arc;
14

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

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

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

            
33
use super::KeystoreEntryResult;
34

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
241
1272
        Ok(checked_op!(write_and_replace, path, item_bytes)
242
1272
            .map_err(|err| FilesystemError::FsMistrust {
243
                action: FilesystemAction::Write,
244
                path: unchecked_path.into(),
245
                err: err.into(),
246
1272
            })
247
1272
            .map_err(ArtiNativeKeystoreError::Filesystem)?)
248
1272
    }
249

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

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

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

            
290
5586
                let path = entry.path();
291
5586

            
292
5586
                // Skip over directories as they won't be valid arti-paths
293
5586
                //
294
5586
                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
295
5586
                if entry.file_type().is_dir() {
296
2028
                    return Ok(None);
297
3558
                }
298

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

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

            
323
3556
                let unrecognized_entry_err = |path: &Path, err| {
324
2
                    let error = ArtiNativeKeystoreError::MalformedPath {
325
2
                        path: path.into(),
326
2
                        err,
327
2
                    };
328
2
                    let entry = UnrecognizedEntryId::Path(path.into());
329
2
                    Some(Err(UnrecognizedEntryError::new(entry, Arc::new(error))))
330
2
                };
331

            
332
3556
                let Some(ext) = path.extension() else {
333
2
                    return Ok(unrecognized_entry_err(
334
2
                        path,
335
2
                        err::MalformedPathError::NoExtension,
336
2
                    ));
337
                };
338

            
339
3554
                let Some(extension) = ext.to_str() else {
340
                    return Ok(unrecognized_entry_err(path, err::MalformedPathError::Utf8));
341
                };
342

            
343
3554
                let item_type = KeystoreItemType::from(extension);
344
3554
                // Strip away the file extension
345
3554
                let p = path.with_extension("");
346
3554
                // Construct slugs in platform-independent way
347
3554
                let slugs = p
348
3554
                    .components()
349
14216
                    .map(|component| component.as_os_str().to_string_lossy())
350
3554
                    .collect::<Vec<_>>()
351
3554
                    .join(&arti_path::PATH_SEP.to_string());
352
3554
                let opt = match ArtiPath::new(slugs) {
353
3554
                    Ok(path) => Some(Ok((path.into(), item_type))),
354
                    Err(e) => {
355
                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
356
                    }
357
                };
358
3554
                Ok(opt)
359
5613
            })
360
462
            .flatten_ok()
361
462
            .collect()
362
462
    }
363
}
364

            
365
#[cfg(test)]
366
mod tests {
367
    // @@ begin test lint list maintained by maint/add_warning @@
368
    #![allow(clippy::bool_assert_comparison)]
369
    #![allow(clippy::clone_on_copy)]
370
    #![allow(clippy::dbg_macro)]
371
    #![allow(clippy::mixed_attributes_style)]
372
    #![allow(clippy::print_stderr)]
373
    #![allow(clippy::print_stdout)]
374
    #![allow(clippy::single_char_pattern)]
375
    #![allow(clippy::unwrap_used)]
376
    #![allow(clippy::unchecked_duration_subtraction)]
377
    #![allow(clippy::useless_vec)]
378
    #![allow(clippy::needless_pass_by_value)]
379
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
380
    use super::*;
381
    use crate::test_utils::ssh_keys::*;
382
    use crate::test_utils::sshkeygen_ed25519_strings;
383
    use crate::test_utils::TEST_SPECIFIER_PATH;
384
    use crate::test_utils::{assert_found, TestSpecifier};
385
    use crate::KeyPath;
386
    use std::cmp::Ordering;
387
    use std::fs;
388
    use std::path::PathBuf;
389
    use std::time::{Duration, SystemTime};
390
    use tempfile::{tempdir, TempDir};
391
    use tor_cert::{CertifiedKey, Ed25519Cert};
392
    use tor_checkable::{SelfSigned, Timebound};
393
    use tor_key_forge::{CertType, KeyType, ParsedEd25519Cert};
394
    use tor_llcrypto::pk::ed25519::{self, Ed25519PublicKey as _};
395

            
396
    #[cfg(unix)]
397
    use std::os::unix::fs::PermissionsExt;
398

            
399
    impl Ord for KeyPath {
400
        fn cmp(&self, other: &Self) -> Ordering {
401
            match (self, other) {
402
                (KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
403
                _ => unimplemented!("not supported"),
404
            }
405
        }
406
    }
407

            
408
    impl PartialOrd for KeyPath {
409
        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
410
            Some(self.cmp(other))
411
        }
412
    }
413

            
414
    fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
415
        let rel_key_path = key_store
416
            .rel_path(&TestSpecifier::default(), &key_type.clone().into())
417
            .unwrap();
418

            
419
        rel_key_path.checked_path().unwrap()
420
    }
421

            
422
    fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
423
        let keystore_dir = tempdir().unwrap();
424

            
425
        #[cfg(unix)]
426
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
427

            
428
        let key_store =
429
            ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
430
                .unwrap();
431

            
432
        if gen_keys {
433
            let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
434
            let parent = key_path.parent().unwrap();
435
            fs::create_dir_all(parent).unwrap();
436
            #[cfg(unix)]
437
            fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
438

            
439
            fs::write(key_path, ED25519_OPENSSH).unwrap();
440
        }
441

            
442
        (key_store, keystore_dir)
443
    }
444

            
445
    /// Checks if the `expected` list of `ArtiPath`s is the same as the specified `list`.
446
    macro_rules! assert_contains_arti_paths {
447
        ($expected:expr, $list:expr) => {{
448
            let mut expected = Vec::from_iter($expected.iter().cloned().map(KeyPath::Arti));
449
            expected.sort();
450

            
451
            let mut sorted_list = $list
452
                .iter()
453
                .filter_map(|entry| {
454
                    if let Ok((path, _)) = entry {
455
                        Some(path.clone())
456
                    } else {
457
                        None
458
                    }
459
                })
460
                .collect::<Vec<_>>();
461
            sorted_list.sort();
462

            
463
            assert_eq!(expected, sorted_list);
464
        }};
465
    }
466

            
467
    #[test]
468
    #[cfg(unix)]
469
    fn init_failure_perms() {
470
        use std::os::unix::fs::PermissionsExt;
471

            
472
        let keystore_dir = tempdir().unwrap();
473

            
474
        // Too permissive
475
        let mode = 0o777;
476

            
477
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
478
        let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
479
            .expect_err(&format!("expected failure (perms = {mode:o})"));
480

            
481
        assert_eq!(
482
            err.to_string(),
483
            format!(
484
                "Inaccessible path or bad permissions on {} while attempting to Init",
485
                keystore_dir.path().display_lossy()
486
            ),
487
            "expected keystore init failure (perms = {:o})",
488
            mode
489
        );
490
    }
491

            
492
    #[test]
493
    fn key_path_repr() {
494
        let (key_store, _) = init_keystore(false);
495

            
496
        assert_eq!(
497
            key_store
498
                .rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
499
                .unwrap()
500
                .rel_path_unchecked(),
501
            PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
502
        );
503

            
504
        assert_eq!(
505
            key_store
506
                .rel_path(
507
                    &TestSpecifier::default(),
508
                    &KeyType::X25519StaticKeypair.into()
509
                )
510
                .unwrap()
511
                .rel_path_unchecked(),
512
            PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
513
        );
514
    }
515

            
516
    #[cfg(unix)]
517
    #[test]
518
    fn get_and_rm_bad_perms() {
519
        use std::os::unix::fs::PermissionsExt;
520

            
521
        let (key_store, _keystore_dir) = init_keystore(true);
522

            
523
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
524

            
525
        // Make the permissions of the test key too permissive
526
        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
527
        assert!(key_store
528
            .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
529
            .is_err());
530

            
531
        // Make the permissions of the parent directory too lax
532
        fs::set_permissions(
533
            key_path.parent().unwrap(),
534
            fs::Permissions::from_mode(0o777),
535
        )
536
        .unwrap();
537

            
538
        assert!(key_store.list().is_err());
539

            
540
        let key_spec = TestSpecifier::default();
541
        let ed_key_type = &KeyType::Ed25519Keypair.into();
542
        assert_eq!(
543
            key_store
544
                .remove(&key_spec, ed_key_type)
545
                .unwrap_err()
546
                .to_string(),
547
            format!(
548
                "Inaccessible path or bad permissions on {} while attempting to Remove",
549
                key_store
550
                    .rel_path(&key_spec, ed_key_type)
551
                    .unwrap()
552
                    .rel_path_unchecked()
553
                    .display_lossy()
554
            ),
555
        );
556
    }
557

            
558
    #[test]
559
    fn get() {
560
        // Initialize an empty key store
561
        let (key_store, _keystore_dir) = init_keystore(false);
562

            
563
        let mut expected_arti_paths = Vec::new();
564

            
565
        // Not found
566
        assert_found!(
567
            key_store,
568
            &TestSpecifier::default(),
569
            &KeyType::Ed25519Keypair,
570
            false
571
        );
572
        assert!(key_store.list().unwrap().is_empty());
573

            
574
        // Initialize a key store with some test keys
575
        let (key_store, _keystore_dir) = init_keystore(true);
576

            
577
        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
578

            
579
        // Found!
580
        assert_found!(
581
            key_store,
582
            &TestSpecifier::default(),
583
            &KeyType::Ed25519Keypair,
584
            true
585
        );
586

            
587
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
588
    }
589

            
590
    #[test]
591
    fn insert() {
592
        // Initialize an empty key store
593
        let (key_store, keystore_dir) = init_keystore(false);
594

            
595
        let mut expected_arti_paths = Vec::new();
596

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

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

            
608
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
609
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
610
        }
611

            
612
        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
613
            // Insert the keys
614
            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
615
            let erased_kp = key
616
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
617
                .unwrap();
618

            
619
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
620
                panic!("failed to downcast key to ed25519::Keypair")
621
            };
622

            
623
            let path = keystore_dir.as_ref().join(
624
                key_store
625
                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
626
                    .unwrap()
627
                    .rel_path_unchecked(),
628
            );
629

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

            
634
            assert!(key_store.insert(&*key, key_spec).is_ok());
635

            
636
            // Update expected_arti_paths after inserting key
637
            expected_arti_paths.push(key_spec.arti_path().unwrap());
638

            
639
            // insert() is supposed to create the missing directories
640
            assert!(path.parent().unwrap().try_exists().unwrap());
641

            
642
            // Found!
643
            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
644

            
645
            // Check keystore list
646
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
647
        }
648
    }
649

            
650
    #[test]
651
    fn remove() {
652
        // Initialize the key store
653
        let (key_store, _keystore_dir) = init_keystore(true);
654

            
655
        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
656
        let mut specs = vec![TestSpecifier::default()];
657

            
658
        assert_found!(
659
            key_store,
660
            &TestSpecifier::default(),
661
            &KeyType::Ed25519Keypair,
662
            true
663
        );
664

            
665
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
666
            // Insert ssh-keygen key
667
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
668
            let erased_kp = key
669
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
670
                .unwrap();
671

            
672
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
673
                panic!("failed to downcast key to ed25519::Keypair")
674
            };
675

            
676
            let key_spec = TestSpecifier::new("-sshkeygen");
677

            
678
            assert!(key_store.insert(&*key, &key_spec).is_ok());
679

            
680
            expected_arti_paths.push(key_spec.arti_path().unwrap());
681
            specs.push(key_spec);
682
        }
683

            
684
        let ed_key_type = &KeyType::Ed25519Keypair.into();
685

            
686
        for spec in specs {
687
            // Found!
688
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
689

            
690
            // Check keystore list before removing key
691
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
692

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

            
696
            // Remove the current key_spec's ArtiPath from expected_arti_paths
697
            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
698

            
699
            // Can't find it anymore!
700
            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
701

            
702
            // Check keystore list after removing key
703
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
704

            
705
            // remove() returns Ok(None) now.
706
            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
707
        }
708

            
709
        assert!(key_store.list().unwrap().is_empty());
710
    }
711

            
712
    #[test]
713
    fn list() {
714
        // Initialize the key store
715
        let (key_store, keystore_dir) = init_keystore(true);
716

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

            
719
        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
720

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

            
724
        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
725
            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
726
        }
727

            
728
        // Insert more keys
729
        for (key, key_spec) in keys_and_specs {
730
            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
731
            let erased_kp = key
732
                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
733
                .unwrap();
734

            
735
            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
736
                panic!("failed to downcast key to ed25519::Keypair")
737
            };
738

            
739
            assert!(key_store.insert(&*key, &key_spec).is_ok());
740

            
741
            expected_arti_paths.push(key_spec.arti_path().unwrap());
742

            
743
            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
744
        }
745

            
746
        // Insert key with invalid ArtiPath
747
        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
748
        let entries = key_store.list().unwrap();
749
        let mut unrecognized_entries = entries.iter().filter_map(|e| {
750
            let Err(entry) = e else {
751
                return None;
752
            };
753
            #[allow(irrefutable_let_patterns)]
754
            let UnrecognizedEntryId::Path(path) = entry.entry() else {
755
                return None;
756
            };
757
            Some(path)
758
        });
759
        assert_eq!(
760
            unrecognized_entries.next().unwrap(),
761
            Path::new(TEST_SPECIFIER_PATH)
762
        );
763
        assert!(unrecognized_entries.next().is_none());
764
    }
765

            
766
    #[test]
767
    fn key_path_not_regular_file() {
768
        let (key_store, _keystore_dir) = init_keystore(false);
769

            
770
        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
771
        // The key is a directory, not a regular file
772
        fs::create_dir_all(&key_path).unwrap();
773
        assert!(key_path.try_exists().unwrap());
774
        let parent = key_path.parent().unwrap();
775
        #[cfg(unix)]
776
        fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
777

            
778
        let err = key_store
779
            .contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
780
            .unwrap_err();
781
        assert!(err.to_string().contains("not a regular file"), "{err}");
782
    }
783

            
784
    #[test]
785
    fn certs() {
786
        let (key_store, _keystore_dir) = init_keystore(false);
787

            
788
        let mut rng = rand::rng();
789
        let subject_key = ed25519::Keypair::generate(&mut rng);
790
        let signing_key = ed25519::Keypair::generate(&mut rng);
791

            
792
        // Note: the cert constructor rounds the expiration forward to the nearest hour
793
        // after the epoch.
794
        let cert_exp = SystemTime::UNIX_EPOCH + Duration::from_secs(60 * 60);
795

            
796
        let encoded_cert = Ed25519Cert::constructor()
797
            .cert_type(tor_cert::CertType::IDENTITY_V_SIGNING)
798
            .expiration(cert_exp)
799
            .signing_key(signing_key.public_key().into())
800
            .cert_key(CertifiedKey::Ed25519(subject_key.public_key().into()))
801
            .encode_and_sign(&signing_key)
802
            .unwrap();
803

            
804
        // The specifier doesn't really matter.
805
        let cert_spec = TestSpecifier::default();
806
        assert!(key_store.insert(&encoded_cert, &cert_spec).is_ok());
807

            
808
        let erased_cert = key_store
809
            .get(&cert_spec, &CertType::Ed25519TorCert.into())
810
            .unwrap()
811
            .unwrap();
812
        let Ok(found_cert) = erased_cert.downcast::<ParsedEd25519Cert>() else {
813
            panic!("failed to downcast cert to KewUnknownCert")
814
        };
815

            
816
        let found_cert = found_cert
817
            .should_be_signed_with(&signing_key.public_key().into())
818
            .unwrap()
819
            .dangerously_assume_wellsigned()
820
            .dangerously_assume_timely();
821

            
822
        assert_eq!(
823
            found_cert.as_ref().cert_type(),
824
            tor_cert::CertType::IDENTITY_V_SIGNING
825
        );
826
        assert_eq!(found_cert.as_ref().expiry(), cert_exp);
827
        assert_eq!(
828
            found_cert.as_ref().signing_key(),
829
            Some(&signing_key.public_key().into())
830
        );
831
        assert_eq!(
832
            found_cert.subject_key().unwrap(),
833
            &subject_key.public_key().into()
834
        );
835
    }
836
}