Skip to main content

oqs_safe/
handshake.rs

1use crate::{
2    hybrid::derive_hybrid_secret,
3    kem::{Kem, KemAlgorithm, KemInstance, SecretKey},
4    session::SecureSession,
5    OqsError,
6};
7
8use rand_core::OsRng;
9use sha2::{Digest, Sha256};
10use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
11
12const HANDSHAKE_CONTEXT: &[u8] = b"oqs-safe-v0.5.0-hybrid-handshake";
13const HANDSHAKE_TRANSCRIPT_DOMAIN: &[u8] = b"oqs-safe-v0.6.0-handshake-transcript";
14
15#[cfg_attr(
16    feature = "serialization",
17    derive(serde::Serialize, serde::Deserialize)
18)]
19#[derive(Clone, Debug)]
20pub struct ClientHello {
21    pub client_x25519_public: Vec<u8>,
22    pub client_kem_public: Vec<u8>,
23}
24
25#[cfg_attr(
26    feature = "serialization",
27    derive(serde::Serialize, serde::Deserialize)
28)]
29#[derive(Clone, Debug)]
30pub struct ServerHello {
31    pub server_x25519_public: Vec<u8>,
32    pub kem_ciphertext: Vec<u8>,
33}
34
35#[cfg(feature = "serialization")]
36impl ClientHello {
37    pub fn to_bytes(&self) -> Result<Vec<u8>, HandshakeError> {
38        bincode::serialize(self).map_err(|_| HandshakeError::InvalidHandshakeState)
39    }
40
41    pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError> {
42        bincode::deserialize(bytes).map_err(|_| HandshakeError::InvalidHandshakeState)
43    }
44}
45
46#[cfg(feature = "serialization")]
47impl ServerHello {
48    pub fn to_bytes(&self) -> Result<Vec<u8>, HandshakeError> {
49        bincode::serialize(self).map_err(|_| HandshakeError::InvalidHandshakeState)
50    }
51
52    pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError> {
53        bincode::deserialize(bytes).map_err(|_| HandshakeError::InvalidHandshakeState)
54    }
55}
56
57#[derive(Debug, Clone)]
58pub struct HandshakeTranscript {
59    hasher: Sha256,
60}
61
62impl HandshakeTranscript {
63    pub fn new() -> Self {
64        let mut hasher = Sha256::new();
65        hasher.update(HANDSHAKE_TRANSCRIPT_DOMAIN);
66        Self { hasher }
67    }
68
69    pub fn update_labelled(&mut self, label: &[u8], data: &[u8]) {
70        update_len_prefixed(&mut self.hasher, label);
71        update_len_prefixed(&mut self.hasher, data);
72    }
73
74    pub fn update_algorithm(&mut self, algorithm: KemAlgorithm) {
75        self.update_labelled(b"kem_algorithm", format!("{algorithm:?}").as_bytes());
76    }
77
78    pub fn update_client_hello(&mut self, client_hello: &ClientHello) {
79        self.update_labelled(b"client_x25519_public", &client_hello.client_x25519_public);
80        self.update_labelled(b"client_kem_public", &client_hello.client_kem_public);
81    }
82
83    pub fn update_server_hello(&mut self, server_hello: &ServerHello) {
84        self.update_labelled(b"server_x25519_public", &server_hello.server_x25519_public);
85        self.update_labelled(b"kem_ciphertext", &server_hello.kem_ciphertext);
86    }
87
88    pub fn finalize(self) -> [u8; 32] {
89        let digest = self.hasher.finalize();
90        let mut out = [0u8; 32];
91        out.copy_from_slice(&digest);
92        out
93    }
94}
95
96impl Default for HandshakeTranscript {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102fn update_len_prefixed(hasher: &mut Sha256, data: &[u8]) {
103    hasher.update((data.len() as u64).to_be_bytes());
104    hasher.update(data);
105}
106
107fn transcript_bound_context(transcript_hash: &[u8; 32]) -> Vec<u8> {
108    let mut context = Vec::with_capacity(HANDSHAKE_CONTEXT.len() + transcript_hash.len());
109    context.extend_from_slice(HANDSHAKE_CONTEXT);
110    context.extend_from_slice(transcript_hash);
111    context
112}
113
114#[derive(Debug)]
115pub enum HandshakeError {
116    MissingClientState,
117    MissingServerState,
118    InvalidHandshakeState,
119    CryptoError(OqsError),
120}
121
122impl core::fmt::Display for HandshakeError {
123    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
124        match self {
125            HandshakeError::MissingClientState => write!(f, "missing client handshake state"),
126            HandshakeError::MissingServerState => write!(f, "missing server handshake state"),
127            HandshakeError::InvalidHandshakeState => write!(f, "invalid handshake state"),
128            HandshakeError::CryptoError(err) => write!(f, "cryptographic operation failed: {err}"),
129        }
130    }
131}
132
133impl std::error::Error for HandshakeError {}
134
135impl From<OqsError> for HandshakeError {
136    fn from(value: OqsError) -> Self {
137        HandshakeError::CryptoError(value)
138    }
139}
140
141pub struct HybridClient {
142    kem: KemInstance,
143    state: Option<ClientHandshakeState>,
144}
145
146struct ClientHandshakeState {
147    x25519_secret: StaticSecret,
148    kem_secret: SecretKey,
149    client_hello: ClientHello,
150}
151
152impl HybridClient {
153    pub fn new() -> Self {
154        Self {
155            kem: KemInstance::new(KemAlgorithm::MlKem768),
156            state: None,
157        }
158    }
159
160    pub fn with_algorithm(algorithm: KemAlgorithm) -> Self {
161        Self {
162            kem: KemInstance::new(algorithm),
163            state: None,
164        }
165    }
166
167    pub fn start_handshake(&mut self) -> Result<ClientHello, HandshakeError> {
168        let client_x25519_secret = StaticSecret::random_from_rng(OsRng);
169        let client_x25519_public = X25519PublicKey::from(&client_x25519_secret);
170
171        let (client_kem_public, client_kem_secret) = self.kem.keypair()?;
172
173        let client_hello = ClientHello {
174            client_x25519_public: client_x25519_public.as_bytes().to_vec(),
175            client_kem_public: client_kem_public.as_bytes().to_vec(),
176        };
177
178        self.state = Some(ClientHandshakeState {
179            x25519_secret: client_x25519_secret,
180            kem_secret: client_kem_secret,
181            client_hello: client_hello.clone(),
182        });
183
184        Ok(client_hello)
185    }
186
187    pub fn finish(&mut self, server_hello: ServerHello) -> Result<SecureSession, HandshakeError> {
188        let state = self
189            .state
190            .take()
191            .ok_or(HandshakeError::MissingClientState)?;
192
193        if server_hello.server_x25519_public.len() != 32 || server_hello.kem_ciphertext.is_empty() {
194            return Err(HandshakeError::InvalidHandshakeState);
195        }
196
197        let server_public_bytes: [u8; 32] = server_hello
198            .server_x25519_public
199            .as_slice()
200            .try_into()
201            .map_err(|_| HandshakeError::InvalidHandshakeState)?;
202
203        let server_x25519_public = X25519PublicKey::from(server_public_bytes);
204        let classical_secret = state.x25519_secret.diffie_hellman(&server_x25519_public);
205
206        let pqc_secret = client_pqc_secret(
207            self.kem.algorithm(),
208            &server_hello.kem_ciphertext,
209            &state.kem_secret,
210        )?;
211
212        let mut transcript = HandshakeTranscript::new();
213        transcript.update_algorithm(self.kem.algorithm());
214        transcript.update_client_hello(&state.client_hello);
215        transcript.update_server_hello(&server_hello);
216        let transcript_hash = transcript.finalize();
217
218        let context = transcript_bound_context(&transcript_hash);
219
220        let hybrid_secret = derive_hybrid_secret(
221            pqc_secret.as_slice(),
222            classical_secret.as_bytes(),
223            context.as_slice(),
224        );
225
226        Ok(SecureSession::new(hybrid_secret.as_bytes().to_vec()))
227    }
228}
229
230impl Default for HybridClient {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236pub struct HybridServer {
237    kem: KemInstance,
238    session: Option<SecureSession>,
239}
240
241impl HybridServer {
242    pub fn new() -> Self {
243        Self {
244            kem: KemInstance::new(KemAlgorithm::MlKem768),
245            session: None,
246        }
247    }
248
249    pub fn with_algorithm(algorithm: KemAlgorithm) -> Self {
250        Self {
251            kem: KemInstance::new(algorithm),
252            session: None,
253        }
254    }
255
256    pub fn respond(&mut self, client_hello: ClientHello) -> Result<ServerHello, HandshakeError> {
257        if client_hello.client_x25519_public.len() != 32
258            || client_hello.client_kem_public.is_empty()
259        {
260            return Err(HandshakeError::InvalidHandshakeState);
261        }
262
263        let client_public_bytes: [u8; 32] = client_hello
264            .client_x25519_public
265            .as_slice()
266            .try_into()
267            .map_err(|_| HandshakeError::InvalidHandshakeState)?;
268
269        let client_x25519_public = X25519PublicKey::from(client_public_bytes);
270
271        let server_x25519_secret = StaticSecret::random_from_rng(OsRng);
272        let server_x25519_public = X25519PublicKey::from(&server_x25519_secret);
273
274        let classical_secret = server_x25519_secret.diffie_hellman(&client_x25519_public);
275
276        let (kem_ciphertext, pqc_secret) =
277            server_pqc_secret(self.kem.algorithm(), &client_hello.client_kem_public)?;
278
279        let server_hello = ServerHello {
280            server_x25519_public: server_x25519_public.as_bytes().to_vec(),
281            kem_ciphertext,
282        };
283
284        let mut transcript = HandshakeTranscript::new();
285        transcript.update_algorithm(self.kem.algorithm());
286        transcript.update_client_hello(&client_hello);
287        transcript.update_server_hello(&server_hello);
288        let transcript_hash = transcript.finalize();
289
290        let context = transcript_bound_context(&transcript_hash);
291
292        let hybrid_secret = derive_hybrid_secret(
293            pqc_secret.as_slice(),
294            classical_secret.as_bytes(),
295            context.as_slice(),
296        );
297
298        self.session = Some(SecureSession::new(hybrid_secret.as_bytes().to_vec()));
299
300        Ok(server_hello)
301    }
302
303    pub fn session(&self) -> Result<&SecureSession, HandshakeError> {
304        self.session
305            .as_ref()
306            .ok_or(HandshakeError::MissingServerState)
307    }
308}
309
310impl Default for HybridServer {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316#[cfg(feature = "liboqs")]
317fn server_pqc_secret(
318    algorithm: KemAlgorithm,
319    client_kem_public: &[u8],
320) -> Result<(Vec<u8>, Vec<u8>), HandshakeError> {
321    use crate::kem::PublicKey;
322
323    let kem = KemInstance::new(algorithm);
324    let client_public_key = PublicKey::new(algorithm, client_kem_public.to_vec());
325
326    let (ciphertext, shared_secret) = kem.encapsulate(&client_public_key)?;
327
328    Ok((
329        ciphertext.as_bytes().to_vec(),
330        shared_secret.as_bytes().to_vec(),
331    ))
332}
333
334#[cfg(feature = "liboqs")]
335fn client_pqc_secret(
336    algorithm: KemAlgorithm,
337    kem_ciphertext: &[u8],
338    kem_secret: &SecretKey,
339) -> Result<Vec<u8>, HandshakeError> {
340    use crate::kem::Ciphertext;
341
342    let kem = KemInstance::new(algorithm);
343    let ciphertext = Ciphertext::new(algorithm, kem_ciphertext.to_vec());
344
345    let shared_secret = kem.decapsulate(&ciphertext, kem_secret)?;
346
347    Ok(shared_secret.as_bytes().to_vec())
348}
349
350#[cfg(not(feature = "liboqs"))]
351fn server_pqc_secret(
352    algorithm: KemAlgorithm,
353    client_kem_public: &[u8],
354) -> Result<(Vec<u8>, Vec<u8>), HandshakeError> {
355    let ciphertext = mock_ciphertext(algorithm, client_kem_public);
356    let shared_secret = mock_shared_secret(algorithm, &ciphertext);
357
358    Ok((ciphertext, shared_secret))
359}
360
361#[cfg(not(feature = "liboqs"))]
362fn client_pqc_secret(
363    algorithm: KemAlgorithm,
364    kem_ciphertext: &[u8],
365    kem_secret: &SecretKey,
366) -> Result<Vec<u8>, HandshakeError> {
367    let _ = kem_secret;
368
369    Ok(mock_shared_secret(algorithm, kem_ciphertext))
370}
371
372#[cfg(not(feature = "liboqs"))]
373fn mock_ciphertext(algorithm: KemAlgorithm, client_kem_public: &[u8]) -> Vec<u8> {
374    let mut ciphertext = vec![0u8; algorithm.ciphertext_len()];
375    let mut counter = 0u64;
376    let mut offset = 0usize;
377
378    while offset < ciphertext.len() {
379        let mut hasher = Sha256::new();
380        hasher.update(b"oqs-safe-v0.5.0-mock-ciphertext");
381        hasher.update(client_kem_public);
382        hasher.update(counter.to_le_bytes());
383
384        let block = hasher.finalize();
385        let take = core::cmp::min(block.len(), ciphertext.len() - offset);
386
387        ciphertext[offset..offset + take].copy_from_slice(&block[..take]);
388        offset += take;
389        counter += 1;
390    }
391
392    ciphertext
393}
394
395#[cfg(not(feature = "liboqs"))]
396fn mock_shared_secret(algorithm: KemAlgorithm, kem_ciphertext: &[u8]) -> Vec<u8> {
397    let mut hasher = Sha256::new();
398
399    hasher.update(b"oqs-safe-v0.5.0-mock-pqc-secret");
400    hasher.update(format!("{algorithm:?}").as_bytes());
401    hasher.update(kem_ciphertext);
402
403    hasher.finalize().to_vec()
404}
405
406#[cfg(test)]
407mod transcript_tests {
408    use super::*;
409
410    #[test]
411    fn same_transcript_inputs_produce_same_hash() {
412        let client_hello = ClientHello {
413            client_x25519_public: vec![1; 32],
414            client_kem_public: vec![2; 1184],
415        };
416
417        let server_hello = ServerHello {
418            server_x25519_public: vec![3; 32],
419            kem_ciphertext: vec![4; 1088],
420        };
421
422        let mut t1 = HandshakeTranscript::new();
423        t1.update_algorithm(KemAlgorithm::MlKem768);
424        t1.update_client_hello(&client_hello);
425        t1.update_server_hello(&server_hello);
426
427        let mut t2 = HandshakeTranscript::new();
428        t2.update_algorithm(KemAlgorithm::MlKem768);
429        t2.update_client_hello(&client_hello);
430        t2.update_server_hello(&server_hello);
431
432        assert_eq!(t1.finalize(), t2.finalize());
433    }
434
435    #[test]
436    fn transcript_hash_changes_when_client_hello_changes() {
437        let client_hello_a = ClientHello {
438            client_x25519_public: vec![1; 32],
439            client_kem_public: vec![2; 1184],
440        };
441
442        let client_hello_b = ClientHello {
443            client_x25519_public: vec![9; 32],
444            client_kem_public: vec![2; 1184],
445        };
446
447        let server_hello = ServerHello {
448            server_x25519_public: vec![3; 32],
449            kem_ciphertext: vec![4; 1088],
450        };
451
452        let mut t1 = HandshakeTranscript::new();
453        t1.update_algorithm(KemAlgorithm::MlKem768);
454        t1.update_client_hello(&client_hello_a);
455        t1.update_server_hello(&server_hello);
456
457        let mut t2 = HandshakeTranscript::new();
458        t2.update_algorithm(KemAlgorithm::MlKem768);
459        t2.update_client_hello(&client_hello_b);
460        t2.update_server_hello(&server_hello);
461
462        assert_ne!(t1.finalize(), t2.finalize());
463    }
464
465    #[test]
466    fn transcript_hash_changes_when_server_hello_changes() {
467        let client_hello = ClientHello {
468            client_x25519_public: vec![1; 32],
469            client_kem_public: vec![2; 1184],
470        };
471
472        let server_hello_a = ServerHello {
473            server_x25519_public: vec![3; 32],
474            kem_ciphertext: vec![4; 1088],
475        };
476
477        let server_hello_b = ServerHello {
478            server_x25519_public: vec![3; 32],
479            kem_ciphertext: vec![8; 1088],
480        };
481
482        let mut t1 = HandshakeTranscript::new();
483        t1.update_algorithm(KemAlgorithm::MlKem768);
484        t1.update_client_hello(&client_hello);
485        t1.update_server_hello(&server_hello_a);
486
487        let mut t2 = HandshakeTranscript::new();
488        t2.update_algorithm(KemAlgorithm::MlKem768);
489        t2.update_client_hello(&client_hello);
490        t2.update_server_hello(&server_hello_b);
491
492        assert_ne!(t1.finalize(), t2.finalize());
493    }
494
495    #[test]
496    fn transcript_hash_changes_when_algorithm_changes() {
497        let client_hello = ClientHello {
498            client_x25519_public: vec![1; 32],
499            client_kem_public: vec![2; 1184],
500        };
501
502        let server_hello = ServerHello {
503            server_x25519_public: vec![3; 32],
504            kem_ciphertext: vec![4; 1088],
505        };
506
507        let mut t1 = HandshakeTranscript::new();
508        t1.update_algorithm(KemAlgorithm::MlKem512);
509        t1.update_client_hello(&client_hello);
510        t1.update_server_hello(&server_hello);
511
512        let mut t2 = HandshakeTranscript::new();
513        t2.update_algorithm(KemAlgorithm::MlKem768);
514        t2.update_client_hello(&client_hello);
515        t2.update_server_hello(&server_hello);
516
517        assert_ne!(t1.finalize(), t2.finalize());
518    }
519
520    #[test]
521    fn transcript_bound_context_is_deterministic() {
522        let transcript_hash = [7u8; 32];
523
524        let context_a = transcript_bound_context(&transcript_hash);
525        let context_b = transcript_bound_context(&transcript_hash);
526
527        assert_eq!(context_a, context_b);
528        assert!(context_a.starts_with(HANDSHAKE_CONTEXT));
529        assert!(context_a.ends_with(&transcript_hash));
530    }
531}
532
533#[cfg(all(test, feature = "serialization"))]
534mod serialization_tests {
535    use super::*;
536
537    #[test]
538    fn client_hello_roundtrips_through_bytes() {
539        let client_hello = ClientHello {
540            client_x25519_public: vec![1; 32],
541            client_kem_public: vec![2; 1184],
542        };
543
544        let encoded = client_hello
545            .to_bytes()
546            .expect("client hello should serialize");
547
548        let decoded = ClientHello::from_bytes(&encoded).expect("client hello should deserialize");
549
550        assert_eq!(
551            decoded.client_x25519_public,
552            client_hello.client_x25519_public
553        );
554        assert_eq!(decoded.client_kem_public, client_hello.client_kem_public);
555    }
556
557    #[test]
558    fn server_hello_roundtrips_through_bytes() {
559        let server_hello = ServerHello {
560            server_x25519_public: vec![3; 32],
561            kem_ciphertext: vec![4; 1088],
562        };
563
564        let encoded = server_hello
565            .to_bytes()
566            .expect("server hello should serialize");
567
568        let decoded = ServerHello::from_bytes(&encoded).expect("server hello should deserialize");
569
570        assert_eq!(
571            decoded.server_x25519_public,
572            server_hello.server_x25519_public
573        );
574        assert_eq!(decoded.kem_ciphertext, server_hello.kem_ciphertext);
575    }
576
577    #[test]
578    fn invalid_client_hello_bytes_fail_to_deserialize() {
579        let invalid = b"not-a-valid-client-hello";
580
581        assert!(ClientHello::from_bytes(invalid).is_err());
582    }
583
584    #[test]
585    fn invalid_server_hello_bytes_fail_to_deserialize() {
586        let invalid = b"not-a-valid-server-hello";
587
588        assert!(ServerHello::from_bytes(invalid).is_err());
589    }
590}