tor_keymgr/keystore/ctor/
client.rsuse std::fs;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::str::FromStr as _;
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
use crate::keystore::ctor::CTorKeystore;
use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
use crate::{CTorPath, KeyPath, KeystoreId, Result};
use fs_mistrust::Mistrust;
use itertools::Itertools as _;
use tor_basic_utils::PathExt as _;
use tor_error::debug_report;
use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
use tor_key_forge::{KeyType, KeystoreItemType};
use tor_llcrypto::pk::curve25519;
use tracing::debug;
pub struct CTorClientKeystore(CTorKeystore);
impl CTorClientKeystore {
pub fn from_path_and_mistrust(
keystore_dir: impl AsRef<Path>,
mistrust: &Mistrust,
id: KeystoreId,
) -> Result<Self> {
CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
}
}
macro_rules! hsid_if_supported {
($spec:expr, $ret:expr, $key_type:expr) => {{
let Some(ctor_path) = $spec.ctor_path() else {
return $ret;
};
let CTorPath::ClientHsDescEncKey(hsid) = ctor_path else {
return $ret;
};
if *$key_type != KeyType::X25519StaticKeypair.into() {
return Err(CTorKeystoreError::InvalidKeystoreItemType {
item_type: $key_type.clone(),
item: "client restricted discovery key".into(),
}
.into());
}
hsid
}};
}
impl CTorClientKeystore {
fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
let entries = checked_op!(read_directory, dir)
.map_err(|e| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: dir.rel_path_unchecked().into(),
err: e.into(),
})
.map_err(CTorKeystoreError::Filesystem)?;
Ok(entries)
}
}
const KEY_EXTENSION: &str = "auth_private";
impl CTorClientKeystore {
fn read_key(&self, key_path: &Path) -> Result<Option<String>> {
let key_path = self.0.rel_path(key_path.into());
let content = match checked_op!(read_to_string, key_path) {
Err(fs_mistrust::Error::NotFound(_)) => {
return Ok(None);
}
res => res
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: key_path.rel_path_unchecked().into(),
err: err.into(),
})
.map_err(CTorKeystoreError::Filesystem)?,
};
Ok(Some(content))
}
fn list_keys(&self) -> Result<impl Iterator<Item = (HsId, HsClientDescEncKeypair)> + '_> {
let dir = self.0.rel_path(PathBuf::from("."));
Ok(self.list_entries(&dir)?.filter_map(|entry| {
let entry = entry
.map_err(|e| {
debug!("cannot access key entry: {e}");
})
.ok()?;
let file_name = entry.file_name();
let path: &Path = file_name.as_ref();
let extension = path.extension().and_then(|e| e.to_str());
if extension != Some(KEY_EXTENSION) {
debug!(
"found entry {} with unrecognized extension {} in C Tor client keystore",
path.display_lossy(),
extension.unwrap_or_default()
);
return None;
}
let content = self
.read_key(path)
.map_err(|e| {
debug_report!(e, "failed to read {}", path.display_lossy());
})
.ok()
.flatten()?;
let (hsid, key) = parse_client_keypair(content.trim())
.map_err(|e| CTorKeystoreError::MalformedKey {
path: path.into(),
err: e.into(),
})
.map_err(|e| {
debug_report!(
e,
"cannot parse C Tor client keystore entry {}",
path.display_lossy()
);
})
.ok()?;
Some((hsid, key))
}))
}
}
fn parse_client_keypair(
key: impl AsRef<str>,
) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
let key = key.as_ref();
let (hsid, auth_type, key_type, encoded_key) = key
.split(':')
.collect_tuple()
.ok_or(MalformedClientKeyError::InvalidFormat)?;
if auth_type != "descriptor" {
return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
}
if key_type != "x25519" {
return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
}
let encoded_key = encoded_key.to_uppercase();
let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
let x25519_sk: [u8; 32] = x25519_sk
.try_into()
.map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;
let secret = curve25519::StaticSecret::from(x25519_sk);
let public = (&secret).into();
let x25519_keypair = curve25519::StaticKeypair { secret, public };
let hsid = HsId::from_str(&format!("{hsid}.onion"))?;
Ok((hsid, x25519_keypair.into()))
}
impl Keystore for CTorClientKeystore {
fn id(&self) -> &KeystoreId {
&self.0.id
}
fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
self.get(key_spec, item_type).map(|k| k.is_some())
}
fn get(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<ErasedKey>> {
let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
Ok(self
.list_keys()?
.find_map(|(hsid, key)| (hsid == want_hsid).then(|| key.into()))
.map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
}
fn insert(
&self,
_key: &dyn EncodableItem,
_key_spec: &dyn KeySpecifier,
_item_type: &KeystoreItemType,
) -> Result<()> {
Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
}
fn remove(
&self,
_key_spec: &dyn KeySpecifier,
_item_type: &KeystoreItemType,
) -> Result<Option<()>> {
Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
}
fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
let keys = self
.list_keys()?
.map(|(hsid, _)| {
(
CTorPath::ClientHsDescEncKey(hsid).into(),
KeyType::X25519StaticKeypair.into(),
)
})
.collect();
Ok(keys)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use std::fs;
use tempfile::{tempdir, TempDir};
use crate::test_utils::{assert_found, DummyKey, TestCTorSpecifier};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");
const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");
const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");
const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");
const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";
fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
let keystore_dir = tempdir().unwrap();
#[cfg(unix)]
fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
let id = KeystoreId::from_str(id).unwrap();
let keystore =
CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
.unwrap();
let keys: &[(&str, &str)] = &[
("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
(
"alice-truncated.auth_private",
&ALICE_AUTH_PRIVATE_VALID[..100],
),
("carol.auth", CAROL_AUTH_PRIVATE_VALID),
("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
];
for (name, key) in keys {
fs::write(keystore_dir.path().join(name), key).unwrap();
}
(keystore, keystore_dir)
}
#[test]
fn get() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair,
false
);
for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
let onion = hsid.split(":").next().unwrap();
let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
let path = CTorPath::ClientHsDescEncKey(hsid.clone());
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair,
true
);
}
let keys: Vec<_> = keystore.list().unwrap();
assert_eq!(keys.len(), 2);
assert!(keys
.iter()
.all(|(_, key_type)| *key_type == KeyType::X25519StaticKeypair.into()));
}
#[test]
fn unsupported_operation() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
let err = keystore
.remove(
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: remove");
let err = keystore
.insert(
&DummyKey,
&TestCTorSpecifier(path),
&KeyType::X25519StaticKeypair.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: insert");
}
#[test]
fn wrong_keytype() {
let (keystore, _keystore_dir) = init_keystore("foo");
let path = CTorPath::ClientHsDescEncKey(HsId::from_str(HSID).unwrap());
let err = keystore
.get(
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey.into(),
)
.map(|_| ())
.unwrap_err();
assert_eq!(
err.to_string(),
"Invalid item type Ed25519PublicKey for client restricted discovery key"
);
}
}