Skip to main content

av_receipts/
keys.rs

1//! Signing keys: the `Signer` trait (in-process Ed25519 now, KMS post-MVP) and
2//! an offline `Keyring` for verification with key rotation via key ids.
3
4use ed25519_dalek::{Signature, Signer as DalekSigner, SigningKey, Verifier, VerifyingKey};
5use std::collections::HashMap;
6
7/// Abstract signer — the KMS integration point (brief Module G, post-MVP).
8pub trait Signer: Send + Sync {
9    /// Stable identifier for the signing key (embedded in receipts).
10    fn key_id(&self) -> &str;
11    /// Sign `msg`, returning the 64-byte Ed25519 signature.
12    fn sign(&self, msg: &[u8]) -> [u8; 64];
13    /// The corresponding public key (32 bytes).
14    fn public_key_bytes(&self) -> [u8; 32];
15}
16
17/// In-process Ed25519 signer.
18pub struct Ed25519Signer {
19    key_id: String,
20    key: SigningKey,
21}
22
23impl std::fmt::Debug for Ed25519Signer {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        // Never print secret key material.
26        f.debug_struct("Ed25519Signer")
27            .field("key_id", &self.key_id)
28            .finish_non_exhaustive()
29    }
30}
31
32impl Ed25519Signer {
33    /// Generate a fresh keypair. The key id is derived from the public key
34    /// (first 32 hex chars of its SHA-256) so ids are collision-resistant and
35    /// never chosen by an attacker.
36    ///
37    /// Round-20 F5: defense-in-depth against a compromised
38    /// `getrandom` (VM without entropy, cloud image with a broken
39    /// `/dev/urandom`) — regenerate rather than accept an
40    /// all-zero or all-0xFF seed. Reader-side already refuses
41    /// these known-weak seeds (round-14); this closes the gap
42    /// for generator-side. Astronomically unlikely from a healthy
43    /// OsRng, but the failure mode is silent installation of a
44    /// globally-predictable keypair — cheap to guard.
45    pub fn generate() -> Self {
46        loop {
47            let key = SigningKey::generate(&mut rand::rngs::OsRng);
48            // Round-21 F2: wrap the raw seed comparison buffer in
49            // `Zeroizing` so the stack slot zeroes on scope exit.
50            // A bare `[u8; 32]` has no Drop, so it lingers in
51            // freed stack memory (recoverable from a core dump).
52            // The round-20 F5 loop reintroduced that leak by
53            // reading `key.to_bytes()` into a bare local. Equality
54            // through Deref still works.
55            let bytes = zeroize::Zeroizing::new(key.to_bytes());
56            if *bytes == [0u8; 32] || *bytes == [0xFFu8; 32] {
57                continue;
58            }
59            let key_id = derive_key_id(&key.verifying_key());
60            return Self { key_id, key };
61        }
62    }
63
64    /// Load from a 32-byte secret seed.
65    ///
66    /// Round-19 F2: takes `&[u8; 32]` (borrow), not by value. Passing
67    /// the seed by value materializes a caller-owned temp slot on
68    /// the stack that Rust does not guarantee to zeroize on drop —
69    /// the round-18 F5 `Zeroizing<[u8; 32]>` wrapper only zeroizes
70    /// the slot IT owns, not the copy the callee received. By taking
71    /// a reference we let the caller keep the seed inside a
72    /// `Zeroizing` and never lose control of the memory.
73    pub fn from_seed(seed: &[u8; 32]) -> Self {
74        let key = SigningKey::from_bytes(seed);
75        let key_id = derive_key_id(&key.verifying_key());
76        Self { key_id, key }
77    }
78
79    /// Export the 32-byte secret seed (for `avctl keygen` persistence).
80    ///
81    /// Round-19 F2: returns `Zeroizing<[u8; 32]>` so the caller's
82    /// receiving slot zeroes on drop — historically the bare
83    /// `[u8; 32]` return let a copy linger in freed stack/heap.
84    pub fn seed(&self) -> zeroize::Zeroizing<[u8; 32]> {
85        zeroize::Zeroizing::new(self.key.to_bytes())
86    }
87}
88
89/// `sha256(pubkey)[..16]` hex — 32 chars (128-bit collision resistance).
90///
91/// 64 bits would put birthday attacks at ~2^32, cheap on modern hardware;
92/// 128 bits pushes the birthday bound to ~2^64, comfortably infeasible.
93fn derive_key_id(vk: &VerifyingKey) -> String {
94    let digest = av_core::digest::sha256_hex(vk.as_bytes());
95    digest.chars().take(32).collect()
96}
97
98impl Signer for Ed25519Signer {
99    fn key_id(&self) -> &str {
100        &self.key_id
101    }
102
103    fn sign(&self, msg: &[u8]) -> [u8; 64] {
104        self.key.sign(msg).to_bytes()
105    }
106
107    fn public_key_bytes(&self) -> [u8; 32] {
108        self.key.verifying_key().to_bytes()
109    }
110}
111
112/// Verification keyring: key id → public key. Old receipts stay verifiable
113/// after rotation as long as their key remains in the ring.
114#[derive(Debug, Default, Clone)]
115pub struct Keyring {
116    keys: HashMap<String, VerifyingKey>,
117}
118
119/// Keyring / verification errors.
120#[derive(Debug, thiserror::Error)]
121#[non_exhaustive]
122pub enum KeyError {
123    /// Public key bytes malformed.
124    #[error("invalid public key: {0}")]
125    InvalidKey(String),
126    /// Signature bytes malformed.
127    #[error("invalid signature encoding")]
128    InvalidSignature,
129    /// No key with this id.
130    #[error("unknown key id {0:?}")]
131    UnknownKeyId(String),
132    /// Signature did not verify.
133    #[error("signature verification failed for key id {0:?}")]
134    BadSignature(String),
135    /// Two distinct public keys derived the same key id.
136    #[error("key id {0:?} is already registered to a different public key")]
137    KeyMismatch(String),
138}
139
140impl Keyring {
141    /// Empty ring.
142    pub fn new() -> Self {
143        Self::default()
144    }
145
146    /// Add a key by raw public bytes; returns its derived key id.
147    ///
148    /// If the derived id already exists with a different public key, refuses
149    /// silently overwriting it and returns `KeyMismatch` — otherwise an
150    /// attacker who found a 128-bit collision could substitute their key for
151    /// an honest signer's.
152    pub fn add_key_bytes(&mut self, bytes: &[u8; 32]) -> Result<String, KeyError> {
153        let vk = VerifyingKey::from_bytes(bytes).map_err(|e| KeyError::InvalidKey(e.to_string()))?;
154        let id = derive_key_id(&vk);
155        if let Some(existing) = self.keys.get(&id) {
156            if existing.as_bytes() != vk.as_bytes() {
157                return Err(KeyError::KeyMismatch(id));
158            }
159            return Ok(id);
160        }
161        self.keys.insert(id.clone(), vk);
162        Ok(id)
163    }
164
165    /// Add the public half of a signer.
166    pub fn add_signer(&mut self, signer: &dyn Signer) -> Result<String, KeyError> {
167        self.add_key_bytes(&signer.public_key_bytes())
168    }
169
170    /// Verify `sig` over `msg` with the key identified by `key_id`.
171    pub fn verify(&self, key_id: &str, msg: &[u8], sig: &[u8]) -> Result<(), KeyError> {
172        let vk = self
173            .keys
174            .get(key_id)
175            .ok_or_else(|| KeyError::UnknownKeyId(key_id.to_owned()))?;
176        let sig_bytes: [u8; 64] = sig.try_into().map_err(|_| KeyError::InvalidSignature)?;
177        let signature = Signature::from_bytes(&sig_bytes);
178        vk.verify(msg, &signature)
179            .map_err(|_| KeyError::BadSignature(key_id.to_owned()))
180    }
181
182    /// Number of keys in the ring.
183    pub fn len(&self) -> usize {
184        self.keys.len()
185    }
186
187    /// True if the ring holds no keys.
188    pub fn is_empty(&self) -> bool {
189        self.keys.is_empty()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
196
197    use super::*;
198
199    #[test]
200    fn sign_verify_roundtrip() {
201        let signer = Ed25519Signer::generate();
202        let mut ring = Keyring::new();
203        let id = ring.add_signer(&signer).unwrap();
204        assert_eq!(id, signer.key_id());
205        let sig = signer.sign(b"hello");
206        ring.verify(&id, b"hello", &sig).unwrap();
207    }
208
209    #[test]
210    fn wrong_message_fails() {
211        let signer = Ed25519Signer::generate();
212        let mut ring = Keyring::new();
213        let id = ring.add_signer(&signer).unwrap();
214        let sig = signer.sign(b"hello");
215        assert!(matches!(
216            ring.verify(&id, b"HELLO", &sig),
217            Err(KeyError::BadSignature(_))
218        ));
219    }
220
221    #[test]
222    fn wrong_key_fails() {
223        let a = Ed25519Signer::generate();
224        let b = Ed25519Signer::generate();
225        let mut ring = Keyring::new();
226        ring.add_signer(&b).unwrap();
227        let sig = a.sign(b"msg");
228        // b's ring doesn't know a's key id.
229        assert!(matches!(
230            ring.verify(a.key_id(), b"msg", &sig),
231            Err(KeyError::UnknownKeyId(_))
232        ));
233    }
234
235    #[test]
236    fn truncated_signature_rejected() {
237        let signer = Ed25519Signer::generate();
238        let mut ring = Keyring::new();
239        let id = ring.add_signer(&signer).unwrap();
240        let sig = signer.sign(b"msg");
241        assert!(matches!(
242            ring.verify(&id, b"msg", &sig[..63]),
243            Err(KeyError::InvalidSignature)
244        ));
245    }
246
247    #[test]
248    fn seed_roundtrip_preserves_identity() {
249        let a = Ed25519Signer::generate();
250        let b = Ed25519Signer::from_seed(&a.seed());
251        assert_eq!(a.key_id(), b.key_id());
252        assert_eq!(a.public_key_bytes(), b.public_key_bytes());
253    }
254
255    #[test]
256    fn rotation_keeps_old_receipts_verifiable() {
257        let old = Ed25519Signer::generate();
258        let new = Ed25519Signer::generate();
259        let mut ring = Keyring::new();
260        ring.add_signer(&old).unwrap();
261        ring.add_signer(&new).unwrap();
262        let old_sig = old.sign(b"old receipt");
263        let new_sig = new.sign(b"new receipt");
264        ring.verify(old.key_id(), b"old receipt", &old_sig).unwrap();
265        ring.verify(new.key_id(), b"new receipt", &new_sig).unwrap();
266        assert_eq!(ring.len(), 2);
267    }
268
269    #[test]
270    fn debug_never_leaks_secret() {
271        let signer = Ed25519Signer::generate();
272        let dbg = format!("{signer:?}");
273        let seed_hex = hex::encode(signer.seed());
274        assert!(!dbg.contains(&seed_hex), "Debug output leaked the seed");
275        // Debug must contain something identifying (the key_id label).
276        assert!(dbg.contains(signer.key_id()));
277    }
278
279    #[test]
280    fn keyring_is_empty_reflects_the_ring_state() {
281        let mut ring = Keyring::new();
282        assert!(ring.is_empty(), "fresh ring must be empty");
283        assert_eq!(ring.len(), 0);
284        let s = Ed25519Signer::generate();
285        ring.add_signer(&s).unwrap();
286        assert!(!ring.is_empty(), "ring with one key must not be empty");
287        assert_eq!(ring.len(), 1);
288    }
289
290    #[test]
291    fn key_id_is_128_bits_wide() {
292        let signer = Ed25519Signer::generate();
293        assert_eq!(
294            signer.key_id().len(),
295            32,
296            "key_id must be 32 hex chars (128 bits) so birthday collisions cost ~2^64"
297        );
298    }
299
300    #[test]
301    fn keyring_refuses_to_overwrite_colliding_id_with_different_key() {
302        let mut ring = Keyring::new();
303        let honest = Ed25519Signer::generate();
304        let id = ring.add_signer(&honest).unwrap();
305        let attacker = Ed25519Signer::generate();
306        assert_ne!(honest.public_key_bytes(), attacker.public_key_bytes());
307        // We can't manufacture a real SHA-256 collision, so simulate one at
308        // the low-level map: a ring already holding the ATTACKER's key under
309        // the honest key's id. Adding the honest key (whose derived id now
310        // collides with different bytes) must be refused, not overwritten.
311        let attacker_vk = VerifyingKey::from_bytes(&attacker.public_key_bytes()).unwrap();
312        let mut fresh = Keyring::new();
313        fresh.keys.insert(id.clone(), attacker_vk);
314        let err = fresh
315            .add_key_bytes(&honest.public_key_bytes())
316            .expect_err("must refuse overwrite of a colliding id");
317        assert!(matches!(err, KeyError::KeyMismatch(_)), "got {err:?}");
318    }
319
320    #[test]
321    fn keyring_add_is_idempotent_for_the_same_key() {
322        let mut ring = Keyring::new();
323        let signer = Ed25519Signer::generate();
324        let id1 = ring.add_signer(&signer).unwrap();
325        let id2 = ring.add_signer(&signer).unwrap();
326        assert_eq!(id1, id2);
327        assert_eq!(ring.len(), 1);
328    }
329}