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::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
16use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
17use crate::raw::{RawEntryId, RawKeystoreEntry};
18use crate::{
19    arti_path, ArtiPath, ArtiPathUnavailableError, KeystoreEntry, KeystoreId, Result,
20    UnknownKeyTypeError, UnrecognizedEntryError,
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::test_utils::ssh_keys::*;
416    use crate::test_utils::sshkeygen_ed25519_strings;
417    use crate::test_utils::TEST_SPECIFIER_PATH;
418    use crate::test_utils::{assert_found, TestSpecifier};
419    use crate::KeyPath;
420    use crate::UnrecognizedEntry;
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!(key_store
563            .get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
564            .is_err());
565
566        // Make the permissions of the parent directory too lax
567        fs::set_permissions(
568            key_path.parent().unwrap(),
569            fs::Permissions::from_mode(0o777),
570        )
571        .unwrap();
572
573        assert!(key_store.list().is_err());
574
575        let key_spec = TestSpecifier::default();
576        let ed_key_type = &KeyType::Ed25519Keypair.into();
577        assert_eq!(
578            key_store
579                .remove(&key_spec, ed_key_type)
580                .unwrap_err()
581                .to_string(),
582            format!(
583                "Inaccessible path or bad permissions on {} while attempting to Remove",
584                key_store
585                    .rel_path(&key_spec, ed_key_type)
586                    .unwrap()
587                    .rel_path_unchecked()
588                    .display_lossy()
589            ),
590        );
591    }
592
593    #[test]
594    fn get() {
595        // Initialize an empty key store
596        let (key_store, _keystore_dir) = init_keystore(false);
597
598        let mut expected_arti_paths = Vec::new();
599
600        // Not found
601        assert_found!(
602            key_store,
603            &TestSpecifier::default(),
604            &KeyType::Ed25519Keypair,
605            false
606        );
607        assert!(key_store.list().unwrap().is_empty());
608
609        // Initialize a key store with some test keys
610        let (key_store, _keystore_dir) = init_keystore(true);
611
612        expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
613
614        // Found!
615        assert_found!(
616            key_store,
617            &TestSpecifier::default(),
618            &KeyType::Ed25519Keypair,
619            true
620        );
621
622        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
623    }
624
625    #[test]
626    fn insert() {
627        // Initialize an empty key store
628        let (key_store, keystore_dir) = init_keystore(false);
629
630        let mut expected_arti_paths = Vec::new();
631
632        // Not found
633        assert_found!(
634            key_store,
635            &TestSpecifier::default(),
636            &KeyType::Ed25519Keypair,
637            false
638        );
639        assert!(key_store.list().unwrap().is_empty());
640
641        let mut keys_and_specs = vec![(ED25519_OPENSSH.into(), TestSpecifier::default())];
642
643        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
644            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
645        }
646
647        for (i, (key, key_spec)) in keys_and_specs.iter().enumerate() {
648            // Insert the keys
649            let key = UnparsedOpenSshKey::new(key.into(), PathBuf::from("/test/path"));
650            let erased_kp = key
651                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
652                .unwrap();
653
654            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
655                panic!("failed to downcast key to ed25519::Keypair")
656            };
657
658            let path = keystore_dir.as_ref().join(
659                key_store
660                    .rel_path(key_spec, &KeyType::Ed25519Keypair.into())
661                    .unwrap()
662                    .rel_path_unchecked(),
663            );
664
665            // The key and its parent directories don't exist for first key.
666            // They are created after the first key is inserted.
667            assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
668
669            assert!(key_store.insert(&*key, key_spec).is_ok());
670
671            // Update expected_arti_paths after inserting key
672            expected_arti_paths.push(key_spec.arti_path().unwrap());
673
674            // insert() is supposed to create the missing directories
675            assert!(path.parent().unwrap().try_exists().unwrap());
676
677            // Found!
678            assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
679
680            // Check keystore list
681            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
682        }
683    }
684
685    #[test]
686    fn remove() {
687        // Initialize the key store
688        let (key_store, _keystore_dir) = init_keystore(true);
689
690        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
691        let mut specs = vec![TestSpecifier::default()];
692
693        assert_found!(
694            key_store,
695            &TestSpecifier::default(),
696            &KeyType::Ed25519Keypair,
697            true
698        );
699
700        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
701            // Insert ssh-keygen key
702            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
703            let erased_kp = key
704                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
705                .unwrap();
706
707            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
708                panic!("failed to downcast key to ed25519::Keypair")
709            };
710
711            let key_spec = TestSpecifier::new("-sshkeygen");
712
713            assert!(key_store.insert(&*key, &key_spec).is_ok());
714
715            expected_arti_paths.push(key_spec.arti_path().unwrap());
716            specs.push(key_spec);
717        }
718
719        let ed_key_type = &KeyType::Ed25519Keypair.into();
720
721        for spec in specs {
722            // Found!
723            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
724
725            // Check keystore list before removing key
726            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
727
728            // Now remove the key... remove() should indicate success by returning Ok(Some(()))
729            assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
730
731            // Remove the current key_spec's ArtiPath from expected_arti_paths
732            expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
733
734            // Can't find it anymore!
735            assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
736
737            // Check keystore list after removing key
738            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
739
740            // remove() returns Ok(None) now.
741            assert!(key_store.remove(&spec, ed_key_type).unwrap().is_none());
742        }
743
744        assert!(key_store.list().unwrap().is_empty());
745    }
746
747    #[test]
748    fn list() {
749        // Initialize the key store
750        let (key_store, keystore_dir) = init_keystore(true);
751
752        let mut expected_arti_paths = vec![TestSpecifier::default().arti_path().unwrap()];
753
754        assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
755
756        let mut keys_and_specs =
757            vec![(ED25519_OPENSSH.into(), TestSpecifier::new("-i-am-a-suffix"))];
758
759        if let Ok((key, _)) = sshkeygen_ed25519_strings() {
760            keys_and_specs.push((key, TestSpecifier::new("-sshkeygen")));
761        }
762
763        // Insert more keys
764        for (key, key_spec) in keys_and_specs {
765            let key = UnparsedOpenSshKey::new(key, PathBuf::from("/test/path"));
766            let erased_kp = key
767                .parse_ssh_format_erased(&KeyType::Ed25519Keypair)
768                .unwrap();
769
770            let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
771                panic!("failed to downcast key to ed25519::Keypair")
772            };
773
774            assert!(key_store.insert(&*key, &key_spec).is_ok());
775
776            expected_arti_paths.push(key_spec.arti_path().unwrap());
777
778            assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
779        }
780
781        // Insert key with invalid ArtiPath
782        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
783        let entries = key_store.list().unwrap();
784        let mut unrecognized_entries = entries.iter().filter_map(|e| {
785            let Err(entry) = e else {
786                return None;
787            };
788            Some(entry.entry())
789        });
790        let expected_entry = UnrecognizedEntry::from(RawKeystoreEntry::new(
791            RawEntryId::Path(PathBuf::from(TEST_SPECIFIER_PATH)),
792            key_store.id().clone(),
793        ));
794        assert_eq!(unrecognized_entries.next().unwrap(), &expected_entry);
795        assert!(unrecognized_entries.next().is_none());
796    }
797
798    #[cfg(feature = "onion-service-cli-extra")]
799    #[test]
800    fn remove_unchecked() {
801        // Initialize the key store
802        let (key_store, keystore_dir) = init_keystore(true);
803
804        // Insert key with invalid ArtiPath
805        let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
806
807        // Keystore contains a valid entry and an unrecognized one
808        let entries = key_store.list().unwrap();
809
810        // Remove valid entry
811        let vaild_spcifier = entries
812            .iter()
813            .find_map(|res| {
814                let Ok(entry) = res else {
815                    return None;
816                };
817                match entry.key_path() {
818                    KeyPath::Arti(a) => {
819                        let mut path_str = a.to_string();
820                        path_str.push('.');
821                        path_str.push_str(&entry.key_type().arti_extension());
822                        let raw_id = RawEntryId::Path(PathBuf::from(&path_str));
823                        Some(RawKeystoreEntry::new(raw_id, key_store.id().to_owned()))
824                    }
825                    _ => {
826                        panic!("Unexpected KeyPath variant encountered")
827                    }
828                }
829            })
830            .unwrap();
831        key_store.remove_unchecked(vaild_spcifier.raw_id()).unwrap();
832        let entries = key_store.list().unwrap();
833        // Assert no valid entries are encountered
834        assert!(
835            entries.iter().all(|res| res.is_err()),
836            "the only valid entry should've been removed!"
837        );
838
839        // Remove unrecognized entry
840        let unrecognized_raw = entries
841            .iter()
842            .find_map(|res| match res {
843                Ok(_) => None,
844                Err(e) => Some(e.entry()),
845            })
846            .unwrap();
847        key_store
848            .remove_unchecked(unrecognized_raw.raw_id())
849            .unwrap();
850        let entries = key_store.list().unwrap();
851        // Assert the last entry (unrecognized) has been removed
852        assert_eq!(entries.len(), 0);
853
854        // Try to remove a non existing entry
855        let _ = key_store
856            .remove_unchecked(unrecognized_raw.raw_id())
857            .unwrap_err();
858    }
859
860    #[test]
861    fn key_path_not_regular_file() {
862        let (key_store, _keystore_dir) = init_keystore(false);
863
864        let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
865        // The key is a directory, not a regular file
866        fs::create_dir_all(&key_path).unwrap();
867        assert!(key_path.try_exists().unwrap());
868        let parent = key_path.parent().unwrap();
869        #[cfg(unix)]
870        fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
871
872        let err = key_store
873            .contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
874            .unwrap_err();
875        assert!(err.to_string().contains("not a regular file"), "{err}");
876    }
877
878    #[test]
879    fn certs() {
880        let (key_store, _keystore_dir) = init_keystore(false);
881
882        let mut rng = rand::rng();
883        let subject_key = ed25519::Keypair::generate(&mut rng);
884        let signing_key = ed25519::Keypair::generate(&mut rng);
885
886        // Note: the cert constructor rounds the expiration forward to the nearest hour
887        // after the epoch.
888        let cert_exp = SystemTime::UNIX_EPOCH + Duration::from_secs(60 * 60);
889
890        let encoded_cert = Ed25519Cert::constructor()
891            .cert_type(tor_cert::CertType::IDENTITY_V_SIGNING)
892            .expiration(cert_exp)
893            .signing_key(signing_key.public_key().into())
894            .cert_key(CertifiedKey::Ed25519(subject_key.public_key().into()))
895            .encode_and_sign(&signing_key)
896            .unwrap();
897
898        // The specifier doesn't really matter.
899        let cert_spec = TestSpecifier::default();
900        assert!(key_store.insert(&encoded_cert, &cert_spec).is_ok());
901
902        let erased_cert = key_store
903            .get(&cert_spec, &CertType::Ed25519TorCert.into())
904            .unwrap()
905            .unwrap();
906        let Ok(found_cert) = erased_cert.downcast::<ParsedEd25519Cert>() else {
907            panic!("failed to downcast cert to KewUnknownCert")
908        };
909
910        let found_cert = found_cert
911            .should_be_signed_with(&signing_key.public_key().into())
912            .unwrap()
913            .dangerously_assume_wellsigned()
914            .dangerously_assume_timely();
915
916        assert_eq!(
917            found_cert.as_ref().cert_type(),
918            tor_cert::CertType::IDENTITY_V_SIGNING
919        );
920        assert_eq!(found_cert.as_ref().expiry(), cert_exp);
921        assert_eq!(
922            found_cert.as_ref().signing_key(),
923            Some(&signing_key.public_key().into())
924        );
925        assert_eq!(
926            found_cert.subject_key().unwrap(),
927            &subject_key.public_key().into()
928        );
929    }
930}