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}