Skip to main content

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;
18use crate::{
19    ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result, UnknownKeyTypeError,
20    UnrecognizedEntry, 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.clone()))
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 = UnrecognizedEntry::new(raw_id, self.id().clone());
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_time_subtraction)]
411    #![allow(clippy::useless_vec)]
412    #![allow(clippy::needless_pass_by_value)]
413    #![allow(clippy::string_slice)] // See arti#2571
414    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
415    use super::*;
416    use crate::KeyPath;
417    use crate::UnrecognizedEntry;
418    use crate::test_utils::TEST_SPECIFIER_PATH;
419    use crate::test_utils::ssh_keys::*;
420    use crate::test_utils::sshkeygen_ed25519_strings;
421    use crate::test_utils::{TestSpecifier, assert_found};
422    use std::cmp::Ordering;
423    use std::fs;
424    use std::path::PathBuf;
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    use web_time_compat::{Duration, SystemTime};
431
432    #[cfg(unix)]
433    use std::os::unix::fs::PermissionsExt;
434
435    impl Ord for KeyPath {
436        fn cmp(&self, other: &Self) -> Ordering {
437            match (self, other) {
438                (KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
439                _ => unimplemented!("not supported"),
440            }
441        }
442    }
443
444    impl PartialOrd for KeyPath {
445        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
446            Some(self.cmp(other))
447        }
448    }
449
450    fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
451        let rel_key_path = key_store
452            .rel_path(&TestSpecifier::default(), &key_type.clone().into())
453            .unwrap();
454
455        rel_key_path.checked_path().unwrap()
456    }
457
458    fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
459        let keystore_dir = tempdir().unwrap();
460
461        #[cfg(unix)]
462        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
463
464        let key_store =
465            ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
466                .unwrap();
467
468        if gen_keys {
469            let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
470            let parent = key_path.parent().unwrap();
471            fs::create_dir_all(parent).unwrap();
472            #[cfg(unix)]
473            fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
474
475            fs::write(key_path, ED25519_OPENSSH).unwrap();
476        }
477
478        (key_store, keystore_dir)
479    }
480
481    /// Checks if the `expected` list of `ArtiPath`s is the same as the specified `list`.
482    macro_rules! assert_contains_arti_paths {
483        ($expected:expr, $list:expr) => {{
484            let mut expected = Vec::from_iter($expected.iter().cloned().map(KeyPath::Arti));
485            expected.sort();
486
487            let mut sorted_list = $list
488                .iter()
489                .filter_map(|entry| {
490                    if let Ok(entry) = entry {
491                        Some(entry.key_path().clone())
492                    } else {
493                        None
494                    }
495                })
496                .collect::<Vec<_>>();
497            sorted_list.sort();
498
499            assert_eq!(expected, sorted_list);
500        }};
501    }
502
503    #[test]
504    #[cfg(unix)]
505    fn init_failure_perms() {
506        use std::os::unix::fs::PermissionsExt;
507
508        let keystore_dir = tempdir().unwrap();
509
510        // Too permissive
511        let mode = 0o777;
512
513        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
514        let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
515            .expect_err(&format!("expected failure (perms = {mode:o})"));
516
517        assert_eq!(
518            err.to_string(),
519            format!(
520                "Inaccessible path or bad permissions on {} while attempting to Init",
521                keystore_dir.path().display_lossy()
522            ),
523            "expected keystore init failure (perms = {:o})",
524            mode
525        );
526    }
527
528    #[test]
529    fn key_path_repr() {
530        let (key_store, _) = init_keystore(false);
531
532        assert_eq!(
533            key_store
534                .rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
535                .unwrap()
536                .rel_path_unchecked(),
537            PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
538        );
539
540        assert_eq!(
541            key_store
542                .rel_path(
543                    &TestSpecifier::default(),
544                    &KeyType::X25519StaticKeypair.into()
545                )
546                .unwrap()
547                .rel_path_unchecked(),
548            PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
549        );
550    }
551
552    #[cfg(unix)]
553    #[test]
554    fn get_and_rm_bad_perms() {
555        use std::os::unix::fs::PermissionsExt;
556
557        let (key_store, _keystore_dir) = init_keystore(true);
558
559        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
560
561        // Make the permissions of the test key too permissive
562        fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
563        assert!(
564            key_store
565                .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
566                .is_err()
567        );
568
569        // Make the permissions of the parent directory too lax
570        fs::set_permissions(
571            key_path.parent().unwrap(),
572            fs::Permissions::from_mode(0o777),
573        )
574        .unwrap();
575
576        assert!(key_store.list().is_err());
577
578        let key_spec = TestSpecifier::default();
579        let ed_key_type = &KeyType::Ed25519Keypair.into();
580        assert_eq!(
581            key_store
582                .remove(&key_spec, ed_key_type)
583                .unwrap_err()
584                .to_string(),
585            format!(
586                "Inaccessible path or bad permissions on {} while attempting to Remove",
587                key_store
588                    .rel_path(&key_spec, ed_key_type)
589                    .unwrap()
590                    .rel_path_unchecked()
591                    .display_lossy()
592            ),
593        );
594    }
595
596    #[test]
597    fn get() {
598        // Initialize an empty key store
599        let (key_store, _keystore_dir) = init_keystore(false);
600
601        let mut expected_arti_paths = Vec::new();
602
603        // Not found
604        assert_found!(
605            key_store,
606            &TestSpecifier::default(),
607            &KeyType::Ed25519Keypair,
608            false
609        );
610        assert!(key_store.list().unwrap().is_empty());
611
612        // Initialize a key store with some test keys
613        let (key_store, _keystore_dir) = init_keystore(true);
614
615        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
616
617        // Found!
618        assert_found!(
619            key_store,
620            &TestSpecifier::default(),
621            &KeyType::Ed25519Keypair,
622            true
623        );
624
625        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
626    }
627
628    #[test]
629    fn insert() {
630        // Initialize an empty key store
631        let (key_store, keystore_dir) = init_keystore(false);
632
633        let mut expected_arti_paths = Vec::new();
634
635        // Not found
636        assert_found!(
637            key_store,
638            &TestSpecifier::default(),
639            &KeyType::Ed25519Keypair,
640            false
641        );
642        assert!(key_store.list().unwrap().is_empty());
643
644        let mut keys_and_specs = vec![(ED25519_OPENSSH.into(), TestSpecifier::default())];
645
646        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
647            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
648        }
649
650        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
651            // Insert the keys
652            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
653            let erased_kp = key
654                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
655                .unwrap();
656
657            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
658                panic!("failed to downcast key to ed25519::Keypair")
659            };
660
661            let path = keystore_dir.as_ref().join(
662                key_store
663                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
664                    .unwrap()
665                    .rel_path_unchecked(),
666            );
667
668            // The key and its parent directories don't exist for first key.
669            // They are created after the first key is inserted.
670            assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
671
672            assert!(key_store.insert(&*key, key_spec).is_ok());
673
674            // Update expected_arti_paths after inserting key
675            expected_arti_paths.push(key_spec.arti_path().unwrap());
676
677            // insert() is supposed to create the missing directories
678            assert!(path.parent().unwrap().try_exists().unwrap());
679
680            // Found!
681            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
682
683            // Check keystore list
684            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
685        }
686    }
687
688    #[test]
689    fn remove() {
690        // Initialize the key store
691        let (key_store, _keystore_dir) = init_keystore(true);
692
693        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
694        let mut specs = vec![TestSpecifier::default()];
695
696        assert_found!(
697            key_store,
698            &TestSpecifier::default(),
699            &KeyType::Ed25519Keypair,
700            true
701        );
702
703        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
704            // Insert ssh-keygen key
705            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
706            let erased_kp = key
707                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
708                .unwrap();
709
710            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
711                panic!("failed to downcast key to ed25519::Keypair")
712            };
713
714            let key_spec = TestSpecifier::new("-sshkeygen");
715
716            assert!(key_store.insert(&*key, &key_spec).is_ok());
717
718            expected_arti_paths.push(key_spec.arti_path().unwrap());
719            specs.push(key_spec);
720        }
721
722        let ed_key_type = &KeyType::Ed25519Keypair.into();
723
724        for spec in specs {
725            // Found!
726            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
727
728            // Check keystore list before removing key
729            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
730
731            // Now remove the key... remove() should indicate success by returning Ok(Some(()))
732            assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
733
734            // Remove the current key_spec's ArtiPath from expected_arti_paths
735            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
736
737            // Can't find it anymore!
738            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
739
740            // Check keystore list after removing key
741            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
742
743            // remove() returns Ok(None) now.
744            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
745        }
746
747        assert!(key_store.list().unwrap().is_empty());
748    }
749
750    #[test]
751    fn list() {
752        // Initialize the key store
753        let (key_store, keystore_dir) = init_keystore(true);
754
755        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
756
757        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
758
759        let mut keys_and_specs =
760            vec![(ED25519_OPENSSH.into(), TestSpecifier::new("-i-am-a-suffix"))];
761
762        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
763            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
764        }
765
766        // Insert more keys
767        for (key, key_spec) in keys_and_specs {
768            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
769            let erased_kp = key
770                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
771                .unwrap();
772
773            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
774                panic!("failed to downcast key to ed25519::Keypair")
775            };
776
777            assert!(key_store.insert(&*key, &key_spec).is_ok());
778
779            expected_arti_paths.push(key_spec.arti_path().unwrap());
780
781            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
782        }
783
784        // Insert key with invalid ArtiPath
785        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
786        let entries = key_store.list().unwrap();
787        let mut unrecognized_entries = entries.iter().filter_map(|e| {
788            let Err(entry) = e else {
789                return None;
790            };
791            Some(entry.entry())
792        });
793        let expected_entry = UnrecognizedEntry::new(
794            RawEntryId::Path(PathBuf::from(TEST_SPECIFIER_PATH)),
795            key_store.id().clone(),
796        );
797        assert_eq!(unrecognized_entries.next().unwrap(), &expected_entry);
798        assert!(unrecognized_entries.next().is_none());
799    }
800
801    #[cfg(feature = "onion-service-cli-extra")]
802    #[test]
803    fn remove_unchecked() {
804        // Initialize the key store
805        let (key_store, keystore_dir) = init_keystore(true);
806
807        // Insert key with invalid ArtiPath
808        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
809
810        // Keystore contains a valid entry and an unrecognized one
811        let entries = key_store.list().unwrap();
812
813        // Remove valid entry
814        let raw_id = entries
815            .iter()
816            .find_map(|res| {
817                let Ok(entry) = res else {
818                    return None;
819                };
820                match entry.key_path() {
821                    KeyPath::Arti(a) => {
822                        let mut path_str = a.to_string();
823                        path_str.push('.');
824                        path_str.push_str(&entry.key_type().arti_extension());
825                        Some(RawEntryId::Path(PathBuf::from(&path_str)))
826                    }
827                    _ => {
828                        panic!("Unexpected KeyPath variant encountered")
829                    }
830                }
831            })
832            .unwrap();
833        key_store.remove_unchecked(&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::builder()
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}