Skip to main content

libsignal_protocol/
pqxdh.rs

1//
2// Copyright 2026 Signal Messenger, LLC.
3// SPDX-License-Identifier: AGPL-3.0-only
4//
5
6//! PQXDH key agreement protocol.
7//!
8//! This module implements the PQXDH (Post-Quantum Extended Diffie-Hellman) key
9//! agreement, extracting the pure key agreement computation from ratchet
10//! initialization. The output includes derived keys ready for ratchet setup;
11//! the actual ratchet initialization is handled separately in the internal
12//! ratchet module.
13//!
14//! ## Future direction
15//!
16//! The KDF output shape (`RootKey`, `ChainKey`, `[u8; 32]`) is currently
17//! coupled to the Double Ratchet's initialization requirements. Ideally, the
18//! handshake would output a single 32-byte secret and the ratchet layer would
19//! derive whatever it needs from that. This requires a protocol version bump
20//! and should be done alongside a future handshake protocol revision.
21
22use libsignal_core::derive_arrays;
23use rand::{CryptoRng, Rng};
24
25use crate::handshake::Handshake;
26use crate::ratchet::{ChainKey, RootKey};
27use crate::{
28    CiphertextMessageType, IdentityKey, IdentityKeyPair, KeyPair, PublicKey, Result,
29    SignalProtocolError, kem,
30};
31
32/// The PQXDH key agreement protocol.
33///
34/// Implements [`Handshake`] for the Post-Quantum Extended Diffie-Hellman
35/// protocol (4 EC DH + 1 ML-KEM encapsulation/decapsulation).
36pub(crate) struct Pqxdh;
37
38impl Handshake for Pqxdh {
39    type InitiatorParams = InitiatorParameters;
40    type RecipientParams<'a> = RecipientParameters<'a>;
41    type InitiatorMessage = kem::SerializedCiphertext;
42    type SessionSecret = HandshakeKeys;
43
44    fn initiate<R: Rng + CryptoRng>(
45        params: &Self::InitiatorParams,
46        rng: &mut R,
47    ) -> Result<(Self::InitiatorMessage, Self::SessionSecret)> {
48        let result = pqxdh_initiate(params, rng)?;
49        Ok((result.kyber_ciphertext, result.keys))
50    }
51
52    fn accept(params: &Self::RecipientParams<'_>) -> Result<Self::SessionSecret> {
53        pqxdh_accept(params)
54    }
55}
56
57/// The initial PQR (post-quantum ratchet) key derived from the handshake.
58pub(crate) type InitialPQRKey = [u8; 32];
59
60/// Keys derived from a PQXDH handshake, ready for ratchet initialization.
61///
62/// This bundles the KDF output in the shape the ratchet layer expects.
63/// See module-level docs for why this is coupled and the plan to decouple.
64pub(crate) struct HandshakeKeys {
65    pub root_key: RootKey,
66    pub chain_key: ChainKey,
67    pub pqr_key: InitialPQRKey,
68}
69
70impl HandshakeKeys {
71    /// Derive ratchet initialization keys from raw PQXDH shared secret material.
72    fn derive(secret_input: &[u8]) -> Self {
73        Self::derive_with_label(
74            b"WhisperText_X25519_SHA-256_CRYSTALS-KYBER-1024",
75            secret_input,
76        )
77    }
78
79    fn derive_with_label(label: &[u8], secret_input: &[u8]) -> Self {
80        let (root_key_bytes, chain_key_bytes, pqr_bytes) = derive_arrays(|bytes| {
81            hkdf::Hkdf::<sha2::Sha256>::new(None, secret_input)
82                .expand(label, bytes)
83                .expect("valid length")
84        });
85
86        Self {
87            root_key: RootKey::new(root_key_bytes),
88            chain_key: ChainKey::new(chain_key_bytes, 0),
89            pqr_key: pqr_bytes,
90        }
91    }
92}
93
94// ── Initiator ────────────────────────────────────────────────────────
95
96/// The output of a PQXDH key agreement from the initiator's side.
97///
98/// Contains the derived handshake keys and the KEM ciphertext that the
99/// recipient needs to complete the agreement.
100pub(crate) struct InitiatorAgreement {
101    keys: HandshakeKeys,
102    kyber_ciphertext: kem::SerializedCiphertext,
103}
104
105/// Parameters for the initiator side of a PQXDH key agreement.
106///
107/// The initiator fetches the recipient's pre-key bundle from the server
108/// and uses it together with their own identity and ephemeral keys to
109/// compute a shared secret.
110pub struct InitiatorParameters {
111    our_identity_key_pair: IdentityKeyPair,
112    our_ephemeral_key_pair: KeyPair,
113
114    their_identity_key: IdentityKey,
115    their_signed_pre_key: PublicKey,
116    their_one_time_pre_key: Option<PublicKey>,
117    their_ratchet_key: PublicKey,
118    their_kyber_pre_key: kem::PublicKey,
119
120    self_session: bool,
121}
122
123impl InitiatorParameters {
124    pub fn new(
125        our_identity_key_pair: IdentityKeyPair,
126        our_ephemeral_key_pair: KeyPair,
127        their_identity_key: IdentityKey,
128        their_signed_pre_key: PublicKey,
129        their_ratchet_key: PublicKey,
130        their_kyber_pre_key: kem::PublicKey,
131        self_session: bool,
132    ) -> Self {
133        Self {
134            our_identity_key_pair,
135            our_ephemeral_key_pair,
136            their_identity_key,
137            their_one_time_pre_key: None,
138            their_signed_pre_key,
139            their_ratchet_key,
140            their_kyber_pre_key,
141            self_session,
142        }
143    }
144
145    pub fn set_their_one_time_pre_key(&mut self, ec_public: PublicKey) {
146        self.their_one_time_pre_key = Some(ec_public);
147    }
148
149    #[inline]
150    pub fn our_identity_key_pair(&self) -> &IdentityKeyPair {
151        &self.our_identity_key_pair
152    }
153
154    #[inline]
155    pub fn our_ephemeral_key_pair(&self) -> &KeyPair {
156        &self.our_ephemeral_key_pair
157    }
158
159    #[inline]
160    pub fn their_identity_key(&self) -> &IdentityKey {
161        &self.their_identity_key
162    }
163
164    #[inline]
165    pub fn their_signed_pre_key(&self) -> &PublicKey {
166        &self.their_signed_pre_key
167    }
168
169    #[inline]
170    pub fn their_one_time_pre_key(&self) -> Option<&PublicKey> {
171        self.their_one_time_pre_key.as_ref()
172    }
173
174    #[inline]
175    pub fn their_kyber_pre_key(&self) -> &kem::PublicKey {
176        &self.their_kyber_pre_key
177    }
178
179    #[inline]
180    pub fn their_ratchet_key(&self) -> &PublicKey {
181        &self.their_ratchet_key
182    }
183
184    #[inline]
185    pub fn self_session(&self) -> bool {
186        self.self_session
187    }
188}
189
190/// Perform the initiator side of the PQXDH key agreement.
191///
192/// Computes DH shared secrets and KEM encapsulation, then applies the KDF
193/// to produce keys ready for ratchet initialization.
194pub(crate) fn pqxdh_initiate<R: Rng + CryptoRng>(
195    parameters: &InitiatorParameters,
196    mut csprng: &mut R,
197) -> Result<InitiatorAgreement> {
198    let mut secrets = Vec::with_capacity(32 * 6);
199
200    secrets.extend_from_slice(&[0xFFu8; 32]); // discontinuity bytes
201
202    secrets.extend_from_slice(
203        &parameters
204            .our_identity_key_pair
205            .private_key()
206            .calculate_agreement(&parameters.their_signed_pre_key)?,
207    );
208
209    let our_ephemeral_private_key = parameters.our_ephemeral_key_pair.private_key;
210
211    secrets.extend_from_slice(
212        &our_ephemeral_private_key
213            .calculate_agreement(parameters.their_identity_key.public_key())?,
214    );
215
216    secrets.extend_from_slice(
217        &our_ephemeral_private_key.calculate_agreement(&parameters.their_signed_pre_key)?,
218    );
219
220    if let Some(their_one_time_prekey) = &parameters.their_one_time_pre_key {
221        secrets.extend_from_slice(
222            &our_ephemeral_private_key.calculate_agreement(their_one_time_prekey)?,
223        );
224    }
225
226    let kyber_ciphertext = {
227        let (ss, ct) = parameters.their_kyber_pre_key.encapsulate(&mut csprng)?;
228        secrets.extend_from_slice(ss.as_ref());
229        ct
230    };
231
232    Ok(InitiatorAgreement {
233        keys: HandshakeKeys::derive(&secrets),
234        kyber_ciphertext,
235    })
236}
237
238// ── Recipient ────────────────────────────────────────────────────────
239
240/// Parameters for the recipient side of a PQXDH key agreement.
241///
242/// The recipient uses their own pre-keys together with the initiator's
243/// identity and base keys (received in the pre-key message) to compute
244/// the same shared secret.
245pub struct RecipientParameters<'a> {
246    our_identity_key_pair: IdentityKeyPair,
247    our_signed_pre_key_pair: KeyPair,
248    our_one_time_pre_key_pair: Option<KeyPair>,
249    our_kyber_pre_key_pair: kem::KeyPair,
250
251    their_identity_key: IdentityKey,
252    their_ephemeral_key: PublicKey,
253    their_kyber_ciphertext: &'a kem::SerializedCiphertext,
254
255    self_session: bool,
256}
257
258impl<'a> RecipientParameters<'a> {
259    pub fn new(
260        our_identity_key_pair: IdentityKeyPair,
261        our_signed_pre_key_pair: KeyPair,
262        our_one_time_pre_key_pair: Option<KeyPair>,
263        our_kyber_pre_key_pair: kem::KeyPair,
264        their_identity_key: IdentityKey,
265        their_ephemeral_key: PublicKey,
266        their_kyber_ciphertext: &'a kem::SerializedCiphertext,
267        self_session: bool,
268    ) -> Self {
269        Self {
270            our_identity_key_pair,
271            our_signed_pre_key_pair,
272            our_one_time_pre_key_pair,
273            our_kyber_pre_key_pair,
274            their_identity_key,
275            their_ephemeral_key,
276            their_kyber_ciphertext,
277            self_session,
278        }
279    }
280
281    #[inline]
282    pub fn our_identity_key_pair(&self) -> &IdentityKeyPair {
283        &self.our_identity_key_pair
284    }
285
286    #[inline]
287    pub fn our_signed_pre_key_pair(&self) -> &KeyPair {
288        &self.our_signed_pre_key_pair
289    }
290
291    #[inline]
292    pub fn our_one_time_pre_key_pair(&self) -> Option<&KeyPair> {
293        self.our_one_time_pre_key_pair.as_ref()
294    }
295
296    #[inline]
297    pub fn our_kyber_pre_key_pair(&self) -> &kem::KeyPair {
298        &self.our_kyber_pre_key_pair
299    }
300
301    #[inline]
302    pub fn their_identity_key(&self) -> &IdentityKey {
303        &self.their_identity_key
304    }
305
306    #[inline]
307    pub fn their_ephemeral_key(&self) -> &PublicKey {
308        &self.their_ephemeral_key
309    }
310
311    #[inline]
312    pub fn their_kyber_ciphertext(&self) -> &kem::SerializedCiphertext {
313        self.their_kyber_ciphertext
314    }
315
316    #[inline]
317    pub fn self_session(&self) -> bool {
318        self.self_session
319    }
320}
321
322/// Perform the recipient side of the PQXDH key agreement.
323///
324/// Computes DH shared secrets and KEM decapsulation, then applies the KDF
325/// to produce keys ready for ratchet initialization.
326pub(crate) fn pqxdh_accept(parameters: &RecipientParameters) -> Result<HandshakeKeys> {
327    // Validate the initiator's base key before doing any computation.
328    if !parameters.their_ephemeral_key.is_canonical() {
329        return Err(SignalProtocolError::InvalidMessage(
330            CiphertextMessageType::PreKey,
331            "incoming base key is invalid",
332        ));
333    }
334
335    let mut secrets = Vec::with_capacity(32 * 6);
336
337    secrets.extend_from_slice(&[0xFFu8; 32]); // discontinuity bytes
338
339    secrets.extend_from_slice(
340        &parameters
341            .our_signed_pre_key_pair
342            .private_key
343            .calculate_agreement(parameters.their_identity_key.public_key())?,
344    );
345
346    secrets.extend_from_slice(
347        &parameters
348            .our_identity_key_pair
349            .private_key()
350            .calculate_agreement(&parameters.their_ephemeral_key)?,
351    );
352
353    secrets.extend_from_slice(
354        &parameters
355            .our_signed_pre_key_pair
356            .private_key
357            .calculate_agreement(&parameters.their_ephemeral_key)?,
358    );
359
360    if let Some(our_one_time_pre_key_pair) = &parameters.our_one_time_pre_key_pair {
361        secrets.extend_from_slice(
362            &our_one_time_pre_key_pair
363                .private_key
364                .calculate_agreement(&parameters.their_ephemeral_key)?,
365        );
366    }
367
368    secrets.extend_from_slice(
369        &parameters
370            .our_kyber_pre_key_pair
371            .secret_key
372            .decapsulate(parameters.their_kyber_ciphertext)?,
373    );
374
375    Ok(HandshakeKeys::derive(&secrets))
376}