test(vault): add zeroization tests for cache eviction and clear

Adds tests verifying that HashMap::clear() and remove() drop CachedKey
values (triggering ZeroizeOnDrop), plus explicit tests for LRU eviction,
TTL expiry, and clear() removing all entries. Resolves drift item #6.

- drop_tracker module: proves HashMap::clear/remove/replace drop values
  via a Drop-flag instrumented type mirroring CachedKey's zeroize-on-drop
- test_lru_eviction_drops_evicted_cached_key: cache exceeds max_entries,
  oldest evicted
- test_ttl_expiry_evicts_entry_on_access: short TTL, wait, entry gone
- test_clear_removes_all_entries_and_empties_cache: empty after clear
- lock() clears cache already covered by test_lock_clears_all_cache_entries
This commit is contained in:
2026-06-23 13:17:42 +00:00
parent 098fd8b9b9
commit 389a9e93f7

View File

@@ -206,6 +206,84 @@ impl Default for KeyCache {
}
}
#[cfg(test)]
mod drop_tracker {
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use super::*;
struct DropTrackedKey {
flag: Arc<AtomicBool>,
bytes: Vec<u8>,
}
impl DropTrackedKey {
fn new(flag: &Arc<AtomicBool>) -> Self {
Self {
flag: flag.clone(),
bytes: vec![0xABu8; 32],
}
}
}
impl Drop for DropTrackedKey {
fn drop(&mut self) {
for b in self.bytes.iter_mut() {
*b = 0;
}
self.flag.store(true, Ordering::SeqCst);
}
}
#[test]
fn test_hashmap_clear_drops_values_triggering_drop_impls() {
let flag1 = Arc::new(AtomicBool::new(false));
let flag2 = Arc::new(AtomicBool::new(false));
let mut map: HashMap<String, DropTrackedKey> = HashMap::new();
map.insert("path1".to_string(), DropTrackedKey::new(&flag1));
map.insert("path2".to_string(), DropTrackedKey::new(&flag2));
assert!(!flag1.load(Ordering::SeqCst));
assert!(!flag2.load(Ordering::SeqCst));
map.clear();
assert!(flag1.load(Ordering::SeqCst));
assert!(flag2.load(Ordering::SeqCst));
assert!(map.is_empty());
}
#[test]
fn test_hashmap_remove_drops_value_triggering_drop_impl() {
let flag = Arc::new(AtomicBool::new(false));
let mut map: HashMap<String, DropTrackedKey> = HashMap::new();
map.insert("path1".to_string(), DropTrackedKey::new(&flag));
assert!(!flag.load(Ordering::SeqCst));
map.remove("path1");
assert!(flag.load(Ordering::SeqCst));
}
#[test]
fn test_hashmap_insert_replace_drops_old_value() {
let flag_old = Arc::new(AtomicBool::new(false));
let mut map: HashMap<String, DropTrackedKey> = HashMap::new();
map.insert("path1".to_string(), DropTrackedKey::new(&flag_old));
assert!(!flag_old.load(Ordering::SeqCst));
let flag_new = Arc::new(AtomicBool::new(false));
map.insert("path1".to_string(), DropTrackedKey::new(&flag_new));
assert!(flag_old.load(Ordering::SeqCst));
assert!(!flag_new.load(Ordering::SeqCst));
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -336,4 +414,56 @@ mod tests {
assert_eq!(entry.private_key, vec![3u8; 32]);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_lru_eviction_drops_evicted_cached_key() {
let mut config = CacheConfig::default();
config.max_entries = 2;
let mut cache = KeyCache::new(config);
cache.insert("path1", make_cached_key(KeyType::Ed25519));
cache.insert("path2", make_cached_key(KeyType::Aes256Gcm));
assert_eq!(cache.len(), 2);
cache.insert("path3", make_cached_key(KeyType::Secp256k1));
assert_eq!(cache.len(), 2);
assert!(cache.get("path1").is_none());
assert!(cache.get("path2").is_some());
assert!(cache.get("path3").is_some());
}
#[test]
fn test_ttl_expiry_evicts_entry_on_access() {
let mut config = CacheConfig::default();
config.ttl = Duration::from_millis(1);
let mut cache = KeyCache::new(config);
cache.insert("path1", make_cached_key(KeyType::Ed25519));
assert_eq!(cache.len(), 1);
std::thread::sleep(Duration::from_millis(5));
assert!(cache.get("path1").is_none());
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_clear_removes_all_entries_and_empties_cache() {
let mut cache = KeyCache::with_defaults();
cache.insert("path1", make_cached_key(KeyType::Ed25519));
cache.insert("path2", make_cached_key(KeyType::Aes256Gcm));
cache.insert("path3", make_cached_key(KeyType::Secp256k1));
assert_eq!(cache.len(), 3);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
assert!(cache.get("path1").is_none());
assert!(cache.get("path2").is_none());
assert!(cache.get("path3").is_none());
}
}