| // Copyright 2026 The BoringSSL Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! CPace | |
| //! | |
| //! This is an implementation of [CPace IETF draft]. | |
| //! | |
| //! CPace is a quantum-annoying balanced Password-Authenticated Key Exchange scheme. | |
| //! | |
| //! ``` | |
| //! # use bssl_crypto::cpace::{Context, Role}; | |
| //! | |
| //! let password = b"Password"; | |
| //! let channel_id = b"ChannelId"; | |
| //! let session_id = b"SessionId"; | |
| //! let alice_ad = b"ADa"; | |
| //! let bob_ad = b"ADb"; | |
| //! | |
| //! // Alice (initiator) starts the exchange and generates a message. | |
| //! let (alice_msg, alice_ctx) = Context::new( | |
| //! Role::Initiator, | |
| //! password, | |
| //! channel_id, | |
| //! alice_ad, | |
| //! session_id, | |
| //! ); | |
| //! | |
| //! // Bob (responder) starts the exchange and generates a message. | |
| //! let (bob_msg, bob_ctx) = Context::new( | |
| //! Role::Responder, | |
| //! password, | |
| //! channel_id, | |
| //! bob_ad, | |
| //! session_id, | |
| //! ); | |
| //! | |
| //! // Alice and Bob exchange messages and associated data to derive the shared secret. | |
| //! let alice_key = alice_ctx.finalize(&bob_msg, bob_ad).unwrap(); | |
| //! let bob_key = bob_ctx.finalize(&alice_msg, alice_ad).unwrap(); | |
| //! assert_eq!(alice_key, bob_key); | |
| //! ``` | |
| //! | |
| //! [CPace IETF draft]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-cpace | |
| use core::{ | |
| mem::MaybeUninit, | |
| ptr::{ | |
| null_mut, | |
| NonNull, // | |
| }, // | |
| }; | |
| use crate::FfiSlice; | |
| /// CPace context for a single protocol execution. | |
| pub struct Context(NonNull<bssl_sys::CPACE_CTX>); | |
| // Safety: this context data does not use thread-local storage | |
| unsafe impl Send for Context {} | |
| // Safety: public API guarantees that the access will be thread-safe | |
| unsafe impl Sync for Context {} | |
| /// The role that a party plays in the CPace key agreement protocol. | |
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| pub enum Role { | |
| /// Initiator role, corresponding to party A in the specification. | |
| Initiator, | |
| /// Responder role, corresponding to party B in the specification. | |
| Responder, | |
| } | |
| impl From<Role> for bssl_sys::cpace_role_t { | |
| fn from(role: Role) -> Self { | |
| match role { | |
| Role::Initiator => bssl_sys::cpace_role_t_cpace_role_initiator, | |
| Role::Responder => bssl_sys::cpace_role_t_cpace_role_responder, | |
| } | |
| } | |
| } | |
| /// A CPace message to be sent to the peer. | |
| pub type Message = [u8; bssl_sys::CPACE_MSG_LEN as usize]; | |
| /// The shared secret, ISK, derived from a CPace exchange. | |
| pub type Output = [u8; bssl_sys::CPACE_SHARED_SECRET_LEN as usize]; | |
| impl Drop for Context { | |
| fn drop(&mut self) { | |
| unsafe { | |
| // Safety: `self.0` is still a valid point at this point. | |
| bssl_sys::CPACE_CTX_free(self.0.as_ptr()); | |
| } | |
| } | |
| } | |
| impl Context { | |
| /// Create a new CPace context and generate the message to send to the peer. | |
| pub fn new( | |
| role: Role, | |
| password: &[u8], | |
| channel_id: &[u8], | |
| assoc_data: &[u8], | |
| session_id: &[u8], | |
| ) -> (Message, Self) { | |
| let (password, password_len) = (password.as_ffi_ptr(), password.len()); | |
| let (channel_id, channel_id_len) = (channel_id.as_ffi_ptr(), channel_id.len()); | |
| let (assoc_data, assoc_data_len) = (assoc_data.as_ffi_ptr(), assoc_data.len()); | |
| let (sess_id, sess_id_len) = (session_id.as_ffi_ptr(), session_id.len()); | |
| let ptr = unsafe { | |
| // Safety: all pointer and length pairs reference valid slices for the | |
| // duration of the call, and `CPACE_CTX_new` copies the inputs. | |
| bssl_sys::CPACE_CTX_new( | |
| role.into(), | |
| password, | |
| password_len, | |
| channel_id, | |
| channel_id_len, | |
| assoc_data, | |
| assoc_data_len, | |
| sess_id, | |
| sess_id_len, | |
| ) | |
| }; | |
| let ptr = NonNull::new(ptr).expect("allocation failed"); | |
| let mut msg = [MaybeUninit::uninit(); bssl_sys::CPACE_MSG_LEN as usize]; | |
| let rc = unsafe { | |
| // Safety: `ptr` is a valid newly initialized `CPACE_CTX` and `msg` has | |
| // `CPACE_MSG_LEN` bytes of space. | |
| bssl_sys::CPACE_generate_msg(ptr.as_ptr(), msg.as_mut_ptr() as *mut _) | |
| }; | |
| assert!(rc == 1, "unexpected failure of message generation"); | |
| ( | |
| unsafe { | |
| // Safety: By BoringSSL invariant, we always write out some point data to the out | |
| // pointer. | |
| MaybeUninit::from(msg).assume_init() | |
| }, | |
| Self(ptr), | |
| ) | |
| } | |
| /// Finalize the CPace protocol by deriving the shared secret. | |
| /// | |
| /// This function returns error if `peer_message` is an invalid message. | |
| pub fn finalize(self, peer_message: &Message, peer_assoc_data: &[u8]) -> Result<Output, ()> { | |
| let (peer_assoc_data, peer_assoc_data_len) = | |
| (peer_assoc_data.as_ffi_ptr(), peer_assoc_data.len()); | |
| let mut out = [MaybeUninit::uninit(); bssl_sys::CPACE_SHARED_SECRET_LEN as usize]; | |
| let rc = unsafe { | |
| // Safety: | |
| // - `self` witnesses the validity of `self.0` | |
| // - `peer_message` and `peer_assoc_data` do not escape the call | |
| // - type state transition ensures that we are at the correct state of the protocol. | |
| bssl_sys::CPACE_process_msg( | |
| self.0.as_ptr(), | |
| peer_message.as_ptr(), | |
| peer_assoc_data, | |
| peer_assoc_data_len, | |
| out.as_mut_ptr() as *mut _, | |
| null_mut(), | |
| ) | |
| }; | |
| if rc == 1 { | |
| unsafe { | |
| // Safety: By BoringSSL invariant, we always write out some point data to the out | |
| // pointer if the operation was successful. | |
| Ok(MaybeUninit::from(out).assume_init()) | |
| } | |
| } else { | |
| unsafe { | |
| // Safety: we are only clearing a thread-local error queue. | |
| bssl_sys::ERR_clear_error(); | |
| } | |
| Err(()) | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| const PASSWORD: &[u8] = b"Password"; | |
| const CHANNEL_ID: &[u8] = b"some channel"; | |
| const ADA: &[u8] = b"ADa"; | |
| const ADB: &[u8] = b"ADb"; | |
| const SID: &[u8] = &[ | |
| 0x7e, 0x4b, 0x47, 0x91, 0xd6, 0xa8, 0xef, 0x01, 0x9b, 0x93, 0x6c, 0x79, 0xfb, 0x7f, 0x2c, | |
| 0x57, | |
| ]; | |
| #[test] | |
| fn key_exchange() -> Result<(), ()> { | |
| let (alice_msg, alice) = Context::new(Role::Initiator, PASSWORD, CHANNEL_ID, ADA, SID); | |
| let (bob_msg, bob) = Context::new(Role::Responder, PASSWORD, CHANNEL_ID, ADB, SID); | |
| let alice_key = alice.finalize(&bob_msg, ADB)?; | |
| let bob_key = bob.finalize(&alice_msg, ADA)?; | |
| assert_eq!(alice_key.as_ref(), bob_key.as_ref()); | |
| Ok(()) | |
| } | |
| } |