use tor_error::internal;
use tor_key_forge::{ErasedKey, KeyType, SshKeyAlgorithm, SshKeyData};
use crate::Result;
use crate::keystore::arti::err::ArtiNativeKeystoreError;
use std::path::PathBuf;
use zeroize::Zeroizing;
pub(super) struct UnparsedOpenSshKey {
inner: Zeroizing<String>,
path: PathBuf,
}
macro_rules! parse_openssh {
(PRIVATE $key:expr, $key_type:expr) => {{
SshKeyData::try_from_keypair_data(parse_openssh!(
$key,
$key_type,
ssh_key::private::PrivateKey::from_openssh
).key_data().clone())?
}};
(PUBLIC $key:expr, $key_type:expr) => {{
SshKeyData::try_from_key_data(parse_openssh!(
$key,
$key_type,
ssh_key::public::PublicKey::from_openssh
).key_data().clone())?
}};
($key:expr, $key_type:expr, $parse_fn:path) => {{
let key = $parse_fn(&*$key.inner).map_err(|e| {
ArtiNativeKeystoreError::SshKeyParse {
path: $key.path.clone(),
key_type: $key_type.clone().clone(),
err: e.into(),
}
})?;
let wanted_key_algo = ssh_algorithm($key_type)?;
if SshKeyAlgorithm::from(key.algorithm()) != wanted_key_algo {
return Err(ArtiNativeKeystoreError::UnexpectedSshKeyType {
path: $key.path,
wanted_key_algo,
found_key_algo: key.algorithm().into(),
}.into());
}
key
}};
}
fn ssh_algorithm(key_type: &KeyType) -> Result<SshKeyAlgorithm> {
match key_type {
KeyType::Ed25519Keypair | KeyType::Ed25519PublicKey => Ok(SshKeyAlgorithm::Ed25519),
KeyType::X25519StaticKeypair | KeyType::X25519PublicKey => Ok(SshKeyAlgorithm::X25519),
KeyType::Ed25519ExpandedKeypair => Ok(SshKeyAlgorithm::Ed25519Expanded),
KeyType::RsaKeypair | KeyType::RsaPublicKey => Ok(SshKeyAlgorithm::Rsa),
&_ => {
Err(ArtiNativeKeystoreError::Bug(internal!("Unknown SSH key type {key_type:?}")).into())
}
}
}
impl UnparsedOpenSshKey {
pub(crate) fn new(inner: String, path: PathBuf) -> Self {
Self {
inner: Zeroizing::new(inner),
path,
}
}
pub(crate) fn parse_ssh_format_erased(self, key_type: &KeyType) -> Result<ErasedKey> {
match key_type {
KeyType::Ed25519Keypair
| KeyType::X25519StaticKeypair
| KeyType::Ed25519ExpandedKeypair
| KeyType::RsaKeypair => Ok(parse_openssh!(PRIVATE self, key_type).into_erased()?),
KeyType::Ed25519PublicKey | KeyType::X25519PublicKey | KeyType::RsaPublicKey => {
Ok(parse_openssh!(PUBLIC self, key_type).into_erased()?)
}
&_ => Err(ArtiNativeKeystoreError::Bug(internal!("Unknown SSH key type")).into()),
}
}
}
#[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_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use crate::test_utils::ssh_keys::*;
use crate::test_utils::sshkeygen_ed25519_strings;
use tor_key_forge::{EncodableItem, KeystoreItem};
use tor_llcrypto::pk::{curve25519, ed25519};
use super::*;
const ED25519_OPENSSH_COMMENT: &str = "armadillo@example.com";
const ED25519_EXPANDED_OPENSSH_COMMENT: &str = "armadillo@example.com";
const X25519_OPENSSH_COMMENT: &str = "test-key";
const ED25519_SSHKEYGEN_COMMENT: &str = "";
trait ToBytes {
type Bytes;
fn to_bytes(&self) -> Self::Bytes;
}
impl ToBytes for ed25519::Keypair {
type Bytes = [u8; 32];
fn to_bytes(&self) -> Self::Bytes {
self.to_bytes()
}
}
impl ToBytes for ed25519::PublicKey {
type Bytes = [u8; 32];
fn to_bytes(&self) -> Self::Bytes {
self.to_bytes()
}
}
impl ToBytes for ed25519::ExpandedKeypair {
type Bytes = [u8; 64];
fn to_bytes(&self) -> Self::Bytes {
self.to_secret_key_bytes()
}
}
impl ToBytes for curve25519::StaticKeypair {
type Bytes = [u8; 32];
fn to_bytes(&self) -> Self::Bytes {
self.secret.to_bytes()
}
}
impl ToBytes for curve25519::PublicKey {
type Bytes = [u8; 32];
fn to_bytes(&self) -> Self::Bytes {
self.to_bytes()
}
}
fn mangle_ed25519(key: &mut String) {
if key.len() > 150 {
key.replace_range(107..178, "hello");
} else {
key.insert_str(12, "garbage");
}
}
macro_rules! test_parse_ssh_format_erased {
($key_ty:tt, $key:expr, err = $expect_err:expr) => {{
let key_type = KeyType::$key_ty;
let key = UnparsedOpenSshKey::new($key.into(), PathBuf::from("/dummy/path"));
let err = key
.parse_ssh_format_erased(&key_type)
.map(|_| "<type erased key>")
.unwrap_err();
assert_eq!(err.to_string(), $expect_err);
}};
($key_ty:tt, $enc1:expr, $expected_ty:path, $comment:expr) => {{
let enc1 = $enc1.trim();
let key_type = KeyType::$key_ty;
let key = UnparsedOpenSshKey::new(enc1.into(), PathBuf::from("/test/path"));
let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
let Ok(dec1) = erased_key.downcast::<$expected_ty>() else {
panic!("failed to downcast");
};
let keystore_item = EncodableItem::as_keystore_item(&*dec1).unwrap();
let enc2 = match keystore_item {
KeystoreItem::Key(key) => key.to_openssh_string($comment).unwrap(),
_ => panic!("unexpected keystore item type {keystore_item:?}"),
};
let enc2 = enc2.trim();
match key_type {
KeyType::Ed25519Keypair |
KeyType::X25519StaticKeypair |
KeyType::Ed25519ExpandedKeypair => (),
_ => assert_eq!(enc1, enc2),
}
let key = UnparsedOpenSshKey::new(enc2.into(), PathBuf::from("/test/path"));
let erased_key = key.parse_ssh_format_erased(&key_type).unwrap();
let Ok(dec2) = erased_key.downcast::<$expected_ty>() else {
panic!("failed to downcast");
};
assert_eq!(dec1.to_bytes(), dec2.to_bytes());
}};
}
#[test]
fn wrong_key_type() {
let key_type = KeyType::Ed25519Keypair;
let key = UnparsedOpenSshKey::new(DSA_OPENSSH.into(), PathBuf::from("/test/path"));
let err = key
.parse_ssh_format_erased(&key_type)
.map(|_| "<type erased key>")
.unwrap_err();
assert_eq!(
err.to_string(),
format!(
"Unexpected OpenSSH key type: wanted {}, found {}",
SshKeyAlgorithm::Ed25519,
SshKeyAlgorithm::Dsa
)
);
test_parse_ssh_format_erased!(
Ed25519Keypair,
DSA_OPENSSH,
err = format!(
"Unexpected OpenSSH key type: wanted {}, found {}",
SshKeyAlgorithm::Ed25519,
SshKeyAlgorithm::Dsa
)
);
}
#[test]
fn invalid_ed25519_key() {
test_parse_ssh_format_erased!(
Ed25519Keypair,
ED25519_OPENSSH_BAD,
err = "Failed to parse OpenSSH with type Ed25519Keypair"
);
test_parse_ssh_format_erased!(
Ed25519Keypair,
ED25519_OPENSSH_BAD_PUB,
err = "Failed to parse OpenSSH with type Ed25519Keypair"
);
test_parse_ssh_format_erased!(
Ed25519PublicKey,
ED25519_OPENSSH_BAD_PUB,
err = "Failed to parse OpenSSH with type Ed25519PublicKey"
);
if let Ok((mut bad, mut bad_pub)) = sshkeygen_ed25519_strings() {
mangle_ed25519(&mut bad);
mangle_ed25519(&mut bad_pub);
test_parse_ssh_format_erased!(
Ed25519Keypair,
&bad,
err = "Failed to parse OpenSSH with type Ed25519Keypair"
);
test_parse_ssh_format_erased!(
Ed25519Keypair,
&bad_pub,
err = "Failed to parse OpenSSH with type Ed25519Keypair"
);
test_parse_ssh_format_erased!(
Ed25519PublicKey,
&bad_pub,
err = "Failed to parse OpenSSH with type Ed25519PublicKey"
);
}
}
#[test]
fn ed25519_key() {
test_parse_ssh_format_erased!(
Ed25519Keypair,
ED25519_OPENSSH,
ed25519::Keypair,
ED25519_OPENSSH_COMMENT
);
test_parse_ssh_format_erased!(
Ed25519PublicKey,
ED25519_OPENSSH_PUB,
ed25519::PublicKey,
ED25519_OPENSSH_COMMENT
);
if let Ok((enc1, enc1_pub)) = sshkeygen_ed25519_strings() {
test_parse_ssh_format_erased!(
Ed25519Keypair,
enc1,
ed25519::Keypair,
ED25519_SSHKEYGEN_COMMENT
);
test_parse_ssh_format_erased!(
Ed25519PublicKey,
enc1_pub,
ed25519::PublicKey,
ED25519_SSHKEYGEN_COMMENT
);
}
}
#[test]
fn invalid_expanded_ed25519_key() {
test_parse_ssh_format_erased!(
Ed25519ExpandedKeypair,
ED25519_EXPANDED_OPENSSH_BAD,
err = "Failed to parse OpenSSH with type Ed25519ExpandedKeypair"
);
}
#[test]
fn expanded_ed25519_key() {
test_parse_ssh_format_erased!(
Ed25519ExpandedKeypair,
ED25519_EXPANDED_OPENSSH,
ed25519::ExpandedKeypair,
ED25519_EXPANDED_OPENSSH_COMMENT
);
test_parse_ssh_format_erased!(
Ed25519PublicKey,
ED25519_EXPANDED_OPENSSH_PUB, err = "Failed to parse OpenSSH with type Ed25519PublicKey"
);
}
#[test]
fn x25519_key() {
test_parse_ssh_format_erased!(
X25519StaticKeypair,
X25519_OPENSSH,
curve25519::StaticKeypair,
X25519_OPENSSH_COMMENT
);
test_parse_ssh_format_erased!(
X25519PublicKey,
X25519_OPENSSH_PUB,
curve25519::PublicKey,
X25519_OPENSSH_COMMENT
);
}
#[test]
fn invalid_x25519_key() {
test_parse_ssh_format_erased!(
X25519StaticKeypair,
X25519_OPENSSH_UNKNOWN_ALGORITHM,
err = "Unexpected OpenSSH key type: wanted X25519, found pangolin@torproject.org"
);
test_parse_ssh_format_erased!(
X25519PublicKey,
X25519_OPENSSH_UNKNOWN_ALGORITHM, err = "Failed to parse OpenSSH with type X25519PublicKey"
);
test_parse_ssh_format_erased!(
X25519PublicKey,
X25519_OPENSSH_UNKNOWN_ALGORITHM_PUB,
err = "Unexpected OpenSSH key type: wanted X25519, found armadillo@torproject.org"
);
}
}