Skip to main content

vcl_protocol/
pq_crypto.rs

1//! # VCL Post-Quantum Cryptography
2//!
3//! Hybrid key exchange combining classical X25519 with post-quantum
4//! CRYSTALS-Kyber768 (ML-KEM). Even if quantum computers break X25519,
5//! the Kyber layer keeps the session secure.
6//!
7//! ## How hybrid KEM works
8//!
9//! ```text
10//! Client                                    Server
11//!    |                                         |
12//!    | -- X25519 pubkey + Kyber768 pubkey ---> |
13//!    |                                         |
14//!    | <-- X25519 pubkey + Kyber ciphertext -- |
15//!    |                                         |
16//! shared_secret = SHA-256(x25519_secret || kyber_secret)
17//! ```
18//!
19//! ## Example
20//!
21//! ```rust,ignore
22//! use vcl_protocol::pq_crypto::{PqKeyPair, PqHandshake};
23//!
24//! let mut client_kp = PqKeyPair::generate();
25//! let client_hello = client_kp.client_hello();
26//!
27//! let mut server_kp = PqKeyPair::generate();
28//! let (server_hello, server_secret) = server_kp.server_respond(&client_hello).unwrap();
29//!
30//! let client_secret = client_kp.client_finalize(&server_hello).unwrap();
31//!
32//! assert_eq!(client_secret, server_secret);
33//! ```
34
35use crate::error::VCLError;
36use pqcrypto_kyber::kyber768;
37use pqcrypto_traits::kem::{PublicKey, SecretKey, Ciphertext, SharedSecret};
38use sha2::{Sha256, Digest};
39use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
40use rand::rngs::OsRng;
41use tracing::{debug, info};
42
43/// Size of the combined hybrid shared secret.
44pub const HYBRID_SECRET_SIZE: usize = 32;
45
46/// Public key bundle sent during handshake (X25519 + Kyber768).
47#[derive(Debug, Clone)]
48pub struct PqPublicBundle {
49    /// X25519 public key (32 bytes).
50    pub x25519_pub: [u8; 32],
51    /// Kyber768 public key.
52    pub kyber_pub: Vec<u8>,
53}
54
55impl PqPublicBundle {
56    /// Serialize to bytes: [x25519_pub (32)] [kyber_pub_len (4 BE)] [kyber_pub]
57    pub fn to_bytes(&self) -> Vec<u8> {
58        let mut out = Vec::with_capacity(32 + 4 + self.kyber_pub.len());
59        out.extend_from_slice(&self.x25519_pub);
60        let klen = self.kyber_pub.len() as u32;
61        out.extend_from_slice(&klen.to_be_bytes());
62        out.extend_from_slice(&self.kyber_pub);
63        out
64    }
65
66    /// Deserialize from bytes.
67    pub fn from_bytes(data: &[u8]) -> Result<Self, VCLError> {
68        if data.len() < 36 {
69            return Err(VCLError::InvalidPacket(
70                "PqPublicBundle: too short".to_string()
71            ));
72        }
73        let mut x25519_pub = [0u8; 32];
74        x25519_pub.copy_from_slice(&data[0..32]);
75        let klen = u32::from_be_bytes([data[32], data[33], data[34], data[35]]) as usize;
76        if data.len() < 36 + klen {
77            return Err(VCLError::InvalidPacket(
78                "PqPublicBundle: kyber key truncated".to_string()
79            ));
80        }
81        let kyber_pub = data[36..36 + klen].to_vec();
82        Ok(PqPublicBundle { x25519_pub, kyber_pub })
83    }
84}
85
86/// Server response bundle: X25519 public key + Kyber ciphertext.
87#[derive(Debug, Clone)]
88pub struct PqServerResponse {
89    /// Server X25519 public key (32 bytes).
90    pub x25519_pub: [u8; 32],
91    /// Kyber768 ciphertext encapsulating the Kyber shared secret.
92    pub kyber_ciphertext: Vec<u8>,
93}
94
95impl PqServerResponse {
96    /// Serialize to bytes.
97    pub fn to_bytes(&self) -> Vec<u8> {
98        let mut out = Vec::with_capacity(32 + 4 + self.kyber_ciphertext.len());
99        out.extend_from_slice(&self.x25519_pub);
100        let clen = self.kyber_ciphertext.len() as u32;
101        out.extend_from_slice(&clen.to_be_bytes());
102        out.extend_from_slice(&self.kyber_ciphertext);
103        out
104    }
105
106    /// Deserialize from bytes.
107    pub fn from_bytes(data: &[u8]) -> Result<Self, VCLError> {
108        if data.len() < 36 {
109            return Err(VCLError::InvalidPacket(
110                "PqServerResponse: too short".to_string()
111            ));
112        }
113        let mut x25519_pub = [0u8; 32];
114        x25519_pub.copy_from_slice(&data[0..32]);
115        let clen = u32::from_be_bytes([data[32], data[33], data[34], data[35]]) as usize;
116        if data.len() < 36 + clen {
117            return Err(VCLError::InvalidPacket(
118                "PqServerResponse: ciphertext truncated".to_string()
119            ));
120        }
121        let kyber_ciphertext = data[36..36 + clen].to_vec();
122        Ok(PqServerResponse { x25519_pub, kyber_ciphertext })
123    }
124}
125
126/// Post-quantum hybrid key pair (X25519 + Kyber768).
127pub struct PqKeyPair {
128    /// X25519 ephemeral secret (consumed during finalize).
129    x25519_secret: Option<EphemeralSecret>,
130    /// X25519 public key.
131    x25519_pub: X25519PublicKey,
132    /// Kyber768 public key.
133    kyber_pub: kyber768::PublicKey,
134    /// Kyber768 secret key.
135    kyber_sec: kyber768::SecretKey,
136}
137
138impl PqKeyPair {
139    /// Generate a new hybrid key pair.
140    pub fn generate() -> Self {
141        let x25519_secret = EphemeralSecret::random_from_rng(OsRng);
142        let x25519_pub = X25519PublicKey::from(&x25519_secret);
143        let (kyber_pub, kyber_sec) = kyber768::keypair();
144
145        debug!("PqKeyPair generated (X25519 + Kyber768)");
146
147        PqKeyPair {
148            x25519_secret: Some(x25519_secret),
149            x25519_pub,
150            kyber_pub,
151            kyber_sec,
152        }
153    }
154
155    /// Build the client hello bundle (public keys to send to server).
156    pub fn client_hello(&self) -> PqPublicBundle {
157        PqPublicBundle {
158            x25519_pub: *self.x25519_pub.as_bytes(),
159            kyber_pub: self.kyber_pub.as_bytes().to_vec(),
160        }
161    }
162
163    /// Server: receive client hello, produce response and derive shared secret.
164    ///
165    /// Returns `(PqServerResponse, shared_secret_32_bytes)`.
166    pub fn server_respond(
167        &mut self,
168        client_hello: &PqPublicBundle,
169    ) -> Result<(PqServerResponse, [u8; HYBRID_SECRET_SIZE]), VCLError> {
170        // X25519 server side
171        let x25519_secret = self.x25519_secret.take().ok_or_else(|| {
172            VCLError::HandshakeFailed("X25519 secret already consumed".to_string())
173        })?;
174        let client_x25519 = X25519PublicKey::from(
175            TryInto::<[u8; 32]>::try_into(client_hello.x25519_pub)
176                .map_err(|_| VCLError::InvalidKey("X25519 pubkey wrong size".to_string()))?
177        );
178        let x25519_shared = x25519_secret.diffie_hellman(&client_x25519);
179
180        // Kyber encapsulation — server encapsulates to client's public key
181        let client_kyber_pub = kyber768::PublicKey::from_bytes(&client_hello.kyber_pub)
182            .map_err(|_| VCLError::InvalidKey("Kyber public key invalid".to_string()))?;
183        let (kyber_shared, kyber_ct) = kyber768::encapsulate(&client_kyber_pub);
184
185        // Hybrid: SHA-256(x25519_shared || kyber_shared)
186        let secret = hybrid_secret(x25519_shared.as_bytes(), kyber_shared.as_bytes());
187
188        let response = PqServerResponse {
189            x25519_pub: *self.x25519_pub.as_bytes(),
190            kyber_ciphertext: kyber_ct.as_bytes().to_vec(),
191        };
192
193        info!("PQ server handshake complete (hybrid X25519+Kyber768)");
194        Ok((response, secret))
195    }
196
197    /// Client: receive server response, derive shared secret.
198    pub fn client_finalize(
199        &mut self,
200        server_response: &PqServerResponse,
201    ) -> Result<[u8; HYBRID_SECRET_SIZE], VCLError> {
202        // X25519 client side
203        let x25519_secret = self.x25519_secret.take().ok_or_else(|| {
204            VCLError::HandshakeFailed("X25519 secret already consumed".to_string())
205        })?;
206        let server_x25519 = X25519PublicKey::from(
207            TryInto::<[u8; 32]>::try_into(server_response.x25519_pub)
208                .map_err(|_| VCLError::InvalidKey("X25519 pubkey wrong size".to_string()))?
209        );
210        let x25519_shared = x25519_secret.diffie_hellman(&server_x25519);
211
212        // Kyber decapsulation — client decapsulates with its secret key
213        let kyber_ct = kyber768::Ciphertext::from_bytes(&server_response.kyber_ciphertext)
214            .map_err(|_| VCLError::InvalidPacket("Kyber ciphertext invalid".to_string()))?;
215        let kyber_shared = kyber768::decapsulate(&kyber_ct, &self.kyber_sec);
216
217        // Hybrid: SHA-256(x25519_shared || kyber_shared)
218        let secret = hybrid_secret(x25519_shared.as_bytes(), kyber_shared.as_bytes());
219
220        info!("PQ client handshake complete (hybrid X25519+Kyber768)");
221        Ok(secret)
222    }
223}
224
225/// Combine X25519 and Kyber shared secrets into a single 32-byte key.
226/// Uses SHA-256(x25519_bytes || kyber_bytes).
227fn hybrid_secret(x25519: &[u8], kyber: &[u8]) -> [u8; HYBRID_SECRET_SIZE] {
228    let mut hasher = Sha256::new();
229    hasher.update(x25519);
230    hasher.update(kyber);
231    let result = hasher.finalize();
232    let mut out = [0u8; HYBRID_SECRET_SIZE];
233    out.copy_from_slice(&result);
234    out
235}
236
237/// Convenience struct for a complete PQ handshake.
238pub struct PqHandshake;
239
240impl PqHandshake {
241    /// Run a full client+server handshake and return both secrets.
242    /// They must be equal. Useful for testing.
243    pub fn run_local() -> Result<([u8; 32], [u8; 32]), VCLError> {
244        let mut client = PqKeyPair::generate();
245        let mut server = PqKeyPair::generate();
246
247        let hello = client.client_hello();
248        let (response, server_secret) = server.server_respond(&hello)?;
249        let client_secret = client.client_finalize(&response)?;
250
251        Ok((client_secret, server_secret))
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_keypair_generate() {
261        let kp = PqKeyPair::generate();
262        assert_eq!(kp.x25519_pub.as_bytes().len(), 32);
263    }
264
265    #[test]
266    fn test_client_hello_serialization() {
267        let kp = PqKeyPair::generate();
268        let hello = kp.client_hello();
269        let bytes = hello.to_bytes();
270        let restored = PqPublicBundle::from_bytes(&bytes).unwrap();
271        assert_eq!(restored.x25519_pub, hello.x25519_pub);
272        assert_eq!(restored.kyber_pub, hello.kyber_pub);
273    }
274
275    #[test]
276    fn test_server_response_serialization() {
277        let mut client = PqKeyPair::generate();
278        let mut server = PqKeyPair::generate();
279        let hello = client.client_hello();
280        let (response, _) = server.server_respond(&hello).unwrap();
281        let bytes = response.to_bytes();
282        let restored = PqServerResponse::from_bytes(&bytes).unwrap();
283        assert_eq!(restored.x25519_pub, response.x25519_pub);
284        assert_eq!(restored.kyber_ciphertext, response.kyber_ciphertext);
285    }
286
287    #[test]
288    fn test_full_handshake_secrets_match() {
289        let (client_secret, server_secret) = PqHandshake::run_local().unwrap();
290        assert_eq!(client_secret, server_secret);
291        assert_eq!(client_secret.len(), 32);
292    }
293
294    #[test]
295    fn test_different_keypairs_different_secrets() {
296        let (s1, _) = PqHandshake::run_local().unwrap();
297        let (s2, _) = PqHandshake::run_local().unwrap();
298        assert_ne!(s1, s2);
299    }
300
301    #[test]
302    fn test_secret_not_all_zeros() {
303        let (secret, _) = PqHandshake::run_local().unwrap();
304        assert_ne!(secret, [0u8; 32]);
305    }
306
307    #[test]
308    fn test_secret_is_32_bytes() {
309        let (secret, _) = PqHandshake::run_local().unwrap();
310        assert_eq!(secret.len(), HYBRID_SECRET_SIZE);
311    }
312
313    #[test]
314    fn test_public_bundle_from_bytes_too_short() {
315        let result = PqPublicBundle::from_bytes(&[0u8; 10]);
316        assert!(result.is_err());
317    }
318
319    #[test]
320    fn test_server_response_from_bytes_too_short() {
321        let result = PqServerResponse::from_bytes(&[0u8; 10]);
322        assert!(result.is_err());
323    }
324
325    #[test]
326    fn test_client_secret_consumed_once() {
327        let mut client = PqKeyPair::generate();
328        let mut server = PqKeyPair::generate();
329        let hello = client.client_hello();
330        let (response, _) = server.server_respond(&hello).unwrap();
331        client.client_finalize(&response).unwrap();
332        // Second call should fail — secret consumed
333        let mut server2 = PqKeyPair::generate();
334        let hello2 = PqKeyPair::generate().client_hello();
335        let (response2, _) = server2.server_respond(&hello2).unwrap();
336        let result = client.client_finalize(&response2);
337        assert!(result.is_err());
338    }
339
340    #[test]
341    fn test_hybrid_secret_deterministic() {
342        let x = [1u8; 32];
343        let k = [2u8; 32];
344        let s1 = hybrid_secret(&x, &k);
345        let s2 = hybrid_secret(&x, &k);
346        assert_eq!(s1, s2);
347    }
348
349    #[test]
350    fn test_hybrid_secret_different_inputs() {
351        let s1 = hybrid_secret(&[1u8; 32], &[2u8; 32]);
352        let s2 = hybrid_secret(&[3u8; 32], &[4u8; 32]);
353        assert_ne!(s1, s2);
354    }
355
356    #[test]
357    fn test_client_hello_has_kyber_key() {
358        let kp = PqKeyPair::generate();
359        let hello = kp.client_hello();
360        assert!(!hello.kyber_pub.is_empty());
361    }
362}