pake_cpace_embedded/
lib.rs

1//! CPace PAKE (Password-Authenticated Key Exchange) implementation using Ristretto255.
2//!
3//! # Overview
4//! This crate implements CPace, a secure and efficient password-authenticated key exchange protocol. The implementation is
5//! designed to operate without the standard library (`#![no_std]`).
6//!
7//! ## Parties
8//! - **Initiating Party**: The party initiating the key exchange (e.g., the client).
9//! - **Peer**: The other party participating in the key exchange (e.g., the server).
10//!
11//! ## Protocol Workflow
12//! 1. **Step 1**: The initiating party generates a `Step1Out` object containing a context and a step 1 packet.
13//! 2. **Step 2**: The responder processes the step 1 packet and generates a `Step2Out` object with shared keys and a step 2 packet.
14//! 3. **Step 3**: The initiating party finalizes the exchange using the step 2 packet, obtaining the shared keys.
15//!
16//! ## Constants
17//! - `SESSION_ID_BYTES`: Length of the session ID in bytes.
18//! - `STEP1_PACKET_BYTES`: Length of the step 1 packet in bytes.
19//! - `STEP2_PACKET_BYTES`: Length of the step 2 packet in bytes.
20//! - `SHARED_KEY_BYTES`: Length of each derived shared key in bytes.
21//!
22//! ## Errors
23//! The `Error` enum defines possible errors, including:
24//! - Overflow conditions.
25//! - Random number generator failures.
26//! - Invalid public keys.
27//!
28//! ## Example Usage
29//! ```rust
30//! use pake_cpace_embedded::*;
31//! use rand::rngs::OsRng;
32//!
33//! let initiating_party = CPace::step1_with_rng("password", "initiating_party", "responder", Some("ad"), OsRng).unwrap();
34//! let responder = CPace::step2_with_rng(&initiating_party.packet(), "password", "initiating_party", "responder", Some("ad"), OsRng).unwrap();
35//! let shared_keys = initiating_party.step3(&responder.packet()).unwrap();
36//!
37//! assert_eq!(shared_keys.k1, responder.shared_keys().k1);
38//! assert_eq!(shared_keys.k2, responder.shared_keys().k2);
39//! ```
40
41#![no_std]
42#![forbid(unsafe_code)]
43
44use core::fmt;
45use curve25519_dalek::{
46    ristretto::{CompressedRistretto, RistrettoPoint},
47    scalar::Scalar,
48    traits::IsIdentity,
49};
50use hmac_sha512::{Hash, BLOCKBYTES};
51use rand_core::{CryptoRng, RngCore};
52
53/// Length of the session ID in bytes.
54pub const SESSION_ID_BYTES: usize = 16;
55/// Length of the step 1 packet in bytes.
56pub const STEP1_PACKET_BYTES: usize = 16 + 32;
57/// Length of the step 2 packet in bytes.
58pub const STEP2_PACKET_BYTES: usize = 32;
59/// Length of each shared key in bytes.
60pub const SHARED_KEY_BYTES: usize = 32;
61
62const DSI1: &str = "CPaceRistretto255-1";
63const DSI2: &str = "CPaceRistretto255-2";
64
65/// Errors that may occur during the CPace protocol.
66#[derive(Debug)]
67pub enum Error {
68    /// Overflow in input lengths.
69    Overflow(&'static str),
70    /// Random number generator failure.
71    Random(rand_core::Error),
72    /// Invalid public key received.
73    InvalidPublicKey,
74}
75
76impl fmt::Display for Error {
77    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78        write!(f, "{:?}", &self)
79    }
80}
81
82impl From<rand_core::Error> for Error {
83    fn from(e: rand_core::Error) -> Self {
84        Error::Random(e)
85    }
86}
87
88/// Shared keys derived from the CPace protocol.
89#[derive(Debug, Copy, Clone)]
90pub struct SharedKeys {
91    /// First shared key.
92    pub k1: [u8; SHARED_KEY_BYTES],
93    /// Second shared key.
94    pub k2: [u8; SHARED_KEY_BYTES],
95}
96
97/// Internal CPace context.
98#[derive(Debug, Clone)]
99pub struct CPace {
100    session_id: [u8; SESSION_ID_BYTES],
101    p: RistrettoPoint,
102    r: Scalar,
103}
104
105/// Output of the first step of the CPace protocol.
106pub struct Step1Out {
107    ctx: CPace,
108    step1_packet: [u8; STEP1_PACKET_BYTES],
109}
110
111impl Step1Out {
112    /// Retrieves the step 1 packet to send to the peer.
113    pub fn packet(&self) -> [u8; STEP1_PACKET_BYTES] {
114        self.step1_packet
115    }
116
117    /// Completes the protocol using the step 2 packet received from the peer.
118    pub fn step3(&self, step2_packet: &[u8; STEP2_PACKET_BYTES]) -> Result<SharedKeys, Error> {
119        self.ctx.step3(step2_packet)
120    }
121}
122
123/// Output of the second step of the CPace protocol.
124pub struct Step2Out {
125    shared_keys: SharedKeys,
126    step2_packet: [u8; STEP2_PACKET_BYTES],
127}
128
129impl Step2Out {
130    /// Retrieves the shared keys derived from the protocol.
131    pub fn shared_keys(&self) -> SharedKeys {
132        self.shared_keys
133    }
134
135    /// Retrieves the step 2 packet to send to the initiating party.
136    pub fn packet(&self) -> [u8; STEP2_PACKET_BYTES] {
137        self.step2_packet
138    }
139}
140
141impl CPace {
142    /// Creates a new CPace context with a secure random number generator.
143    #[cfg(feature = "getrandom")]
144    fn new<T: AsRef<[u8]>>(
145        session_id: [u8; SESSION_ID_BYTES],
146        password: impl AsRef<[u8]>,
147        id_a: impl AsRef<[u8]>,
148        id_b: impl AsRef<[u8]>,
149        ad: Option<T>,
150    ) -> Result<Self, Error> {
151        Self::new_with_rng(session_id, password, id_a, id_b, ad, rand::rngs::OsRng)
152    }
153
154    /// Creates a new CPace context using a specified random number generator.
155    fn new_with_rng<T: AsRef<[u8]>>(
156        session_id: [u8; SESSION_ID_BYTES],
157        password: impl AsRef<[u8]>,
158        id_a: impl AsRef<[u8]>,
159        id_b: impl AsRef<[u8]>,
160        ad: Option<T>,
161        mut rng: impl CryptoRng + RngCore,
162    ) -> Result<Self, Error> {
163        if id_a.as_ref().len() > 0xff || id_b.as_ref().len() > 0xff {
164            return Err(Error::Overflow(
165                "Identifiers must be at most 255 bytes long",
166            ));
167        }
168        let zpad = [0u8; BLOCKBYTES];
169        let pad_len = zpad
170            .len()
171            .wrapping_sub(DSI1.len() + password.as_ref().len())
172            & (zpad.len() - 1);
173        let mut st = Hash::new();
174        st.update(DSI1);
175        st.update(password);
176        st.update(&zpad[..pad_len]);
177        st.update(session_id);
178        st.update([id_a.as_ref().len() as u8]);
179        st.update(id_a);
180        st.update([id_b.as_ref().len() as u8]);
181        st.update(id_b);
182        st.update(ad.as_ref().map(|ad| ad.as_ref()).unwrap_or_default());
183        let h = st.finalize();
184        let mut p = RistrettoPoint::from_uniform_bytes(&h);
185        let mut r = [0u8; 64];
186        rng.try_fill_bytes(&mut r[..])?;
187        let r = Scalar::from_bytes_mod_order_wide(&r);
188        p *= r;
189        Ok(CPace { session_id, p, r })
190    }
191
192    /// Derives shared keys after validating the peer's public key.
193    fn finalize(
194        &self,
195        op: RistrettoPoint,
196        ya: RistrettoPoint,
197        yb: RistrettoPoint,
198    ) -> Result<SharedKeys, Error> {
199        if op.is_identity() {
200            return Err(Error::InvalidPublicKey);
201        }
202        let p = op * self.r;
203        let mut st = Hash::new();
204        st.update(DSI2);
205        st.update(p.compress().as_bytes());
206        st.update(ya.compress().as_bytes());
207        st.update(yb.compress().as_bytes());
208        let h = st.finalize();
209        let (mut k1, mut k2) = ([0u8; SHARED_KEY_BYTES], [0u8; SHARED_KEY_BYTES]);
210        k1.copy_from_slice(&h[..SHARED_KEY_BYTES]);
211        k2.copy_from_slice(&h[SHARED_KEY_BYTES..]);
212        Ok(SharedKeys { k1, k2 })
213    }
214
215    /// s. [`step1_with_rng`]
216    #[cfg(feature = "getrandom")]
217    pub fn step1<T: AsRef<[u8]>>(
218        password: impl AsRef<[u8]>,
219        id_a: impl AsRef<[u8]>,
220        id_b: impl AsRef<[u8]>,
221        ad: Option<T>,
222    ) -> Result<Step1Out, Error> {
223        Self::step1_with_rng(password, id_a, id_b, ad, rand::rngs::OsRng)
224    }
225
226    /// Executes the first step of CPace with a custom random number generator.
227    ///
228    /// This function is executed by the **initiator** of the CPace exchange (e.g., the client).
229    ///
230    /// It performs the following actions:
231    /// 1. Generates a random session ID.
232    /// 2. Derives a public key (`p`) based on the shared password, identifiers (`id_a`, `id_b`),
233    ///    optional additional data (`ad`), and a random scalar `r`.
234    /// 3. Creates a `step1_packet` containing the session ID and the compressed public key `p`.
235    ///
236    /// # Arguments
237    ///
238    /// * `password`: The shared password.
239    /// * `id_a`: The identifier of the initiator (e.g., "client").
240    /// * `id_b`: The identifier of the responder (e.g., "server").
241    /// * `ad`: Optional additional data.
242    /// * `rng`: A cryptographically secure random number generator.
243    ///
244    /// # Data to be sent over the wire:
245    ///
246    /// The `step1_packet` returned by this function **must be sent to the responder**.
247    /// This packet contains:
248    /// *   `session_id`: A unique identifier for this CPace exchange. (16 bytes)
249    /// *   `p`: The initiator's public key derived from the password. (32 bytes compressed)
250    ///
251    /// # Returns
252    ///
253    /// * `Ok(Step1Out)`: Contains the CPace context and the `step1_packet`.
254    /// * `Err(Error)`: If an error occurs during random number generation or context creation.
255    pub fn step1_with_rng<T: AsRef<[u8]>>(
256        password: impl AsRef<[u8]>,
257        id_a: impl AsRef<[u8]>,
258        id_b: impl AsRef<[u8]>,
259        ad: Option<T>,
260        mut rng: impl CryptoRng + RngCore,
261    ) -> Result<Step1Out, Error> {
262        let mut session_id = [0u8; SESSION_ID_BYTES];
263        rng.try_fill_bytes(&mut session_id)?;
264        let ctx = CPace::new_with_rng(session_id, password, id_a, id_b, ad, rng)?;
265        let mut step1_packet = [0u8; STEP1_PACKET_BYTES];
266        step1_packet[..SESSION_ID_BYTES].copy_from_slice(&ctx.session_id);
267        step1_packet[SESSION_ID_BYTES..].copy_from_slice(ctx.p.compress().as_bytes());
268        Ok(Step1Out { ctx, step1_packet })
269    }
270
271    /// Executes step 2 with a secure random number generator.
272    #[cfg(feature = "getrandom")]
273    pub fn step2<T: AsRef<[u8]>>(
274        step1_packet: &[u8; STEP1_PACKET_BYTES],
275        password: impl AsRef<[u8]>,
276        id_a: impl AsRef<[u8]>,
277        id_b: impl AsRef<[u8]>,
278        ad: Option<T>,
279    ) -> Result<Step2Out, Error> {
280        Self::step2_with_rng(step1_packet, password, id_a, id_b, ad, rand::rngs::OsRng)
281    }
282
283    /// Executes the second step of CPace with a custom random number generator.
284    ///
285    /// This function is executed by the **responder** to the CPace exchange (e.g., the server).
286    ///
287    /// It takes the `step1_packet` received from the initiator as input and performs the following:
288    /// 1. Extracts the session ID and the initiator's public key (`ya`) from the `step1_packet`.
289    /// 2. Derives a public key (`p`) based on the shared password, identifiers, additional data, and a random scalar.
290    /// 3. Creates a `step2_packet` containing the compressed public key `p`.
291    /// 4. Derives the shared keys using `ya`, `ya` and the internal state.
292    ///
293    /// # Arguments
294    ///
295    /// * `step1_packet`: The packet received from the initiator in step 1.
296    /// * `password`: The shared password.
297    /// * `id_a`: The identifier of the initiator.
298    /// * `id_b`: The identifier of the responder.
299    /// * `ad`: Optional additional data.
300    /// * `rng`: A cryptographically secure random number generator.
301    ///
302    /// # Data to be sent over the wire:
303    ///
304    /// The `step2_packet` returned by this function **must be sent back to the initiator**.
305    /// This packet contains:
306    /// *   `p`: The responder's public key derived from the password. (32 bytes compressed)
307    ///
308    /// # Returns
309    ///
310    /// * `Ok(Step2Out)`: Contains the shared keys and the `step2_packet`.
311    /// * `Err(Error)`: If an error occurs during packet processing, context creation, or key derivation.
312    pub fn step2_with_rng<T: AsRef<[u8]>>(
313        step1_packet: &[u8; STEP1_PACKET_BYTES],
314        password: impl AsRef<[u8]>,
315        id_a: impl AsRef<[u8]>,
316        id_b: impl AsRef<[u8]>,
317        ad: Option<T>,
318        rng: impl CryptoRng + RngCore,
319    ) -> Result<Step2Out, Error> {
320        let mut session_id = [0u8; SESSION_ID_BYTES];
321        session_id.copy_from_slice(&step1_packet[..SESSION_ID_BYTES]);
322        let ya = &step1_packet[SESSION_ID_BYTES..];
323        let ctx = CPace::new_with_rng(session_id, password, id_a, id_b, ad, rng)?;
324        let mut step2_packet = [0u8; STEP2_PACKET_BYTES];
325        step2_packet.copy_from_slice(ctx.p.compress().as_bytes());
326        let ya = CompressedRistretto::from_slice(ya)
327            .map_err(|_| Error::InvalidPublicKey)?
328            .decompress()
329            .ok_or(Error::InvalidPublicKey)?;
330        let shared_keys = ctx.finalize(ya, ya, ctx.p)?;
331        Ok(Step2Out {
332            shared_keys,
333            step2_packet,
334        })
335    }
336
337    /// Executes the third step of CPace, deriving the shared keys.
338    ///
339    /// This function is called by the **initiator** (the one who called `step1`) after receiving the `step2_packet`.
340    ///
341    /// It performs:
342    /// 1. Decompresses the received `step2_packet` to obtain the responder's public key (`yb`).
343    /// 2. Derives the final shared keys using `yb`, the local public key (`self.p`), and `yb` again.
344    ///
345    /// # Arguments
346    ///
347    /// * `step2_packet`: The packet received from the responder in step 2.
348    ///
349    /// # Data to be sent over the wire:
350    ///
351    /// **No data is sent over the wire in this step.** This step is performed locally by the initiator.
352    ///
353    /// # Returns
354    ///
355    /// * `Ok(SharedKeys)`: The derived shared keys.
356    /// * `Err(Error)`: If an error occurs during packet processing or key derivation.
357    ///
358    /// # Details
359    ///
360    /// This step completes the key exchange. Both parties now possess the same shared keys (k1 and k2).
361    /// The `finalize` function performs the core cryptographic operations to derive these shared keys.
362    /// The input to finalize is constructed as follows:
363    /// - `op`: Is set to the other party's public key `yb`.
364    /// - `ya`: Is set to the local public key `self.p`.
365    /// - `yb`: Is set to the other party's public key `yb`.
366    /// This construction, along with the internal logic of `finalize`, ensures that both parties derive the same shared secret.
367    pub fn step3(&self, step2_packet: &[u8; STEP2_PACKET_BYTES]) -> Result<SharedKeys, Error> {
368        let yb = CompressedRistretto::from_slice(step2_packet)
369            .map_err(|_| Error::InvalidPublicKey)?
370            .decompress()
371            .ok_or(Error::InvalidPublicKey)?;
372        self.finalize(yb, self.p, yb)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use rand::rngs::OsRng;
380
381    #[test]
382    fn test_cpace() {
383        let client =
384            CPace::step1_with_rng("password", "client", "server", Some("ad"), OsRng).unwrap();
385
386        let step2 = CPace::step2_with_rng(
387            &client.packet(),
388            "password",
389            "client",
390            "server",
391            Some("ad"),
392            OsRng,
393        )
394        .unwrap();
395
396        let shared_keys = client.step3(&step2.packet()).unwrap();
397
398        assert_eq!(shared_keys.k1, step2.shared_keys.k1);
399        assert_eq!(shared_keys.k2, step2.shared_keys.k2);
400    }
401}