pub(crate) mod certs;
pub(crate) mod err;
pub(crate) mod ssh;
use std::io::{self, ErrorKind};
use std::path::Path;
use std::result::Result as StdResult;
use std::str::FromStr;
use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError, RelKeyPath};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
use crate::{
arti_path, ArtiPath, ArtiPathUnavailableError, KeyPath, KeystoreId, Result, UnknownKeyTypeError,
};
use certs::UnparsedCert;
use err::ArtiNativeKeystoreError;
use ssh::UnparsedOpenSshKey;
use fs_mistrust::{CheckedDir, Mistrust};
use itertools::Itertools;
use tor_error::internal;
use walkdir::WalkDir;
use tor_basic_utils::PathExt as _;
use tor_key_forge::{CertData, KeystoreItem, KeystoreItemType};
#[derive(Debug)]
pub struct ArtiNativeKeystore {
keystore_dir: CheckedDir,
id: KeystoreId,
}
impl ArtiNativeKeystore {
pub fn from_path_and_mistrust(
keystore_dir: impl AsRef<Path>,
mistrust: &Mistrust,
) -> Result<Self> {
let keystore_dir = mistrust
.verifier()
.check_content()
.make_secure_dir(&keystore_dir)
.map_err(|e| FilesystemError::FsMistrust {
action: FilesystemAction::Init,
path: keystore_dir.as_ref().into(),
err: e.into(),
})
.map_err(ArtiNativeKeystoreError::Filesystem)?;
let id = KeystoreId::from_str("arti")?;
Ok(Self { keystore_dir, id })
}
fn rel_path(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> StdResult<RelKeyPath, ArtiPathUnavailableError> {
RelKeyPath::arti(&self.keystore_dir, key_spec, item_type)
}
}
macro_rules! rel_path_if_supported {
($res:expr, $ret:expr) => {{
use ArtiPathUnavailableError::*;
match $res {
Ok(path) => path,
Err(ArtiPathUnavailable) => return $ret,
Err(e) => return Err(tor_error::internal!("invalid ArtiPath: {e}").into()),
}
}};
}
impl Keystore for ArtiNativeKeystore {
fn id(&self) -> &KeystoreId {
&self.id
}
fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(false));
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| ArtiNativeKeystoreError::Filesystem(e).into());
}
};
if meta.is_file() {
Ok(true)
} else {
Err(
ArtiNativeKeystoreError::Filesystem(FilesystemError::NotARegularFile(
path.rel_path_unchecked().into(),
))
.into(),
)
}
}
fn get(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<ErasedKey>> {
let path = rel_path_if_supported!(self.rel_path(key_spec, item_type), Ok(None));
let inner = 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(ArtiNativeKeystoreError::Filesystem)?,
};
let abs_path = path
.checked_path()
.map_err(ArtiNativeKeystoreError::Filesystem)?;
match item_type {
KeystoreItemType::Key(key_type) => {
let inner = String::from_utf8(inner).map_err(|_| {
let err = io::Error::new(
io::ErrorKind::InvalidData,
"OpenSSH key is not valid UTF-8".to_string(),
);
ArtiNativeKeystoreError::Filesystem(FilesystemError::Io {
action: FilesystemAction::Read,
path: abs_path.clone(),
err: err.into(),
})
})?;
UnparsedOpenSshKey::new(inner, abs_path)
.parse_ssh_format_erased(key_type)
.map(Some)
}
KeystoreItemType::Cert(cert_type) => UnparsedCert::new(inner, abs_path)
.parse_certificate_erased(cert_type)
.map(Some),
KeystoreItemType::Unknown { arti_extension } => Err(
ArtiNativeKeystoreError::UnknownKeyType(UnknownKeyTypeError {
arti_extension: arti_extension.clone(),
})
.into(),
),
_ => Err(internal!("unknown item type {item_type:?}").into()),
}
}
fn insert(
&self,
key: &dyn EncodableItem,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<()> {
let path = self
.rel_path(key_spec, item_type)
.map_err(|e| tor_error::internal!("{e}"))?;
let unchecked_path = path.rel_path_unchecked();
if let Some(parent) = unchecked_path.parent() {
self.keystore_dir
.make_directory(parent)
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Write,
path: parent.to_path_buf(),
err: err.into(),
})
.map_err(ArtiNativeKeystoreError::Filesystem)?;
}
let item_bytes: Vec<u8> = match key.as_keystore_item()? {
KeystoreItem::Key(key) => {
let comment = "";
key.to_openssh_string(comment)?.into_bytes()
}
KeystoreItem::Cert(cert) => match cert {
CertData::TorEd25519Cert(cert) => cert.into(),
_ => return Err(internal!("unknown cert type {item_type:?}").into()),
},
_ => return Err(internal!("unknown item type {item_type:?}").into()),
};
Ok(checked_op!(write_and_replace, path, item_bytes)
.map_err(|err| FilesystemError::FsMistrust {
action: FilesystemAction::Write,
path: unchecked_path.into(),
err: err.into(),
})
.map_err(ArtiNativeKeystoreError::Filesystem)?)
}
fn remove(
&self,
key_spec: &dyn KeySpecifier,
item_type: &KeystoreItemType,
) -> Result<Option<()>> {
let rel_path = self
.rel_path(key_spec, item_type)
.map_err(|e| tor_error::internal!("{e}"))?;
match checked_op!(remove_file, rel_path) {
Ok(()) => Ok(Some(())),
Err(fs_mistrust::Error::NotFound(_)) => Ok(None),
Err(e) => Err(ArtiNativeKeystoreError::Filesystem(
FilesystemError::FsMistrust {
action: FilesystemAction::Remove,
path: rel_path.rel_path_unchecked().into(),
err: e.into(),
},
))?,
}
}
fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
WalkDir::new(self.keystore_dir.as_path())
.into_iter()
.map(|entry| {
let entry = entry
.map_err(|e| {
let msg = e.to_string();
FilesystemError::Io {
action: FilesystemAction::Read,
path: self.keystore_dir.as_path().into(),
err: e
.into_io_error()
.unwrap_or_else(|| {
io::Error::new(ErrorKind::Other, msg.to_string())
})
.into(),
}
})
.map_err(ArtiNativeKeystoreError::Filesystem)?;
let path = entry.path();
if entry.file_type().is_dir() {
return Ok(None);
}
let path = path
.strip_prefix(self.keystore_dir.as_path())
.map_err(|_| {
tor_error::internal!(
"found key {} outside of keystore_dir {}?!",
path.display_lossy(),
self.keystore_dir.as_path().display_lossy()
)
})?;
if let Some(parent) = path.parent() {
self.keystore_dir
.read_directory(parent)
.map_err(|e| FilesystemError::FsMistrust {
action: FilesystemAction::Read,
path: parent.into(),
err: e.into(),
})
.map_err(ArtiNativeKeystoreError::Filesystem)?;
}
let malformed_err = |path: &Path, err| ArtiNativeKeystoreError::MalformedPath {
path: path.into(),
err,
};
let extension = path
.extension()
.ok_or_else(|| malformed_err(path, err::MalformedPathError::NoExtension))?
.to_str()
.ok_or_else(|| malformed_err(path, err::MalformedPathError::Utf8))?;
let item_type = KeystoreItemType::from(extension);
let path = path.with_extension("");
let slugs = path
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join(&arti_path::PATH_SEP.to_string());
ArtiPath::new(slugs)
.map(|path| Some((path.into(), item_type)))
.map_err(|e| {
malformed_err(&path, err::MalformedPathError::InvalidArtiPath(e)).into()
})
})
.flatten_ok()
.collect()
}
}
#[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 crate::test_utils::ssh_keys::*;
use crate::test_utils::{assert_found, TestSpecifier};
use crate::{ArtiPath, KeyPath};
use std::cmp::Ordering;
use std::fs;
use std::path::PathBuf;
use tempfile::{tempdir, TempDir};
use tor_key_forge::{CertType, EncodedEd25519Cert, KeyType};
use tor_llcrypto::pk::ed25519;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
impl Ord for KeyPath {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(KeyPath::Arti(path1), KeyPath::Arti(path2)) => path1.cmp(path2),
_ => unimplemented!("not supported"),
}
}
}
impl PartialOrd for KeyPath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
let rel_key_path = key_store
.rel_path(&TestSpecifier::default(), &key_type.clone().into())
.unwrap();
rel_key_path.checked_path().unwrap()
}
fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
let keystore_dir = tempdir().unwrap();
#[cfg(unix)]
fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
let key_store =
ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
.unwrap();
if gen_keys {
let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
let parent = key_path.parent().unwrap();
fs::create_dir_all(parent).unwrap();
#[cfg(unix)]
fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
fs::write(key_path, OPENSSH_ED25519).unwrap();
}
(key_store, keystore_dir)
}
macro_rules! assert_contains_arti_paths {
([$($arti_path:expr,)*], $list:expr) => {{
let expected = vec![
$(KeyPath::Arti(ArtiPath::new($arti_path.to_string()).unwrap())),*
];
let mut sorted_list = $list.iter().map(|(path, _)| path.clone()).collect::<Vec<_>>();
sorted_list.sort();
assert_eq!(expected, sorted_list);
}}
}
#[test]
#[cfg(unix)]
fn init_failure_perms() {
use std::os::unix::fs::PermissionsExt;
let keystore_dir = tempdir().unwrap();
let mode = 0o777;
fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(mode)).unwrap();
let err = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default())
.expect_err(&format!("expected failure (perms = {mode:o})"));
assert_eq!(
err.to_string(),
format!(
"Inaccessible path or bad permissions on {} while attempting to Init",
keystore_dir.path().display_lossy()
),
"expected keystore init failure (perms = {:o})",
mode
);
}
#[test]
fn key_path_repr() {
let (key_store, _) = init_keystore(false);
assert_eq!(
key_store
.rel_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
.unwrap()
.rel_path_unchecked(),
PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
);
assert_eq!(
key_store
.rel_path(
&TestSpecifier::default(),
&KeyType::X25519StaticKeypair.into()
)
.unwrap()
.rel_path_unchecked(),
PathBuf::from("parent1/parent2/parent3/test-specifier.x25519_private")
);
}
#[cfg(unix)]
#[test]
fn get_and_rm_bad_perms() {
use std::os::unix::fs::PermissionsExt;
let (key_store, _keystore_dir) = init_keystore(true);
let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o777)).unwrap();
assert!(key_store
.get(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
.is_err());
fs::set_permissions(
key_path.parent().unwrap(),
fs::Permissions::from_mode(0o777),
)
.unwrap();
assert!(key_store.list().is_err());
let key_spec = TestSpecifier::default();
let ed_key_type = &KeyType::Ed25519Keypair.into();
assert_eq!(
key_store
.remove(&key_spec, ed_key_type)
.unwrap_err()
.to_string(),
format!(
"Inaccessible path or bad permissions on {} while attempting to Remove",
key_store
.rel_path(&key_spec, ed_key_type)
.unwrap()
.rel_path_unchecked()
.display_lossy()
),
);
}
#[test]
fn get() {
let (key_store, _keystore_dir) = init_keystore(false);
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
false
);
assert!(key_store.list().unwrap().is_empty());
let (key_store, _keystore_dir) = init_keystore(true);
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
true
);
assert_contains_arti_paths!([TestSpecifier::path_prefix(),], key_store.list().unwrap());
}
#[test]
fn insert() {
let (key_store, keystore_dir) = init_keystore(false);
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
false
);
assert!(key_store.list().unwrap().is_empty());
let key = UnparsedOpenSshKey::new(OPENSSH_ED25519.into(), PathBuf::from("/test/path"));
let erased_kp = key
.parse_ssh_format_erased(&KeyType::Ed25519Keypair)
.unwrap();
let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
panic!("failed to downcast key to ed25519::Keypair")
};
let key_spec = TestSpecifier::default();
let ed_key_type = &KeyType::Ed25519Keypair.into();
let path = keystore_dir.as_ref().join(
key_store
.rel_path(&key_spec, ed_key_type)
.unwrap()
.rel_path_unchecked(),
);
assert!(!path.parent().unwrap().try_exists().unwrap());
assert!(key_store.insert(&*key, &key_spec, ed_key_type).is_ok());
assert!(path.parent().unwrap().try_exists().unwrap());
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
true
);
assert_contains_arti_paths!([TestSpecifier::path_prefix(),], key_store.list().unwrap());
}
#[test]
fn remove() {
let (key_store, _keystore_dir) = init_keystore(true);
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
true
);
assert_eq!(
key_store
.remove(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
.unwrap(),
Some(())
);
assert!(key_store.list().unwrap().is_empty());
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
false
);
assert!(key_store
.remove(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
.unwrap()
.is_none());
assert!(key_store.list().unwrap().is_empty());
}
#[test]
fn list() {
let (key_store, _keystore_dir) = init_keystore(true);
assert_contains_arti_paths!([TestSpecifier::path_prefix(),], key_store.list().unwrap());
let key = UnparsedOpenSshKey::new(OPENSSH_ED25519.into(), PathBuf::from("/test/path"));
let erased_kp = key
.parse_ssh_format_erased(&KeyType::Ed25519Keypair)
.unwrap();
let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
panic!("failed to downcast key to ed25519::Keypair")
};
let key_spec = TestSpecifier::new("-i-am-a-suffix");
let ed_key_type = KeyType::Ed25519Keypair.into();
assert!(key_store.insert(&*key, &key_spec, &ed_key_type).is_ok());
assert_contains_arti_paths!(
[
TestSpecifier::path_prefix(),
format!("{}-i-am-a-suffix", TestSpecifier::path_prefix()),
],
key_store.list().unwrap()
);
}
#[test]
fn key_path_not_regular_file() {
let (key_store, _keystore_dir) = init_keystore(false);
let key_path = key_path(&key_store, &KeyType::Ed25519Keypair);
fs::create_dir_all(&key_path).unwrap();
assert!(key_path.try_exists().unwrap());
let parent = key_path.parent().unwrap();
#[cfg(unix)]
fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
let err = key_store
.contains(&TestSpecifier::default(), &KeyType::Ed25519Keypair.into())
.unwrap_err();
assert!(err.to_string().contains("not a regular file"), "{err}");
}
#[test]
fn certs() {
let (key_store, _keystore_dir) = init_keystore(false);
const DUMMY_CERT: &[u8] = b"not really a cert...";
let cert = EncodedEd25519Cert::from_bytes(DUMMY_CERT);
let cert_spec = TestSpecifier::default();
assert!(key_store
.insert(&cert, &cert_spec, &CertType::Ed25519TorCert.into())
.is_ok());
let erased_cert = key_store
.get(&cert_spec, &CertType::Ed25519TorCert.into())
.unwrap()
.unwrap();
let Ok(found_cert) = erased_cert.downcast::<EncodedEd25519Cert>() else {
panic!("failed to downcast cert to EncodedEd25519Cert")
};
assert_eq!(cert, *found_cert);
}
}