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;
13
14use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
15use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
16use crate::{
17    arti_path, ArtiPath, ArtiPathUnavailableError, KeyPath, KeystoreId, Result, UnknownKeyTypeError,
18};
19use certs::UnparsedCert;
20use err::ArtiNativeKeystoreError;
21use ssh::UnparsedOpenSshKey;
22
23use fs_mistrust::{CheckedDir, Mistrust};
24use itertools::Itertools;
25use tor_error::internal;
26use walkdir::WalkDir;
27
28use tor_basic_utils::PathExt as _;
29use 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)]
51pub 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
60impl 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    pub fn from_path_and_mistrust(
69        keystore_dir: impl AsRef<Path>,
70        mistrust: &Mistrust,
71    ) -> Result<Self> {
72        let keystore_dir = mistrust
73            .verifier()
74            .check_content()
75            .make_secure_dir(&keystore_dir)
76            .map_err(|e| FilesystemError::FsMistrust {
77                action: FilesystemAction::Init,
78                path: keystore_dir.as_ref().into(),
79                err: e.into(),
80            })
81            .map_err(ArtiNativeKeystoreError::Filesystem)?;
82
83        // TODO: load the keystore ID from config.
84        let id = KeystoreId::from_str("arti")?;
85        Ok(Self { keystore_dir, id })
86    }
87
88    /// The path on disk of the key with the specified identity and type, relative to
89    /// `keystore_dir`.
90    fn rel_path(
91        &self,
92        key_spec: &dyn KeySpecifier,
93        item_type: &KeystoreItemType,
94    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
95        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
96    }
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`.
104macro_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
116impl Keystore for ArtiNativeKeystore {
117    fn id(&self) -> &KeystoreId {
118        &self.id
119    }
120
121    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
122        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(false));
123
124        let meta = match checked_op!(metadata, path) {
125            Ok(meta) => meta,
126            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        if meta.is_file() {
139            Ok(true)
140        } else {
141            Err(
142                ArtiNativeKeystoreError::Filesystem(FilesystemError::NotARegularFile(
143                    path.rel_path_unchecked().into(),
144                ))
145                .into(),
146            )
147        }
148    }
149
150    fn get(
151        &self,
152        key_spec: &dyn KeySpecifier,
153        item_type: &KeystoreItemType,
154    ) -> Result<Option<ErasedKey>> {
155        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
156
157        let inner = match checked_op!(read, path) {
158            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
159            res => res
160                .map_err(|err| FilesystemError::FsMistrust {
161                    action: FilesystemAction::Read,
162                    path: path.rel_path_unchecked().into(),
163                    err: err.into(),
164                })
165                .map_err(ArtiNativeKeystoreError::Filesystem)?,
166        };
167
168        let abs_path = path
169            .checked_path()
170            .map_err(ArtiNativeKeystoreError::Filesystem)?;
171
172        match item_type {
173            KeystoreItemType::Key(key_type) => {
174                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                })?;
186
187                UnparsedOpenSshKey::new(inner, abs_path)
188                    .parse_ssh_format_erased(key_type)
189                    .map(Some)
190            }
191            KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
192                .parse_certificate_erased(cert_type)
193                .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    }
203
204    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
205        let keystore_item = key.as_keystore_item()?;
206        let item_type = keystore_item.item_type()?;
207        let path = self
208            .rel_path(key_spec, &item_type)
209            .map_err(|e| tor_error::internal!("{e}"))?;
210        let unchecked_path = path.rel_path_unchecked();
211
212        // Create the parent directories as needed
213        if let Some(parent) = unchecked_path.parent() {
214            self.keystore_dir
215                .make_directory(parent)
216                .map_err(|err| FilesystemError::FsMistrust {
217                    action: FilesystemAction::Write,
218                    path: parent.to_path_buf(),
219                    err: err.into(),
220                })
221                .map_err(ArtiNativeKeystoreError::Filesystem)?;
222        }
223
224        let item_bytes: Vec<u8> = match keystore_item {
225            KeystoreItem::Key(key) => {
226                // TODO (#1095): decide what information, if any, to put in the comment
227                let comment = "";
228                key.to_openssh_string(comment)?.into_bytes()
229            }
230            KeystoreItem::Cert(cert) => match cert {
231                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        Ok(checked_op!(write_and_replace, path, item_bytes)
238            .map_err(|err| FilesystemError::FsMistrust {
239                action: FilesystemAction::Write,
240                path: unchecked_path.into(),
241                err: err.into(),
242            })
243            .map_err(ArtiNativeKeystoreError::Filesystem)?)
244    }
245
246    fn remove(
247        &self,
248        key_spec: &dyn KeySpecifier,
249        item_type: &KeystoreItemType,
250    ) -> Result<Option<()>> {
251        let rel_path = self
252            .rel_path(key_spec, item_type)
253            .map_err(|e| tor_error::internal!("{e}"))?;
254
255        match checked_op!(remove_file, rel_path) {
256            Ok(()) => Ok(Some(())),
257            Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
258            Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
259                FilesystemError::FsMistrust {
260                    action: FilesystemAction::Remove,
261                    path: rel_path.rel_path_unchecked().into(),
262                    err: e.into(),
263                },
264            ))?,
265        }
266    }
267
268    fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
269        WalkDir::new(self.keystore_dir.as_path())
270            .into_iter()
271            .map(|entry| {
272                let entry = entry
273                    .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                    })
284                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
285
286                let path = entry.path();
287
288                // Skip over directories as they won't be valid arti-paths
289                //
290                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
291                if entry.file_type().is_dir() {
292                    return Ok(None);
293                }
294
295                let path = path
296                    .strip_prefix(self.keystore_dir.as_path())
297                    .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                    })?;
305
306                if let Some(parent) = path.parent() {
307                    // Check the properties of the parent directory by attempting to list its
308                    // contents.
309                    self.keystore_dir
310                        .read_directory(parent)
311                        .map_err(|e| FilesystemError::FsMistrust {
312                            action: FilesystemAction::Read,
313                            path: parent.into(),
314                            err: e.into(),
315                        })
316                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
317                }
318
319                let malformed_err = |path: &Path, err| ArtiNativeKeystoreError::MalformedPath {
320                    path: path.into(),
321                    err,
322                };
323
324                let extension = path
325                    .extension()
326                    .ok_or_else(|| malformed_err(path, err::MalformedPathError::NoExtension))?
327                    .to_str()
328                    .ok_or_else(|| malformed_err(path, err::MalformedPathError::Utf8))?;
329
330                let item_type = KeystoreItemType::from(extension);
331                // Strip away the file extension
332                let path = path.with_extension("");
333                // Construct slugs in platform-independent way
334                let slugs = path
335                    .components()
336                    .map(|component| component.as_os_str().to_string_lossy())
337                    .collect::<Vec<_>>()
338                    .join(&arti_path::PATH_SEP.to_string());
339                ArtiPath::new(slugs)
340                    .map(|path| Some((path.into(), item_type)))
341                    .map_err(|e| {
342                        malformed_err(&path, err::MalformedPathError::InvalidArtiPath(e)).into()
343                    })
344            })
345            .flatten_ok()
346            .collect()
347    }
348}
349
350#[cfg(test)]
351mod 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}