Skip to main content

macula_rust/seal/
keyring.rs

1//! A provider's KEM keys (macula 13's E2E design, amendment A1): the key its
2//! advertisements name, rotated every [`KEY_LIFETIME_MS`], and each replaced
3//! key kept for [`RETIRED_KEY_KEPT_MS`], long enough that a request sealed to
4//! it just before the rotation can still be admitted (the last
5//! advertisement's 5 minutes, the clock tolerance's 5, the longest deadline's
6//! 10, admission's 5 past it, and 5 of margin), then forgotten. Keys live in
7//! memory only, never on disk: a restart loses them all, and a caller sealing
8//! to an old one is refused sealed_refused, naming the new one. One node
9//! identity, one keyring, as macula-go's seal.Keyring.
10
11use std::fmt;
12use std::sync::{Arc, Mutex};
13
14use crate::profile::Profile;
15
16use super::{hex, PrivateKey, SealError, KEY_ID_SIZE};
17
18/// How long a key is the current one: 24 hours.
19pub const KEY_LIFETIME_MS: i64 = 24 * 60 * 60 * 1000;
20/// How long a replaced key still opens: 30 minutes.
21pub const RETIRED_KEY_KEPT_MS: i64 = 30 * 60 * 1000;
22
23/// The clock a keyring reads, in unix milliseconds.
24pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
25
26/// One node identity's KEM keys. It is safe to share between links. Showing
27/// it gives the current key id, never a key.
28pub struct Keyring {
29    profile: Profile,
30    now: Clock,
31    held: Mutex<Held>,
32}
33
34struct Held {
35    current: Arc<PrivateKey>,
36    current_id: [u8; KEY_ID_SIZE],
37    since: i64,
38    retired: Vec<Retired>,
39}
40
41struct Retired {
42    key: Arc<PrivateKey>,
43    id: [u8; KEY_ID_SIZE],
44    until: i64,
45}
46
47impl fmt::Debug for Keyring {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        let held = self.lock();
50        write!(
51            f,
52            "Keyring({}, current key id {}, {} replaced kept)",
53            self.profile.name(),
54            hex(&held.current_id),
55            held.retired.len()
56        )
57    }
58}
59
60impl Keyring {
61    /// A keyring of `profile` with a fresh current key, reading the time
62    /// from `now`.
63    pub fn new(profile: Profile, now: Clock) -> Result<Keyring, SealError> {
64        let key = PrivateKey::generate(profile)?;
65        let since = now();
66        Ok(Keyring {
67            profile,
68            now,
69            held: Mutex::new(Held {
70                current_id: key.public_key().key_id(),
71                current: Arc::new(key),
72                since,
73                retired: Vec::new(),
74            }),
75        })
76    }
77
78    /// A keyring of `profile` on the system clock.
79    pub fn system(profile: Profile) -> Result<Keyring, SealError> {
80        Keyring::new(profile, Arc::new(unix_ms))
81    }
82
83    /// The profile of every key the keyring holds.
84    pub fn profile(&self) -> Profile {
85        self.profile
86    }
87
88    /// The key an advertisement names now, rotated first when it has lived
89    /// [`KEY_LIFETIME_MS`]. Every advertisement reads it when it is signed,
90    /// so a renewal names a rotated key.
91    pub fn current(&self) -> Arc<PrivateKey> {
92        let mut held = self.lock();
93        self.rotate(&mut held, (self.now)());
94        held.current.clone()
95    }
96
97    /// The id of the current key, which a sealed_refused names.
98    pub fn current_id(&self) -> [u8; KEY_ID_SIZE] {
99        let mut held = self.lock();
100        self.rotate(&mut held, (self.now)());
101        held.current_id
102    }
103
104    /// The key of `id`: the current key, or a replaced one still kept.
105    pub fn find(&self, id: &[u8; KEY_ID_SIZE]) -> Option<Arc<PrivateKey>> {
106        let mut held = self.lock();
107        let now = (self.now)();
108        self.rotate(&mut held, now);
109        if &held.current_id == id {
110            return Some(held.current.clone());
111        }
112        held.retired
113            .iter()
114            .find(|old| &old.id == id && now < old.until)
115            .map(|old| old.key.clone())
116    }
117
118    fn lock(&self) -> std::sync::MutexGuard<'_, Held> {
119        self.held.lock().unwrap_or_else(|p| p.into_inner())
120    }
121
122    /// Replaces the current key once it has lived [`KEY_LIFETIME_MS`],
123    /// keeping it for [`RETIRED_KEY_KEPT_MS`], and forgets the replaced keys
124    /// past theirs.
125    fn rotate(&self, held: &mut Held, now: i64) {
126        held.retired.retain(|old| now < old.until);
127        if now - held.since < KEY_LIFETIME_MS {
128            return;
129        }
130        // The profile was checked when the keyring was made: a key that
131        // cannot be generated now keeps the current one rather than hold
132        // none.
133        let Ok(next) = PrivateKey::generate(self.profile) else {
134            return;
135        };
136        let next_id = next.public_key().key_id();
137        let replaced = std::mem::replace(&mut held.current, Arc::new(next));
138        held.retired.push(Retired {
139            key: replaced,
140            id: held.current_id,
141            until: now + RETIRED_KEY_KEPT_MS,
142        });
143        held.current_id = next_id;
144        held.since = now;
145    }
146}
147
148fn unix_ms() -> i64 {
149    std::time::SystemTime::now()
150        .duration_since(std::time::UNIX_EPOCH)
151        .map_or(0, |d| d.as_millis() as i64)
152}
153
154#[cfg(test)]
155mod tests {
156    use std::sync::atomic::{AtomicI64, Ordering};
157
158    use super::*;
159    use crate::seal::carried_key_size;
160
161    fn clock(at: &Arc<AtomicI64>) -> Clock {
162        let at = at.clone();
163        Arc::new(move || at.load(Ordering::SeqCst))
164    }
165
166    /// One current key, rotated every 24 hours, a replaced key still opening
167    /// for 30 minutes and then gone.
168    #[test]
169    fn a_keyring_rotates_and_forgets() {
170        let at = Arc::new(AtomicI64::new(1_790_000_000_000));
171        let ring = Keyring::new(Profile::PqPure, clock(&at)).unwrap();
172        let first = ring.current().public_key().key_id();
173        assert_eq!(ring.current().public_key().key_id(), first);
174
175        at.fetch_add(KEY_LIFETIME_MS - 1000, Ordering::SeqCst);
176        assert_eq!(
177            ring.current().public_key().key_id(),
178            first,
179            "a key rotated before its lifetime"
180        );
181        at.fetch_add(1000, Ordering::SeqCst);
182        let second = ring.current().public_key().key_id();
183        assert_ne!(second, first, "a key outlived its lifetime");
184        assert_eq!(ring.current_id(), second);
185        assert!(
186            ring.find(&first).is_some(),
187            "a replaced key stopped at once"
188        );
189
190        at.fetch_add(RETIRED_KEY_KEPT_MS - 1000, Ordering::SeqCst);
191        assert!(ring.find(&first).is_some(), "a replaced key went early");
192        at.fetch_add(1000, Ordering::SeqCst);
193        assert!(
194            ring.find(&first).is_none(),
195            "a replaced key outlived 30 minutes"
196        );
197        assert_eq!(
198            ring.find(&second).map(|k| k.public_key().key_id()),
199            Some(second)
200        );
201        assert!(ring.find(&[1, 0, 0, 0, 0, 0, 0, 0]).is_none());
202    }
203
204    #[test]
205    fn a_keyring_is_of_its_profile_and_shows_no_key() {
206        for profile in [Profile::PqPure, Profile::PqHybrid] {
207            let ring = Keyring::system(profile).unwrap();
208            assert_eq!(ring.profile(), profile);
209            assert_eq!(
210                ring.current().public_key().carried().len(),
211                carried_key_size(profile)
212            );
213            let shown = format!("{ring:?}");
214            assert!(shown.contains(&hex(&ring.current_id())), "{shown}");
215            assert!(shown.len() < 200, "{shown}");
216        }
217    }
218}