Skip to main content

corium_store/
key_manifest.rs

1//! The `keys:<db>` root record: which key encrypts a database, and how.
2//!
3//! The manifest is the bootstrap for storage encryption. It is stored
4//! cleartext next to the other root records because it holds no key material —
5//! only a KEK identity and data keys already wrapped under it — so a restore
6//! can rebuild a working database from the archive plus KMS access and nothing
7//! else.
8
9use std::collections::BTreeMap;
10use std::fmt;
11
12use corium_crypt::{KeyId, Keyring, SecretKey};
13
14use crate::{RootStore, StoreError};
15
16/// Manifest format written by this release.
17pub const KEY_MANIFEST_FORMAT_VERSION: u32 = 1;
18
19const MANIFEST_HEADER: &str = "corium-keys-v";
20const ALGORITHM_AES_256: &str = "aes-256";
21
22/// Log records to seal under one storage epoch before its key must be rotated.
23///
24/// Log records use AES-256-GCM with a random 96-bit nonce, so nonce collision
25/// is a birthday problem rather than an impossibility: after `q` records the
26/// probability is about `q² / 2¹⁹⁷`, which stays negligible up to `2³²` — the
27/// standard ceiling for randomized GCM nonces — and stops being negligible
28/// beyond it. Rotating the storage key opens a new epoch with fresh key
29/// material and resets the count.
30///
31/// (Blobs are unaffected: their nonce is derived, not random, and they use
32/// GCM-SIV precisely so that a derivation collision is not a catastrophe.)
33pub const LOG_RECORDS_PER_EPOCH_LIMIT: u64 = 1 << 32;
34
35/// Where [`KeyManifest::storage_rotation_due`] starts saying yes.
36///
37/// Half the ceiling, so an operator has an epoch's worth of warning rather than
38/// an alarm at the moment the bound is reached.
39pub const LOG_RECORDS_PER_EPOCH_WARN: u64 = LOG_RECORDS_PER_EPOCH_LIMIT / 2;
40
41/// Root-store key for a database's key manifest.
42#[must_use]
43pub fn keys_root_name(db: &str) -> String {
44    format!("keys:{db}")
45}
46
47/// AEAD suite a storage-key epoch is used with.
48///
49/// One name covers both stored formats, because both are keyed by the same
50/// data key: blobs use AES-256-GCM-SIV (deterministic encryption makes
51/// nonce-misuse resistance mandatory) and log records AES-256-GCM. A future
52/// `XChaCha20-Poly1305` or FIPS-mode backend becomes another value here rather
53/// than a format rewrite.
54#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55pub enum StorageAlgorithm {
56    /// AES-256 in GCM-SIV (blobs) and GCM (log records).
57    #[default]
58    Aes256,
59}
60
61impl StorageAlgorithm {
62    fn as_str(self) -> &'static str {
63        match self {
64            Self::Aes256 => ALGORITHM_AES_256,
65        }
66    }
67
68    fn parse(text: &str) -> Option<Self> {
69        match text {
70            ALGORITHM_AES_256 => Some(Self::Aes256),
71            _ => None,
72        }
73    }
74}
75
76impl fmt::Display for StorageAlgorithm {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(self.as_str())
79    }
80}
81
82/// Lifecycle position of one storage-key epoch.
83///
84/// Exactly one epoch is `Active` — the one new writes use. Older epochs stay
85/// `Retiring` while live objects still carry them, and become `Retired` only
86/// once the mark pass counts none, at which point their key material may be
87/// destroyed.
88#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
89pub enum StorageKeyState {
90    /// New writes use this epoch.
91    #[default]
92    Active,
93    /// Readable, still referenced by live objects, no longer written.
94    Retiring,
95    /// Readable while material lasts; no live object carries it.
96    Retired,
97}
98
99impl StorageKeyState {
100    fn as_str(self) -> &'static str {
101        match self {
102            Self::Active => "active",
103            Self::Retiring => "retiring",
104            Self::Retired => "retired",
105        }
106    }
107
108    fn parse(text: &str) -> Option<Self> {
109        match text {
110            "active" => Some(Self::Active),
111            "retiring" => Some(Self::Retiring),
112            "retired" => Some(Self::Retired),
113            _ => None,
114        }
115    }
116}
117
118impl fmt::Display for StorageKeyState {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.write_str(self.as_str())
121    }
122}
123
124/// One storage data key, wrapped under the manifest's KEK.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct StorageKey {
127    /// Epoch this key encrypts under. Stored in every object it produces.
128    pub epoch: u32,
129    /// KEK epoch the material below is wrapped under. Recorded because a KEK
130    /// rotation retires a KEK epoch while the DEK epoch is unchanged.
131    pub kek_epoch: u32,
132    /// AEAD suite this epoch is used with.
133    pub algorithm: StorageAlgorithm,
134    /// Wrapped data key. Never usable without the KEK.
135    pub wrapped_dek: Vec<u8>,
136    /// When the epoch was opened, as Unix milliseconds.
137    pub created_at_unix_ms: i64,
138    /// Transaction number the epoch was opened at.
139    ///
140    /// This is the log-record nonce budget's meter. Log records use AES-256-GCM
141    /// with a random nonce, so the number sealed under one key must stay well
142    /// below [`LOG_RECORDS_PER_EPOCH_LIMIT`] — and that number is exactly the
143    /// span of `t` the epoch covers, because the log seals one record per
144    /// transaction. Recording where an epoch started therefore measures its
145    /// budget with no counter to maintain and no write amplification: the next
146    /// epoch's `opened_at_t` (or the current basis, for the active epoch) ends
147    /// the span. See [`KeyManifest::log_records_sealed`].
148    pub opened_at_t: u64,
149    /// Where the epoch sits in its lifecycle.
150    pub state: StorageKeyState,
151    /// Live objects carrying this epoch as of the last mark pass. An epoch
152    /// retires only at zero; `corium keys status` prints it so a drain is a
153    /// number rather than a guess. This counts stored objects for GC drain, not
154    /// records sealed — the nonce budget is `opened_at_t`'s job.
155    pub live_objects: u64,
156}
157
158/// The key a protection class currently seals under.
159///
160/// This is a cache of what the schema already says, so a process can discover
161/// which key ids it needs before it can read any datoms. The class entities in
162/// `:db.part/db` remain authoritative. Only identities are recorded; class key
163/// material is never stored in Corium.
164#[derive(Clone, Debug, Eq, PartialEq)]
165pub struct ProtectionClassKey {
166    /// Entity id of the class in `:db.part/db`.
167    pub class: u64,
168    /// Identity a process resolves through its own keyring.
169    pub key_id: KeyId,
170    /// Epoch new seals under this class use.
171    pub current_epoch: u32,
172}
173
174/// The `keys:<db>` root record.
175#[derive(Clone, Debug, Eq, PartialEq)]
176pub struct KeyManifest {
177    /// Manifest format version. Readers reject manifests from newer formats.
178    pub format_version: u32,
179    /// Key-encryption key, recorded per database rather than per deployment so
180    /// per-tenant key isolation needs no format change. A deployment-wide KEK
181    /// is the case where every database names the same one.
182    pub kek: KeyId,
183    /// Storage data keys, ascending by epoch.
184    pub storage_keys: Vec<StorageKey>,
185    /// Protection-class key identities, ascending by class entity id.
186    pub classes: Vec<ProtectionClassKey>,
187}
188
189impl KeyManifest {
190    /// Mints a database's first storage key and returns its manifest.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`StoreError::Keyring`] when the KEK cannot be resolved or the
195    /// fresh data key cannot be wrapped, and [`StoreError::Encryption`] when
196    /// the platform has no usable random source.
197    /// A database is created empty, so its first epoch opens at `t` 0 and
198    /// covers every transaction until the first rotation.
199    pub async fn create(
200        keyring: &dyn Keyring,
201        kek: KeyId,
202        created_at_unix_ms: i64,
203    ) -> Result<Self, StoreError> {
204        let mut manifest = Self {
205            format_version: KEY_MANIFEST_FORMAT_VERSION,
206            kek,
207            storage_keys: Vec::new(),
208            classes: Vec::new(),
209        };
210        manifest
211            .mint_storage_key(keyring, 1, created_at_unix_ms, 0)
212            .await?;
213        Ok(manifest)
214    }
215
216    /// Returns the epoch new writes use, if any epoch is active.
217    ///
218    /// [`Self::validate`] admits at most one active epoch, so the `max` below
219    /// is a tie-break that a decoded manifest never needs; it keeps a manifest
220    /// assembled in memory from silently writing under a superseded key.
221    #[must_use]
222    pub fn active_storage_epoch(&self) -> Option<u32> {
223        self.storage_keys
224            .iter()
225            .filter(|key| key.state == StorageKeyState::Active)
226            .map(|key| key.epoch)
227            .max()
228    }
229
230    /// Returns the entry for one storage epoch.
231    #[must_use]
232    pub fn storage_key(&self, epoch: u32) -> Option<&StorageKey> {
233        self.storage_keys.iter().find(|key| key.epoch == epoch)
234    }
235
236    /// Unwraps every storage epoch the manifest carries.
237    ///
238    /// The result is the immutable key snapshot `EncryptedBlobStore` and the
239    /// log cipher hold: KMS access happens here, at open and on manifest
240    /// reload, never on a blob read or a log append. Retired epochs are
241    /// included while their material still resolves — a live object may still
242    /// carry one, and reading it must not depend on when the mark pass last
243    /// ran.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`StoreError::Keyring`] when an epoch cannot be unwrapped.
248    /// A misconfigured process therefore fails at open, naming the epoch,
249    /// rather than at its first read.
250    pub async fn unwrap_storage_keys(
251        &self,
252        keyring: &dyn Keyring,
253    ) -> Result<BTreeMap<u32, SecretKey>, StoreError> {
254        let mut keys = BTreeMap::new();
255        for key in &self.storage_keys {
256            let material = keyring
257                .unwrap(&self.kek, key.kek_epoch, &key.wrapped_dek)
258                .await
259                .map_err(StoreError::Keyring)?;
260            keys.insert(key.epoch, material);
261        }
262        Ok(keys)
263    }
264
265    /// Log records sealed under one epoch as of basis `current_t`.
266    ///
267    /// An epoch covers `t` from where it opened to where its successor did, or
268    /// to the current basis if it is the newest. The log seals one record per
269    /// transaction, so that span *is* the number of nonces drawn under the
270    /// epoch's key — see [`LOG_RECORDS_PER_EPOCH_LIMIT`].
271    ///
272    /// Returns `None` for an epoch the manifest does not carry.
273    #[must_use]
274    pub fn log_records_sealed(&self, epoch: u32, current_t: u64) -> Option<u64> {
275        let key = self.storage_key(epoch)?;
276        let end = self
277            .storage_keys
278            .iter()
279            .filter(|other| other.epoch > epoch)
280            .map(|other| other.opened_at_t)
281            .min()
282            .unwrap_or(current_t);
283        // A manifest read at a basis older than its newest epoch (or a
284        // successor that opened at the same `t`) yields an empty span, not a
285        // negative one.
286        Some(end.saturating_sub(key.opened_at_t))
287    }
288
289    /// Reports whether the active epoch has drawn enough nonces to want a
290    /// rotation, at [`LOG_RECORDS_PER_EPOCH_WARN`].
291    #[must_use]
292    pub fn storage_rotation_due(&self, current_t: u64) -> bool {
293        self.active_storage_epoch()
294            .and_then(|epoch| self.log_records_sealed(epoch, current_t))
295            .is_some_and(|sealed| sealed >= LOG_RECORDS_PER_EPOCH_WARN)
296    }
297
298    /// Opens a new storage epoch that new writes will use, from basis
299    /// `current_t`.
300    ///
301    /// Rotation is layered and cheap: no stored object is rewritten. The
302    /// previous active epoch becomes `Retiring` and stays readable until
303    /// ordinary re-indexing drains it. `current_t` closes the outgoing epoch's
304    /// nonce budget and opens the new one's, so it must be the basis the
305    /// rotation commits at.
306    ///
307    /// # Errors
308    ///
309    /// Returns [`StoreError::Keyring`] when the new key cannot be wrapped,
310    /// [`StoreError::Encryption`] when randomness is unavailable, and
311    /// [`StoreError::InvalidKeyManifest`] when `current_t` precedes the newest
312    /// epoch's opening, which would leave the epochs unordered in `t`.
313    pub async fn rotate_storage_key(
314        &mut self,
315        keyring: &dyn Keyring,
316        created_at_unix_ms: i64,
317        current_t: u64,
318    ) -> Result<u32, StoreError> {
319        let epoch = self
320            .storage_keys
321            .iter()
322            .map(|key| key.epoch)
323            .max()
324            .unwrap_or(0)
325            .checked_add(1)
326            .ok_or(StoreError::StorageEpochExhausted)?;
327        if self
328            .storage_keys
329            .iter()
330            .any(|key| key.opened_at_t > current_t)
331        {
332            return Err(invalid("rotation basis precedes an existing epoch"));
333        }
334        for key in &mut self.storage_keys {
335            if key.state == StorageKeyState::Active {
336                key.state = StorageKeyState::Retiring;
337            }
338        }
339        self.mint_storage_key(keyring, epoch, created_at_unix_ms, current_t)
340            .await?;
341        Ok(epoch)
342    }
343
344    /// Re-wraps every storage key under `kek`, touching no stored data.
345    ///
346    /// `keyring` must resolve both the outgoing and incoming KEK: the
347    /// manifest's own KEK to unwrap, and `kek` to wrap again.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`StoreError::Keyring`] when either KEK cannot be resolved.
352    /// The manifest is left untouched unless every key re-wraps.
353    pub async fn rewrap(&mut self, keyring: &dyn Keyring, kek: KeyId) -> Result<(), StoreError> {
354        let kek_epoch = keyring
355            .current_epoch(&kek)
356            .await
357            .map_err(StoreError::Keyring)?;
358        let mut rewrapped = Vec::with_capacity(self.storage_keys.len());
359        for key in &self.storage_keys {
360            let material = keyring
361                .unwrap(&self.kek, key.kek_epoch, &key.wrapped_dek)
362                .await
363                .map_err(StoreError::Keyring)?;
364            rewrapped.push(StorageKey {
365                kek_epoch,
366                wrapped_dek: keyring
367                    .wrap(&kek, kek_epoch, &material)
368                    .await
369                    .map_err(StoreError::Keyring)?,
370                ..key.clone()
371            });
372        }
373        self.kek = kek;
374        self.storage_keys = rewrapped;
375        Ok(())
376    }
377
378    async fn mint_storage_key(
379        &mut self,
380        keyring: &dyn Keyring,
381        epoch: u32,
382        created_at_unix_ms: i64,
383        opened_at_t: u64,
384    ) -> Result<(), StoreError> {
385        let kek_epoch = keyring
386            .current_epoch(&self.kek)
387            .await
388            .map_err(StoreError::Keyring)?;
389        let dek = SecretKey::generate()?;
390        let wrapped_dek = keyring
391            .wrap(&self.kek, kek_epoch, &dek)
392            .await
393            .map_err(StoreError::Keyring)?;
394        self.storage_keys.push(StorageKey {
395            epoch,
396            kek_epoch,
397            algorithm: StorageAlgorithm::default(),
398            wrapped_dek,
399            created_at_unix_ms,
400            opened_at_t,
401            state: StorageKeyState::Active,
402            live_objects: 0,
403        });
404        self.storage_keys.sort_by_key(|key| key.epoch);
405        Ok(())
406    }
407
408    /// Encodes the manifest for the root store.
409    #[must_use]
410    pub fn encode(&self) -> Vec<u8> {
411        use fmt::Write as _;
412        let mut out = format!("{MANIFEST_HEADER}{}\n{}\n", self.format_version, self.kek);
413        let _ = writeln!(out, "{}", self.storage_keys.len());
414        for key in &self.storage_keys {
415            // The wrapped key is hex so the record stays a line-oriented text
416            // blob the way `DbRoot` is, and so `RootStore::compare_and_set`
417            // keeps comparing bytes over printable content.
418            let _ = writeln!(
419                out,
420                "{} {} {} {} {} {} {} {}",
421                key.epoch,
422                key.kek_epoch,
423                key.algorithm,
424                key.state,
425                key.created_at_unix_ms,
426                key.opened_at_t,
427                key.live_objects,
428                hex(&key.wrapped_dek),
429            );
430        }
431        let _ = writeln!(out, "{}", self.classes.len());
432        for class in &self.classes {
433            // The key id goes last: it is a URI and may contain spaces.
434            let _ = writeln!(
435                out,
436                "{} {} {}",
437                class.class, class.current_epoch, class.key_id,
438            );
439        }
440        out.into_bytes()
441    }
442
443    /// Decodes stored manifest bytes.
444    ///
445    /// # Errors
446    ///
447    /// Returns [`StoreError::InvalidKeyManifest`] for malformed bytes and
448    /// [`StoreError::UnsupportedKeyManifest`] for a manifest written by a
449    /// newer release, which a reader must refuse rather than half-understand.
450    pub fn decode(bytes: &[u8]) -> Result<Self, StoreError> {
451        let text = std::str::from_utf8(bytes).map_err(|_| invalid("manifest is not UTF-8"))?;
452        let mut lines = text.lines();
453        let format_version: u32 = lines
454            .next()
455            .and_then(|line| line.strip_prefix(MANIFEST_HEADER))
456            .and_then(|version| version.parse().ok())
457            .ok_or_else(|| invalid("missing manifest header"))?;
458        if format_version > KEY_MANIFEST_FORMAT_VERSION {
459            return Err(StoreError::UnsupportedKeyManifest {
460                found: format_version,
461                supported: KEY_MANIFEST_FORMAT_VERSION,
462            });
463        }
464        let kek = lines
465            .next()
466            .ok_or_else(|| invalid("missing key-encryption key"))
467            .and_then(|line| KeyId::new(line).map_err(StoreError::Keyring))?;
468
469        // Neither loop below preallocates from its count; see `parse_count`.
470        let storage_count = parse_count(lines.next())?;
471        let mut storage_keys = Vec::new();
472        for _ in 0..storage_count {
473            let line = lines
474                .next()
475                .ok_or_else(|| invalid("truncated storage key"))?;
476            storage_keys.push(decode_storage_key(line)?);
477        }
478        let class_count = parse_count(lines.next())?;
479        let mut classes = Vec::new();
480        for _ in 0..class_count {
481            let line = lines.next().ok_or_else(|| invalid("truncated class key"))?;
482            classes.push(decode_class_key(line)?);
483        }
484        if lines.next().is_some() {
485            return Err(invalid("trailing content after the class entries"));
486        }
487        let manifest = Self {
488            format_version,
489            kek,
490            storage_keys,
491            classes,
492        };
493        manifest.validate()?;
494        Ok(manifest)
495    }
496
497    /// Rejects a manifest that is well-formed but structurally impossible.
498    ///
499    /// This record bootstraps every storage access a process makes, and it is
500    /// cleartext, so "it parsed" is not the bar. A manifest that names an epoch
501    /// twice, or none to write under, or two, describes a database that cannot
502    /// exist — and each of those would surface much later as a wrong key, a
503    /// failed open, or a silent choice between two active epochs.
504    ///
505    /// # Errors
506    ///
507    /// Returns [`StoreError::InvalidKeyManifest`] naming the violated
508    /// invariant.
509    pub fn validate(&self) -> Result<(), StoreError> {
510        // Strictly ascending subsumes uniqueness, and it is what `encode`
511        // emits, so a decode/encode round trip is byte-stable — which the
512        // manifest's compare-and-set update path depends on.
513        for pair in self.storage_keys.windows(2) {
514            if pair[0].epoch >= pair[1].epoch {
515                return Err(invalid("storage-key epochs are not strictly ascending"));
516            }
517            if pair[0].opened_at_t > pair[1].opened_at_t {
518                return Err(invalid("storage-key epochs are not ordered in t"));
519            }
520        }
521        for pair in self.classes.windows(2) {
522            if pair[0].class >= pair[1].class {
523                return Err(invalid("class entries are not strictly ascending"));
524            }
525        }
526        let active = self
527            .storage_keys
528            .iter()
529            .filter(|key| key.state == StorageKeyState::Active)
530            .count();
531        // An empty manifest is legal — it is what a database with classes but
532        // no storage encryption would hold — but a populated one has exactly
533        // one epoch that new writes go to.
534        if !self.storage_keys.is_empty() && active != 1 {
535            return Err(invalid(if active == 0 {
536                "no active storage-key epoch"
537            } else {
538                "more than one active storage-key epoch"
539            }));
540        }
541        Ok(())
542    }
543}
544
545/// Reads and validates a database's key manifest.
546///
547/// `None` means the database is unencrypted, which is a permanent property
548/// fixed at creation: there is no manifest to grow one later, and a reader
549/// that finds none may go on reading plaintext blobs.
550///
551/// # Errors
552///
553/// Returns [`StoreError`] when the root store cannot be read or the stored
554/// manifest is malformed or from a newer release.
555pub async fn load_key_manifest(
556    store: &dyn RootStore,
557    db: &str,
558) -> Result<Option<KeyManifest>, StoreError> {
559    match store.get_root(&keys_root_name(db)).await? {
560        Some(bytes) => KeyManifest::decode(&bytes).map(Some),
561        None => Ok(None),
562    }
563}
564
565/// Publishes a key manifest, fenced on the bytes it was read from.
566///
567/// `previous` is the manifest this update was computed from — `None` to
568/// create one. The compare-and-set means two concurrent rotations cannot
569/// both open an epoch: the loser sees [`StoreError::CasFailed`] and must
570/// re-read before retrying, rather than overwriting an epoch whose objects
571/// are already stored.
572///
573/// # Errors
574///
575/// Returns [`StoreError::CasFailed`] when the stored manifest changed, and
576/// [`StoreError::InvalidKeyManifest`] when `manifest` is structurally
577/// impossible — validation happens before the write so a bad manifest can
578/// never be the record every process bootstraps from.
579pub async fn publish_key_manifest(
580    store: &dyn RootStore,
581    db: &str,
582    previous: Option<&KeyManifest>,
583    manifest: &KeyManifest,
584) -> Result<(), StoreError> {
585    manifest.validate()?;
586    let expected = previous.map(KeyManifest::encode);
587    store
588        .cas_root(&keys_root_name(db), expected.as_deref(), &manifest.encode())
589        .await
590}
591
592fn invalid(reason: &str) -> StoreError {
593    StoreError::InvalidKeyManifest(reason.to_owned())
594}
595
596/// Reads an entry count.
597///
598/// The count is a stated length, not an allocation budget, and it must never
599/// become one. A manifest is a cleartext root record: it is restorable from any
600/// archive and writable by anything that can reach the root store, so a
601/// declared count of `usize::MAX` is a byte sequence a reader has to survive.
602/// Callers therefore grow their vectors as entries arrive and stop at the first
603/// absent line, which bounds both the allocation and the loop by the record's
604/// real length. Nothing here may `with_capacity` on the returned value.
605fn parse_count(line: Option<&str>) -> Result<usize, StoreError> {
606    line.and_then(|line| line.parse().ok())
607        .ok_or_else(|| invalid("missing entry count"))
608}
609
610fn decode_storage_key(line: &str) -> Result<StorageKey, StoreError> {
611    let mut fields = line.split(' ');
612    let mut next = || {
613        fields
614            .next()
615            .ok_or_else(|| invalid("truncated storage key"))
616    };
617    let epoch = next()?
618        .parse()
619        .map_err(|_| invalid("invalid storage-key epoch"))?;
620    let kek_epoch = next()?.parse().map_err(|_| invalid("invalid KEK epoch"))?;
621    let algorithm = next().and_then(|text| {
622        StorageAlgorithm::parse(text)
623            .ok_or_else(|| StoreError::UnsupportedKeyAlgorithm(text.to_owned()))
624    })?;
625    let state = next().and_then(|text| {
626        StorageKeyState::parse(text).ok_or_else(|| invalid("invalid storage-key state"))
627    })?;
628    let created_at_unix_ms = next()?
629        .parse()
630        .map_err(|_| invalid("invalid storage-key timestamp"))?;
631    let opened_at_t = next()?
632        .parse()
633        .map_err(|_| invalid("invalid storage-key basis"))?;
634    let live_objects = next()?
635        .parse()
636        .map_err(|_| invalid("invalid live-object count"))?;
637    let wrapped_dek = next().and_then(unhex)?;
638    if fields.next().is_some() {
639        return Err(invalid("trailing storage-key field"));
640    }
641    Ok(StorageKey {
642        epoch,
643        kek_epoch,
644        algorithm,
645        wrapped_dek,
646        created_at_unix_ms,
647        opened_at_t,
648        state,
649        live_objects,
650    })
651}
652
653fn decode_class_key(line: &str) -> Result<ProtectionClassKey, StoreError> {
654    let mut fields = line.splitn(3, ' ');
655    let mut next = || fields.next().ok_or_else(|| invalid("truncated class key"));
656    let class = next()?.parse().map_err(|_| invalid("invalid class id"))?;
657    let current_epoch = next()?
658        .parse()
659        .map_err(|_| invalid("invalid class epoch"))?;
660    let key_id = next().and_then(|text| KeyId::new(text).map_err(StoreError::Keyring))?;
661    Ok(ProtectionClassKey {
662        class,
663        key_id,
664        current_epoch,
665    })
666}
667
668fn hex(bytes: &[u8]) -> String {
669    use fmt::Write as _;
670    bytes.iter().fold(String::new(), |mut out, byte| {
671        let _ = write!(out, "{byte:02x}");
672        out
673    })
674}
675
676fn unhex(text: &str) -> Result<Vec<u8>, StoreError> {
677    if !text.len().is_multiple_of(2) {
678        return Err(invalid("wrapped key is not hex"));
679    }
680    text.as_bytes()
681        .chunks(2)
682        .map(|pair| {
683            std::str::from_utf8(pair)
684                .ok()
685                .and_then(|pair| u8::from_str_radix(pair, 16).ok())
686                .ok_or_else(|| invalid("wrapped key is not hex"))
687        })
688        .collect()
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use corium_crypt::StaticKeyring;
695
696    fn keyring() -> StaticKeyring {
697        let mut keyring = StaticKeyring::default();
698        keyring.insert(
699            KeyId::new("file:/etc/corium/storage.key").expect("kek"),
700            1,
701            SecretKey::new([9; 32]),
702            true,
703        );
704        keyring
705    }
706
707    fn kek() -> KeyId {
708        KeyId::new("file:/etc/corium/storage.key").expect("kek")
709    }
710
711    #[tokio::test]
712    async fn a_created_manifest_round_trips_and_unwraps() {
713        let keyring = keyring();
714        let mut manifest = KeyManifest::create(&keyring, kek(), 1_700_000_000_000)
715            .await
716            .expect("create");
717        manifest.classes.push(ProtectionClassKey {
718            class: 74,
719            key_id: KeyId::new("awskms:arn:aws:kms:us-west-2:1:key/2f1c").expect("class key"),
720            current_epoch: 3,
721        });
722
723        let encoded = manifest.encode();
724        assert_eq!(KeyManifest::decode(&encoded).expect("decode"), manifest);
725        // The wrapped key is the only key material in the record, and it is
726        // wrapped: the manifest never carries anything usable on its own.
727        assert!(!encoded.windows(32).any(|window| window == [9_u8; 32]));
728
729        assert_eq!(manifest.active_storage_epoch(), Some(1));
730        let keys = manifest
731            .unwrap_storage_keys(&keyring)
732            .await
733            .expect("unwrap");
734        assert_eq!(keys.keys().copied().collect::<Vec<_>>(), vec![1]);
735    }
736
737    #[tokio::test]
738    async fn rotation_opens_an_epoch_and_keeps_the_old_one_readable() {
739        let keyring = keyring();
740        let mut manifest = KeyManifest::create(&keyring, kek(), 1)
741            .await
742            .expect("create");
743        let first = manifest
744            .unwrap_storage_keys(&keyring)
745            .await
746            .expect("unwrap")[&1]
747            .clone();
748
749        assert_eq!(
750            manifest
751                .rotate_storage_key(&keyring, 2, 5_000)
752                .await
753                .expect("rotate"),
754            2
755        );
756        assert_eq!(manifest.active_storage_epoch(), Some(2));
757        assert_eq!(
758            manifest.storage_key(1).expect("epoch 1").state,
759            StorageKeyState::Retiring
760        );
761
762        let keys = manifest
763            .unwrap_storage_keys(&keyring)
764            .await
765            .expect("unwrap");
766        assert_eq!(keys.keys().copied().collect::<Vec<_>>(), vec![1, 2]);
767        assert_eq!(keys[&1], first, "a rotation must not disturb old epochs");
768        assert_ne!(keys[&2], first);
769    }
770
771    #[tokio::test]
772    async fn an_epochs_nonce_budget_is_the_span_of_t_it_covers() {
773        let keyring = keyring();
774        let mut manifest = KeyManifest::create(&keyring, kek(), 1)
775            .await
776            .expect("create");
777        // The first epoch opens on an empty database and covers everything so
778        // far; there is no counter to maintain, only where it started.
779        assert_eq!(manifest.log_records_sealed(1, 900), Some(900));
780        assert!(!manifest.storage_rotation_due(900));
781        assert!(manifest.storage_rotation_due(LOG_RECORDS_PER_EPOCH_WARN));
782
783        manifest
784            .rotate_storage_key(&keyring, 2, 1_000)
785            .await
786            .expect("rotate");
787        // Rotation closes the old span and opens a new one, so the budget the
788        // warning watches resets while the retired epoch's stays fixed.
789        assert_eq!(manifest.log_records_sealed(1, 4_000), Some(1_000));
790        assert_eq!(manifest.log_records_sealed(2, 4_000), Some(3_000));
791        assert_eq!(manifest.log_records_sealed(3, 4_000), None);
792        assert!(!manifest.storage_rotation_due(4_000));
793        assert!(manifest.storage_rotation_due(1_000 + LOG_RECORDS_PER_EPOCH_WARN));
794
795        // A basis behind the newest epoch reports an empty span rather than
796        // underflowing.
797        assert_eq!(manifest.log_records_sealed(2, 10), Some(0));
798        // ...and cannot be used to open one, which would leave the epochs
799        // unordered in `t` and make every span meaningless.
800        assert!(matches!(
801            manifest.rotate_storage_key(&keyring, 3, 10).await,
802            Err(StoreError::InvalidKeyManifest(_))
803        ));
804    }
805
806    #[tokio::test]
807    async fn structurally_impossible_manifests_are_rejected() {
808        let keyring = keyring();
809        let mut manifest = KeyManifest::create(&keyring, kek(), 1)
810            .await
811            .expect("create");
812        manifest
813            .rotate_storage_key(&keyring, 2, 1_000)
814            .await
815            .expect("rotate");
816        assert!(manifest.validate().is_ok());
817        assert!(KeyManifest::decode(&manifest.encode()).is_ok());
818
819        // Two epochs claiming new writes: a reader would have to guess.
820        let mut two_active = manifest.clone();
821        two_active.storage_keys[0].state = StorageKeyState::Active;
822        assert!(two_active.validate().is_err());
823        assert!(KeyManifest::decode(&two_active.encode()).is_err());
824
825        // None: nothing can be written, and nothing says so.
826        let mut none_active = manifest.clone();
827        for key in &mut none_active.storage_keys {
828            key.state = StorageKeyState::Retired;
829        }
830        assert!(none_active.validate().is_err());
831
832        // A duplicated epoch resolves to whichever entry is found first.
833        let mut duplicate = manifest.clone();
834        duplicate.storage_keys[1].epoch = 1;
835        assert!(duplicate.validate().is_err());
836
837        // Epochs running backwards in `t` make every nonce budget nonsense.
838        let mut unordered = manifest.clone();
839        unordered.storage_keys[0].opened_at_t = 9_999;
840        assert!(unordered.validate().is_err());
841
842        // Classes are keyed by entity id; a repeat is two answers to one
843        // question.
844        let mut classes = manifest;
845        classes.classes = vec![
846            ProtectionClassKey {
847                class: 74,
848                key_id: KeyId::new("file:a").expect("key"),
849                current_epoch: 1,
850            },
851            ProtectionClassKey {
852                class: 74,
853                key_id: KeyId::new("file:b").expect("key"),
854                current_epoch: 1,
855            },
856        ];
857        assert!(classes.validate().is_err());
858        assert!(KeyManifest::decode(&classes.encode()).is_err());
859    }
860
861    #[test]
862    fn trailing_content_after_the_entries_is_rejected() {
863        let mut encoded = b"corium-keys-v1\nfile:kek\n0\n0\n".to_vec();
864        assert!(KeyManifest::decode(&encoded).is_ok());
865        encoded.extend_from_slice(b"1 1 aes-256 active 0 0 0 00\n");
866        assert!(matches!(
867            KeyManifest::decode(&encoded),
868            Err(StoreError::InvalidKeyManifest(_))
869        ));
870    }
871
872    #[tokio::test]
873    async fn rewrapping_changes_the_kek_and_no_data_key() {
874        let mut keyring = keyring();
875        let replacement = KeyId::new("awskms:arn:aws:kms:us-west-2:1:key/9ab3").expect("kek");
876        keyring.insert(replacement.clone(), 7, SecretKey::new([4; 32]), true);
877
878        let mut manifest = KeyManifest::create(&keyring, kek(), 1)
879            .await
880            .expect("create");
881        manifest
882            .rotate_storage_key(&keyring, 2, 5_000)
883            .await
884            .expect("rotate");
885        let before = manifest
886            .unwrap_storage_keys(&keyring)
887            .await
888            .expect("unwrap");
889        let wrapped_before: Vec<_> = manifest
890            .storage_keys
891            .iter()
892            .map(|key| key.wrapped_dek.clone())
893            .collect();
894
895        manifest
896            .rewrap(&keyring, replacement.clone())
897            .await
898            .expect("rewrap");
899
900        assert_eq!(manifest.kek, replacement);
901        assert!(manifest.storage_keys.iter().all(|key| key.kek_epoch == 7));
902        assert!(
903            manifest
904                .storage_keys
905                .iter()
906                .zip(&wrapped_before)
907                .all(|(key, before)| key.wrapped_dek != *before)
908        );
909        assert_eq!(
910            manifest
911                .unwrap_storage_keys(&keyring)
912                .await
913                .expect("unwrap"),
914            before,
915            "re-wrapping must not change any data key"
916        );
917    }
918
919    #[tokio::test]
920    async fn an_unresolvable_epoch_fails_at_open_naming_the_key() {
921        let keyring = keyring();
922        let manifest = KeyManifest::create(&keyring, kek(), 1)
923            .await
924            .expect("create");
925        let error = manifest
926            .unwrap_storage_keys(&StaticKeyring::default())
927            .await
928            .expect_err("no KEK");
929        assert!(
930            error.to_string().contains("file:/etc/corium/storage.key"),
931            "{error}"
932        );
933    }
934
935    #[test]
936    fn a_newer_manifest_is_refused_rather_than_half_understood() {
937        let encoded = b"corium-keys-v2\nfile:kek\n0\n0\n".as_slice();
938        assert!(matches!(
939            KeyManifest::decode(encoded),
940            Err(StoreError::UnsupportedKeyManifest {
941                found: 2,
942                supported: 1
943            })
944        ));
945    }
946
947    #[test]
948    fn an_overstated_entry_count_is_bounded_by_the_record() {
949        // The manifest is a cleartext root record, so its declared counts are
950        // attacker-reachable. Decoding must be bounded by the bytes actually
951        // present — never allocate or iterate to a stated length.
952        for bytes in [
953            format!("corium-keys-v1\nfile:kek\n{}\n0\n", usize::MAX),
954            format!("corium-keys-v1\nfile:kek\n0\n{}\n", usize::MAX),
955            format!("corium-keys-v1\nfile:kek\n{}\n", u64::MAX),
956        ] {
957            assert!(matches!(
958                KeyManifest::decode(bytes.as_bytes()),
959                Err(StoreError::InvalidKeyManifest(_))
960            ));
961        }
962    }
963
964    #[test]
965    fn malformed_manifests_are_rejected() {
966        for bytes in [
967            b"".as_slice(),
968            b"corium-keys-v1\n".as_slice(),
969            b"corium-keys-v1\nfile:kek\n1\n".as_slice(),
970            // Wrapped key is not hex.
971            b"corium-keys-v1\nfile:kek\n1\n1 1 aes-256 active 0 0 0 xyz\n0\n".as_slice(),
972            // Unknown lifecycle state.
973            b"corium-keys-v1\nfile:kek\n1\n1 1 aes-256 sideways 0 0 0 00\n0\n".as_slice(),
974            // One field short of a storage key.
975            b"corium-keys-v1\nfile:kek\n1\n1 1 aes-256 active 0 0 00\n0\n".as_slice(),
976            // Empty key-encryption key identity.
977            b"corium-keys-v1\n\n0\n0\n".as_slice(),
978        ] {
979            assert!(
980                KeyManifest::decode(bytes).is_err(),
981                "accepted {:?}",
982                String::from_utf8_lossy(bytes)
983            );
984        }
985        assert!(matches!(
986            KeyManifest::decode(b"corium-keys-v1\nfile:kek\n1\n1 1 rot13 active 0 0 0 00\n0\n"),
987            Err(StoreError::UnsupportedKeyAlgorithm(_))
988        ));
989    }
990}