├── .gitignore ├── README.md ├── Cargo.toml ├── tests └── basic.rs ├── .github └── workflows │ └── quickstart.yml └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpace 2 | 3 | A Rust implementation of the CPace PAKE, instantiated with the 4 | ristretto255 group. 5 | 6 | This implementation is based on the 7 | [go-cpace-ristretto255](https://github.com/FiloSottile/go-cpace-ristretto255) 8 | implementation by Filippo Valsorda, and attempts to be compatible with it. 9 | Note that *that* implementation is loosely based on draft-haase-cpace-01, so all of these implementations are something of a moving target. 10 | 11 | [Documentation](https://docs.rs/cpace). 12 | 13 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpace" 3 | # Before bumping version: 4 | # - update html_root_url 5 | version = "0.1.0" 6 | authors = ["Henry de Valence "] 7 | edition = "2018" 8 | license = "BSD-3-Clause" 9 | description = "An implementation of the CPace password-authenticated key exchange (PAKE)." 10 | repository = "https://github.com/hdevalence/cpace" 11 | readme = "README.md" 12 | keywords = ["crypto", "cryptography", "ristretto", "pake", "cpace"] 13 | 14 | [dependencies] 15 | thiserror = "1.0.14" 16 | curve25519-dalek = "2.0.0" 17 | hkdf = "0.8" 18 | rand_core = "0.5" 19 | sha2 = "0.8.1" 20 | 21 | [dev-dependencies] 22 | rand = "0.7" 23 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use rand::rngs::OsRng; 2 | 3 | use cpace; 4 | 5 | #[test] 6 | fn key_agreement() { 7 | let (init_msg, state) = cpace::init( 8 | "password", 9 | cpace::Context { 10 | initiator_id: "Alice", 11 | responder_id: "Bob", 12 | associated_data: b"", 13 | }, 14 | OsRng, 15 | ) 16 | .unwrap(); 17 | 18 | let (bob_key, rsp_msg) = cpace::respond( 19 | init_msg, 20 | "password", 21 | cpace::Context { 22 | initiator_id: "Alice", 23 | responder_id: "Bob", 24 | associated_data: b"", 25 | }, 26 | OsRng, 27 | ) 28 | .unwrap(); 29 | 30 | let alice_key = state.recv(rsp_msg).unwrap(); 31 | 32 | assert_eq!(alice_key.0[..], bob_key.0[..]); 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/quickstart.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An implementation of the CPace Password-Authenticated Key Exchange (PAKE) 2 | //! using Ristretto255. Note that this is an experimental implementation of a 3 | //! draft spec -- don't deploy it until 1.0. 4 | //! 5 | //! This implementation is based on [`go-cpace-ristretto255`](https://github.com/FiloSottile/go-cpace-ristretto255) by Filippo Valsorda. 6 | //! 7 | //! # Example 8 | //! 9 | //! ``` 10 | //! use rand::rngs::OsRng; 11 | //! use cpace; 12 | //! 13 | //! let (init_msg, state) = cpace::init( 14 | //! "password", 15 | //! cpace::Context { 16 | //! initiator_id: "Alice", 17 | //! responder_id: "Bob", 18 | //! associated_data: b"", 19 | //! }, 20 | //! OsRng, 21 | //! ) 22 | //! .unwrap(); 23 | //! 24 | //! let (bob_key, rsp_msg) = cpace::respond( 25 | //! init_msg, 26 | //! "password", 27 | //! cpace::Context { 28 | //! initiator_id: "Alice", 29 | //! responder_id: "Bob", 30 | //! associated_data: b"", 31 | //! }, 32 | //! OsRng, 33 | //! ) 34 | //! .unwrap(); 35 | //! 36 | //! let alice_key = state.recv(rsp_msg).unwrap(); 37 | //! 38 | //! assert_eq!(alice_key.0[..], bob_key.0[..]); 39 | //! ``` 40 | #![doc(html_root_url = "https://docs.rs/cpace/0.1.0")] 41 | 42 | use std::convert::TryFrom; 43 | 44 | use curve25519_dalek::{ 45 | ristretto::{CompressedRistretto, RistrettoPoint}, 46 | scalar::Scalar, 47 | }; 48 | use hkdf::Hkdf; 49 | use rand_core::{CryptoRng, RngCore}; 50 | use sha2::Sha512; 51 | 52 | #[derive(Copy, Clone)] 53 | /// The output of the PAKE: a password-authenticated key. 54 | /// 55 | /// XXX this should be 32 bytes. 56 | pub struct Key(pub [u8; 64]); 57 | /// The message sent by the initiator to the responder. 58 | #[derive(Copy, Clone)] 59 | pub struct InitMessage(pub [u8; 48]); 60 | /// The message sent by the responder to the initiator. 61 | #[derive(Copy, Clone)] 62 | pub struct ResponseMessage(pub [u8; 32]); 63 | 64 | use thiserror::Error; 65 | /// An error that occurred while performing a PAKE. 66 | #[derive(Error, Debug)] 67 | pub enum Error { 68 | /// The initiator's ID string was longer than `2**16` bytes. 69 | #[error("{0}-byte initiator id is too long.")] 70 | InitiatorIdTooLong(usize), 71 | /// The responder's ID string was longer than `2**16` bytes. 72 | #[error("{0}-byte responder id is too long.")] 73 | ResponderIdTooLong(usize), 74 | /// The associated data was longer than `2**16` bytes. 75 | #[error("{0}-byte associated data is too long.")] 76 | AssociatedDataTooLong(usize), 77 | /// The remote peer sent an invalid curve point. 78 | #[error("Invalid point encoding.")] 79 | InvalidPoint, 80 | } 81 | 82 | /// Contextual data bound to the resulting [`Key`]. 83 | /// 84 | /// Both peers need to construct identical contexts to agree on a key. 85 | /// 86 | /// The `Context` struct is intended to be constructed inline to the call to 87 | /// [`init`] or [`respond`], simulating named parameters. 88 | #[derive(Copy, Clone)] 89 | pub struct Context<'ctx> { 90 | /// A representation of the identity of the initiator. 91 | pub initiator_id: &'ctx str, 92 | /// A representation of the identity of the responder. 93 | pub responder_id: &'ctx str, 94 | /// Optional associated data the key will be bound to (can be empty). 95 | pub associated_data: &'ctx [u8], 96 | } 97 | 98 | const CONTEXT_LABEL: &[u8; 10] = b"cpace-r255"; 99 | 100 | impl<'ctx> Context<'ctx> { 101 | // XXX this defeats the whole point of the alloc-free Context API. 102 | // the problem is that the current HKDF API doesn't allow streaming 103 | // into the expand call, so we have to build a Vec. 104 | // But doing an internal alloc secretly means we can change it later. 105 | fn serialize(&self) -> Result, Error> { 106 | let mut bytes = Vec::new(); 107 | 108 | let len = u16::try_from(CONTEXT_LABEL.len()).unwrap(); 109 | bytes.extend_from_slice(&len.to_be_bytes()); 110 | bytes.extend_from_slice(CONTEXT_LABEL); 111 | 112 | let len = u16::try_from(self.initiator_id.len()) 113 | .map_err(|_| Error::InitiatorIdTooLong(self.initiator_id.len()))?; 114 | bytes.extend_from_slice(&len.to_be_bytes()); 115 | bytes.extend_from_slice(self.initiator_id.as_bytes()); 116 | 117 | let len = u16::try_from(self.responder_id.len()) 118 | .map_err(|_| Error::ResponderIdTooLong(self.responder_id.len()))?; 119 | bytes.extend_from_slice(&len.to_be_bytes()); 120 | bytes.extend_from_slice(self.responder_id.as_bytes()); 121 | 122 | let len = u16::try_from(self.associated_data.len()) 123 | .map_err(|_| Error::AssociatedDataTooLong(self.associated_data.len()))?; 124 | bytes.extend_from_slice(&len.to_be_bytes()); 125 | bytes.extend_from_slice(self.associated_data); 126 | 127 | Ok(bytes) 128 | } 129 | } 130 | 131 | fn secret_generator(password: &str, salt: &[u8], context_bytes: &[u8]) -> RistrettoPoint { 132 | let mut output = [0; 64]; 133 | // XXX Why does this use Sha512 instead of Sha256? 134 | Hkdf::::new(Some(salt), password.as_bytes()) 135 | .expand(context_bytes, &mut output) 136 | .expect("64 bytes is less than max output size"); 137 | RistrettoPoint::from_uniform_bytes(&output) 138 | } 139 | 140 | fn transcript(init_msg: InitMessage, rsp_msg: ResponseMessage) -> [u8; 16 + 32 + 32] { 141 | let mut bytes = [0; 16 + 32 + 32]; 142 | bytes[0..48].copy_from_slice(&init_msg.0[..]); 143 | bytes[48..].copy_from_slice(&rsp_msg.0[..]); 144 | bytes 145 | } 146 | 147 | /// Initiate a PAKE. 148 | #[allow(non_snake_case)] 149 | pub fn init( 150 | password: &str, 151 | context: Context, 152 | mut rng: R, 153 | ) -> Result<(InitMessage, AwaitingResponse), Error> { 154 | let mut msg_bytes = [0u8; 48]; 155 | let (salt, point) = msg_bytes.split_at_mut(16); 156 | 157 | // One way to think about the protocol is as "spicy DH": 158 | // instead of doing DH with a fixed basepoint, we do DH 159 | // using a basepoint derived from the secret password. 160 | rng.fill_bytes(salt); 161 | let H = secret_generator(password, salt, &context.serialize()?); 162 | let a = Scalar::random(&mut rng); 163 | let A = a * H; 164 | 165 | point.copy_from_slice(A.compress().as_bytes()); 166 | 167 | let init_msg = InitMessage(msg_bytes); 168 | 169 | Ok((init_msg, AwaitingResponse { a, init_msg })) 170 | } 171 | 172 | /// Respond to a PAKE [`InitMessage`]. 173 | #[allow(non_snake_case)] 174 | pub fn respond( 175 | init_msg: InitMessage, 176 | password: &str, 177 | context: Context, 178 | mut rng: R, 179 | ) -> Result<(Key, ResponseMessage), Error> { 180 | let (salt, A_bytes) = init_msg.0[..].split_at(16); 181 | 182 | let H = secret_generator(password, salt, &context.serialize()?); 183 | let b = Scalar::random(&mut rng); 184 | let B = b * H; 185 | 186 | let rsp_msg = ResponseMessage(B.compress().to_bytes()); 187 | 188 | let A = CompressedRistretto::from_slice(A_bytes) 189 | .decompress() 190 | .ok_or(Error::InvalidPoint)?; 191 | 192 | let (key_bytes, _) = Hkdf::::extract( 193 | Some(&transcript(init_msg, rsp_msg)[..]), 194 | (b * A).compress().as_bytes(), 195 | ); 196 | 197 | let key = { 198 | // awkward dance to extract from a GenericArray 199 | let mut bytes = [0; 64]; 200 | bytes.copy_from_slice(&key_bytes[..]); 201 | Key(bytes) 202 | }; 203 | 204 | Ok((key, rsp_msg)) 205 | } 206 | 207 | /// An intermediate initiator state. 208 | pub struct AwaitingResponse { 209 | a: Scalar, 210 | init_msg: InitMessage, 211 | } 212 | 213 | impl AwaitingResponse { 214 | /// Receive the [`ResponseMessage`] from the responder and (hopefully) obtain 215 | /// a shared [`Key`]. 216 | /// 217 | /// Note that this function consumes `self` to ensure that at most one 218 | /// response is processed. 219 | #[allow(non_snake_case)] 220 | pub fn recv(self, rsp_msg: ResponseMessage) -> Result { 221 | let B = CompressedRistretto(rsp_msg.0) 222 | .decompress() 223 | .ok_or(Error::InvalidPoint)?; 224 | 225 | let (key_bytes, _) = Hkdf::::extract( 226 | Some(&transcript(self.init_msg, rsp_msg)[..]), 227 | (self.a * B).compress().as_bytes(), 228 | ); 229 | 230 | let key = { 231 | // awkward dance to extract from a GenericArray 232 | let mut bytes = [0; 64]; 233 | bytes.copy_from_slice(&key_bytes[..]); 234 | Key(bytes) 235 | }; 236 | 237 | Ok(key) 238 | } 239 | } 240 | --------------------------------------------------------------------------------