tor_keymgr/keystore/ctor/
service.rsuse crate::keystore::ctor::err::{CTorKeystoreError, MalformedServiceKeyError};
use crate::keystore::ctor::CTorKeystore;
use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore, KeystoreId};
use crate::{CTorPath, CTorServicePath, KeyPath, Result};
use fs_mistrust::Mistrust;
use tor_basic_utils::PathExt as _;
use tor_error::internal;
use tor_key_forge::{KeyType, KeystoreItemType};
use tor_llcrypto::pk::ed25519;
use tor_persist::hsnickname::HsNickname;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::sync::Arc;
pub struct CTorServiceKeystore {
keystore: CTorKeystore,
nickname: HsNickname,
}
impl CTorServiceKeystore {
pub fn from_path_and_mistrust(
keystore_dir: impl AsRef<Path>,
mistrust: &Mistrust,
id: KeystoreId,
nickname: HsNickname,
) -> Result<Self> {
let keystore = CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id)?;
Ok(Self { keystore, nickname })
}
}
macro_rules! rel_path_if_supported {
($self:expr, $spec:expr, $ret:expr, $item_type:expr) => {{
use KeystoreItemType::*;
let Some(ctor_path) = $spec.ctor_path() else {
return $ret;
};
let CTorPath::Service { path, nickname } = ctor_path else {
return $ret;
};
if nickname != $self.nickname {
return $ret;
};
let relpath = $self.keystore.rel_path(PathBuf::from(path.to_string()));
match ($item_type, &path) {
(Key(KeyType::Ed25519ExpandedKeypair), CTorServicePath::PrivateKey)
| (Key(KeyType::Ed25519PublicKey), CTorServicePath::PublicKey) => Ok(()),
_ => Err(CTorKeystoreError::InvalidKeystoreItemType {
item_type: $item_type.clone(),
item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
}),
}?;
relpath
}};
}
impl Keystore for CTorServiceKeystore {
fn id(&self) -> &KeystoreId {
&self.keystore.id
}
fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
let path = rel_path_if_supported!(self, key_spec, Ok(false), item_type);
let meta = match checked_op!(metadata, path) {
Ok(meta) => meta,
Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
Err(e) => {
return Err(FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: path.rel_path_unchecked().into(),
err: e.into(),
})
.map_err(|e| CTorKeystoreError::Filesystem(e).into());
}
};
if meta.is_file() {
Ok(true)
} else {
Err(
CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
path.rel_path_unchecked().into(),
))
.into(),
)
}
}
fn get(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<ErasedKey>> {
use KeystoreItemType::*;
let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
let key = match checked_op!(read, path) {
Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
res => res
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: path.rel_path_unchecked().into(),
err: err.into(),
})
.map_err(CTorKeystoreError::Filesystem)?,
};
let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
path: path.rel_path_unchecked().into(),
err: err.into(),
};
let parsed_key: ErasedKey = match item_type {
Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
.map_err(parse_err)
.map(Box::new)?,
Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
.map_err(parse_err)
.map(Box::new)?,
_ => {
return Err(
internal!("item type was not validated by rel_path_if_supported?!").into(),
);
}
};
Ok(Some(parsed_key))
}
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)>> {
use crate::CTorServicePath::*;
use itertools::Itertools;
let all_keys = [
(
CTorPath::Service {
nickname: self.nickname.clone(),
path: PublicKey,
},
KeyType::Ed25519PublicKey.into(),
),
(
CTorPath::Service {
nickname: self.nickname.clone(),
path: PrivateKey,
},
KeyType::Ed25519ExpandedKeypair.into(),
),
];
all_keys
.into_iter()
.map(|(path, key_type)| {
self.contains(&path, &key_type)
.map(|res: bool| (path, key_type, res))
})
.filter_map_ok(|(path, key_type, res)| res.then_some((path.into(), key_type)))
.collect()
}
}
macro_rules! parse_ed25519 {
($key:expr, $parse_fn:expr, $tag:expr, $key_len:expr) => {{
let expected_len = $tag.len() + $key_len;
if $key.len() != expected_len {
return Err(MalformedServiceKeyError::InvalidKeyLen {
len: $key.len(),
expected_len,
});
}
let (tag, key) = $key.split_at($tag.len());
if tag != $tag {
return Err(MalformedServiceKeyError::InvalidTag {
tag: tag.to_vec(),
expected_tag: $tag.into(),
});
}
($parse_fn)(key)
}};
}
fn parse_ed25519_public(key: &[u8]) -> StdResult<ed25519::PublicKey, MalformedServiceKeyError> {
const PUBKEY_TAG: &[u8] = b"== ed25519v1-public: type0 ==\0\0\0";
const PUBKEY_LEN: usize = 32;
parse_ed25519!(
key,
|key| ed25519::PublicKey::try_from(key)
.map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
PUBKEY_TAG,
PUBKEY_LEN
)
}
fn parse_ed25519_keypair(
key: &[u8],
) -> StdResult<ed25519::ExpandedKeypair, MalformedServiceKeyError> {
const KEYPAIR_TAG: &[u8] = b"== ed25519v1-secret: type0 ==\0\0\0";
const KEYPAIR_LEN: usize = 64;
parse_ed25519!(
key,
|key: &[u8]| {
let key: [u8; 64] = key
.try_into()
.map_err(|_| internal!("bad length on expanded ed25519 secret key "))?;
ed25519::ExpandedKeypair::from_secret_key_bytes(key)
.ok_or(MalformedServiceKeyError::Ed25519Keypair)
},
KEYPAIR_TAG,
KEYPAIR_LEN
)
}
#[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 std::str::FromStr as _;
use tempfile::{tempdir, TempDir};
use crate::test_utils::{assert_found, DummyKey, TestCTorSpecifier};
use crate::CTorServicePath;
const PUBKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_public_key");
const PRIVKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_secret_key");
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
fn init_keystore(id: &str, nickname: &str) -> (CTorServiceKeystore, 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 nickname = HsNickname::from_str(nickname).unwrap();
let keystore = CTorServiceKeystore::from_path_and_mistrust(
&keystore_dir,
&Mistrust::default(),
id,
nickname,
)
.unwrap();
const KEYS: &[(&str, &[u8])] = &[
("hs_ed25519_public_key", PUBKEY),
("hs_ed25519_secret_key", PRIVKEY),
];
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", "allium-cepa");
let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
let path = CTorPath::Service {
nickname: unk_nickname.clone(),
path: CTorServicePath::PublicKey,
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey,
false
);
let path = CTorPath::Service {
nickname: keystore.nickname.clone(),
path: CTorServicePath::PublicKey,
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey,
true
);
let path = CTorPath::Service {
nickname: keystore.nickname.clone(),
path: CTorServicePath::PrivateKey,
};
assert_found!(
keystore,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519ExpandedKeypair,
true
);
}
#[test]
fn unsupported_operation() {
let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
let path = CTorPath::Service {
nickname: keystore.nickname.clone(),
path: CTorServicePath::PublicKey,
};
let err = keystore
.remove(
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: remove");
let err = keystore
.insert(
&DummyKey,
&TestCTorSpecifier(path.clone()),
&KeyType::Ed25519PublicKey.into(),
)
.unwrap_err();
assert_eq!(err.to_string(), "Operation not supported: insert");
}
#[test]
fn wrong_keytype() {
let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
let path = CTorPath::Service {
nickname: keystore.nickname.clone(),
path: CTorServicePath::PublicKey,
};
let err = keystore
.get(
&TestCTorSpecifier(path.clone()),
&KeyType::X25519StaticKeypair.into(),
)
.map(|_| ())
.unwrap_err();
assert_eq!(
err.to_string(),
"Invalid item type X25519StaticKeypair for key hs_ed25519_public_key"
);
}
#[test]
fn list() {
let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
let keys: Vec<_> = keystore.list().unwrap();
assert_eq!(keys.len(), 2);
assert!(keys
.iter()
.any(|(_, key_type)| *key_type == KeyType::Ed25519ExpandedKeypair.into()));
assert!(keys
.iter()
.any(|(_, key_type)| *key_type == KeyType::Ed25519PublicKey.into()));
}
}