Skip to main content

quantum_shield/
keys.rs

1//! Key generation, derivation, and serialization.
2//!
3//! A [`KeyPair`] owns four independent secrets, all stored and exported in
4//! seed form (the FIPS-recommended private-key encoding; every derived key is
5//! recomputed from its seed on import):
6//!
7//! - an X25519 static secret (32 bytes),
8//! - an ML-KEM-1024 (d,z) seed (64 bytes, FIPS 203),
9//! - an Ed25519 seed (32 bytes),
10//! - an ML-DSA-87 xi seed (32 bytes, FIPS 204 Algorithm 6).
11//!
12//! Seeds are zeroized on drop. Public counterparts travel together as a
13//! [`PublicKeyBundle`], which validates every component when parsed.
14
15use crate::constants::*;
16use crate::error::{Error, Result};
17use crate::wire::{read_header, take, write_header};
18use alloc::boxed::Box;
19use alloc::vec::Vec;
20use ml_dsa::signature::Keypair as _;
21use ml_dsa::{KeyExport as _, MlDsa87};
22use ml_kem::{DecapsulationKey1024, EncapsulationKey1024};
23use sha3::{Digest, Sha3_256};
24use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
25
26/// A short, stable identifier for a public-key bundle: the first
27/// [`KEY_ID_LEN`] bytes of `SHA3-256(QSP2 bytes)`. Useful for referencing or
28/// pinning a key (e.g. in a rotation record) without carrying the full bundle.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub struct KeyId([u8; KEY_ID_LEN]);
31
32impl KeyId {
33    /// The raw identifier bytes.
34    pub fn as_bytes(&self) -> &[u8; KEY_ID_LEN] {
35        &self.0
36    }
37}
38
39/// The four private seeds, zeroized on drop.
40#[derive(Zeroize, ZeroizeOnDrop)]
41pub(crate) struct Seeds {
42    pub(crate) x25519_sk: [u8; X25519_SK_LEN],
43    pub(crate) mlkem_seed: [u8; MLKEM_SEED_LEN],
44    pub(crate) ed25519_seed: [u8; ED25519_SEED_LEN],
45    pub(crate) mldsa_seed: [u8; MLDSA_SEED_LEN],
46}
47
48/// A complete hybrid keypair: private seeds plus derived key objects.
49///
50/// Create one with [`KeyPair::generate`] or restore one from a previous
51/// [`KeyPair::to_secret_bytes`] export via [`KeyPair::from_secret_bytes`].
52pub struct KeyPair {
53    seeds: Seeds,
54    pub(crate) x25519_sk: x25519_dalek::StaticSecret,
55    pub(crate) mlkem_dk: Box<DecapsulationKey1024>,
56    pub(crate) ed25519_sk: ed25519_dalek::SigningKey,
57    pub(crate) mldsa_sk: Box<ml_dsa::SigningKey<MlDsa87>>,
58    public: PublicKeyBundle,
59}
60
61impl KeyPair {
62    /// Generate a fresh keypair from operating-system randomness.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`Error::RandomnessUnavailable`] if the OS RNG fails.
67    pub fn generate() -> Result<Self> {
68        let mut seeds = Seeds {
69            x25519_sk: [0u8; X25519_SK_LEN],
70            mlkem_seed: [0u8; MLKEM_SEED_LEN],
71            ed25519_seed: [0u8; ED25519_SEED_LEN],
72            mldsa_seed: [0u8; MLDSA_SEED_LEN],
73        };
74        getrandom::fill(&mut seeds.x25519_sk).map_err(|_| Error::RandomnessUnavailable)?;
75        getrandom::fill(&mut seeds.mlkem_seed).map_err(|_| Error::RandomnessUnavailable)?;
76        getrandom::fill(&mut seeds.ed25519_seed).map_err(|_| Error::RandomnessUnavailable)?;
77        getrandom::fill(&mut seeds.mldsa_seed).map_err(|_| Error::RandomnessUnavailable)?;
78        Ok(Self::from_seeds(seeds))
79    }
80
81    /// Derive all key objects from the given seeds.
82    pub(crate) fn from_seeds(seeds: Seeds) -> Self {
83        let x25519_sk = x25519_dalek::StaticSecret::from(seeds.x25519_sk);
84        let mlkem_dk = Box::new(DecapsulationKey1024::from_seed(seeds.mlkem_seed.into()));
85        let ed25519_sk = ed25519_dalek::SigningKey::from_bytes(&seeds.ed25519_seed);
86        let mldsa_sk = Box::new(ml_dsa::SigningKey::<MlDsa87>::from_seed(
87            &seeds.mldsa_seed.into(),
88        ));
89
90        let public = PublicKeyBundle {
91            x25519: x25519_dalek::PublicKey::from(&x25519_sk),
92            mlkem: Box::new(mlkem_dk.encapsulation_key().clone()),
93            ed25519: ed25519_sk.verifying_key(),
94            mldsa: Box::new(mldsa_sk.verifying_key()),
95        };
96
97        Self {
98            seeds,
99            x25519_sk,
100            mlkem_dk,
101            ed25519_sk,
102            mldsa_sk,
103            public,
104        }
105    }
106
107    /// The public half of this keypair, for sharing with peers.
108    pub fn public_keys(&self) -> &PublicKeyBundle {
109        &self.public
110    }
111
112    /// Export the private seeds as a v2 secret-key bundle (`QSK2`).
113    ///
114    /// The returned buffer is zeroized on drop, but the caller is responsible
115    /// for protecting any copy written to storage.
116    pub fn to_secret_bytes(&self) -> Zeroizing<Vec<u8>> {
117        let mut out = Vec::with_capacity(SECRET_BUNDLE_LEN);
118        write_header(&mut out, MAGIC_SECRET_BUNDLE);
119        out.extend_from_slice(&self.seeds.x25519_sk);
120        out.extend_from_slice(&self.seeds.mlkem_seed);
121        out.extend_from_slice(&self.seeds.ed25519_seed);
122        out.extend_from_slice(&self.seeds.mldsa_seed);
123        debug_assert_eq!(out.len(), SECRET_BUNDLE_LEN);
124        Zeroizing::new(out)
125    }
126
127    /// Restore a keypair from a [`KeyPair::to_secret_bytes`] export.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`Error::InvalidKey`] on malformed input, and version/suite
132    /// errors for artifacts from other format versions.
133    pub fn from_secret_bytes(bytes: &[u8]) -> Result<Self> {
134        let mut rest = read_header(bytes, MAGIC_SECRET_BUNDLE, Error::InvalidKey)?;
135        let seeds = Seeds {
136            x25519_sk: take(&mut rest, Error::InvalidKey)?,
137            mlkem_seed: take(&mut rest, Error::InvalidKey)?,
138            ed25519_seed: take(&mut rest, Error::InvalidKey)?,
139            mldsa_seed: take(&mut rest, Error::InvalidKey)?,
140        };
141        if !rest.is_empty() {
142            return Err(Error::InvalidKey);
143        }
144        Ok(Self::from_seeds(seeds))
145    }
146}
147
148impl core::fmt::Debug for KeyPair {
149    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
150        f.debug_struct("KeyPair").finish_non_exhaustive()
151    }
152}
153
154/// The public keys of a hybrid keypair.
155///
156/// Serialize with [`PublicKeyBundle::to_bytes`]; parsing via
157/// [`PublicKeyBundle::from_bytes`] validates every component.
158#[derive(Clone)]
159pub struct PublicKeyBundle {
160    pub(crate) x25519: x25519_dalek::PublicKey,
161    pub(crate) mlkem: Box<EncapsulationKey1024>,
162    pub(crate) ed25519: ed25519_dalek::VerifyingKey,
163    pub(crate) mldsa: Box<ml_dsa::VerifyingKey<MlDsa87>>,
164}
165
166impl PublicKeyBundle {
167    /// Serialize to the v2 public-key bundle format (`QSP2`).
168    pub fn to_bytes(&self) -> Vec<u8> {
169        let mut out = Vec::with_capacity(PUBLIC_BUNDLE_LEN);
170        write_header(&mut out, MAGIC_PUBLIC_BUNDLE);
171        out.extend_from_slice(self.x25519.as_bytes());
172        out.extend_from_slice(&self.mlkem.to_bytes());
173        out.extend_from_slice(self.ed25519.as_bytes());
174        out.extend_from_slice(&self.mldsa.encode());
175        debug_assert_eq!(out.len(), PUBLIC_BUNDLE_LEN);
176        out
177    }
178
179    /// This bundle's [`KeyId`] — `SHA3-256(self.to_bytes())[..16]`.
180    pub fn key_id(&self) -> KeyId {
181        let digest = Sha3_256::digest(self.to_bytes());
182        let mut id = [0u8; KEY_ID_LEN];
183        id.copy_from_slice(&digest[..KEY_ID_LEN]);
184        KeyId(id)
185    }
186
187    /// Parse a v2 public-key bundle, validating the length and the components
188    /// that support validation.
189    ///
190    /// The X25519, ML-KEM-1024, and Ed25519 components are checked (a
191    /// non-canonical Ed25519 point or an out-of-range ML-KEM key is rejected).
192    /// ML-DSA-87 verifying keys have no upstream validation and are only
193    /// length-checked; a structurally-invalid one simply fails verification
194    /// later. X25519 has no point validation by design (every 32-byte string is
195    /// a valid u-coordinate).
196    ///
197    /// # Errors
198    ///
199    /// Returns [`Error::InvalidKey`] if the encoding is malformed or a
200    /// validatable component fails validation.
201    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
202        let mut rest = read_header(bytes, MAGIC_PUBLIC_BUNDLE, Error::InvalidKey)?;
203
204        let x25519_bytes: [u8; X25519_PK_LEN] = take(&mut rest, Error::InvalidKey)?;
205        let mlkem_bytes: [u8; MLKEM1024_EK_LEN] = take(&mut rest, Error::InvalidKey)?;
206        let ed25519_bytes: [u8; ED25519_PK_LEN] = take(&mut rest, Error::InvalidKey)?;
207        let mldsa_bytes: [u8; MLDSA87_VK_LEN] = take(&mut rest, Error::InvalidKey)?;
208        if !rest.is_empty() {
209            return Err(Error::InvalidKey);
210        }
211
212        let x25519 = x25519_dalek::PublicKey::from(x25519_bytes);
213        let mlkem =
214            EncapsulationKey1024::new(&mlkem_bytes.into()).map_err(|_| Error::InvalidKey)?;
215        let ed25519 = ed25519_dalek::VerifyingKey::from_bytes(&ed25519_bytes)
216            .map_err(|_| Error::InvalidKey)?;
217        let mldsa = ml_dsa::VerifyingKey::<MlDsa87>::decode(&mldsa_bytes.into());
218
219        Ok(Self {
220            x25519,
221            mlkem: Box::new(mlkem),
222            ed25519,
223            mldsa: Box::new(mldsa),
224        })
225    }
226}
227
228impl core::fmt::Debug for PublicKeyBundle {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        f.debug_struct("PublicKeyBundle")
231            .field("x25519", &self.x25519)
232            .finish_non_exhaustive()
233    }
234}
235
236impl PartialEq for PublicKeyBundle {
237    fn eq(&self, other: &Self) -> bool {
238        self.to_bytes() == other.to_bytes()
239    }
240}
241
242impl Eq for PublicKeyBundle {}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn constants_match_crate_types() {
250        use ml_dsa::signature::SignatureEncoding as _;
251        let kp = KeyPair::generate().unwrap();
252        assert_eq!(kp.public.mlkem.to_bytes().len(), MLKEM1024_EK_LEN);
253        assert_eq!(kp.public.mldsa.encode().len(), MLDSA87_VK_LEN);
254        use ed25519_dalek::Signer as _;
255        use ml_dsa::signature::Signer as _;
256        let ed_sig = kp.ed25519_sk.sign(b"x");
257        assert_eq!(ed_sig.to_bytes().len(), ED25519_SIG_LEN);
258        let pq_sig: ml_dsa::Signature<MlDsa87> = kp.mldsa_sk.sign(b"x");
259        assert_eq!(pq_sig.to_bytes().len(), MLDSA87_SIG_LEN);
260    }
261
262    #[test]
263    fn public_bundle_roundtrip() {
264        let kp = KeyPair::generate().unwrap();
265        let bytes = kp.public_keys().to_bytes();
266        assert_eq!(bytes.len(), PUBLIC_BUNDLE_LEN);
267        let parsed = PublicKeyBundle::from_bytes(&bytes).unwrap();
268        assert_eq!(parsed, *kp.public_keys());
269    }
270
271    #[test]
272    fn secret_bundle_roundtrip() {
273        let kp = KeyPair::generate().unwrap();
274        let secret = kp.to_secret_bytes();
275        assert_eq!(secret.len(), SECRET_BUNDLE_LEN);
276        let restored = KeyPair::from_secret_bytes(&secret).unwrap();
277        assert_eq!(restored.public_keys(), kp.public_keys());
278    }
279
280    #[test]
281    fn secret_bundle_rejects_bad_input() {
282        let kp = KeyPair::generate().unwrap();
283        let secret = kp.to_secret_bytes();
284        // Truncated
285        assert_eq!(
286            KeyPair::from_secret_bytes(&secret[..secret.len() - 1]).unwrap_err(),
287            Error::InvalidKey
288        );
289        // Extended
290        let mut long = secret.to_vec();
291        long.push(0);
292        assert_eq!(
293            KeyPair::from_secret_bytes(&long).unwrap_err(),
294            Error::InvalidKey
295        );
296        // Public bundle passed as secret bundle
297        assert_eq!(
298            KeyPair::from_secret_bytes(&kp.public_keys().to_bytes()).unwrap_err(),
299            Error::InvalidKey
300        );
301    }
302
303    #[test]
304    fn public_bundle_rejects_invalid_ed25519_point() {
305        // Find an encoding that fails Ed25519 point decompression (about half
306        // of all y-coordinates do), then check our parser propagates the
307        // rejection instead of storing the raw bytes unvalidated.
308        let invalid = (0u8..=255)
309            .map(|b| {
310                let mut k = [b; ED25519_PK_LEN];
311                k[0] = b.wrapping_add(1);
312                k
313            })
314            .find(|k| ed25519_dalek::VerifyingKey::from_bytes(k).is_err())
315            .expect("some encoding must fail decompression");
316
317        let kp = KeyPair::generate().unwrap();
318        let mut bytes = kp.public_keys().to_bytes();
319        let off = HEADER_LEN + X25519_PK_LEN + MLKEM1024_EK_LEN;
320        bytes[off..off + ED25519_PK_LEN].copy_from_slice(&invalid);
321        assert_eq!(
322            PublicKeyBundle::from_bytes(&bytes).unwrap_err(),
323            Error::InvalidKey
324        );
325    }
326
327    #[test]
328    fn public_bundle_rejects_invalid_mlkem_key() {
329        let kp = KeyPair::generate().unwrap();
330        let mut bytes = kp.public_keys().to_bytes();
331        // Saturate the ML-KEM key bytes; coefficients out of range must fail
332        // the modulus check in EncapsulationKey::new.
333        let off = HEADER_LEN + X25519_PK_LEN;
334        bytes[off..off + MLKEM1024_EK_LEN].fill(0xFF);
335        assert_eq!(
336            PublicKeyBundle::from_bytes(&bytes).unwrap_err(),
337            Error::InvalidKey
338        );
339    }
340
341    #[test]
342    fn debug_redacts_secrets() {
343        let kp = KeyPair::generate().unwrap();
344        let dbg = format!("{kp:?}");
345        assert_eq!(dbg, "KeyPair { .. }");
346    }
347}