Skip to main content

confium_composite/
cache.rs

1//! LRU cache for signature verification results.
2//!
3//! Avoids re-verifying identical (algorithm, public_key, message,
4//! signature) tuples. Thread-safe via `Mutex`. LRU eviction by
5//! entry count.
6//!
7//! ## Usage
8//!
9//! ```no_run
10//! use confium_composite::cache::VerificationCache;
11//!
12//! let cache = VerificationCache::new(1024);
13//! // First call: miss → verify → cache result
14//! // Second call: hit → return cached result
15//! ```
16
17use sha2::{Digest, Sha256};
18use std::collections::HashMap;
19use std::sync::Mutex;
20
21/// Cache key — SHA-256 of the verification inputs.
22type CacheKey = [u8; 32];
23
24/// Entry in the LRU cache: the verification result and a sequence
25/// number for LRU eviction.
26struct CacheEntry {
27    verified: bool,
28    seq: u64,
29}
30
31/// Thread-safe LRU cache for signature verification results.
32pub struct VerificationCache {
33    inner: Mutex<CacheInner>,
34}
35
36struct CacheInner {
37    entries: HashMap<CacheKey, CacheEntry>,
38    max_entries: usize,
39    next_seq: u64,
40}
41
42impl VerificationCache {
43    /// Create a new cache with the given capacity.
44    pub fn new(max_entries: usize) -> Self {
45        Self {
46            inner: Mutex::new(CacheInner {
47                entries: HashMap::with_capacity(max_entries),
48                max_entries,
49                next_seq: 0,
50            }),
51        }
52    }
53
54    /// Look up a cached result. Returns `Some(verified)` on hit,
55    /// `None` on miss.
56    pub fn get(
57        &self,
58        algorithm: &str,
59        public_key: &[u8],
60        message: &[u8],
61        signature: &[u8],
62    ) -> Option<bool> {
63        let key = make_key(algorithm, public_key, message, signature);
64        let mut inner = self.inner.lock().unwrap();
65        let next_seq = inner.next_seq;
66        inner.next_seq += 1;
67        if let Some(entry) = inner.entries.get_mut(&key) {
68            entry.seq = next_seq;
69            Some(entry.verified)
70        } else {
71            None
72        }
73    }
74
75    /// Store a verification result.
76    pub fn put(
77        &self,
78        algorithm: &str,
79        public_key: &[u8],
80        message: &[u8],
81        signature: &[u8],
82        verified: bool,
83    ) {
84        let key = make_key(algorithm, public_key, message, signature);
85        let mut inner = self.inner.lock().unwrap();
86        let seq = inner.next_seq;
87        inner.next_seq += 1;
88
89        inner.entries.insert(key, CacheEntry { verified, seq });
90
91        if inner.entries.len() > inner.max_entries {
92            evict_oldest(&mut inner.entries);
93        }
94    }
95
96    /// Current entry count.
97    pub fn len(&self) -> usize {
98        self.inner.lock().unwrap().entries.len()
99    }
100
101    /// Is the cache empty?
102    pub fn is_empty(&self) -> bool {
103        self.len() == 0
104    }
105
106    /// Clear all entries.
107    pub fn clear(&self) {
108        self.inner.lock().unwrap().entries.clear();
109    }
110}
111
112impl Default for VerificationCache {
113    fn default() -> Self {
114        Self::new(1024)
115    }
116}
117
118fn make_key(algorithm: &str, public_key: &[u8], message: &[u8], signature: &[u8]) -> CacheKey {
119    let mut hasher = Sha256::new();
120    hasher.update(algorithm.as_bytes());
121    hasher.update(public_key);
122    hasher.update(message);
123    hasher.update(signature);
124    let result = hasher.finalize();
125    let mut key = [0u8; 32];
126    key.copy_from_slice(&result);
127    key
128}
129
130fn evict_oldest(entries: &mut HashMap<CacheKey, CacheEntry>) {
131    if let Some((&oldest_key, _)) = entries.iter().min_by_key(|(_, v)| v.seq) {
132        entries.remove(&oldest_key);
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn empty_cache_returns_none() {
142        let cache = VerificationCache::new(100);
143        assert_eq!(cache.get("Ed25519", &[1; 32], &[2; 32], &[3; 64]), None);
144    }
145
146    #[test]
147    fn put_then_get_returns_cached() {
148        let cache = VerificationCache::new(100);
149        cache.put("Ed25519", &[1; 32], &[2; 32], &[3; 64], true);
150        assert_eq!(
151            cache.get("Ed25519", &[1; 32], &[2; 32], &[3; 64]),
152            Some(true)
153        );
154    }
155
156    #[test]
157    fn different_message_is_miss() {
158        let cache = VerificationCache::new(100);
159        cache.put("Ed25519", &[1; 32], &[2; 32], &[3; 64], true);
160        assert_eq!(cache.get("Ed25519", &[1; 32], &[9; 32], &[3; 64]), None);
161    }
162
163    #[test]
164    fn different_signature_is_miss() {
165        let cache = VerificationCache::new(100);
166        cache.put("Ed25519", &[1; 32], &[2; 32], &[3; 64], true);
167        assert_eq!(cache.get("Ed25519", &[1; 32], &[2; 32], &[9; 64]), None);
168    }
169
170    #[test]
171    fn lru_eviction_removes_oldest() {
172        let cache = VerificationCache::new(3);
173        cache.put("a", &[1], &[1], &[1], true);
174        cache.put("b", &[2], &[2], &[2], true);
175        cache.put("c", &[3], &[3], &[3], true);
176
177        // Access "a" to make it more recent than "b"
178        cache.get("a", &[1], &[1], &[1]);
179
180        // Insert "d" → should evict "b" (least recently used)
181        cache.put("d", &[4], &[4], &[4], true);
182
183        assert_eq!(cache.get("a", &[1], &[1], &[1]), Some(true));
184        assert_eq!(cache.get("b", &[2], &[2], &[2]), None); // evicted
185        assert_eq!(cache.get("c", &[3], &[3], &[3]), Some(true));
186        assert_eq!(cache.get("d", &[4], &[4], &[4]), Some(true));
187        assert_eq!(cache.len(), 3);
188    }
189
190    #[test]
191    fn clear_empties_cache() {
192        let cache = VerificationCache::new(100);
193        cache.put("a", &[1], &[1], &[1], true);
194        assert_eq!(cache.len(), 1);
195        cache.clear();
196        assert_eq!(cache.len(), 0);
197    }
198
199    #[test]
200    fn overwrite_updates_value() {
201        let cache = VerificationCache::new(100);
202        cache.put("Ed25519", &[1], &[2], &[3], false);
203        assert_eq!(cache.get("Ed25519", &[1], &[2], &[3]), Some(false));
204        cache.put("Ed25519", &[1], &[2], &[3], true);
205        assert_eq!(cache.get("Ed25519", &[1], &[2], &[3]), Some(true));
206    }
207
208    #[test]
209    fn capacity_one_always_evicts() {
210        let cache = VerificationCache::new(1);
211        cache.put("a", &[1], &[1], &[1], true);
212        assert_eq!(cache.len(), 1);
213        cache.put("b", &[2], &[2], &[2], true);
214        assert_eq!(cache.len(), 1);
215        assert_eq!(cache.get("a", &[1], &[1], &[1]), None);
216        assert_eq!(cache.get("b", &[2], &[2], &[2]), Some(true));
217    }
218
219    #[test]
220    fn default_capacity_is_1024() {
221        let cache = VerificationCache::default();
222        assert_eq!(cache.len(), 0);
223        for i in 0..100 {
224            cache.put("alg", &[i], &[i], &[i], true);
225        }
226        assert_eq!(cache.len(), 100);
227    }
228
229    #[test]
230    fn thread_safe_concurrent_access() {
231        use std::sync::Arc;
232        use std::thread;
233
234        let cache = Arc::new(VerificationCache::new(100));
235        let mut handles = Vec::new();
236
237        for i in 0..4 {
238            let cache = Arc::clone(&cache);
239            handles.push(thread::spawn(move || {
240                for j in 0..10 {
241                    let val = i * 10 + j;
242                    cache.put("alg", &[val as u8], &[val as u8], &[val as u8], true);
243                    cache.get("alg", &[val as u8], &[val as u8], &[val as u8]);
244                }
245            }));
246        }
247
248        for h in handles {
249            h.join().unwrap();
250        }
251        assert!(cache.len() <= 100);
252    }
253}