zerodds_security_keyexchange/
lib.rs1#![cfg_attr(not(feature = "std"), no_std)]
55#![forbid(unsafe_code)]
56#![warn(missing_docs)]
57
58extern crate alloc;
59
60use alloc::vec::Vec;
61
62use ring::agreement::{self, ECDH_P256, EphemeralPrivateKey, PublicKey, X25519};
63use ring::hkdf;
64use ring::rand::SystemRandom;
65use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum KxSuite {
74 X25519,
76 EcdhP256,
78}
79
80impl Default for KxSuite {
81 fn default() -> Self {
82 Self::X25519
83 }
84}
85
86impl KxSuite {
87 #[must_use]
89 pub const fn public_key_len(self) -> usize {
90 match self {
91 Self::X25519 => 32,
92 Self::EcdhP256 => 65,
93 }
94 }
95
96 fn algorithm(self) -> &'static agreement::Algorithm {
97 match self {
98 Self::X25519 => &X25519,
99 Self::EcdhP256 => &ECDH_P256,
100 }
101 }
102}
103
104pub struct KeyExchange {
110 suite: KxSuite,
111 private: EphemeralPrivateKey,
112 public: PublicKey,
113}
114
115impl KeyExchange {
116 pub fn new() -> SecurityResult<Self> {
122 Self::with_suite(KxSuite::X25519)
123 }
124
125 pub fn with_suite(suite: KxSuite) -> SecurityResult<Self> {
130 let rng = SystemRandom::new();
131 let private = EphemeralPrivateKey::generate(suite.algorithm(), &rng).map_err(|_| {
132 SecurityError::new(
133 SecurityErrorKind::CryptoFailed,
134 "keyexchange: ephemeral-key generation failed",
135 )
136 })?;
137 let public = private.compute_public_key().map_err(|_| {
138 SecurityError::new(
139 SecurityErrorKind::CryptoFailed,
140 "keyexchange: public-key derivation failed",
141 )
142 })?;
143 Ok(Self {
144 suite,
145 private,
146 public,
147 })
148 }
149
150 #[must_use]
152 pub fn suite(&self) -> KxSuite {
153 self.suite
154 }
155
156 #[must_use]
159 pub fn public_key(&self) -> &[u8] {
160 self.public.as_ref()
161 }
162
163 pub fn derive_shared_secret(self, remote_public_key: &[u8]) -> SecurityResult<Vec<u8>> {
171 if remote_public_key.len() != self.suite.public_key_len() {
172 return Err(SecurityError::new(
173 SecurityErrorKind::BadArgument,
174 alloc::format!(
175 "keyexchange: {:?} public-key muss {} byte sein",
176 self.suite,
177 self.suite.public_key_len()
178 ),
179 ));
180 }
181 let peer = agreement::UnparsedPublicKey::new(self.suite.algorithm(), remote_public_key);
182 agreement::agree_ephemeral(self.private, &peer, |raw_dh| {
183 let salt = hkdf::Salt::new(hkdf::HKDF_SHA256, b"zerodds-security-v1/shared-secret");
186 let prk = salt.extract(raw_dh);
187 let info_parts = [b"DDS:Auth:PKI-DH:secret".as_slice()];
188 let okm = prk.expand(&info_parts, hkdf::HKDF_SHA256).map_err(|_| {
189 SecurityError::new(
190 SecurityErrorKind::CryptoFailed,
191 "keyexchange: HKDF expand failed",
192 )
193 })?;
194 let mut out = [0u8; 32];
195 okm.fill(&mut out).map_err(|_| {
196 SecurityError::new(
197 SecurityErrorKind::CryptoFailed,
198 "keyexchange: HKDF fill failed",
199 )
200 })?;
201 Ok(out.to_vec())
202 })
203 .map_err(|_| {
204 SecurityError::new(
205 SecurityErrorKind::CryptoFailed,
206 "keyexchange: DH agreement rejected (invalid peer key?)",
207 )
208 })
209 .and_then(|r| r)
210 }
211}
212
213#[cfg(test)]
214#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn public_key_is_32_bytes() {
220 let kx = KeyExchange::new().unwrap();
221 assert_eq!(kx.public_key().len(), 32);
222 }
223
224 #[test]
225 fn two_parties_derive_identical_secret() {
226 let alice = KeyExchange::new().unwrap();
227 let bob = KeyExchange::new().unwrap();
228
229 let a_pub = alice.public_key().to_vec();
230 let b_pub = bob.public_key().to_vec();
231
232 let s1 = alice.derive_shared_secret(&b_pub).unwrap();
233 let s2 = bob.derive_shared_secret(&a_pub).unwrap();
234
235 assert_eq!(s1.len(), 32);
236 assert_eq!(s1, s2, "alice + bob muessen identisches secret ableiten");
237 }
238
239 #[test]
240 fn different_pairs_produce_different_secrets() {
241 let alice = KeyExchange::new().unwrap();
242 let bob = KeyExchange::new().unwrap();
243 let b_pub = bob.public_key().to_vec();
244 let s1 = alice.derive_shared_secret(&b_pub).unwrap();
245
246 let alice2 = KeyExchange::new().unwrap();
247 let bob2 = KeyExchange::new().unwrap();
248 let b2_pub = bob2.public_key().to_vec();
249 let s2 = alice2.derive_shared_secret(&b2_pub).unwrap();
250
251 assert_ne!(s1, s2, "andere ephemerals → anderes secret (PFS)");
252 }
253
254 #[test]
255 fn wrong_length_public_key_rejected() {
256 let alice = KeyExchange::new().unwrap();
257 let err = alice.derive_shared_secret(&[0u8; 16]).unwrap_err();
258 assert_eq!(err.kind, SecurityErrorKind::BadArgument);
259 }
260
261 #[test]
262 fn zero_public_key_rejected_by_ring() {
263 let alice = KeyExchange::new().unwrap();
266 let err = alice.derive_shared_secret(&[0u8; 32]).unwrap_err();
267 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
268 }
269
270 #[test]
275 fn default_suite_is_x25519() {
276 let kx = KeyExchange::new().unwrap();
277 assert_eq!(kx.suite(), KxSuite::X25519);
278 assert_eq!(kx.public_key().len(), 32);
279 }
280
281 #[test]
282 fn p256_public_key_is_65_bytes() {
283 let kx = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
284 assert_eq!(kx.suite(), KxSuite::EcdhP256);
285 assert_eq!(kx.public_key().len(), 65);
286 assert_eq!(kx.public_key()[0], 0x04);
288 }
289
290 #[test]
291 fn p256_two_parties_derive_identical_secret() {
292 let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
293 let bob = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
294 let a_pub = alice.public_key().to_vec();
295 let b_pub = bob.public_key().to_vec();
296 let s1 = alice.derive_shared_secret(&b_pub).unwrap();
297 let s2 = bob.derive_shared_secret(&a_pub).unwrap();
298 assert_eq!(s1.len(), 32);
299 assert_eq!(s1, s2);
300 }
301
302 #[test]
303 fn p256_rejects_wrong_length_public_key() {
304 let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
305 let err = alice.derive_shared_secret(&[0u8; 32]).unwrap_err();
306 assert_eq!(err.kind, SecurityErrorKind::BadArgument);
307 }
308
309 #[test]
310 fn p256_rejects_off_curve_point() {
311 let alice = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
314 let mut bogus = [0u8; 65];
315 bogus[0] = 0x04;
316 let err = alice.derive_shared_secret(&bogus).unwrap_err();
317 assert_eq!(err.kind, SecurityErrorKind::CryptoFailed);
318 }
319
320 #[test]
321 fn x25519_and_p256_produce_different_public_key_lengths() {
322 let a = KeyExchange::new().unwrap();
323 let b = KeyExchange::with_suite(KxSuite::EcdhP256).unwrap();
324 assert_ne!(a.public_key().len(), b.public_key().len());
325 }
326}