tor_keymgr/keystore/ctor/
client.rs1use std::fs;
6use std::path::{Path, PathBuf};
7use std::result::Result as StdResult;
8use std::str::FromStr as _;
9use std::sync::Arc;
10
11use crate::keystore::ctor::CTorKeystore;
12use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
13use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
14use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
15use crate::raw::{RawEntryId, RawKeystoreEntry};
16use crate::{
17 CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId, Result,
18 UnrecognizedEntryError,
19};
20
21use fs_mistrust::Mistrust;
22use itertools::Itertools as _;
23use tor_basic_utils::PathExt;
24use tor_error::debug_report;
25use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
26use tor_key_forge::{KeyType, KeystoreItemType};
27use tor_llcrypto::pk::curve25519;
28use tracing::debug;
29
30pub struct CTorClientKeystore(CTorKeystore);
52
53impl CTorClientKeystore {
54 pub fn from_path_and_mistrust(
59 keystore_dir: impl AsRef<Path>,
60 mistrust: &Mistrust,
61 id: KeystoreId,
62 ) -> Result<Self> {
63 CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
64 }
65}
66
67macro_rules! hsid_if_supported {
69 ($spec:expr, $ret:expr, $key_type:expr) => {{
70 let Some(ctor_path) = $spec.ctor_path() else {
73 return $ret;
74 };
75
76 let CTorPath::ClientHsDescEncKey(hsid) = ctor_path else {
78 return $ret;
79 };
80
81 if *$key_type != KeyType::X25519StaticKeypair.into() {
82 return Err(CTorKeystoreError::InvalidKeystoreItemType {
83 item_type: $key_type.clone(),
84 item: "client restricted discovery key".into(),
85 }
86 .into());
87 }
88
89 hsid
90 }};
91}
92
93impl CTorClientKeystore {
94 fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
96 let entries = checked_op!(read_directory, dir)
97 .map_err(|e| FilesystemError::FsMistrust {
98 action: FilesystemAction::Read,
99 path: dir.rel_path_unchecked().into(),
100 err: e.into(),
101 })
102 .map_err(CTorKeystoreError::Filesystem)?;
103
104 Ok(entries)
105 }
106}
107
108const KEY_EXTENSION: &str = "auth_private";
110
111impl CTorClientKeystore {
112 fn read_key(&self, key_path: &Path) -> StdResult<Option<String>, CTorKeystoreError> {
116 let key_path = self.0.rel_path(key_path.into());
117
118 let content = match checked_op!(read_to_string, key_path) {
120 Err(fs_mistrust::Error::NotFound(_)) => {
121 return Ok(None);
123 }
124 res => res
125 .map_err(|err| FilesystemError::FsMistrust {
126 action: FilesystemAction::Read,
127 path: key_path.rel_path_unchecked().into(),
128 err: err.into(),
129 })
130 .map_err(CTorKeystoreError::Filesystem)?,
131 };
132
133 Ok(Some(content))
134 }
135
136 fn list_keys(
145 &self,
146 ) -> Result<
147 impl Iterator<Item = StdResult<(HsId, HsClientDescEncKeypair), CTorKeystoreError>> + '_,
148 > {
149 use CTorKeystoreError::*;
150
151 let dir = self.0.rel_path(PathBuf::from("."));
152 Ok(self.list_entries(&dir)?.filter_map(|entry| {
153 let entry = entry
154 .map_err(|e| {
155 debug!("cannot access key entry: {e}");
163 })
164 .ok()?;
165
166 let file_name = entry.file_name();
167 let path: &Path = file_name.as_ref();
168 let Some(KEY_EXTENSION) = path.extension().and_then(|e| e.to_str()) else {
169 return Some(Err(MalformedKey {
170 path: entry.path(),
171 err: MalformedClientKeyError::InvalidFormat.into(),
172 }));
173 };
174
175 let content = match self.read_key(path) {
176 Ok(c) => c,
177 Err(e) => {
178 debug_report!(&e, "failed to read {}", path.display_lossy());
179 return Some(Err(e));
180 }
181 }?;
182 Some(
183 parse_client_keypair(content.trim()).map_err(|e| MalformedKey {
184 path: path.into(),
185 err: e.into(),
186 }),
187 )
188 }))
189 }
190}
191
192fn parse_client_keypair(
203 key: impl AsRef<str>,
204) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
205 let key = key.as_ref();
206 let (hsid, auth_type, key_type, encoded_key) = key
207 .split(':')
208 .collect_tuple()
209 .ok_or(MalformedClientKeyError::InvalidFormat)?;
210
211 if auth_type != "descriptor" {
212 return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
213 }
214
215 if key_type != "x25519" {
216 return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
217 }
218
219 let encoded_key = encoded_key.to_uppercase();
225 let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
226 let x25519_sk: [u8; 32] = x25519_sk
227 .try_into()
228 .map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
229
230 let secret = curve25519::StaticSecret::from(x25519_sk);
231 let public = (&secret).into();
232 let x25519_keypair = curve25519::StaticKeypair { secret, public };
233 let hsid = HsId::from_str(&format!("{hsid}.onion"))?;
234
235 Ok((hsid, x25519_keypair.into()))
236}
237
238impl Keystore for CTorClientKeystore {
239 fn id(&self) -> &KeystoreId {
240 &self.0.id
241 }
242
243 fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
244 self.get(key_spec, item_type).map(|k| k.is_some())
245 }
246
247 fn get(
248 &self,
249 key_spec: &dyn KeySpecifier,
250 item_type: &KeystoreItemType,
251 ) -> Result<Option<ErasedKey>> {
252 let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
253 Ok(self
254 .list_keys()?
255 .find_map(|entry| {
256 if let Ok((hsid, key)) = entry {
257 (hsid == want_hsid).then(|| key.into())
258 } else {
259 None
260 }
261 })
262 .map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
263 }
264
265 #[cfg(feature = "onion-service-cli-extra")]
266 fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
267 Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
268 }
269
270 fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
271 Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
272 }
273
274 fn remove(
275 &self,
276 _key_spec: &dyn KeySpecifier,
277 _item_type: &KeystoreItemType,
278 ) -> Result<Option<()>> {
279 Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
280 }
281
282 #[cfg(feature = "onion-service-cli-extra")]
283 fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
284 Err(CTorKeystoreError::NotSupported {
285 action: "remove_unchecked",
286 }
287 .into())
288 }
289
290 fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
291 use CTorKeystoreError::*;
292
293 let keys = self
294 .list_keys()?
295 .filter_map(|entry| match entry {
296 Ok((hsid, _)) => {
297 let key_path: KeyPath = CTorPath::ClientHsDescEncKey(hsid).into();
298 let key_type: KeystoreItemType = KeyType::X25519StaticKeypair.into();
299 let raw_id = RawEntryId::Path(key_path.ctor()?.to_string().into());
300 Some(Ok(KeystoreEntry::new(
301 key_path,
302 key_type,
303 self.id(),
304 raw_id,
305 )))
306 }
307 Err(e) => match e {
308 MalformedKey { ref path, err: _ } => {
309 let raw_id = RawEntryId::Path(path.clone());
310 let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
311 Some(Err(UnrecognizedEntryError::new(entry, Arc::new(e))))
312 }
313 InvalidKeystoreItemType { .. } => None,
316 Filesystem(_) => None,
319 NotSupported { .. } => None,
320 Bug(_) => None,
321 },
322 })
323 .collect();
324
325 Ok(keys)
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 #![allow(clippy::bool_assert_comparison)]
333 #![allow(clippy::clone_on_copy)]
334 #![allow(clippy::dbg_macro)]
335 #![allow(clippy::mixed_attributes_style)]
336 #![allow(clippy::print_stderr)]
337 #![allow(clippy::print_stdout)]
338 #![allow(clippy::single_char_pattern)]
339 #![allow(clippy::unwrap_used)]
340 #![allow(clippy::unchecked_time_subtraction)]
341 #![allow(clippy::useless_vec)]
342 #![allow(clippy::needless_pass_by_value)]
343 use super::*;
345 use std::fs;
346 use tempfile::{TempDir, tempdir};
347
348 use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};
349
350 #[cfg(unix)]
351 use std::os::unix::fs::PermissionsExt;
352
353 const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");
355
356 const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");
358
359 const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");
361
362 const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");
364
365 const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";
367
368 fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
369 let keystore_dir = tempdir().unwrap();
370
371 #[cfg(unix)]
372 fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
373
374 let id = KeystoreId::from_str(id).unwrap();
375 let keystore =
376 CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
377 .unwrap();
378
379 let keys: &[(&str, &str)] = &[
380 ("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
381 ("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
383 (
384 "alice-truncated.auth_private",
385 &ALICE_AUTH_PRIVATE_VALID[..100],
386 ),
387 ("carol.auth", CAROL_AUTH_PRIVATE_VALID),
389 ("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
390 ];
391
392 for (name, key) in keys {
393 fs::write(keystore_dir.path().join(name), key).unwrap();
394 }
395
396 (keystore, keystore_dir)
397 }
398
399 #[test]
400 fn get() {
401 let (keystore, _keystore_dir) = init_keystore("foo");
402 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
403
404 assert_found!(
406 keystore,
407 &TestCTorSpecifier(path.clone()),
408 &KeyType::X25519StaticKeypair,
409 false
410 );
411
412 for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
413 let onion = hsid.split(":").next().unwrap();
415 let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
416 let path = CTorPath::ClientHsDescEncKey(hsid.clone());
417
418 assert_found!(
420 keystore,
421 &TestCTorSpecifier(path.clone()),
422 &KeyType::X25519StaticKeypair,
423 true
424 );
425 }
426
427 let keys: Vec<_> = keystore
428 .list()
429 .unwrap()
430 .into_iter()
431 .filter(|e| e.is_ok())
432 .collect();
433
434 assert_eq!(keys.len(), 2);
435 assert!(keys.iter().all(|entry| {
436 entry.as_ref().unwrap().key_type() == &KeyType::X25519StaticKeypair.into()
437 }));
438 }
439
440 #[test]
441 fn unsupported_operation() {
442 let (keystore, _keystore_dir) = init_keystore("foo");
443 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
444
445 let err = keystore
446 .remove(
447 &TestCTorSpecifier(path.clone()),
448 &KeyType::X25519StaticKeypair.into(),
449 )
450 .unwrap_err();
451
452 assert_eq!(err.to_string(), "Operation not supported: remove");
453
454 let err = keystore
455 .insert(&DummyKey, &TestCTorSpecifier(path))
456 .unwrap_err();
457
458 assert_eq!(err.to_string(), "Operation not supported: insert");
459 }
460
461 #[test]
462 fn wrong_keytype() {
463 let (keystore, _keystore_dir) = init_keystore("foo");
464 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
465
466 let err = keystore
467 .get(
468 &TestCTorSpecifier(path.clone()),
469 &KeyType::Ed25519PublicKey.into(),
470 )
471 .map(|_| ())
472 .unwrap_err();
473
474 assert_eq!(
475 err.to_string(),
476 "Invalid item type Ed25519PublicKey for client restricted discovery key"
477 );
478 }
479
480 #[test]
481 fn list() {
482 let (keystore, _keystore_dir) = init_keystore("foo");
483 let mut recognized = 0;
486 let mut unrecognized = 0;
487 for e in keystore.list().unwrap() {
488 if e.is_ok() {
489 recognized += 1;
490 } else {
491 unrecognized += 1;
492 }
493 }
494 assert_eq!(recognized, 2);
495 assert_eq!(unrecognized, 3);
496 }
497}