1pub(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#[derive(Debug)]
56pub struct ArtiNativeKeystore {
57 keystore_dir: CheckedDir,
61 id: KeystoreId,
63}
64
65impl ArtiNativeKeystore {
66 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 let id = KeystoreId::from_str("arti")?;
90 Ok(Self { keystore_dir, id })
91 }
92
93 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
104macro_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 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 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 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 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 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 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 let p = path.with_extension("");
372 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 #![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 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 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 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 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 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 let (key_store, _keystore_dir) = init_keystore(false);
597
598 let mut expected_arti_paths = Vec::new();
599
600 assert_found!(
602 key_store,
603 &TestSpecifier::default(),
604 &KeyType::Ed25519Keypair,
605 false
606 );
607 assert!(key_store.list().unwrap().is_empty());
608
609 let (key_store, _keystore_dir) = init_keystore(true);
611
612 expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
613
614 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 let (key_store, keystore_dir) = init_keystore(false);
629
630 let mut expected_arti_paths = Vec::new();
631
632 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 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 assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
668
669 assert!(key_store.insert(&*key, key_spec).is_ok());
670
671 expected_arti_paths.push(key_spec.arti_path().unwrap());
673
674 assert!(path.parent().unwrap().try_exists().unwrap());
676
677 assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
679
680 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
682 }
683 }
684
685 #[test]
686 fn remove() {
687 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 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 assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
724
725 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
727
728 assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
730
731 expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
733
734 assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
736
737 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
739
740 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 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 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 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 let (key_store, keystore_dir) = init_keystore(true);
803
804 let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
806
807 let entries = key_store.list().unwrap();
809
810 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!(
835 entries.iter().all(|res| res.is_err()),
836 "the only valid entry should've been removed!"
837 );
838
839 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_eq!(entries.len(), 0);
853
854 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 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 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 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}