tor_keymgr/keystore/ctor/
client.rs1use std::fs;
6use std::path::{Path, PathBuf};
7use std::result::Result as StdResult;
8use std::str::FromStr as _;
9
10use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
11use crate::keystore::ctor::CTorKeystore;
12use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
13use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
14use crate::{CTorPath, KeyPath, KeystoreId, Result};
15
16use fs_mistrust::Mistrust;
17use itertools::Itertools as _;
18use tor_basic_utils::PathExt as _;
19use tor_error::debug_report;
20use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
21use tor_key_forge::{KeyType, KeystoreItemType};
22use tor_llcrypto::pk::curve25519;
23use tracing::debug;
24
25pub struct CTorClientKeystore(CTorKeystore);
47
48impl CTorClientKeystore {
49 pub fn from_path_and_mistrust(
54 keystore_dir: impl AsRef<Path>,
55 mistrust: &Mistrust,
56 id: KeystoreId,
57 ) -> Result<Self> {
58 CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
59 }
60}
61
62macro_rules! hsid_if_supported {
64 ($spec:expr, $ret:expr, $key_type:expr) => {{
65 let Some(ctor_path) = $spec.ctor_path() else {
68 return $ret;
69 };
70
71 let CTorPath::ClientHsDescEncKey(hsid) = ctor_path else {
73 return $ret;
74 };
75
76 if *$key_type != KeyType::X25519StaticKeypair.into() {
77 return Err(CTorKeystoreError::InvalidKeystoreItemType {
78 item_type: $key_type.clone(),
79 item: "client restricted discovery key".into(),
80 }
81 .into());
82 }
83
84 hsid
85 }};
86}
87
88impl CTorClientKeystore {
89 fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
91 let entries = checked_op!(read_directory, dir)
92 .map_err(|e| FilesystemError::FsMistrust {
93 action: FilesystemAction::Read,
94 path: dir.rel_path_unchecked().into(),
95 err: e.into(),
96 })
97 .map_err(CTorKeystoreError::Filesystem)?;
98
99 Ok(entries)
100 }
101}
102
103const KEY_EXTENSION: &str = "auth_private";
105
106impl CTorClientKeystore {
107 fn read_key(&self, key_path: &Path) -> Result<Option<String>> {
111 let key_path = self.0.rel_path(key_path.into());
112
113 let content = match checked_op!(read_to_string, key_path) {
115 Err(fs_mistrust::Error::NotFound(_)) => {
116 return Ok(None);
118 }
119 res => res
120 .map_err(|err| FilesystemError::FsMistrust {
121 action: FilesystemAction::Read,
122 path: key_path.rel_path_unchecked().into(),
123 err: err.into(),
124 })
125 .map_err(CTorKeystoreError::Filesystem)?,
126 };
127
128 Ok(Some(content))
129 }
130
131 fn list_keys(&self) -> Result<impl Iterator<Item = (HsId, HsClientDescEncKeypair)> + '_> {
133 let dir = self.0.rel_path(PathBuf::from("."));
134 Ok(self.list_entries(&dir)?.filter_map(|entry| {
135 let entry = entry
136 .map_err(|e| {
137 debug!("cannot access key entry: {e}");
145 })
146 .ok()?;
147
148 let file_name = entry.file_name();
149 let path: &Path = file_name.as_ref();
150 let extension = path.extension().and_then(|e| e.to_str());
151 if extension != Some(KEY_EXTENSION) {
152 debug!(
153 "found entry {} with unrecognized extension {} in C Tor client keystore",
154 path.display_lossy(),
155 extension.unwrap_or_default()
156 );
157 return None;
158 }
159
160 let content = self
161 .read_key(path)
162 .map_err(|e| {
163 debug_report!(e, "failed to read {}", path.display_lossy());
164 })
165 .ok()
166 .flatten()?;
167
168 let (hsid, key) = parse_client_keypair(content.trim())
169 .map_err(|e| CTorKeystoreError::MalformedKey {
170 path: path.into(),
171 err: e.into(),
172 })
173 .map_err(|e| {
174 debug_report!(
175 e,
176 "cannot parse C Tor client keystore entry {}",
177 path.display_lossy()
178 );
179 })
180 .ok()?;
181
182 Some((hsid, key))
183 }))
184 }
185}
186
187fn parse_client_keypair(
198 key: impl AsRef<str>,
199) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
200 let key = key.as_ref();
201 let (hsid, auth_type, key_type, encoded_key) = key
202 .split(':')
203 .collect_tuple()
204 .ok_or(MalformedClientKeyError::InvalidFormat)?;
205
206 if auth_type != "descriptor" {
207 return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
208 }
209
210 if key_type != "x25519" {
211 return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
212 }
213
214 let encoded_key = encoded_key.to_uppercase();
220 let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
221 let x25519_sk: [u8; 32] = x25519_sk
222 .try_into()
223 .map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
224
225 let secret = curve25519::StaticSecret::from(x25519_sk);
226 let public = (&secret).into();
227 let x25519_keypair = curve25519::StaticKeypair { secret, public };
228 let hsid = HsId::from_str(&format!("{hsid}.onion"))?;
229
230 Ok((hsid, x25519_keypair.into()))
231}
232
233impl Keystore for CTorClientKeystore {
234 fn id(&self) -> &KeystoreId {
235 &self.0.id
236 }
237
238 fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
239 self.get(key_spec, item_type).map(|k| k.is_some())
240 }
241
242 fn get(
243 &self,
244 key_spec: &dyn KeySpecifier,
245 item_type: &KeystoreItemType,
246 ) -> Result<Option<ErasedKey>> {
247 let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
248 Ok(self
249 .list_keys()?
250 .find_map(|(hsid, key)| (hsid == want_hsid).then(|| key.into()))
251 .map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
252 }
253
254 fn insert(
255 &self,
256 _key: &dyn EncodableItem,
257 _key_spec: &dyn KeySpecifier,
258 _item_type: &KeystoreItemType,
259 ) -> Result<()> {
260 Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
261 }
262
263 fn remove(
264 &self,
265 _key_spec: &dyn KeySpecifier,
266 _item_type: &KeystoreItemType,
267 ) -> Result<Option<()>> {
268 Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
269 }
270
271 fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
272 let keys = self
273 .list_keys()?
274 .map(|(hsid, _)| {
275 (
276 CTorPath::ClientHsDescEncKey(hsid).into(),
277 KeyType::X25519StaticKeypair.into(),
278 )
279 })
280 .collect();
281
282 Ok(keys)
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 #![allow(clippy::bool_assert_comparison)]
290 #![allow(clippy::clone_on_copy)]
291 #![allow(clippy::dbg_macro)]
292 #![allow(clippy::mixed_attributes_style)]
293 #![allow(clippy::print_stderr)]
294 #![allow(clippy::print_stdout)]
295 #![allow(clippy::single_char_pattern)]
296 #![allow(clippy::unwrap_used)]
297 #![allow(clippy::unchecked_duration_subtraction)]
298 #![allow(clippy::useless_vec)]
299 #![allow(clippy::needless_pass_by_value)]
300 use super::*;
302 use std::fs;
303 use tempfile::{tempdir, TempDir};
304
305 use crate::test_utils::{assert_found, DummyKey, TestCTorSpecifier};
306
307 #[cfg(unix)]
308 use std::os::unix::fs::PermissionsExt;
309
310 const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");
312
313 const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");
315
316 const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");
318
319 const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");
321
322 const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";
324
325 fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
326 let keystore_dir = tempdir().unwrap();
327
328 #[cfg(unix)]
329 fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
330
331 let id = KeystoreId::from_str(id).unwrap();
332 let keystore =
333 CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
334 .unwrap();
335
336 let keys: &[(&str, &str)] = &[
337 ("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
338 ("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
340 (
341 "alice-truncated.auth_private",
342 &ALICE_AUTH_PRIVATE_VALID[..100],
343 ),
344 ("carol.auth", CAROL_AUTH_PRIVATE_VALID),
346 ("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
347 ];
348
349 for (name, key) in keys {
350 fs::write(keystore_dir.path().join(name), key).unwrap();
351 }
352
353 (keystore, keystore_dir)
354 }
355
356 #[test]
357 fn get() {
358 let (keystore, _keystore_dir) = init_keystore("foo");
359 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
360
361 assert_found!(
363 keystore,
364 &TestCTorSpecifier(path.clone()),
365 &KeyType::X25519StaticKeypair,
366 false
367 );
368
369 for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
370 let onion = hsid.split(":").next().unwrap();
372 let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
373 let path = CTorPath::ClientHsDescEncKey(hsid.clone());
374
375 assert_found!(
377 keystore,
378 &TestCTorSpecifier(path.clone()),
379 &KeyType::X25519StaticKeypair,
380 true
381 );
382 }
383
384 let keys: Vec<_> = keystore.list().unwrap();
385
386 assert_eq!(keys.len(), 2);
387 assert!(keys
388 .iter()
389 .all(|(_, key_type)| *key_type == KeyType::X25519StaticKeypair.into()));
390 }
391
392 #[test]
393 fn unsupported_operation() {
394 let (keystore, _keystore_dir) = init_keystore("foo");
395 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
396
397 let err = keystore
398 .remove(
399 &TestCTorSpecifier(path.clone()),
400 &KeyType::X25519StaticKeypair.into(),
401 )
402 .unwrap_err();
403
404 assert_eq!(err.to_string(), "Operation not supported: remove");
405
406 let err = keystore
407 .insert(
408 &DummyKey,
409 &TestCTorSpecifier(path),
410 &KeyType::X25519StaticKeypair.into(),
411 )
412 .unwrap_err();
413
414 assert_eq!(err.to_string(), "Operation not supported: insert");
415 }
416
417 #[test]
418 fn wrong_keytype() {
419 let (keystore, _keystore_dir) = init_keystore("foo");
420 let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
421
422 let err = keystore
423 .get(
424 &TestCTorSpecifier(path.clone()),
425 &KeyType::Ed25519PublicKey.into(),
426 )
427 .map(|_| ())
428 .unwrap_err();
429
430 assert_eq!(
431 err.to_string(),
432 "Invalid item type Ed25519PublicKey for client restricted discovery key"
433 );
434 }
435}