use auths_core::api::runtime::export_key_openssh_pub;
use auths_core::crypto::signer::encrypt_keypair;
use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
use auths_core::storage::memory::{MEMORY_KEYCHAIN, MemoryKeychainHandle};
use pkcs8::der::Encode;
use pkcs8::der::asn1::ObjectIdentifier;
use pkcs8::{AlgorithmIdentifierRef, PrivateKeyInfo};
use ring::signature::{Ed25519KeyPair, KeyPair};
const OID_ED25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
fn fresh_keychain() -> Box<dyn KeyStorage + Send + Sync> {
MEMORY_KEYCHAIN.lock().unwrap().clear_all().ok();
Box::new(MemoryKeychainHandle)
}
fn create_ring_compatible_pkcs8() -> (Vec<u8>, Vec<u8>) {
let rng = ring::rand::SystemRandom::new();
let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate key");
let pkcs8_bytes = pkcs8_doc.as_ref().to_vec();
let keypair = Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).expect("Failed to parse generated key");
let pubkey = keypair.public_key().as_ref().to_vec();
(pkcs8_bytes, pubkey)
}
fn create_non_ring_pkcs8() -> (Vec<u8>, Vec<u8>) {
let rng = ring::rand::SystemRandom::new();
let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate key");
let keypair =
Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref()).expect("Failed to parse generated key");
let pubkey = keypair.public_key().as_ref().to_vec();
let pk_info =
PrivateKeyInfo::try_from(pkcs8_doc.as_ref()).expect("Failed to parse as PrivateKeyInfo");
let wrapped_seed = pk_info.private_key;
assert_eq!(wrapped_seed.len(), 34, "Expected wrapped seed");
let seed = &wrapped_seed[2..];
let non_ring_pkcs8 = PrivateKeyInfo {
algorithm: AlgorithmIdentifierRef {
oid: OID_ED25519,
parameters: None,
},
private_key: seed, public_key: None,
}
.to_der()
.expect("Failed to encode PKCS#8");
assert!(
Ed25519KeyPair::from_pkcs8(&non_ring_pkcs8).is_err(),
"Ring should NOT be able to parse this format"
);
(non_ring_pkcs8, pubkey)
}
#[test]
fn test_export_ring_compatible_key() {
let keychain = fresh_keychain();
let alias = "test-ring-key";
let passphrase = "Test-P@ss12345";
let identity_did = IdentityDID::parse("did:keri:test123").unwrap();
let (pkcs8_bytes, _expected_pubkey) = create_ring_compatible_pkcs8();
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
keychain
.store_key(
&KeyAlias::new_unchecked(alias),
&identity_did,
KeyRole::Primary,
&encrypted,
)
.expect("Failed to store key");
let result = export_key_openssh_pub(alias, passphrase, keychain.as_ref());
assert!(result.is_ok(), "Export failed: {:?}", result.err());
let openssh_pubkey = result.unwrap();
assert!(
openssh_pubkey.starts_with("ssh-ed25519 "),
"Should be ssh-ed25519 format"
);
assert!(
openssh_pubkey.contains(alias),
"Should contain alias as comment"
);
assert!(openssh_pubkey.len() > 50, "Should have reasonable length");
}
#[test]
fn test_export_non_ring_compatible_key() {
let keychain = fresh_keychain();
let alias = "test-nonring-key";
let passphrase = "Test-P@ss12345";
let identity_did = IdentityDID::parse("did:keri:test456").unwrap();
let (pkcs8_bytes, _) = create_non_ring_pkcs8();
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
keychain
.store_key(
&KeyAlias::new_unchecked(alias),
&identity_did,
KeyRole::Primary,
&encrypted,
)
.expect("Failed to store key");
let result = export_key_openssh_pub(alias, passphrase, keychain.as_ref());
assert!(
result.is_ok(),
"Export should succeed via fallback: {:?}",
result.err()
);
let openssh_pubkey = result.unwrap();
assert!(
openssh_pubkey.starts_with("ssh-ed25519 "),
"Should be ssh-ed25519 format"
);
assert!(
openssh_pubkey.contains(alias),
"Should contain alias as comment"
);
}
#[test]
fn test_export_with_wrong_passphrase() {
let keychain = fresh_keychain();
let alias = "test-wrong-pass";
let passphrase = "Corr3ct-P@sswd!";
let wrong_passphrase = "Wr0ng-P@ssword!";
let identity_did = IdentityDID::parse("did:keri:test789").unwrap();
let (pkcs8_bytes, _) = create_ring_compatible_pkcs8();
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("Failed to encrypt");
keychain
.store_key(
&KeyAlias::new_unchecked(alias),
&identity_did,
KeyRole::Primary,
&encrypted,
)
.expect("Failed to store key");
let result = export_key_openssh_pub(alias, wrong_passphrase, keychain.as_ref());
assert!(result.is_err(), "Should fail with wrong passphrase");
}
#[test]
fn test_export_nonexistent_key() {
let keychain = fresh_keychain();
let result = export_key_openssh_pub("nonexistent-alias", "any-pass", keychain.as_ref());
assert!(result.is_err(), "Should fail for nonexistent key");
}
fn byte_seq_present(haystack: &[u8], needle: &[u8]) -> bool {
!needle.is_empty() && haystack.windows(needle.len()).any(|w| w == needle)
}
#[test]
fn export_output_contains_no_raw_secret() {
let keychain = fresh_keychain();
let alias = "export-secret";
let passphrase = "Test-passphrase1!";
let identity_did = IdentityDID::parse("did:keri:ExportSecret").unwrap();
let (pkcs8_bytes, _pubkey) = create_ring_compatible_pkcs8();
let seed = pkcs8_bytes[16..48].to_vec();
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("encrypt");
keychain
.store_key(
&KeyAlias::new_unchecked(alias),
&identity_did,
KeyRole::Primary,
&encrypted,
)
.expect("store");
let pub_out = export_key_openssh_pub(alias, passphrase, keychain.as_ref()).expect("pub export");
assert!(
!byte_seq_present(pub_out.as_bytes(), &seed),
"public export leaked the seed"
);
assert!(
!byte_seq_present(pub_out.as_bytes(), &pkcs8_bytes),
"public export leaked the private key"
);
let (_did, _role, enc_out) = keychain
.load_key(&KeyAlias::new_unchecked(alias))
.expect("load");
assert!(
!byte_seq_present(&enc_out, &seed),
"encrypted export leaked the seed"
);
}
#[test]
fn no_secret_in_logs() {
use std::sync::{Mutex, OnceLock};
static LOG_BUF: OnceLock<Mutex<Vec<u8>>> = OnceLock::new();
struct CaptureLogger;
impl log::Log for CaptureLogger {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
if let Some(buf) = LOG_BUF.get() {
use std::io::Write;
let _ = writeln!(buf.lock().unwrap(), "{} {}", record.level(), record.args());
}
}
fn flush(&self) {}
}
LOG_BUF.set(Mutex::new(Vec::new())).ok();
let _ = log::set_boxed_logger(Box::new(CaptureLogger));
log::set_max_level(log::LevelFilter::Trace);
let keychain = fresh_keychain();
let alias = "log-secret";
let passphrase = "Test-passphrase1!";
let identity_did = IdentityDID::parse("did:keri:LogSecret").unwrap();
let (pkcs8_bytes, _pubkey) = create_ring_compatible_pkcs8();
let seed = pkcs8_bytes[16..48].to_vec();
let encrypted = encrypt_keypair(&pkcs8_bytes, passphrase).expect("encrypt");
keychain
.store_key(
&KeyAlias::new_unchecked(alias),
&identity_did,
KeyRole::Primary,
&encrypted,
)
.expect("store");
let _ = export_key_openssh_pub(alias, passphrase, keychain.as_ref());
let logs = LOG_BUF.get().unwrap().lock().unwrap().clone();
assert!(!byte_seq_present(&logs, &seed), "seed leaked in logs");
assert!(
!byte_seq_present(&logs, &pkcs8_bytes),
"private key leaked in logs"
);
assert!(
!byte_seq_present(&logs, passphrase.as_bytes()),
"passphrase leaked in logs"
);
}