use std::collections::HashMap;
use aes_gcm::{
Aes256Gcm, Nonce,
aead::{Aead, KeyInit, consts::U12},
};
use ed25519_dalek::SigningKey;
use rand::Rng;
use sha2::{Digest, Sha256};
use zeroize::Zeroize;
use std::path::PathBuf;
use super::crypto::{compute_envelope_hmac, derive_aes_key_sha256};
use super::store::{KeyRecord, KeyRecordMap, KeyStore, default_algorithm};
fn temp_path(label: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"test-keystore-{label}-{}.json",
uuid::Uuid::now_v7()
))
}
fn temp_store() -> KeyStore {
KeyStore::open(temp_path("default"), "test-passphrase").expect("open key store")
}
fn temp_store_at(label: &str, passphrase: &str) -> (KeyStore, PathBuf) {
let path = temp_path(label);
let store = KeyStore::open(&path, passphrase).expect("open key store");
(store, path)
}
#[test]
fn generate_then_load_roundtrip() {
let store = temp_store();
let generated = store.generate_key("issuer-1").expect("generate key");
let loaded = store.load_key("issuer-1").expect("load key");
assert_eq!(generated.fingerprint, loaded.fingerprint);
assert_eq!(
generated.verifying_key.as_bytes(),
loaded.verifying_key.as_bytes()
);
}
#[test]
fn stored_bytes_differ_from_plaintext() {
let (store, path) = temp_store_at("enc", "test-passphrase");
let key = store.generate_key("issuer-enc").expect("generate");
let raw_file = std::fs::read_to_string(&path).expect("read file");
let plaintext_hex = hex::encode(key.signing_key.as_bytes());
assert!(
!raw_file.contains(&plaintext_hex),
"plaintext key bytes must not appear in the store file"
);
}
#[test]
fn store_file_contains_argon2id_kdf_marker() {
let (store, path) = temp_store_at("kdf", "test-passphrase");
store.generate_key("issuer-kdf").expect("generate");
let raw_file = std::fs::read_to_string(&path).expect("read file");
assert!(
raw_file.contains("argon2id"),
"store file must contain argon2id KDF marker"
);
assert!(
raw_file.contains("salt"),
"store file must contain salt field"
);
}
#[test]
fn reopen_store_from_disk_with_argon2id() {
let (store, path) = temp_store_at("reopen", "my-passphrase");
let generated = store.generate_key("issuer-reopen").expect("generate");
drop(store);
let store2 = KeyStore::open(&path, "my-passphrase").expect("reopen");
let loaded = store2.load_key("issuer-reopen").expect("load");
assert_eq!(generated.fingerprint, loaded.fingerprint);
}
#[test]
fn legacy_sha256_store_can_be_opened_and_migrated() {
let path = temp_path("legacy");
let passphrase = "legacy-pass";
let legacy_key = derive_aes_key_sha256(passphrase);
let cipher = Aes256Gcm::new(&legacy_key);
let signing_key = SigningKey::generate(&mut crate::os_rng());
let verifying_key = signing_key.verifying_key();
let fingerprint = hex::encode(Sha256::digest(verifying_key.as_bytes()));
let mut nonce_bytes = [0u8; 12];
crate::os_rng().fill_bytes(&mut nonce_bytes);
let nonce = <&Nonce<U12>>::from(&nonce_bytes);
let mut raw = signing_key.to_bytes();
let encrypted = cipher.encrypt(nonce, raw.as_ref()).expect("encrypt");
raw.zeroize();
let record = KeyRecord {
encrypted_signing_key: encrypted,
nonce: nonce_bytes.to_vec(),
fingerprint: fingerprint.clone(),
verifying_key_hex: hex::encode(verifying_key.as_bytes()),
revoked: false,
algorithm: default_algorithm(),
};
let mut map = HashMap::new();
map.insert("legacy-key".to_string(), record);
let bytes = serde_json::to_vec(&map).expect("serialize");
std::fs::write(&path, bytes).expect("write");
let store = KeyStore::open(&path, passphrase).expect("open legacy store");
let loaded = store.load_key("legacy-key").expect("load legacy key");
assert_eq!(loaded.fingerprint, fingerprint);
store
.migrate_if_needed(passphrase)
.expect("migration failed");
let raw_file = std::fs::read_to_string(&path).expect("read");
assert!(raw_file.contains("argon2id"), "file must be migrated");
}
#[test]
fn malformed_nonce_returns_error_not_panic() {
let path = temp_path("badnonce");
let record = KeyRecord {
encrypted_signing_key: vec![0u8; 48],
nonce: vec![0u8; 11], fingerprint: "deadbeef".to_string(),
verifying_key_hex: format!("{:064x}", 0),
revoked: false,
algorithm: default_algorithm(),
};
let mut map: KeyRecordMap = HashMap::new();
map.insert("bad-key".to_string(), record);
std::fs::write(&path, serde_json::to_vec(&map).expect("serialize")).expect("write");
let store = KeyStore::open(&path, "any-pass").expect("open legacy store");
assert!(
store.load_key("bad-key").is_err(),
"malformed nonce must return Err, not panic"
);
}
#[test]
fn archive_key_creates_archived_entry() {
let store = temp_store();
store.generate_key("issuer-arc").expect("generate");
store.archive_key("issuer-arc").expect("archive");
let archived = store.load_archived_keys("issuer-arc");
assert_eq!(archived.len(), 1, "expected one archived key");
}
#[test]
fn load_archived_keys_empty_before_rotation() {
let store = temp_store();
store.generate_key("issuer-noarc").expect("generate");
let archived = store.load_archived_keys("issuer-noarc");
assert!(
archived.is_empty(),
"no archived keys before first rotation"
);
}
#[test]
fn rotate_key_archives_old_and_installs_new() {
let store = temp_store();
let k1 = store.generate_key("iss").expect("gen");
let k2 = store.rotate_key("iss").expect("rotate");
assert_ne!(
k1.fingerprint, k2.fingerprint,
"rotation installs a new key"
);
assert_eq!(
store.load_key("iss").unwrap().fingerprint,
k2.fingerprint,
"current key is the new one"
);
let archived = store.load_archived_keys("iss");
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].fingerprint, k1.fingerprint);
assert!(
!archived[0].revoked,
"hygiene rotation keeps the old key valid"
);
}
#[test]
fn revoke_and_rotate_marks_old_key_revoked() {
let store = temp_store();
let k1 = store.generate_key("iss").expect("gen");
let k2 = store.revoke_and_rotate("iss").expect("revoke+rotate");
assert_ne!(k1.fingerprint, k2.fingerprint);
assert!(!k2.revoked, "the new current key is not revoked");
let archived = store.load_archived_keys("iss");
assert_eq!(archived.len(), 1);
assert_eq!(archived[0].fingerprint, k1.fingerprint);
assert!(
archived[0].revoked,
"compromise rotation marks the old key revoked"
);
}
#[test]
fn revoked_state_persists_across_reopen() {
let (store, path) = temp_store_at("revoke", "rev");
store.generate_key("iss").expect("gen");
let revoked_fp = store.load_key("iss").unwrap().fingerprint.clone();
store.revoke_and_rotate("iss").expect("revoke+rotate");
drop(store);
let store2 = KeyStore::open(&path, "rev").expect("reopen");
let archived = store2.load_archived_keys("iss");
assert!(
archived
.iter()
.any(|k| k.fingerprint == revoked_fp && k.revoked),
"revoked flag must survive a reopen (and the HMAC must still verify)"
);
}
#[test]
fn store_file_contains_hmac() {
let (store, path) = temp_store_at("hmac", "hmac-test");
store.generate_key("issuer-hmac").expect("generate");
let raw = std::fs::read_to_string(&path).expect("read file");
assert!(
raw.contains("\"hmac\""),
"store file must contain HMAC field"
);
let envelope: serde_json::Value = serde_json::from_str(&raw).unwrap();
let hmac_val = envelope["hmac"].as_str().unwrap();
assert_eq!(hmac_val.len(), 64, "HMAC must be 64 hex chars");
}
#[test]
fn tampered_store_file_rejected_on_open() {
let (store, path) = temp_store_at("tamper", "tamper-test");
store.generate_key("issuer-tamper").expect("generate");
drop(store);
let mut raw = std::fs::read_to_string(&path).expect("read");
if let Some(pos) = raw.find("\"fingerprint\":\"") {
let fp_start = pos + "\"fingerprint\":\"".len();
let old_char = raw.as_bytes()[fp_start];
let new_char = if old_char == b'a' { b'b' } else { b'a' };
unsafe {
raw.as_bytes_mut()[fp_start] = new_char;
}
}
std::fs::write(&path, &raw).expect("write tampered file");
let result = KeyStore::open(&path, "tamper-test");
assert!(
result.is_err(),
"tampered store should fail integrity check"
);
let Err(e) = result else {
panic!("tampered store should fail integrity check");
};
let err = e.to_string();
assert!(
err.contains("integrity") || err.contains("tamper"),
"error should mention integrity, got: {err}"
);
}
#[test]
fn reopen_with_hmac_succeeds() {
let (store, path) = temp_store_at("hmac-ok", "hmac-ok");
store.generate_key("issuer-ok").expect("generate");
drop(store);
let store2 = KeyStore::open(&path, "hmac-ok").expect("reopen");
let loaded = store2.load_key("issuer-ok").expect("load");
assert!(!loaded.fingerprint.is_empty());
}
#[test]
fn hmac_is_stable_across_map_insertion_order() {
let integrity_key = [42u8; 32];
let make_record = |n: u8| KeyRecord {
encrypted_signing_key: vec![n; 48],
nonce: vec![n; 12],
fingerprint: format!("fp{n:02x}"),
verifying_key_hex: format!("{:064x}", n),
revoked: false,
algorithm: default_algorithm(),
};
let keys_fwd = ["zebra", "alpha", "mango", "delta", "beta"];
let mut map_a: KeyRecordMap = HashMap::new();
for (i, k) in keys_fwd.iter().enumerate() {
map_a.insert(k.to_string(), make_record(i as u8));
}
let mut map_b: KeyRecordMap = HashMap::new();
for (i, k) in keys_fwd.iter().enumerate() {
map_b.insert(k.to_string(), make_record(i as u8));
}
let entries: Vec<_> = map_b.drain().collect();
let mut map_b: KeyRecordMap = HashMap::new();
for (k, v) in entries.into_iter().rev() {
map_b.insert(k, v);
}
let test_salt = "dGVzdHNhbHQ="; let hmac_a = compute_envelope_hmac(&integrity_key, "argon2id", test_salt, &map_a).unwrap();
let hmac_b = compute_envelope_hmac(&integrity_key, "argon2id", test_salt, &map_b).unwrap();
assert_eq!(
hmac_a, hmac_b,
"HMAC must be identical regardless of HashMap insertion/iteration order"
);
}