tor_keymgr/keystore/
arti.rs

1//! The Arti key store.
2//!
3//! See the [`ArtiNativeKeystore`] docs for more details.
4
5pub(crate) mod certs;
6pub(crate) mod err;
7pub(crate) mod ssh;
8
9use std::io::{self};
10use std::path::Path;
11use std::result::Result as StdResult;
12use std::str::FromStr;
13use std::sync::Arc;
14
15use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
16use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
17use crate::{
18    arti_path, ArtiPath, ArtiPathUnavailableError, KeyPath, KeystoreId, Result,
19    UnknownKeyTypeError, UnrecognizedEntryError, UnrecognizedEntryId,
20};
21use certs::UnparsedCert;
22use err::ArtiNativeKeystoreError;
23use ssh::UnparsedOpenSshKey;
24
25use fs_mistrust::{CheckedDir, Mistrust};
26use itertools::Itertools;
27use tor_error::internal;
28use walkdir::WalkDir;
29
30use tor_basic_utils::PathExt as _;
31use tor_key_forge::{CertData, KeystoreItem, KeystoreItemType};
32
33use 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)]
55pub 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
64impl 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    pub fn from_path_and_mistrust(
73        keystore_dir: impl AsRef<Path>,
74        mistrust: &Mistrust,
75    ) -> Result<Self> {
76        let keystore_dir = mistrust
77            .verifier()
78            .check_content()
79            .make_secure_dir(&keystore_dir)
80            .map_err(|e| FilesystemError::FsMistrust {
81                action: FilesystemAction::Init,
82                path: keystore_dir.as_ref().into(),
83                err: e.into(),
84            })
85            .map_err(ArtiNativeKeystoreError::Filesystem)?;
86
87        // TODO: load the keystore ID from config.
88        let id = KeystoreId::from_str("arti")?;
89        Ok(Self { keystore_dir, id })
90    }
91
92    /// The path on disk of the key with the specified identity and type, relative to
93    /// `keystore_dir`.
94    fn rel_path(
95        &self,
96        key_spec: &dyn KeySpecifier,
97        item_type: &KeystoreItemType,
98    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
99        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
100    }
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`.
108macro_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
120impl Keystore for ArtiNativeKeystore {
121    fn id(&self) -> &KeystoreId {
122        &self.id
123    }
124
125    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
126        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(false));
127
128        let meta = match checked_op!(metadata, path) {
129            Ok(meta) => meta,
130            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        if meta.is_file() {
143            Ok(true)
144        } else {
145            Err(
146                ArtiNativeKeystoreError::Filesystem(FilesystemError::NotARegularFile(
147                    path.rel_path_unchecked().into(),
148                ))
149                .into(),
150            )
151        }
152    }
153
154    fn get(
155        &self,
156        key_spec: &dyn KeySpecifier,
157        item_type: &KeystoreItemType,
158    ) -> Result<Option<ErasedKey>> {
159        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
160
161        let inner = match checked_op!(read, path) {
162            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
163            res => res
164                .map_err(|err| FilesystemError::FsMistrust {
165                    action: FilesystemAction::Read,
166                    path: path.rel_path_unchecked().into(),
167                    err: err.into(),
168                })
169                .map_err(ArtiNativeKeystoreError::Filesystem)?,
170        };
171
172        let abs_path = path
173            .checked_path()
174            .map_err(ArtiNativeKeystoreError::Filesystem)?;
175
176        match item_type {
177            KeystoreItemType::Key(key_type) => {
178                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                })?;
190
191                UnparsedOpenSshKey::new(inner, abs_path)
192                    .parse_ssh_format_erased(key_type)
193                    .map(Some)
194            }
195            KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
196                .parse_certificate_erased(cert_type)
197                .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    }
207
208    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
209        let keystore_item = key.as_keystore_item()?;
210        let item_type = keystore_item.item_type()?;
211        let path = self
212            .rel_path(key_spec, &item_type)
213            .map_err(|e| tor_error::internal!("{e}"))?;
214        let unchecked_path = path.rel_path_unchecked();
215
216        // Create the parent directories as needed
217        if let Some(parent) = unchecked_path.parent() {
218            self.keystore_dir
219                .make_directory(parent)
220                .map_err(|err| FilesystemError::FsMistrust {
221                    action: FilesystemAction::Write,
222                    path: parent.to_path_buf(),
223                    err: err.into(),
224                })
225                .map_err(ArtiNativeKeystoreError::Filesystem)?;
226        }
227
228        let item_bytes: Vec<u8> = match keystore_item {
229            KeystoreItem::Key(key) => {
230                // TODO (#1095): decide what information, if any, to put in the comment
231                let comment = "";
232                key.to_openssh_string(comment)?.into_bytes()
233            }
234            KeystoreItem::Cert(cert) => match cert {
235                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        Ok(checked_op!(write_and_replace, path, item_bytes)
242            .map_err(|err| FilesystemError::FsMistrust {
243                action: FilesystemAction::Write,
244                path: unchecked_path.into(),
245                err: err.into(),
246            })
247            .map_err(ArtiNativeKeystoreError::Filesystem)?)
248    }
249
250    fn remove(
251        &self,
252        key_spec: &dyn KeySpecifier,
253        item_type: &KeystoreItemType,
254    ) -> Result<Option<()>> {
255        let rel_path = self
256            .rel_path(key_spec, item_type)
257            .map_err(|e| tor_error::internal!("{e}"))?;
258
259        match checked_op!(remove_file, rel_path) {
260            Ok(()) => Ok(Some(())),
261            Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
262            Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
263                FilesystemError::FsMistrust {
264                    action: FilesystemAction::Remove,
265                    path: rel_path.rel_path_unchecked().into(),
266                    err: e.into(),
267                },
268            ))?,
269        }
270    }
271
272    fn list(&self) -> Result<Vec<KeystoreEntryResult<(KeyPath, KeystoreItemType)>>> {
273        WalkDir::new(self.keystore_dir.as_path())
274            .into_iter()
275            .map(|entry| {
276                let entry = entry
277                    .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                    })
288                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
289
290                let path = entry.path();
291
292                // Skip over directories as they won't be valid arti-paths
293                //
294                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
295                if entry.file_type().is_dir() {
296                    return Ok(None);
297                }
298
299                let path = path
300                    .strip_prefix(self.keystore_dir.as_path())
301                    .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                    })?;
309
310                if let Some(parent) = path.parent() {
311                    // Check the properties of the parent directory by attempting to list its
312                    // contents.
313                    self.keystore_dir
314                        .read_directory(parent)
315                        .map_err(|e| FilesystemError::FsMistrust {
316                            action: FilesystemAction::Read,
317                            path: parent.into(),
318                            err: e.into(),
319                        })
320                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
321                }
322
323                let unrecognized_entry_err = |path: &Path, err| {
324                    let error = ArtiNativeKeystoreError::MalformedPath {
325                        path: path.into(),
326                        err,
327                    };
328                    let entry = UnrecognizedEntryId::Path(path.into());
329                    Some(Err(UnrecognizedEntryError::new(entry, Arc::new(error))))
330                };
331
332                let Some(ext) = path.extension() else {
333                    return Ok(unrecognized_entry_err(
334                        path,
335                        err::MalformedPathError::NoExtension,
336                    ));
337                };
338
339                let Some(extension) = ext.to_str() else {
340                    return Ok(unrecognized_entry_err(path, err::MalformedPathError::Utf8));
341                };
342
343                let item_type = KeystoreItemType::from(extension);
344                // Strip away the file extension
345                let p = path.with_extension("");
346                // Construct slugs in platform-independent way
347                let slugs = p
348                    .components()
349                    .map(|component| component.as_os_str().to_string_lossy())
350                    .collect::<Vec<_>>()
351                    .join(&arti_path::PATH_SEP.to_string());
352                let opt = match ArtiPath::new(slugs) {
353                    Ok(path) => Some(Ok((path.into(), item_type))),
354                    Err(e) => {
355                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
356                    }
357                };
358                Ok(opt)
359            })
360            .flatten_ok()
361            .collect()
362    }
363}
364
365#[cfg(test)]
366mod 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}