pub(crate) mod err;
use std::fs;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::str::FromStr;
use crate::key_type::ssh::UnparsedOpenSshKey;
use crate::keystore::{EncodableKey, ErasedKey, KeySpecifier, Keystore};
use crate::{ArtiPath, ArtiPathUnavailableError, KeyPath, KeyType, KeystoreId, Result};
use err::{ArtiNativeKeystoreError, FilesystemAction};
use fs_mistrust::{CheckedDir, Mistrust};
use itertools::Itertools;
use ssh_key::private::PrivateKey;
use ssh_key::{LineEnding, PublicKey};
use walkdir::WalkDir;
use super::SshKeyData;
#[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| ArtiNativeKeystoreError::FsMistrust {
action: FilesystemAction::Init,
path: keystore_dir.as_ref().into(),
err: e.into(),
})?;
let id = KeystoreId::from_str("arti")?;
Ok(Self { keystore_dir, id })
}
fn key_path(
&self,
key_spec: &dyn KeySpecifier,
key_type: &KeyType,
) -> StdResult<PathBuf, ArtiPathUnavailableError> {
let arti_path: String = key_spec.arti_path()?.into();
let mut rel_path = PathBuf::from(arti_path);
rel_path.set_extension(key_type.arti_extension());
Ok(rel_path)
}
}
macro_rules! key_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, key_type: &KeyType) -> Result<bool> {
let path = key_path_if_supported!(self.key_path(key_spec, key_type), Ok(false));
Ok(path.exists())
}
fn get(&self, key_spec: &dyn KeySpecifier, key_type: &KeyType) -> Result<Option<ErasedKey>> {
let path = key_path_if_supported!(self.key_path(key_spec, key_type), Ok(None));
let inner = match self.keystore_dir.read_to_string(&path) {
Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
Err(fs_mistrust::Error::Io { err, .. }) if err.kind() == ErrorKind::NotFound => {
return Ok(None);
}
res => res.map_err(|err| ArtiNativeKeystoreError::FsMistrust {
action: FilesystemAction::Read,
path: path.clone(),
err: err.into(),
})?,
};
key_type
.parse_ssh_format_erased(UnparsedOpenSshKey::new(inner, path))
.map(Some)
}
fn insert(
&self,
key: &dyn EncodableKey,
key_spec: &dyn KeySpecifier,
key_type: &KeyType,
) -> Result<()> {
let path = self
.key_path(key_spec, key_type)
.map_err(|e| tor_error::internal!("{e}"))?;
if let Some(parent) = path.parent() {
self.keystore_dir.make_directory(parent).map_err(|err| {
ArtiNativeKeystoreError::FsMistrust {
action: FilesystemAction::Write,
path: parent.to_path_buf(),
err: err.into(),
}
})?;
}
let key = key.as_ssh_key_data()?;
let comment = "";
let openssh_key = match key {
SshKeyData::Public(key_data) => {
let openssh_key = PublicKey::new(key_data, comment);
openssh_key
.to_openssh()
.map_err(|_| tor_error::internal!("failed to encode SSH key"))?
}
SshKeyData::Private(keypair) => {
let openssh_key = PrivateKey::new(keypair, comment)
.map_err(|_| tor_error::internal!("failed to create SSH private key"))?;
openssh_key
.to_openssh(LineEnding::LF)
.map_err(|_| tor_error::internal!("failed to encode SSH key"))?
.to_string()
}
};
Ok(self
.keystore_dir
.write_and_replace(&path, openssh_key)
.map_err(|err| ArtiNativeKeystoreError::FsMistrust {
action: FilesystemAction::Write,
path,
err: err.into(),
})?)
}
fn remove(&self, key_spec: &dyn KeySpecifier, key_type: &KeyType) -> Result<Option<()>> {
let key_path = self
.key_path(key_spec, key_type)
.map_err(|e| tor_error::internal!("{e}"))?;
let abs_key_path =
self.keystore_dir
.join(&key_path)
.map_err(|e| ArtiNativeKeystoreError::FsMistrust {
action: FilesystemAction::Remove,
path: key_path.clone(),
err: e.into(),
})?;
match fs::remove_file(abs_key_path) {
Ok(()) => Ok(Some(())),
Err(e) if matches!(e.kind(), ErrorKind::NotFound) => Ok(None),
Err(e) => Err(ArtiNativeKeystoreError::Filesystem {
action: FilesystemAction::Remove,
path: key_path,
err: e.into(),
}
.into()),
}
}
fn list(&self) -> Result<Vec<(KeyPath, KeyType)>> {
WalkDir::new(self.keystore_dir.as_path())
.into_iter()
.map(|entry| {
let entry = entry.map_err(|e| {
let msg = e.to_string();
ArtiNativeKeystoreError::Filesystem {
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(),
}
})?;
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(),
self.keystore_dir.as_path().display()
)
})?;
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 key_type = KeyType::from(extension);
let path = path.with_extension("");
ArtiPath::new(path.display().to_string())
.map(|path| Some((path.into(), key_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::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::{ArtiPath, CTorPath, KeyPath};
use std::fs;
use tempfile::{tempdir, TempDir};
use tor_llcrypto::pk::ed25519;
const OPENSSH_ED25519: &str = include_str!("../../testdata/ed25519_openssh.private");
const TEST_SPECIFIER_PATH: &str = "parent1/parent2/parent3/test-specifier";
#[derive(Default)]
struct TestSpecifier(String);
impl KeySpecifier for TestSpecifier {
fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
Ok(ArtiPath::new(format!("{TEST_SPECIFIER_PATH}{}", self.0))
.map_err(|e| tor_error::internal!("{e}"))?)
}
fn ctor_path(&self) -> Option<CTorPath> {
None
}
}
fn key_path(key_store: &ArtiNativeKeystore, key_type: &KeyType) -> PathBuf {
let rel_key_path = key_store
.key_path(&TestSpecifier::default(), key_type)
.unwrap();
key_store.keystore_dir.as_path().join(rel_key_path)
}
fn init_keystore(gen_keys: bool) -> (ArtiNativeKeystore, TempDir) {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
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();
fs::set_permissions(parent, fs::Permissions::from_mode(0o700)).unwrap();
fs::write(key_path, OPENSSH_ED25519).unwrap();
}
(key_store, keystore_dir)
}
macro_rules! assert_found {
($key_store:expr, $key_spec:expr, $key_type:expr, $found:expr) => {{
let res = $key_store.get($key_spec, $key_type).unwrap();
if $found {
assert!(res.is_some());
} else {
assert!(res.is_none());
}
}};
}
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!(
"Invalid path or permissions on {} while attempting to Init",
keystore_dir.path().display()
),
"expected keystore init failure (perms = {:o})",
mode
);
}
#[test]
fn key_path_repr() {
let (key_store, _) = init_keystore(false);
assert_eq!(
key_store
.key_path(&TestSpecifier::default(), &KeyType::Ed25519Keypair)
.unwrap(),
PathBuf::from("parent1/parent2/parent3/test-specifier.ed25519_private")
);
assert_eq!(
key_store
.key_path(&TestSpecifier::default(), &KeyType::X25519StaticKeypair)
.unwrap(),
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)
.is_err());
assert_eq!(
key_store
.remove(&TestSpecifier::default(), &KeyType::Ed25519Keypair)
.unwrap(),
Some(())
);
}
#[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!([TEST_SPECIFIER_PATH,], 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 = KeyType::Ed25519Keypair
.parse_ssh_format_erased(key)
.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;
let path = keystore_dir
.as_ref()
.join(key_store.key_path(&key_spec, ed_key_type).unwrap());
assert!(!path.parent().unwrap().exists());
assert!(key_store.insert(&*key, &key_spec, ed_key_type).is_ok());
assert!(path.parent().unwrap().exists());
assert_found!(
key_store,
&TestSpecifier::default(),
&KeyType::Ed25519Keypair,
true
);
assert_contains_arti_paths!([TEST_SPECIFIER_PATH,], 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)
.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)
.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!([TEST_SPECIFIER_PATH,], key_store.list().unwrap());
let key = UnparsedOpenSshKey::new(OPENSSH_ED25519.into(), PathBuf::from("/test/path"));
let erased_kp = KeyType::Ed25519Keypair
.parse_ssh_format_erased(key)
.unwrap();
let Ok(key) = erased_kp.downcast::<ed25519::Keypair>() else {
panic!("failed to downcast key to ed25519::Keypair")
};
let key_spec = TestSpecifier("-i-am-a-suffix".into());
let ed_key_type = KeyType::Ed25519Keypair;
assert!(key_store.insert(&*key, &key_spec, &ed_key_type).is_ok());
assert_contains_arti_paths!(
[
TEST_SPECIFIER_PATH,
format!("{TEST_SPECIFIER_PATH}-i-am-a-suffix"),
],
key_store.list().unwrap()
);
}
}