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, PathBuf};
11use std::result::Result as StdResult;
12use std::str::FromStr;
13use std::sync::Arc;
14
15use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
16use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
17use crate::raw::{RawEntryId, RawKeystoreEntry};
18use crate::{
19    ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result, UnknownKeyTypeError,
20    UnrecognizedEntryError, arti_path,
21};
22use certs::UnparsedCert;
23use err::ArtiNativeKeystoreError;
24use ssh::UnparsedOpenSshKey;
25
26use fs_mistrust::{CheckedDir, Mistrust};
27use itertools::Itertools;
28use tor_error::internal;
29use walkdir::WalkDir;
30
31use tor_basic_utils::PathExt as _;
32use tor_key_forge::{CertData, KeystoreItem, KeystoreItemType};
33
34use super::KeystoreEntryResult;
35
36/// The Arti key store.
37///
38/// This is a disk-based key store that encodes keys in OpenSSH format.
39///
40/// Some of the key types supported by the [`ArtiNativeKeystore`]
41/// don't have a predefined SSH public key [algorithm name],
42/// so we define several custom SSH algorithm names.
43/// As per [RFC4251 § 6], our custom SSH algorithm names use the
44/// `<something@subdomain.torproject.org>` format.
45///
46/// We have assigned the following custom algorithm names:
47///   * `x25519@spec.torproject.org`, for x25519 keys
48///   * `ed25519-expanded@spec.torproject.org`, for expanded ed25519 keys
49///
50/// See [SSH protocol extensions] for more details.
51///
52/// [algorithm name]: https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-19
53/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
54/// [SSH protocol extensions]: https://spec.torproject.org/ssh-protocols.html
55#[derive(Debug)]
56pub struct ArtiNativeKeystore {
57    /// The root of the key store.
58    ///
59    /// All the keys are stored within this directory.
60    keystore_dir: CheckedDir,
61    /// The unique identifier of this instance.
62    id: KeystoreId,
63}
64
65impl ArtiNativeKeystore {
66    /// Create a new [`ArtiNativeKeystore`] rooted at the specified `keystore_dir` directory.
67    ///
68    /// The `keystore_dir` directory is created if it doesn't exist.
69    ///
70    /// This function returns an error if `keystore_dir` is not a directory, if it does not conform
71    /// to the requirements of the specified `Mistrust`, or if there was a problem creating the
72    /// directory.
73    pub fn from_path_and_mistrust(
74        keystore_dir: impl AsRef<Path>,
75        mistrust: &Mistrust,
76    ) -> Result<Self> {
77        let keystore_dir = mistrust
78            .verifier()
79            .check_content()
80            .make_secure_dir(&keystore_dir)
81            .map_err(|e| FilesystemError::FsMistrust {
82                action: FilesystemAction::Init,
83                path: keystore_dir.as_ref().into(),
84                err: e.into(),
85            })
86            .map_err(ArtiNativeKeystoreError::Filesystem)?;
87
88        // TODO: load the keystore ID from config.
89        let id = KeystoreId::from_str("arti")?;
90        Ok(Self { keystore_dir, id })
91    }
92
93    /// The path on disk of the key with the specified identity and type, relative to
94    /// `keystore_dir`.
95    fn rel_path(
96        &self,
97        key_spec: &dyn KeySpecifier,
98        item_type: &KeystoreItemType,
99    ) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
100        RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
101    }
102}
103
104/// Extract the key path (relative to the keystore root) from the specified result `res`,
105/// or return an error.
106///
107/// If the underlying error is `ArtiPathUnavailable` (i.e. the `KeySpecifier` cannot provide
108/// an `ArtiPath`), return `ret`.
109macro_rules! rel_path_if_supported {
110    ($res:expr, $ret:expr) => {{
111        use ArtiPathUnavailableError::*;
112
113        match $res {
114            Ok(path) => path,
115            Err(ArtiPathUnavailable) => return $ret,
116            Err(e) => return Err(tor_error::internal!("invalid ArtiPath: {e}").into()),
117        }
118    }};
119}
120
121impl Keystore for ArtiNativeKeystore {
122    fn id(&self) -> &KeystoreId {
123        &self.id
124    }
125
126    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
127        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(false));
128
129        let meta = match checked_op!(metadata, path) {
130            Ok(meta) => meta,
131            Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
132            Err(e) => {
133                return Err(FilesystemError::FsMistrust {
134                    action: FilesystemAction::Read,
135                    path: path.rel_path_unchecked().into(),
136                    err: e.into(),
137                })
138                .map_err(|e| ArtiNativeKeystoreError::Filesystem(e).into());
139            }
140        };
141
142        // The path exists, now check that it's actually a file and not a directory or symlink.
143        if meta.is_file() {
144            Ok(true)
145        } else {
146            Err(
147                ArtiNativeKeystoreError::Filesystem(FilesystemError::NotARegularFile(
148                    path.rel_path_unchecked().into(),
149                ))
150                .into(),
151            )
152        }
153    }
154
155    fn get(
156        &self,
157        key_spec: &dyn KeySpecifier,
158        item_type: &KeystoreItemType,
159    ) -> Result<Option<ErasedKey>> {
160        let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
161
162        let inner = match checked_op!(read, path) {
163            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
164            res => res
165                .map_err(|err| FilesystemError::FsMistrust {
166                    action: FilesystemAction::Read,
167                    path: path.rel_path_unchecked().into(),
168                    err: err.into(),
169                })
170                .map_err(ArtiNativeKeystoreError::Filesystem)?,
171        };
172
173        let abs_path = path
174            .checked_path()
175            .map_err(ArtiNativeKeystoreError::Filesystem)?;
176
177        match item_type {
178            KeystoreItemType::Key(key_type) => {
179                let inner = String::from_utf8(inner).map_err(|_| {
180                    let err = io::Error::new(
181                        io::ErrorKind::InvalidData,
182                        "OpenSSH key is not valid UTF-8".to_string(),
183                    );
184
185                    ArtiNativeKeystoreError::Filesystem(FilesystemError::Io {
186                        action: FilesystemAction::Read,
187                        path: abs_path.clone(),
188                        err: err.into(),
189                    })
190                })?;
191
192                UnparsedOpenSshKey::new(inner, abs_path)
193                    .parse_ssh_format_erased(key_type)
194                    .map(Some)
195            }
196            KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
197                .parse_certificate_erased(cert_type)
198                .map(Some),
199            KeystoreItemType::Unknown { arti_extension } => Err(
200                ArtiNativeKeystoreError::UnknownKeyType(UnknownKeyTypeError {
201                    arti_extension: arti_extension.clone(),
202                })
203                .into(),
204            ),
205            _ => Err(internal!("unknown item type {item_type:?}").into()),
206        }
207    }
208
209    #[cfg(feature = "onion-service-cli-extra")]
210    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
211        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
212    }
213
214    fn insert(&self, key: &dyn EncodableItem, key_spec: &dyn KeySpecifier) -> Result<()> {
215        let keystore_item = key.as_keystore_item()?;
216        let item_type = keystore_item.item_type()?;
217        let path = self
218            .rel_path(key_spec, &item_type)
219            .map_err(|e| tor_error::internal!("{e}"))?;
220        let unchecked_path = path.rel_path_unchecked();
221
222        // Create the parent directories as needed
223        if let Some(parent) = unchecked_path.parent() {
224            self.keystore_dir
225                .make_directory(parent)
226                .map_err(|err| FilesystemError::FsMistrust {
227                    action: FilesystemAction::Write,
228                    path: parent.to_path_buf(),
229                    err: err.into(),
230                })
231                .map_err(ArtiNativeKeystoreError::Filesystem)?;
232        }
233
234        let item_bytes: Vec<u8> = match keystore_item {
235            KeystoreItem::Key(key) => {
236                // TODO (#1095): decide what information, if any, to put in the comment
237                let comment = "";
238                key.to_openssh_string(comment)?.into_bytes()
239            }
240            KeystoreItem::Cert(cert) => match cert {
241                CertData::TorEd25519Cert(cert) => cert.into(),
242                _ => return Err(internal!("unknown cert type {item_type:?}").into()),
243            },
244            _ => return Err(internal!("unknown item type {item_type:?}").into()),
245        };
246
247        Ok(checked_op!(write_and_replace, path, item_bytes)
248            .map_err(|err| FilesystemError::FsMistrust {
249                action: FilesystemAction::Write,
250                path: unchecked_path.into(),
251                err: err.into(),
252            })
253            .map_err(ArtiNativeKeystoreError::Filesystem)?)
254    }
255
256    fn remove(
257        &self,
258        key_spec: &dyn KeySpecifier,
259        item_type: &KeystoreItemType,
260    ) -> Result<Option<()>> {
261        let rel_path = self
262            .rel_path(key_spec, item_type)
263            .map_err(|e| tor_error::internal!("{e}"))?;
264
265        match checked_op!(remove_file, rel_path) {
266            Ok(()) => Ok(Some(())),
267            Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
268            Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
269                FilesystemError::FsMistrust {
270                    action: FilesystemAction::Remove,
271                    path: rel_path.rel_path_unchecked().into(),
272                    err: e.into(),
273                },
274            ))?,
275        }
276    }
277
278    #[cfg(feature = "onion-service-cli-extra")]
279    fn remove_unchecked(&self, raw_id: &RawEntryId) -> Result<()> {
280        match raw_id {
281            RawEntryId::Path(path) => {
282                self.keystore_dir.remove_file(path).map_err(|e| {
283                    ArtiNativeKeystoreError::Filesystem(FilesystemError::FsMistrust {
284                        action: FilesystemAction::Remove,
285                        path: path.clone(),
286                        err: e.into(),
287                    })
288                })?;
289            }
290            _other => {
291                return Err(ArtiNativeKeystoreError::UnsupportedRawEntry(raw_id.clone()).into());
292            }
293        }
294        Ok(())
295    }
296
297    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
298        WalkDir::new(self.keystore_dir.as_path())
299            .into_iter()
300            .map(|entry| {
301                let entry = entry
302                    .map_err(|e| {
303                        let msg = e.to_string();
304                        FilesystemError::Io {
305                            action: FilesystemAction::Read,
306                            path: self.keystore_dir.as_path().into(),
307                            err: e
308                                .into_io_error()
309                                .unwrap_or_else(|| io::Error::other(msg.to_string()))
310                                .into(),
311                        }
312                    })
313                    .map_err(ArtiNativeKeystoreError::Filesystem)?;
314
315                let path = entry.path();
316
317                // Skip over directories as they won't be valid arti-paths
318                //
319                // TODO (#1118): provide a mechanism for warning about unrecognized keys?
320                if entry.file_type().is_dir() {
321                    return Ok(None);
322                }
323
324                let path = path
325                    .strip_prefix(self.keystore_dir.as_path())
326                    .map_err(|_| {
327                        /* This error should be impossible. */
328                        tor_error::internal!(
329                            "found key {} outside of keystore_dir {}?!",
330                            path.display_lossy(),
331                            self.keystore_dir.as_path().display_lossy()
332                        )
333                    })?;
334
335                if let Some(parent) = path.parent() {
336                    // Check the properties of the parent directory by attempting to list its
337                    // contents.
338                    self.keystore_dir
339                        .read_directory(parent)
340                        .map_err(|e| FilesystemError::FsMistrust {
341                            action: FilesystemAction::Read,
342                            path: parent.into(),
343                            err: e.into(),
344                        })
345                        .map_err(ArtiNativeKeystoreError::Filesystem)?;
346                }
347
348                let unrecognized_entry_err = |path: &Path, err| {
349                    let error = ArtiNativeKeystoreError::MalformedPath {
350                        path: path.into(),
351                        err,
352                    };
353                    let raw_id = RawEntryId::Path(path.into());
354                    let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
355                    Some(Err(UnrecognizedEntryError::new(entry, Arc::new(error))))
356                };
357
358                let Some(ext) = path.extension() else {
359                    return Ok(unrecognized_entry_err(
360                        path,
361                        err::MalformedPathError::NoExtension,
362                    ));
363                };
364
365                let Some(extension) = ext.to_str() else {
366                    return Ok(unrecognized_entry_err(path, err::MalformedPathError::Utf8));
367                };
368
369                let item_type = KeystoreItemType::from(extension);
370                // Strip away the file extension
371                let p = path.with_extension("");
372                // Construct slugs in platform-independent way
373                let slugs = p
374                    .components()
375                    .map(|component| component.as_os_str().to_string_lossy())
376                    .collect::<Vec<_>>()
377                    .join(&arti_path::PATH_SEP.to_string());
378                let opt = match ArtiPath::new(slugs) {
379                    Ok(arti_path) => {
380                        let raw_id = RawEntryId::Path(path.to_owned());
381                        Some(Ok(KeystoreEntry::new(
382                            arti_path.into(),
383                            item_type,
384                            self.id(),
385                            raw_id,
386                        )))
387                    }
388                    Err(e) => {
389                        unrecognized_entry_err(path, err::MalformedPathError::InvalidArtiPath(e))
390                    }
391                };
392                Ok(opt)
393            })
394            .flatten_ok()
395            .collect()
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    // @@ begin test lint list maintained by maint/add_warning @@
402    #![allow(clippy::bool_assert_comparison)]
403    #![allow(clippy::clone_on_copy)]
404    #![allow(clippy::dbg_macro)]
405    #![allow(clippy::mixed_attributes_style)]
406    #![allow(clippy::print_stderr)]
407    #![allow(clippy::print_stdout)]
408    #![allow(clippy::single_char_pattern)]
409    #![allow(clippy::unwrap_used)]
410    #![allow(clippy::unchecked_duration_subtraction)]
411    #![allow(clippy::useless_vec)]
412    #![allow(clippy::needless_pass_by_value)]
413    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
414    use super::*;
415    use crate::KeyPath;
416    use crate::UnrecognizedEntry;
417    use crate::test_utils::TEST_SPECIFIER_PATH;
418    use crate::test_utils::ssh_keys::*;
419    use crate::test_utils::sshkeygen_ed25519_strings;
420    use crate::test_utils::{TestSpecifier, assert_found};
421    use std::cmp::Ordering;
422    use std::fs;
423    use std::path::PathBuf;
424    use std::time::{Duration, SystemTime};
425    use tempfile::{TempDir, tempdir};
426    use tor_cert::{CertifiedKey, Ed25519Cert};
427    use tor_checkable::{SelfSigned, Timebound};
428    use tor_key_forge::{CertType, KeyType, ParsedEd25519Cert};
429    use tor_llcrypto::pk::ed25519::{self, Ed25519PublicKey as _};
430
431    #[cfg(unix)]
432    use std::os::unix::fs::PermissionsExt;
433
434    impl Ord for KeyPath {
435        fn cmp(&self, other: &Self) -> Ordering {
436            match (self, other) {
437                (KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
438                _ => unimplemented!("not supported"),
439            }
440        }
441    }
442
443    impl PartialOrd for KeyPath {
444        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
445            Some(self.cmp(other))
446        }
447    }
448
449    fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
450        let rel_key_path = key_store
451            .rel_path(&TestSpecifier::default(), &key_type.clone().into())
452            .unwrap();
453
454        rel_key_path.checked_path().unwrap()
455    }
456
457    fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
458        let keystore_dir = tempdir().unwrap();
459
460        #[cfg(unix)]
461        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
462
463        let key_store =
464            ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
465                .unwrap();
466
467        if gen_keys {
468            let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
469            let parent = key_path.parent().unwrap();
470            fs::create_dir_all(parent).unwrap();
471            #[cfg(unix)]
472            fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
473
474            fs::write(key_path, ED25519_OPENSSH).unwrap();
475        }
476
477        (key_store, keystore_dir)
478    }
479
480    /// Checks if the `expected` list of `ArtiPath`s is the same as the specified `list`.
481    macro_rules! assert_contains_arti_paths {
482        ($expected:expr, $list:expr) => {{
483            let mut expected = Vec::from_iter($expected.iter().cloned().map(KeyPath::Arti));
484            expected.sort();
485
486            let mut sorted_list = $list
487                .iter()
488                .filter_map(|entry| {
489                    if let Ok(entry) = entry {
490                        Some(entry.key_path().clone())
491                    } else {
492                        None
493                    }
494                })
495                .collect::<Vec<_>>();
496            sorted_list.sort();
497
498            assert_eq!(expected, sorted_list);
499        }};
500    }
501
502    #[test]
503    #[cfg(unix)]
504    fn init_failure_perms() {
505        use std::os::unix::fs::PermissionsExt;
506
507        let keystore_dir = tempdir().unwrap();
508
509        // Too permissive
510        let mode = 0o777;
511
512        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
513        let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
514            .expect_err(&format!("expected failure (perms = {mode:o})"));
515
516        assert_eq!(
517            err.to_string(),
518            format!(
519                "Inaccessible path or bad permissions on {} while attempting to Init",
520                keystore_dir.path().display_lossy()
521            ),
522            "expected keystore init failure (perms = {:o})",
523            mode
524        );
525    }
526
527    #[test]
528    fn key_path_repr() {
529        let (key_store, _) = init_keystore(false);
530
531        assert_eq!(
532            key_store
533                .rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
534                .unwrap()
535                .rel_path_unchecked(),
536            PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
537        );
538
539        assert_eq!(
540            key_store
541                .rel_path(
542                    &TestSpecifier::default(),
543                    &KeyType::X25519StaticKeypair.into()
544                )
545                .unwrap()
546                .rel_path_unchecked(),
547            PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
548        );
549    }
550
551    #[cfg(unix)]
552    #[test]
553    fn get_and_rm_bad_perms() {
554        use std::os::unix::fs::PermissionsExt;
555
556        let (key_store, _keystore_dir) = init_keystore(true);
557
558        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
559
560        // Make the permissions of the test key too permissive
561        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
562        assert!(
563            key_store
564                .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
565                .is_err()
566        );
567
568        // Make the permissions of the parent directory too lax
569        fs::set_permissions(
570            key_path.parent().unwrap(),
571            fs::Permissions::from_mode(0o777),
572        )
573        .unwrap();
574
575        assert!(key_store.list().is_err());
576
577        let key_spec = TestSpecifier::default();
578        let ed_key_type = &KeyType::Ed25519Keypair.into();
579        assert_eq!(
580            key_store
581                .remove(&key_spec, ed_key_type)
582                .unwrap_err()
583                .to_string(),
584            format!(
585                "Inaccessible path or bad permissions on {} while attempting to Remove",
586                key_store
587                    .rel_path(&key_spec, ed_key_type)
588                    .unwrap()
589                    .rel_path_unchecked()
590                    .display_lossy()
591            ),
592        );
593    }
594
595    #[test]
596    fn get() {
597        // Initialize an empty key store
598        let (key_store, _keystore_dir) = init_keystore(false);
599
600        let mut expected_arti_paths = Vec::new();
601
602        // Not found
603        assert_found!(
604            key_store,
605            &TestSpecifier::default(),
606            &KeyType::Ed25519Keypair,
607            false
608        );
609        assert!(key_store.list().unwrap().is_empty());
610
611        // Initialize a key store with some test keys
612        let (key_store, _keystore_dir) = init_keystore(true);
613
614        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
615
616        // Found!
617        assert_found!(
618            key_store,
619            &TestSpecifier::default(),
620            &KeyType::Ed25519Keypair,
621            true
622        );
623
624        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
625    }
626
627    #[test]
628    fn insert() {
629        // Initialize an empty key store
630        let (key_store, keystore_dir) = init_keystore(false);
631
632        let mut expected_arti_paths = Vec::new();
633
634        // Not found
635        assert_found!(
636            key_store,
637            &TestSpecifier::default(),
638            &KeyType::Ed25519Keypair,
639            false
640        );
641        assert!(key_store.list().unwrap().is_empty());
642
643        let mut keys_and_specs = vec![(ED25519_OPENSSH.into(), TestSpecifier::default())];
644
645        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
646            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
647        }
648
649        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
650            // Insert the keys
651            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
652            let erased_kp = key
653                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
654                .unwrap();
655
656            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
657                panic!("failed to downcast key to ed25519::Keypair")
658            };
659
660            let path = keystore_dir.as_ref().join(
661                key_store
662                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
663                    .unwrap()
664                    .rel_path_unchecked(),
665            );
666
667            // The key and its parent directories don't exist for first key.
668            // They are created after the first key is inserted.
669            assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
670
671            assert!(key_store.insert(&*key, key_spec).is_ok());
672
673            // Update expected_arti_paths after inserting key
674            expected_arti_paths.push(key_spec.arti_path().unwrap());
675
676            // insert() is supposed to create the missing directories
677            assert!(path.parent().unwrap().try_exists().unwrap());
678
679            // Found!
680            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
681
682            // Check keystore list
683            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
684        }
685    }
686
687    #[test]
688    fn remove() {
689        // Initialize the key store
690        let (key_store, _keystore_dir) = init_keystore(true);
691
692        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
693        let mut specs = vec![TestSpecifier::default()];
694
695        assert_found!(
696            key_store,
697            &TestSpecifier::default(),
698            &KeyType::Ed25519Keypair,
699            true
700        );
701
702        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
703            // Insert ssh-keygen key
704            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
705            let erased_kp = key
706                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
707                .unwrap();
708
709            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
710                panic!("failed to downcast key to ed25519::Keypair")
711            };
712
713            let key_spec = TestSpecifier::new("-sshkeygen");
714
715            assert!(key_store.insert(&*key, &key_spec).is_ok());
716
717            expected_arti_paths.push(key_spec.arti_path().unwrap());
718            specs.push(key_spec);
719        }
720
721        let ed_key_type = &KeyType::Ed25519Keypair.into();
722
723        for spec in specs {
724            // Found!
725            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
726
727            // Check keystore list before removing key
728            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
729
730            // Now remove the key... remove() should indicate success by returning Ok(Some(()))
731            assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
732
733            // Remove the current key_spec's ArtiPath from expected_arti_paths
734            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
735
736            // Can't find it anymore!
737            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
738
739            // Check keystore list after removing key
740            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
741
742            // remove() returns Ok(None) now.
743            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
744        }
745
746        assert!(key_store.list().unwrap().is_empty());
747    }
748
749    #[test]
750    fn list() {
751        // Initialize the key store
752        let (key_store, keystore_dir) = init_keystore(true);
753
754        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
755
756        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
757
758        let mut keys_and_specs =
759            vec![(ED25519_OPENSSH.into(), TestSpecifier::new("-i-am-a-suffix"))];
760
761        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
762            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
763        }
764
765        // Insert more keys
766        for (key, key_spec) in keys_and_specs {
767            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
768            let erased_kp = key
769                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
770                .unwrap();
771
772            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
773                panic!("failed to downcast key to ed25519::Keypair")
774            };
775
776            assert!(key_store.insert(&*key, &key_spec).is_ok());
777
778            expected_arti_paths.push(key_spec.arti_path().unwrap());
779
780            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
781        }
782
783        // Insert key with invalid ArtiPath
784        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
785        let entries = key_store.list().unwrap();
786        let mut unrecognized_entries = entries.iter().filter_map(|e| {
787            let Err(entry) = e else {
788                return None;
789            };
790            Some(entry.entry())
791        });
792        let expected_entry = UnrecognizedEntry::from(RawKeystoreEntry::new(
793            RawEntryId::Path(PathBuf::from(TEST_SPECIFIER_PATH)),
794            key_store.id().clone(),
795        ));
796        assert_eq!(unrecognized_entries.next().unwrap(), &expected_entry);
797        assert!(unrecognized_entries.next().is_none());
798    }
799
800    #[cfg(feature = "onion-service-cli-extra")]
801    #[test]
802    fn remove_unchecked() {
803        // Initialize the key store
804        let (key_store, keystore_dir) = init_keystore(true);
805
806        // Insert key with invalid ArtiPath
807        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
808
809        // Keystore contains a valid entry and an unrecognized one
810        let entries = key_store.list().unwrap();
811
812        // Remove valid entry
813        let vaild_spcifier = entries
814            .iter()
815            .find_map(|res| {
816                let Ok(entry) = res else {
817                    return None;
818                };
819                match entry.key_path() {
820                    KeyPath::Arti(a) => {
821                        let mut path_str = a.to_string();
822                        path_str.push('.');
823                        path_str.push_str(&entry.key_type().arti_extension());
824                        let raw_id = RawEntryId::Path(PathBuf::from(&path_str));
825                        Some(RawKeystoreEntry::new(raw_id, key_store.id().to_owned()))
826                    }
827                    _ => {
828                        panic!("Unexpected KeyPath variant encountered")
829                    }
830                }
831            })
832            .unwrap();
833        key_store.remove_unchecked(vaild_spcifier.raw_id()).unwrap();
834        let entries = key_store.list().unwrap();
835        // Assert no valid entries are encountered
836        assert!(
837            entries.iter().all(|res| res.is_err()),
838            "the only valid entry should've been removed!"
839        );
840
841        // Remove unrecognized entry
842        let unrecognized_raw = entries
843            .iter()
844            .find_map(|res| match res {
845                Ok(_) => None,
846                Err(e) => Some(e.entry()),
847            })
848            .unwrap();
849        key_store
850            .remove_unchecked(unrecognized_raw.raw_id())
851            .unwrap();
852        let entries = key_store.list().unwrap();
853        // Assert the last entry (unrecognized) has been removed
854        assert_eq!(entries.len(), 0);
855
856        // Try to remove a non existing entry
857        let _ = key_store
858            .remove_unchecked(unrecognized_raw.raw_id())
859            .unwrap_err();
860    }
861
862    #[test]
863    fn key_path_not_regular_file() {
864        let (key_store, _keystore_dir) = init_keystore(false);
865
866        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
867        // The key is a directory, not a regular file
868        fs::create_dir_all(&key_path).unwrap();
869        assert!(key_path.try_exists().unwrap());
870        let parent = key_path.parent().unwrap();
871        #[cfg(unix)]
872        fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
873
874        let err = key_store
875            .contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
876            .unwrap_err();
877        assert!(err.to_string().contains("not a regular file"), "{err}");
878    }
879
880    #[test]
881    fn certs() {
882        let (key_store, _keystore_dir) = init_keystore(false);
883
884        let mut rng = rand::rng();
885        let subject_key = ed25519::Keypair::generate(&mut rng);
886        let signing_key = ed25519::Keypair::generate(&mut rng);
887
888        // Note: the cert constructor rounds the expiration forward to the nearest hour
889        // after the epoch.
890        let cert_exp = SystemTime::UNIX_EPOCH + Duration::from_secs(60 * 60);
891
892        let encoded_cert = Ed25519Cert::constructor()
893            .cert_type(tor_cert::CertType::IDENTITY_V_SIGNING)
894            .expiration(cert_exp)
895            .signing_key(signing_key.public_key().into())
896            .cert_key(CertifiedKey::Ed25519(subject_key.public_key().into()))
897            .encode_and_sign(&signing_key)
898            .unwrap();
899
900        // The specifier doesn't really matter.
901        let cert_spec = TestSpecifier::default();
902        assert!(key_store.insert(&encoded_cert, &cert_spec).is_ok());
903
904        let erased_cert = key_store
905            .get(&cert_spec, &CertType::Ed25519TorCert.into())
906            .unwrap()
907            .unwrap();
908        let Ok(found_cert) = erased_cert.downcast::<ParsedEd25519Cert>() else {
909            panic!("failed to downcast cert to KewUnknownCert")
910        };
911
912        let found_cert = found_cert
913            .should_be_signed_with(&signing_key.public_key().into())
914            .unwrap()
915            .dangerously_assume_wellsigned()
916            .dangerously_assume_timely();
917
918        assert_eq!(
919            found_cert.as_ref().cert_type(),
920            tor_cert::CertType::IDENTITY_V_SIGNING
921        );
922        assert_eq!(found_cert.as_ref().expiry(), cert_exp);
923        assert_eq!(
924            found_cert.as_ref().signing_key(),
925            Some(&signing_key.public_key().into())
926        );
927        assert_eq!(
928            found_cert.subject_key().unwrap(),
929            &subject_key.public_key().into()
930        );
931    }
932}