Skip to main content

dpp_crypto/keystore/
rotation.rs

1use aes_gcm::{
2    Nonce,
3    aead::{Aead, consts::U12},
4};
5use anyhow::Result;
6use rand::Rng;
7use sha2::{Digest, Sha256};
8use zeroize::Zeroize;
9
10use super::entry::KeyEntry;
11use super::store::{KeyRecord, KeyStore};
12use ed25519_dalek::SigningKey;
13
14/// Build the archived-record key for `key_id`.
15///
16/// Suffixed with a time-ordered UUID (v7) rather than a raw nanosecond
17/// timestamp: two rotations landing on the same clock tick (or a tight
18/// successive-rotation loop, e.g. in tests) previously collided on the same
19/// map key, silently overwriting an already-archived record — a signature
20/// made with the overwritten key would then never verify again. `load_archived_keys`'s
21/// `entries.sort_by_key` still recovers chronological order, since a v7 UUID's
22/// leading bits are a millisecond timestamp.
23fn archived_key_name(key_id: &str) -> String {
24    format!("{key_id}#archived-{}", uuid::Uuid::now_v7())
25}
26
27impl KeyStore {
28    /// Archive the current key under a timestamped key so it can still be used
29    /// to verify older signatures after rotation.
30    pub fn archive_key(&self, key_id: &str) -> Result<()> {
31        if *self.needs_migration.read().expect("lock") {
32            anyhow::bail!(
33                "key store requires KDF migration before writes are allowed — \
34                 call migrate_if_needed() first"
35            );
36        }
37        let mut map = self.records.write().expect("key store write lock poisoned");
38        if let Some(record) = map.get(key_id) {
39            let archived = record.clone();
40            let archive_key = archived_key_name(key_id);
41            map.insert(archive_key, archived);
42            self.persist_envelope(&map)?;
43        }
44        Ok(())
45    }
46
47    /// Atomically rotate the key: archive the current key (kept valid so older
48    /// signatures still verify) and install a fresh current key, in a **single**
49    /// persisted write. Use for routine/hygiene rotation. Returns the new key.
50    ///
51    /// Unlike calling [`archive_key`](Self::archive_key) then
52    /// [`generate_key`](Self::generate_key), there is no intermediate on-disk
53    /// state where the archive exists but the new key does not (identity I4).
54    pub fn rotate_key(&self, key_id: &str) -> Result<KeyEntry> {
55        self.rotate_inner(key_id, false)
56    }
57
58    /// Atomically **revoke** the current key and install a fresh one. The old key
59    /// is archived but marked revoked, so `dpp_vc::did_builder` drops it from
60    /// the published DID document and signatures it produced no longer verify.
61    /// Use this on key **compromise** (vs. hygiene rotation, where the old key
62    /// stays valid — see [`rotate_key`](Self::rotate_key)).
63    pub fn revoke_and_rotate(&self, key_id: &str) -> Result<KeyEntry> {
64        self.rotate_inner(key_id, true)
65    }
66
67    fn rotate_inner(&self, key_id: &str, revoke_old: bool) -> Result<KeyEntry> {
68        if *self.needs_migration.read().expect("lock") {
69            anyhow::bail!(
70                "key store requires KDF migration before writes are allowed — \
71                 call migrate_if_needed() first"
72            );
73        }
74
75        // Prepare the new key material up front so the lock-held section is just
76        // the in-memory swap + single persist.
77        let signing_key = SigningKey::generate(&mut crate::os_rng());
78        let verifying_key = signing_key.verifying_key();
79        let fingerprint = hex::encode(Sha256::digest(verifying_key.as_bytes()));
80        let verifying_key_hex = hex::encode(verifying_key.as_bytes());
81        let mut nonce_bytes = [0u8; 12];
82        crate::os_rng().fill_bytes(&mut nonce_bytes);
83        let nonce = <&Nonce<U12>>::from(&nonce_bytes);
84        let mut raw = signing_key.to_bytes();
85        // Bound to the new key's own fingerprint. The archived copy below keeps
86        // the one it was sealed under, which is why the binding is to the
87        // fingerprint rather than the map key it is filed at.
88        let encrypted = self
89            .cipher
90            .encrypt(
91                nonce,
92                aes_gcm::aead::Payload {
93                    msg: raw.as_ref(),
94                    aad: fingerprint.as_bytes(),
95                },
96            )
97            .map_err(|_| anyhow::anyhow!("AES-GCM encrypt failed"))?;
98        raw.zeroize();
99        let new_record = KeyRecord::new(
100            encrypted,
101            nonce_bytes.to_vec(),
102            fingerprint.clone(),
103            verifying_key_hex,
104        );
105
106        {
107            let mut map = self.records.write().expect("key store write lock poisoned");
108            // Archive the existing current key (if any), marking it revoked when
109            // this is a compromise rotation.
110            if let Some(record) = map.get(key_id) {
111                let archive_name = archived_key_name(key_id);
112                let archived = KeyRecord {
113                    revoked: revoke_old,
114                    ..record.clone()
115                };
116                map.insert(archive_name, archived);
117            }
118            map.insert(key_id.to_owned(), new_record);
119            self.persist_envelope(&map)?;
120        }
121
122        Ok(KeyEntry {
123            signing_key,
124            verifying_key,
125            fingerprint,
126            revoked: false,
127            algorithm: super::store::default_algorithm(),
128        })
129    }
130}