Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_crypto/src/agree.rs

12.5 KiB, 1 run

created by r1870400018:35396, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! X25519 key agreement: encapsulating a session key to a named recipient.
2//!
3//! This is the classical half of [`oxedyne_fe2o3_iop_crypto::kem::KeyExchanger`],
4//! beside the post-quantum [`crate::kem`], and it is deliberately not shaped on
5//! it: that module's `encap` ignores the public key it is given and encapsulates
6//! to the key the scheme itself holds, which is the opposite of what a caller
7//! wrapping a secret for somebody else needs.
8//!
9//! # What it costs to build
10//!
11//! Nothing new in the dependency graph. `curve25519-dalek` is already there
12//! through `ed25519-dalek`, and [`MontgomeryPoint::mul_clamped`] is public and
13//! unfeatured, so the whole of X25519 is those two calls and a digest.
14//!
15//! # Where it is exercised
16//!
17//! In `ore_store`, not here. This crate's test target has not linked since some
18//! time before 12026-08-22 -- `rust-lld` reports `jent_entropy_collector_alloc`
19//! and three siblings undefined, jitter entropy symbols from the `pqcrypto`
20//! C build -- so a test written beside this code could not be run. That is a
21//! separate defect on the post-quantum path; the consequence here is only that
22//! the tests live at the first downstream caller that links.
23
24use crate::keys::Keys;
25
26use oxedyne_fe2o3_core::prelude::*;
27use oxedyne_fe2o3_hash::hash::HashScheme;
28use oxedyne_fe2o3_iop_crypto::{
29 kem::KeyExchanger,
30 keys::KeyManager,
31};
32use oxedyne_fe2o3_iop_hash::api::{
33 Hasher,
34 HashForm,
35};
36use oxedyne_fe2o3_namex::id::{
37 InNamex,
38 LocalId,
39 NamexId,
40};
41
42use std::{
43 convert::TryFrom,
44 fmt,
45 str,
46};
47
48use curve25519_dalek::montgomery::MontgomeryPoint;
49use rand_core::{
50 OsRng,
51 RngCore,
52};
53use secrecy::{
54 ExposeSecret,
55 Secret,
56};
57
58
59#[derive(Clone)]
60pub enum AgreementScheme {
61 X25519(Keys<
62 {Self::X25519_PK_LEN},
63 {Self::X25519_SK_LEN},
64 >),
65}
66
67impl fmt::Display for AgreementScheme {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 write!(f, "{:?}", self)
70 }
71}
72
73impl fmt::Debug for AgreementScheme {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match self {
76 Self::X25519(..) => write!(f, "X25519"),
77 }
78 }
79}
80
81impl InNamex for AgreementScheme {
82
83 fn name_id(&self) -> Outcome<NamexId> {
84 Ok(match self {
85 Self::X25519(..) =>
86 res!(NamexId::try_from("HFN5dPSwWFeAktUWQEr0S9Zn1LyMSaurR4tPSPM9c0w=")),
87 })
88 }
89
90 /// Version-dependent identifier for the agreement scheme, which is a far more
91 /// compact alternative to the 256 bit Namex id.
92 fn local_id(&self) -> LocalId {
93 match self {
94 Self::X25519(..) => LocalId(1),
95 }
96 }
97
98 fn assoc_names_base64(
99 gname: &'static str,
100 )
101 -> Outcome<Option<Vec<(
102 &'static str,
103 &'static str,
104 )>>>
105 {
106 let ids = match gname {
107 "schemes" => [
108 ("X25519", "HFN5dPSwWFeAktUWQEr0S9Zn1LyMSaurR4tPSPM9c0w="),
109 ],
110 _ => return Err(err!(
111 "The Namex group name '{}' is not recognised for AgreementScheme.", gname;
112 Invalid, Input)),
113 };
114 Ok(if ids.len() == 0 {
115 None
116 } else {
117 Some(ids.to_vec())
118 })
119 }
120}
121
122impl KeyManager for AgreementScheme {
123
124 fn clone_with_keys(&self, pk: Option<&[u8]>, sk: Option<&[u8]>) -> Outcome<Self> {
125 Ok(match self {
126 Self::X25519(..) => Self::X25519(Keys {
127 pk: match pk {
128 Some(pk) => Some(res!(<[u8; Self::X25519_PK_LEN]>::try_from(&pk[..]))),
129 None => None,
130 },
131 sks: match sk {
132 Some(sk) => Some(Secret::new(res!(
133 <[u8; Self::X25519_SK_LEN]>::try_from(&sk[..])
134 ))),
135 None => None,
136 },
137 }),
138 })
139 }
140
141 fn get_public_key(&self) -> Outcome<Option<&[u8]>> {
142 Ok(match self {
143 Self::X25519(keys) => match &keys.pk {
144 Some(k) => Some(&k[..]),
145 None => None,
146 },
147 })
148 }
149
150 fn get_secret_key(&self) -> Outcome<Option<&[u8]>> {
151 Ok(match self {
152 Self::X25519(keys) => match &keys.sks {
153 Some(sks) => {
154 let sk = sks.expose_secret();
155 Some(&sk[..])
156 },
157 None => None,
158 },
159 })
160 }
161
162 fn set_public_key(mut self, pk: Option<&[u8]>) -> Outcome<Self> {
163 match &mut self {
164 Self::X25519(keys) => keys.pk = match pk {
165 Some(pk) => Some(res!(<[u8; Self::X25519_PK_LEN]>::try_from(&pk[..]))),
166 None => None,
167 },
168 }
169 Ok(self)
170 }
171
172 fn set_secret_key(mut self, sk: Option<&[u8]>) -> Outcome<Self> {
173 match &mut self {
174 Self::X25519(keys) => keys.sks = match sk {
175 Some(sk) => Some(Secret::new(res!(
176 <[u8; Self::X25519_SK_LEN]>::try_from(&sk[..])
177 ))),
178 None => None,
179 },
180 }
181 Ok(self)
182 }
183}
184
185impl KeyExchanger for AgreementScheme {
186
187 /// Mints an ephemeral key pair, agrees a session key with `pk`, and hands
188 /// back the ephemeral public key as the encapsulation of it.
189 ///
190 /// The ephemeral key is what makes this worth the thirty-two bytes it costs:
191 /// under a static sender key, one leaked recipient secret opens every session
192 /// key ever sent to that recipient. Here it opens only the encapsulations an
193 /// attacker can still lay hands on. **That protects the encapsulation and not
194 /// whatever was encrypted under the session key**, which is a distinction
195 /// anybody describing this to a user has to keep.
196 fn encap<
197 const PK_LEN: usize,
198 const SESSION_KEY_LEN: usize,
199 const CIPHERTEXT_LEN: usize,
200 >(
201 &self,
202 pk: [u8; PK_LEN],
203 )
204 -> Outcome<(
205 [u8; SESSION_KEY_LEN],
206 [u8; CIPHERTEXT_LEN],
207 )>
208 {
209 match self {
210 Self::X25519(..) => {
211 let theirs = res!(<[u8; Self::X25519_PK_LEN]>::try_from(&pk[..]));
212 let mut eph_sk = [0u8; Self::X25519_SK_LEN];
213 OsRng.fill_bytes(&mut eph_sk);
214 let eph_pk = MontgomeryPoint::mul_base_clamped(eph_sk).to_bytes();
215 let shared = res!(Self::agree(&eph_sk, &theirs));
216 let session = res!(Self::derive(&eph_pk, &theirs, &shared));
217 Ok((
218 res!(<[u8; SESSION_KEY_LEN]>::try_from(&session[..])),
219 res!(<[u8; CIPHERTEXT_LEN]>::try_from(&eph_pk[..])),
220 ))
221 },
222 }
223 }
224
225 /// Recovers the session key from the ephemeral public key [`Self::encap`]
226 /// published beside it.
227 ///
228 /// The recipient's own public key goes into the digest, and it is derived
229 /// from the secret rather than read out of the pair, so a pair holding a
230 /// public key that does not belong to its secret fails to agree rather than
231 /// quietly deriving something the sender never derived.
232 fn decap<
233 const SESSION_KEY_LEN: usize,
234 const CIPHERTEXT_LEN: usize,
235 >(
236 &self,
237 ciphertext: [u8; CIPHERTEXT_LEN],
238 )
239 -> Outcome<[u8; SESSION_KEY_LEN]>
240 {
241 match self {
242 Self::X25519(keys) => match &keys.sks {
243 Some(sks) => {
244 let sk = sks.expose_secret();
245 let eph_pk = res!(<[u8; Self::X25519_PK_LEN]>::try_from(&ciphertext[..]));
246 let ours = MontgomeryPoint::mul_base_clamped(*sk).to_bytes();
247 let shared = res!(Self::agree(sk, &eph_pk));
248 let session = res!(Self::derive(&eph_pk, &ours, &shared));
249 Ok(res!(<[u8; SESSION_KEY_LEN]>::try_from(&session[..])))
250 },
251 None => Err(err!(
252 "Require secret key to de-encapsulate.";
253 Missing, Configuration)),
254 },
255 }
256 }
257}
258
259impl str::FromStr for AgreementScheme {
260 type Err = Error<ErrTag>;
261
262 fn from_str(name: &str) -> std::result::Result<Self, Self::Err> {
263 match name {
264 "X25519" => Ok(Self::new_x25519()),
265 _ => Err(err!(
266 "The key agreement scheme '{}' is not recognised.", name;
267 Invalid, Input)),
268 }
269 }
270}
271
272impl TryFrom<LocalId> for AgreementScheme {
273 type Error = Error<ErrTag>;
274
275 fn try_from(n: LocalId) -> std::result::Result<Self, Self::Error> {
276 match n {
277 LocalId(1) => Ok(Self::new_x25519()),
278 _ => Err(err!(
279 "The key agreement scheme with local id {} is not recognised.", n;
280 Invalid, Input)),
281 }
282 }
283}
284
285impl AgreementScheme {
286
287 pub const X25519_PK_LEN: usize = 32;
288 pub const X25519_SK_LEN: usize = 32;
289 pub const X25519_SESSION_KEY_LEN: usize = 32;
290 // The encapsulation is the ephemeral public key, which is what a
291 // Diffie-Hellman KEM's ciphertext is.
292 pub const X25519_CIPHERTEXT_LEN: usize = 32;
293
294 /// What goes into the digest ahead of the keys, so that a session key
295 /// derived here can never collide with one derived by another protocol from
296 /// the same shared secret.
297 pub const X25519_KDF_TAG: &'static str = "FE2O3-X25519-SHA3-256-1";
298
299 /// Mints a fresh key pair.
300 pub fn new_x25519() -> Self {
301 let mut sk = [0u8; Self::X25519_SK_LEN];
302 OsRng.fill_bytes(&mut sk);
303 let pk = MontgomeryPoint::mul_base_clamped(sk).to_bytes();
304 Self::X25519(Keys::new(Some(pk), Some(Secret::new(sk))))
305 }
306
307 /// The scheme holding no keys, for a caller that is about to install its own.
308 pub fn empty_x25519() -> Self {
309 Self::X25519(Keys::default())
310 }
311
312 /// Takes a secret somebody already holds, deriving the public key from it
313 /// rather than being told it.
314 pub fn x25519_with_secret(sk: &[u8])
315 -> Outcome<Self>
316 {
317 let sk = res!(<[u8; Self::X25519_SK_LEN]>::try_from(sk));
318 let pk = MontgomeryPoint::mul_base_clamped(sk).to_bytes();
319 Ok(Self::X25519(Keys::new(Some(pk), Some(Secret::new(sk)))))
320 }
321
322 /// The public key belonging to an X25519 secret.
323 pub fn x25519_public_of(sk: &[u8])
324 -> Outcome<[u8; Self::X25519_PK_LEN]>
325 {
326 let sk = res!(<[u8; Self::X25519_SK_LEN]>::try_from(sk));
327 Ok(MontgomeryPoint::mul_base_clamped(sk).to_bytes())
328 }
329
330 /// The raw Diffie-Hellman, refusing the all zero result.
331 ///
332 /// A public key of small order drives every secret to the same point,
333 /// whoever holds it, so an all zero agreement is not a shared secret at all;
334 /// RFC 7748 §6.1 says to check for it and this is that check.
335 fn agree(sk: &[u8; Self::X25519_SK_LEN], pk: &[u8; Self::X25519_PK_LEN])
336 -> Outcome<[u8; Self::X25519_SESSION_KEY_LEN]>
337 {
338 let shared = MontgomeryPoint(*pk).mul_clamped(*sk).to_bytes();
339 if shared.iter().all(|b| *b == 0) {
340 return Err(err!(
341 "The X25519 agreement came to zero, which means the public key it was \
342 made against is of small order and agrees the same thing with every \
343 secret. It is refused rather than used.";
344 Invalid, Input, Key));
345 }
346 Ok(shared)
347 }
348
349 /// The session key, over a transcript that names both public keys.
350 ///
351 /// Both ends put in the same three values in the same order, so an attacker
352 /// who substitutes either public key gets a different session key rather
353 /// than one the other end will also derive.
354 fn derive(
355 eph: &[u8; Self::X25519_PK_LEN],
356 theirs: &[u8; Self::X25519_PK_LEN],
357 shared: &[u8; Self::X25519_SESSION_KEY_LEN],
358 )
359 -> Outcome<[u8; Self::X25519_SESSION_KEY_LEN]>
360 {
361 let hashed = HashScheme::new_sha3_256().hash::<0>(&[
362 Self::X25519_KDF_TAG.as_bytes(),
363 &eph[..],
364 &theirs[..],
365 &shared[..],
366 ], []);
367 match hashed.as_hashform() {
368 HashForm::Bytes32(bytes) => Ok(bytes),
369 // SHA3-256 gives thirty-two bytes and nothing else reaches here. It
370 // is an error rather than a fallback value because the one thing a
371 // key derivation must never do quietly is hand back a constant.
372 other => Err(err!(
373 "SHA3-256 returned {:?} rather than thirty-two bytes, so no session \
374 key was derived.", other;
375 Bug, Mismatch)),
376 }
377 }
378}