vcl_protocol/
pq_crypto.rs1use 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
43pub const HYBRID_SECRET_SIZE: usize = 32;
45
46#[derive(Debug, Clone)]
48pub struct PqPublicBundle {
49 pub x25519_pub: [u8; 32],
51 pub kyber_pub: Vec<u8>,
53}
54
55impl PqPublicBundle {
56 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 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#[derive(Debug, Clone)]
88pub struct PqServerResponse {
89 pub x25519_pub: [u8; 32],
91 pub kyber_ciphertext: Vec<u8>,
93}
94
95impl PqServerResponse {
96 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 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
126pub struct PqKeyPair {
128 x25519_secret: Option<EphemeralSecret>,
130 x25519_pub: X25519PublicKey,
132 kyber_pub: kyber768::PublicKey,
134 kyber_sec: kyber768::SecretKey,
136}
137
138impl PqKeyPair {
139 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 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 pub fn server_respond(
167 &mut self,
168 client_hello: &PqPublicBundle,
169 ) -> Result<(PqServerResponse, [u8; HYBRID_SECRET_SIZE]), VCLError> {
170 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 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 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 pub fn client_finalize(
199 &mut self,
200 server_response: &PqServerResponse,
201 ) -> Result<[u8; HYBRID_SECRET_SIZE], VCLError> {
202 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 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 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
225fn 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
237pub struct PqHandshake;
239
240impl PqHandshake {
241 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 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}