Skip to main content

typesec_integrations/did/
keystore.rs

1//! Key-store and envelope crypto boundary, plus the production Ed25519/X25519
2//! key store.
3
4use std::collections::{HashMap, HashSet};
5
6use super::crypto::{hex_decode, hex_encode, sha256_tagged};
7use super::document::{DidDocument, VerificationMethod};
8use super::error::DidError;
9use super::identifier::Did;
10
11/// Key-store and envelope crypto boundary.
12pub trait DidKeyStore: Send + Sync {
13    /// Sign bytes as `signer`.
14    fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError>;
15
16    /// Verify a signature with the public key in `method`.
17    fn verify(
18        &self,
19        method: &VerificationMethod,
20        message: &[u8],
21        signature: &str,
22    ) -> Result<(), DidError>;
23
24    /// Encrypt bytes from `sender` to the recipient public key.
25    ///
26    /// `associated_data` is authenticated but not encrypted (AEAD AAD); the
27    /// recipient must supply the identical bytes to [`decrypt_for`][Self::decrypt_for].
28    fn encrypt_for(
29        &self,
30        sender: &Did,
31        recipient_public_key: &[u8],
32        plaintext: &[u8],
33        nonce: &[u8],
34        associated_data: &[u8],
35    ) -> Result<String, DidError>;
36
37    /// Decrypt bytes addressed to `recipient` from the sender public key.
38    ///
39    /// `associated_data` must match the bytes passed to [`encrypt_for`][Self::encrypt_for]
40    /// or authentication fails.
41    fn decrypt_for(
42        &self,
43        recipient: &Did,
44        sender_public_key: &[u8],
45        nonce: &[u8],
46        ciphertext_hex: &str,
47        associated_data: &[u8],
48    ) -> Result<Vec<u8>, DidError>;
49}
50
51// ── Production key store ──────────────────────────────────────────────────────
52
53/// Real key material for a local DID subject.
54///
55/// Holds an Ed25519 signing key (advertised as the DID document's
56/// authentication key) and an independent X25519 static secret (advertised as
57/// the key-agreement key).
58#[derive(Clone)]
59pub struct Ed25519DidKey {
60    signing: ed25519_dalek::SigningKey,
61    agreement: x25519_dalek::StaticSecret,
62}
63
64impl Ed25519DidKey {
65    /// Generate a key pair from the operating system RNG.
66    pub fn generate() -> Result<Self, DidError> {
67        let mut signing_seed = [0u8; 32];
68        let mut agreement_seed = [0u8; 32];
69        getrandom::getrandom(&mut signing_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
70        getrandom::getrandom(&mut agreement_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
71        Ok(Self::from_seeds(signing_seed, agreement_seed))
72    }
73
74    /// Derive a key pair deterministically from a seed via SHA-256 expansion.
75    ///
76    /// Only as strong as the seed's entropy — use [`generate`][Self::generate]
77    /// unless you need reproducible keys (tests, fixtures).
78    pub fn from_seed(seed: impl AsRef<[u8]>) -> Self {
79        let signing_seed = sha256_tagged(b"typesec-ed25519-signing", seed.as_ref());
80        let agreement_seed = sha256_tagged(b"typesec-x25519-agreement", seed.as_ref());
81        Self::from_seeds(signing_seed, agreement_seed)
82    }
83
84    fn from_seeds(signing_seed: [u8; 32], agreement_seed: [u8; 32]) -> Self {
85        Self {
86            signing: ed25519_dalek::SigningKey::from_bytes(&signing_seed),
87            agreement: x25519_dalek::StaticSecret::from(agreement_seed),
88        }
89    }
90
91    /// Ed25519 public key bytes (the DID document authentication key).
92    pub fn signing_public(&self) -> [u8; 32] {
93        self.signing.verifying_key().to_bytes()
94    }
95
96    /// X25519 public key bytes (the DID document key-agreement key).
97    pub fn agreement_public(&self) -> [u8; 32] {
98        x25519_dalek::PublicKey::from(&self.agreement).to_bytes()
99    }
100
101    /// Build a DID document advertising this key pair's public halves.
102    pub fn document(&self, did: Did) -> DidDocument {
103        DidDocument::with_signing_and_agreement_keys(
104            did,
105            self.signing_public(),
106            self.agreement_public(),
107        )
108    }
109}
110
111impl std::fmt::Debug for Ed25519DidKey {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("Ed25519DidKey")
114            .field("signing_public", &hex_encode(&self.signing_public()))
115            .field("agreement_public", &hex_encode(&self.agreement_public()))
116            .finish_non_exhaustive()
117    }
118}
119
120/// Production [`DidKeyStore`]: Ed25519 signatures, X25519 ECDH, and
121/// ChaCha20-Poly1305 authenticated payload encryption.
122#[derive(Debug, Default, Clone)]
123pub struct Ed25519DidKeyStore {
124    keys: HashMap<Did, Vec<Ed25519DidKeyRecord>>,
125    retired_methods: HashSet<String>,
126}
127
128#[derive(Debug, Clone)]
129struct Ed25519DidKeyRecord {
130    version: u64,
131    key: Ed25519DidKey,
132    retired: bool,
133}
134
135impl Ed25519DidKeyStore {
136    /// Create an empty key store.
137    pub fn new() -> Self {
138        Self::default()
139    }
140
141    /// Add a key pair for a DID.
142    pub fn with_key(mut self, did: Did, key: Ed25519DidKey) -> Self {
143        self.keys.insert(
144            did,
145            vec![Ed25519DidKeyRecord {
146                version: 1,
147                key,
148                retired: false,
149            }],
150        );
151        self
152    }
153
154    /// Rotate a DID to a new active key version.
155    ///
156    /// Existing non-retired versions remain in the DID document for in-flight
157    /// envelope verification until explicitly retired.
158    pub fn rotate_key(&mut self, did: &Did, key: Ed25519DidKey) -> Result<u64, DidError> {
159        let records = self
160            .keys
161            .get_mut(did)
162            .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
163        let next_version = records
164            .iter()
165            .map(|record| record.version)
166            .max()
167            .unwrap_or(0)
168            + 1;
169        records.push(Ed25519DidKeyRecord {
170            version: next_version,
171            key,
172            retired: false,
173        });
174        Ok(next_version)
175    }
176
177    /// Retire an old key version.
178    ///
179    /// Retired authentication methods are omitted from newly generated DID
180    /// documents and are rejected by this store's verifier.
181    pub fn retire_key(&mut self, did: &Did, version: u64) -> Result<(), DidError> {
182        if self.active_key_version(did)? == version {
183            return Err(DidError::CannotRetireActiveKey {
184                did: did.to_string(),
185                version,
186            });
187        }
188
189        let records = self
190            .keys
191            .get_mut(did)
192            .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
193        let record = records
194            .iter_mut()
195            .find(|record| record.version == version)
196            .ok_or_else(|| DidError::MissingKeyVersion {
197                did: did.to_string(),
198                version,
199            })?;
200        record.retired = true;
201        self.retired_methods
202            .insert(Self::signing_method_id(did, version));
203        self.retired_methods
204            .insert(Self::agreement_method_id(did, version));
205        Ok(())
206    }
207
208    /// Active signing/encryption version for `did`.
209    pub fn active_key_version(&self, did: &Did) -> Result<u64, DidError> {
210        Ok(self.active_record(did)?.version)
211    }
212
213    /// Build a rotation-aware DID document for one local DID.
214    pub fn document(&self, did: &Did) -> Result<DidDocument, DidError> {
215        let records = self
216            .keys
217            .get(did)
218            .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
219        let active_version = self.active_key_version(did)?;
220        let mut verification_method = Vec::new();
221        let mut authentication = Vec::new();
222        let mut key_agreement = Vec::new();
223
224        for record in records.iter().filter(|record| !record.retired) {
225            let status = if record.version == active_version {
226                "active"
227            } else {
228                "previous"
229            };
230            let signing_id = Self::signing_method_id(did, record.version);
231            let agreement_id = Self::agreement_method_id(did, record.version);
232            verification_method.push(VerificationMethod {
233                id: signing_id.clone(),
234                method_type: "Ed25519VerificationKey2020".to_owned(),
235                controller: did.clone(),
236                public_key_hex: hex_encode(&record.key.signing_public()),
237                key_version: Some(record.version),
238                key_status: Some(status.to_owned()),
239            });
240            verification_method.push(VerificationMethod {
241                id: agreement_id.clone(),
242                method_type: "X25519KeyAgreementKey2020".to_owned(),
243                controller: did.clone(),
244                public_key_hex: hex_encode(&record.key.agreement_public()),
245                key_version: Some(record.version),
246                key_status: Some(status.to_owned()),
247            });
248
249            if record.version == active_version {
250                authentication.insert(0, signing_id);
251                key_agreement.insert(0, agreement_id);
252            } else {
253                authentication.push(signing_id);
254                key_agreement.push(agreement_id);
255            }
256        }
257
258        Ok(DidDocument {
259            id: did.clone(),
260            verification_method,
261            authentication,
262            key_agreement,
263            service: Vec::new(),
264        })
265    }
266
267    fn active_record(&self, did: &Did) -> Result<&Ed25519DidKeyRecord, DidError> {
268        self.keys
269            .get(did)
270            .and_then(|records| {
271                records
272                    .iter()
273                    .filter(|record| !record.retired)
274                    .max_by_key(|record| record.version)
275            })
276            .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))
277    }
278
279    fn signing_method_id(did: &Did, version: u64) -> String {
280        if version == 1 {
281            format!("{did}#key-1")
282        } else {
283            format!("{did}#key-signing-v{version}")
284        }
285    }
286
287    fn agreement_method_id(did: &Did, version: u64) -> String {
288        if version == 1 {
289            format!("{did}#key-2")
290        } else {
291            format!("{did}#key-agreement-v{version}")
292        }
293    }
294
295    fn aead_key(shared_secret: &[u8; 32]) -> chacha20poly1305::Key {
296        let digest = sha256_tagged(b"typesec-did-aead", shared_secret);
297        chacha20poly1305::Key::from(digest)
298    }
299}
300
301impl DidKeyStore for Ed25519DidKeyStore {
302    fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError> {
303        use ed25519_dalek::Signer;
304        let record = self.active_record(signer)?;
305        Ok(hex_encode(&record.key.signing.sign(message).to_bytes()))
306    }
307
308    fn verify(
309        &self,
310        method: &VerificationMethod,
311        message: &[u8],
312        signature: &str,
313    ) -> Result<(), DidError> {
314        use ed25519_dalek::Verifier;
315        if self.retired_methods.contains(&method.id) {
316            return Err(DidError::RetiredKey(method.id.clone()));
317        }
318        let public: [u8; 32] = method
319            .public_key()?
320            .try_into()
321            .map_err(|_| DidError::InvalidKey("ed25519 public key must be 32 bytes".into()))?;
322        let verifying = ed25519_dalek::VerifyingKey::from_bytes(&public)
323            .map_err(|e| DidError::InvalidKey(e.to_string()))?;
324        let signature_bytes: [u8; 64] = hex_decode(signature)?
325            .try_into()
326            .map_err(|_| DidError::InvalidSignature)?;
327        verifying
328            .verify(
329                message,
330                &ed25519_dalek::Signature::from_bytes(&signature_bytes),
331            )
332            .map_err(|_| DidError::InvalidSignature)
333    }
334
335    fn encrypt_for(
336        &self,
337        sender: &Did,
338        recipient_public_key: &[u8],
339        plaintext: &[u8],
340        nonce: &[u8],
341        associated_data: &[u8],
342    ) -> Result<String, DidError> {
343        use chacha20poly1305::KeyInit;
344        use chacha20poly1305::aead::{Aead, Payload};
345        let sender_key = &self.active_record(sender)?.key;
346        let recipient: [u8; 32] = recipient_public_key
347            .try_into()
348            .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
349        let shared = sender_key
350            .agreement
351            .diffie_hellman(&x25519_dalek::PublicKey::from(recipient));
352        let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
353        let cipher = chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
354        let ciphertext = cipher
355            .encrypt(
356                &chacha20poly1305::Nonce::from(nonce),
357                Payload {
358                    msg: plaintext,
359                    aad: associated_data,
360                },
361            )
362            .map_err(|_| DidError::EncryptionFailed)?;
363        Ok(hex_encode(&ciphertext))
364    }
365
366    fn decrypt_for(
367        &self,
368        recipient: &Did,
369        sender_public_key: &[u8],
370        nonce: &[u8],
371        ciphertext_hex: &str,
372        associated_data: &[u8],
373    ) -> Result<Vec<u8>, DidError> {
374        use chacha20poly1305::KeyInit;
375        use chacha20poly1305::aead::{Aead, Payload};
376        let sender: [u8; 32] = sender_public_key
377            .try_into()
378            .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
379        let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
380        let ciphertext = hex_decode(ciphertext_hex)?;
381        let records = self
382            .keys
383            .get(recipient)
384            .ok_or_else(|| DidError::MissingPrivateKey(recipient.to_string()))?;
385
386        for record in records.iter().filter(|record| !record.retired) {
387            let shared = record
388                .key
389                .agreement
390                .diffie_hellman(&x25519_dalek::PublicKey::from(sender));
391            let cipher =
392                chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
393            if let Ok(plaintext) = cipher.decrypt(
394                &chacha20poly1305::Nonce::from(nonce),
395                Payload {
396                    msg: ciphertext.as_slice(),
397                    aad: associated_data,
398                },
399            ) {
400                return Ok(plaintext);
401            }
402        }
403
404        Err(DidError::DecryptionFailed)
405    }
406}