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::{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#[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.clone()))
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 = 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 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_time_subtraction)]
411 #![allow(clippy::useless_vec)]
412 #![allow(clippy::needless_pass_by_value)]
413 #![allow(clippy::string_slice)] 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 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 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 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 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 let (key_store, _keystore_dir) = init_keystore(false);
600
601 let mut expected_arti_paths = Vec::new();
602
603 assert_found!(
605 key_store,
606 &TestSpecifier::default(),
607 &KeyType::Ed25519Keypair,
608 false
609 );
610 assert!(key_store.list().unwrap().is_empty());
611
612 let (key_store, _keystore_dir) = init_keystore(true);
614
615 expected_arti_paths.push(TestSpecifier::default().arti_path().unwrap());
616
617 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 let (key_store, keystore_dir) = init_keystore(false);
632
633 let mut expected_arti_paths = Vec::new();
634
635 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 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 assert_eq!(!path.parent().unwrap().try_exists().unwrap(), i == 0);
671
672 assert!(key_store.insert(&*key, key_spec).is_ok());
673
674 expected_arti_paths.push(key_spec.arti_path().unwrap());
676
677 assert!(path.parent().unwrap().try_exists().unwrap());
679
680 assert_found!(key_store, key_spec, &KeyType::Ed25519Keypair, true);
682
683 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
685 }
686 }
687
688 #[test]
689 fn remove() {
690 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 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 assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, true);
727
728 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
730
731 assert_eq!(key_store.remove(&spec, ed_key_type).unwrap(), Some(()));
733
734 expected_arti_paths.retain(|arti_path| *arti_path != spec.arti_path().unwrap());
736
737 assert_found!(key_store, &spec, &KeyType::Ed25519Keypair, false);
739
740 assert_contains_arti_paths!(expected_arti_paths, key_store.list().unwrap());
742
743 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 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 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 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 let (key_store, keystore_dir) = init_keystore(true);
806
807 let _ = fs::File::create(keystore_dir.path().join(TEST_SPECIFIER_PATH)).unwrap();
809
810 let entries = key_store.list().unwrap();
812
813 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!(
837 entries.iter().all(|res| res.is_err()),
838 "the only valid entry should've been removed!"
839 );
840
841 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_eq!(entries.len(), 0);
855
856 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 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 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 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}