Skip to main content

dryoc/classic/
crypto_kx.rs

1//! # Key exchange
2//!
3//! Implements libsodium's client/server key-exchange construction. Each party
4//! derives one key for receiving and another for sending. Applications must
5//! authenticate peer public keys through a trusted channel.
6//!
7//! ## Classic API example
8//!
9//! ```
10//! use dryoc::classic::crypto_kx::*;
11//!
12//! // Generate random client & server keypairs
13//! let (client_pk, client_sk) = crypto_kx_keypair();
14//! let (server_pk, server_sk) = crypto_kx_keypair();
15//!
16//! // Variables for client & server rx/tx session keys
17//! let (mut crx, mut ctx, mut srx, mut stx) = (
18//!     SessionKey::default(),
19//!     SessionKey::default(),
20//!     SessionKey::default(),
21//!     SessionKey::default(),
22//! );
23//!
24//! // Calculate the client Rx & Tx keys
25//! crypto_kx_client_session_keys(&mut crx, &mut ctx, &client_pk, &client_sk, &server_pk)
26//!     .expect("client kx failed");
27//!
28//! // Calculate the server Rx & Tx keys
29//! crypto_kx_server_session_keys(&mut srx, &mut stx, &server_pk, &server_sk, &client_pk)
30//!     .expect("server kx failed");
31//!
32//! assert_eq!(crx, stx);
33//! assert_eq!(ctx, srx);
34//! ```
35
36use zeroize::Zeroizing;
37
38use super::crypto_core::{crypto_scalarmult, crypto_scalarmult_base};
39use super::crypto_generichash::crypto_generichash;
40use crate::constants::{
41    CRYPTO_KX_PUBLICKEYBYTES, CRYPTO_KX_SECRETKEYBYTES, CRYPTO_KX_SEEDBYTES,
42    CRYPTO_KX_SESSIONKEYBYTES, CRYPTO_SCALARMULT_BYTES,
43};
44use crate::error::Error;
45use crate::types::*;
46
47/// Public key type for key exchange
48pub type PublicKey = [u8; CRYPTO_KX_PUBLICKEYBYTES];
49/// Secret key type for key exchange
50pub type SecretKey = [u8; CRYPTO_KX_SECRETKEYBYTES];
51/// Session data type for key exchange
52pub type SessionKey = [u8; CRYPTO_KX_SESSIONKEYBYTES];
53
54/// Computes and returns a keypair of `(PublicKey, SecretKey)` based on `seed`
55/// upon success. Uses the Blake2b function to derive a secret from `seed`.
56///
57/// Compatible with libsodium's `crypto_kx_seed_keypair`.
58///
59/// # Errors
60///
61/// Returns an error if the underlying generic hash rejects the output. The
62/// fixed output length used here is valid.
63pub fn crypto_kx_seed_keypair(
64    seed: &[u8; CRYPTO_KX_SEEDBYTES],
65) -> Result<(PublicKey, SecretKey), Error> {
66    let mut sk = SecretKey::default();
67    let mut pk = PublicKey::default();
68
69    crypto_generichash(&mut sk, seed, None)?;
70
71    crypto_scalarmult_base(&mut pk, &sk);
72
73    Ok((pk, sk))
74}
75
76/// Returns a randomly generated keypair, suitable for use with key exchange.
77///
78/// Equivalent to libsodium's `crypto_kx_keypair`.
79#[must_use]
80pub fn crypto_kx_keypair() -> (PublicKey, SecretKey) {
81    let sk = SecretKey::generate();
82    let mut pk = PublicKey::default();
83
84    crypto_scalarmult_base(&mut pk, &sk);
85
86    (pk, sk)
87}
88
89/// Derives both session keys as BLAKE2b-512(`shared_secret` || `client_pk` ||
90/// `server_pk`). The secret is borrowed, so the caller's [`Zeroizing`] owner
91/// wipes it; the hash input is assembled in wiping storage and hashed in one
92/// call, because moving an incremental hash state into its finalizer leaves
93/// an unwiped copy of the buffered secret in this frame.
94fn crypto_kx(
95    x1: &mut SessionKey,
96    x2: &mut SessionKey,
97    client_pk: &PublicKey,
98    server_pk: &PublicKey,
99    shared_secret: &[u8; CRYPTO_SCALARMULT_BYTES],
100) -> Result<(), Error> {
101    let mut input = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES + 2 * CRYPTO_KX_PUBLICKEYBYTES]);
102    let (secret, public_keys) = input.split_at_mut(CRYPTO_SCALARMULT_BYTES);
103    let (client, server) = public_keys.split_at_mut(CRYPTO_KX_PUBLICKEYBYTES);
104    secret.copy_from_slice(shared_secret);
105    client.copy_from_slice(client_pk);
106    server.copy_from_slice(server_pk);
107
108    let mut keys = Zeroizing::new([0u8; 2 * CRYPTO_KX_SESSIONKEYBYTES]);
109    crypto_generichash(&mut keys[..], &input[..], None)?;
110
111    x1.copy_from_slice(&keys[..CRYPTO_KX_SESSIONKEYBYTES]);
112    x2.copy_from_slice(&keys[CRYPTO_KX_SESSIONKEYBYTES..]);
113
114    Ok(())
115}
116
117/// Computes client session keys for `rx` and `tx`, using `client_pk`,
118/// `client_sk`, and `server_pk`. Returns unit `()` upon success.
119///
120/// Compatible with libsodium's `crypto_kx_client_session_keys`.
121///
122/// # Errors
123///
124/// Returns an error if `server_pk` is an unacceptable low-order public key or
125/// session-key derivation fails.
126pub fn crypto_kx_client_session_keys(
127    rx: &mut SessionKey,
128    tx: &mut SessionKey,
129    client_pk: &PublicKey,
130    client_sk: &SecretKey,
131    server_pk: &PublicKey,
132) -> Result<(), Error> {
133    let mut shared_secret = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES]);
134
135    crypto_scalarmult(&mut shared_secret, client_sk, server_pk)?;
136
137    crypto_kx(rx, tx, client_pk, server_pk, &shared_secret)
138}
139
140/// Computes server session keys for `rx` and `tx`, using `server_pk`,
141/// `server_sk`, and `client_pk`. Returns unit `()` upon success.
142///
143/// Compatible with libsodium's `crypto_kx_server_session_keys`.
144///
145/// # Errors
146///
147/// Returns an error if `client_pk` is an unacceptable low-order public key or
148/// session-key derivation fails.
149pub fn crypto_kx_server_session_keys(
150    rx: &mut SessionKey,
151    tx: &mut SessionKey,
152    server_pk: &PublicKey,
153    server_sk: &SecretKey,
154    client_pk: &PublicKey,
155) -> Result<(), Error> {
156    let mut shared_secret = Zeroizing::new([0u8; CRYPTO_SCALARMULT_BYTES]);
157
158    crypto_scalarmult(&mut shared_secret, server_sk, client_pk)?;
159
160    crypto_kx(tx, rx, client_pk, server_pk, &shared_secret)
161}
162
163#[cfg(all(test, dryoc_native_tests))]
164mod tests {
165    use super::*;
166    use crate::native_test_util::{
167        kx_client_session_keys, kx_seed_keypair, kx_server_session_keys,
168    };
169    use crate::scalarmult_curve25519::test_vectors::low_order_u_encodings;
170    use crate::utils::test_util::XorShift64;
171
172    /// The seed-derived secret key (Blake2b-256 of the seed) and its public
173    /// key are libsodium's, for the all-zero, all-ones and a random seed.
174    #[test]
175    fn test_kx_seed_keypair_matches_libsodium() {
176        let mut rng = XorShift64::new(0x510e_527f_ade6_82d1);
177        for seed in [
178            [0u8; CRYPTO_KX_SEEDBYTES],
179            [0xff; CRYPTO_KX_SEEDBYTES],
180            rng.next_bytes32(),
181        ] {
182            let (pk, sk) = crypto_kx_seed_keypair(&seed).expect("seed keypair failed");
183            let (sodium_pk, sodium_sk) = kx_seed_keypair(&seed);
184            assert_eq!(sk, sodium_sk, "seed {seed:02x?}");
185            assert_eq!(pk, sodium_pk, "seed {seed:02x?}");
186        }
187    }
188
189    /// Both roles derive libsodium's session keys for a fixed seeded pair,
190    /// and each side's rx is the other's tx.
191    #[test]
192    fn test_kx_session_keys_match_libsodium_for_seeded_pair() {
193        let (client_pk, client_sk) = crypto_kx_seed_keypair(&[0x11; CRYPTO_KX_SEEDBYTES]).unwrap();
194        let (server_pk, server_sk) = crypto_kx_seed_keypair(&[0x22; CRYPTO_KX_SEEDBYTES]).unwrap();
195
196        let (mut crx, mut ctx, mut srx, mut stx) = (
197            SessionKey::default(),
198            SessionKey::default(),
199            SessionKey::default(),
200            SessionKey::default(),
201        );
202        crypto_kx_client_session_keys(&mut crx, &mut ctx, &client_pk, &client_sk, &server_pk)
203            .expect("client kx failed");
204        crypto_kx_server_session_keys(&mut srx, &mut stx, &server_pk, &server_sk, &client_pk)
205            .expect("server kx failed");
206        assert_eq!(crx, stx);
207        assert_eq!(ctx, srx);
208
209        let (so_crx, so_ctx) =
210            kx_client_session_keys(&client_pk, &client_sk, &server_pk).expect("libsodium client");
211        let (so_srx, so_stx) =
212            kx_server_session_keys(&server_pk, &server_sk, &client_pk).expect("libsodium server");
213        assert_eq!(crx, so_crx);
214        assert_eq!(ctx, so_ctx);
215        assert_eq!(srx, so_srx);
216        assert_eq!(stx, so_stx);
217    }
218
219    /// Every low-order peer key (libsodium's blacklist, with and without bit
220    /// 255) is rejected by both roles before any session key is written, as
221    /// libsodium does.
222    #[test]
223    fn test_kx_rejects_low_order_public_keys() {
224        let (pk, sk) = crypto_kx_seed_keypair(&[0x33; CRYPTO_KX_SEEDBYTES]).unwrap();
225
226        for peer_pk in low_order_u_encodings() {
227            let mut rx = [0xa5; CRYPTO_KX_SESSIONKEYBYTES];
228            let mut tx = [0x5a; CRYPTO_KX_SESSIONKEYBYTES];
229            assert!(
230                crypto_kx_client_session_keys(&mut rx, &mut tx, &pk, &sk, &peer_pk).is_err(),
231                "client {peer_pk:02x?}"
232            );
233            assert!(
234                crypto_kx_server_session_keys(&mut rx, &mut tx, &pk, &sk, &peer_pk).is_err(),
235                "server {peer_pk:02x?}"
236            );
237            assert_eq!(rx, [0xa5; CRYPTO_KX_SESSIONKEYBYTES]);
238            assert_eq!(tx, [0x5a; CRYPTO_KX_SESSIONKEYBYTES]);
239
240            // The wrappers also assert that libsodium writes neither key.
241            assert!(
242                kx_client_session_keys(&pk, &sk, &peer_pk).is_err(),
243                "libsodium client {peer_pk:02x?}"
244            );
245            assert!(
246                kx_server_session_keys(&pk, &sk, &peer_pk).is_err(),
247                "libsodium server {peer_pk:02x?}"
248            );
249        }
250    }
251
252    #[test]
253    fn test_kx() {
254        for _ in 0..20 {
255            let (client_pk, client_sk) = crypto_kx_keypair();
256            let (server_pk, server_sk) = crypto_kx_keypair();
257
258            let (mut crx, mut ctx, mut srx, mut stx) = (
259                SessionKey::default(),
260                SessionKey::default(),
261                SessionKey::default(),
262                SessionKey::default(),
263            );
264
265            crypto_kx_client_session_keys(&mut crx, &mut ctx, &client_pk, &client_sk, &server_pk)
266                .expect("client kx failed");
267
268            crypto_kx_server_session_keys(&mut srx, &mut stx, &server_pk, &server_sk, &client_pk)
269                .expect("server kx failed");
270
271            assert_eq!(crx, stx);
272            assert_eq!(ctx, srx);
273
274            let (rx1, tx1) = match kx_client_session_keys(&client_pk, &client_sk, &server_pk) {
275                Ok((rx, tx)) => (rx, tx),
276                Err(()) => panic!("bad server signature"),
277            };
278
279            // server performs the same operation
280            let (rx2, tx2) = match kx_server_session_keys(&server_pk, &server_sk, &client_pk) {
281                Ok((rx, tx)) => (rx, tx),
282                Err(()) => panic!("bad client signature"),
283            };
284
285            assert_eq!(rx1, crx);
286            assert_eq!(rx2, srx);
287            assert_eq!(tx1, ctx);
288            assert_eq!(tx2, stx);
289        }
290    }
291}