use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
use super::random::RandomSource;
use crate::keys::generate::generate_x25519_secret;
use crate::{Error, Result};
pub(crate) fn generate(rng: &mut dyn RandomSource) -> Result<(StaticSecret, PublicKey)> {
let secret = generate_x25519_secret(&mut rng.crypto_rng())?;
let public = PublicKey::from(&secret);
Ok((secret, public))
}
pub(crate) fn agree(secret: &StaticSecret, public: &PublicKey) -> Result<Zeroizing<[u8; 32]>> {
#[cfg(test)]
crate::test_counters::ecdh_agreements::tick();
let shared = secret.diffie_hellman(public);
if !shared.was_contributory() {
return Err(Error::InvalidKey("X25519 shared secret is all-zero"));
}
Ok(Zeroizing::new(shared.to_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwa::KeyMgmtAlg;
use crate::jwk::{Jwk, KeyMaterial, OkpCurve, OkpKey};
use crate::rng::insecure_test_vectors::FixedRng;
use crate::test_counters::ecdh_agreements;
use crate::test_support::wycheproof::{self, Deviation, Outcome, Summary, Test};
use crate::util::{b64url_decode, b64url_encode};
fn hex32(text: &str) -> [u8; 32] {
assert_eq!(text.len(), 64, "{text:?} is not 32 bytes of hex");
let mut out = [0u8; 32];
for (byte, pair) in out.iter_mut().zip(text.as_bytes().chunks(2)) {
let pair = core::str::from_utf8(pair).unwrap();
*byte = u8::from_str_radix(pair, 16).unwrap();
}
out
}
fn b64_32(text: &str) -> [u8; 32] {
b64url_decode(text).unwrap().try_into().unwrap()
}
fn x25519(k: [u8; 32], u: [u8; 32]) -> Result<Zeroizing<[u8; 32]>> {
agree(&StaticSecret::from(k), &PublicKey::from(u))
}
const ALICE_PRIVATE: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
const ALICE_PUBLIC: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
const BOB_PRIVATE: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
const BOB_PUBLIC: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
const SHARED_K: &str = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742";
#[test]
fn rfc7748_section_5_2_x25519_function_vectors() {
let vectors = [
(
"a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",
"e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c",
"c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552",
),
(
"4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d",
"e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493",
"95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957",
),
];
for (scalar, u, out) in vectors {
assert_eq!(*x25519(hex32(scalar), hex32(u)).unwrap(), hex32(out));
}
}
#[test]
fn rfc7748_section_5_2_iterated_vectors() {
let mut k = [0u8; 32];
k[0] = 9;
let mut u = k;
let mut step = || {
let out = *x25519(k, u).unwrap();
u = k;
k = out;
k
};
assert_eq!(
step(),
hex32("422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079")
);
for _ in 1..999 {
step();
}
assert_eq!(
step(),
hex32("684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51")
);
}
#[test]
fn rfc7748_section_6_1_alice_and_bob() {
let mut rng = FixedRng::new([hex32(ALICE_PRIVATE), hex32(BOB_PRIVATE)].concat());
let (alice, alice_public) = generate(&mut rng).unwrap();
let (bob, bob_public) = generate(&mut rng).unwrap();
assert_eq!(alice_public.to_bytes(), hex32(ALICE_PUBLIC));
assert_eq!(bob_public.to_bytes(), hex32(BOB_PUBLIC));
assert_eq!(*agree(&alice, &bob_public).unwrap(), hex32(SHARED_K));
assert_eq!(*agree(&bob, &alice_public).unwrap(), hex32(SHARED_K));
}
#[test]
fn rfc8037_appendix_a_6_ecdh_es_with_x25519() {
let bob_public = PublicKey::from(b64_32("3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"));
let mut rng = FixedRng::new(b64_32("dwdtCnMYpX08FsFyUbJmRd9ML4frwJkqsXf7pR25LCo"));
let (ephemeral, epk) = generate(&mut rng).unwrap();
assert_eq!(
b64url_encode(epk.as_bytes()),
"hSDwCYkwp1R0i33ctD73Wg2_Og0mOBr066SpjqqbTmo"
);
let z = hex32(SHARED_K);
assert_eq!(*agree(&ephemeral, &bob_public).unwrap(), z);
let bob = StaticSecret::from(hex32(BOB_PRIVATE));
assert_eq!(PublicKey::from(&bob), bob_public);
assert_eq!(*agree(&bob, &epk).unwrap(), z);
}
#[test]
fn generate_draws_exactly_32_bytes() {
let mut rng = FixedRng::new([0x42u8; 33]);
let (secret, public) = generate(&mut rng).unwrap();
assert_eq!(rng.remaining(), 1);
assert_eq!(secret.to_bytes(), [0x42u8; 32]);
assert_eq!(public, PublicKey::from(&secret));
}
#[test]
fn generate_matches_the_keys_generator_on_the_same_bytes() {
use crate::keys::generate::generate_okp;
use crate::rng::CryptoRngAdapter;
let counting: [u8; 32] = core::array::from_fn(|i| i as u8);
for bytes in [hex32(ALICE_PRIVATE), counting] {
let mut jwe_rng = FixedRng::new([bytes, [0xEE; 32]].concat());
let (secret, public) = generate(&mut jwe_rng).unwrap();
assert_eq!(jwe_rng.remaining(), 32);
let mut keys_rng = FixedRng::new([bytes, [0xEE; 32]].concat());
let jwk = generate_okp(
OkpCurve::X25519,
&mut CryptoRngAdapter::fixed(&mut keys_rng),
)
.unwrap();
assert_eq!(keys_rng.remaining(), 32);
let KeyMaterial::Okp(key) = jwk.material() else {
panic!("an OKP key")
};
assert_eq!(key.crv(), OkpCurve::X25519);
assert_eq!(key.d(), Some(&secret.to_bytes()[..]));
assert_eq!(key.x(), &public.to_bytes()[..]);
assert_eq!(secret.to_bytes(), bytes);
}
}
#[test]
fn generate_goes_through_the_crate_wide_generator() {
let source = include_str!("ecdh_x25519.rs");
let production = &source[..source.find("#[cfg(test)]").unwrap()];
let needle = ["StaticSecret", "::from("].concat();
assert_eq!(production.matches(needle.as_str()).count(), 0);
assert_eq!(
production
.matches("generate_x25519_secret(&mut rng.crypto_rng())")
.count(),
1
);
}
#[test]
fn generate_fails_closed_without_randomness() {
let mut rng = FixedRng::new([0u8; 31]);
assert!(matches!(
generate(&mut rng),
Err(Error::RandomnessUnavailable)
));
}
#[test]
fn every_agreement_ticks_the_counter_once() {
let alice = StaticSecret::from(hex32(ALICE_PRIVATE));
let (ok, n) = ecdh_agreements::count(|| agree(&alice, &PublicKey::from(hex32(BOB_PUBLIC))));
assert!(ok.is_ok());
assert_eq!(n, 1);
let (zero, n) = ecdh_agreements::count(|| agree(&alice, &PublicKey::from([0u8; 32])));
assert!(zero.is_err());
assert_eq!(n, 1);
}
const ZERO_SHARED: &str = "RFC 7748 §6.1: an all-zero shared secret (a low-order public \
key) is rejected, as RFC 8725 §3.4 asks for ECDH-ES";
const SPECIAL_PUBLIC: &str = "RFC 7748 §5: every 32-octet public key is accepted: the top \
bit is masked, a non-canonical u is reduced mod p, twist and torsion points go through \
the clamped ladder (ADR 0043)";
const XDH: &[Deviation] = &[
Deviation::range(32, 33, Outcome::Reject, ZERO_SHARED),
Deviation::range(63, 86, Outcome::Reject, ZERO_SHARED),
Deviation::range(117, 118, Outcome::Reject, ZERO_SHARED),
Deviation::tests(&[154, 165, 166], Outcome::Reject, ZERO_SHARED),
Deviation::range(2, 31, Outcome::Accept, SPECIAL_PUBLIC),
Deviation::range(87, 99, Outcome::Accept, SPECIAL_PUBLIC),
Deviation::tests(
&[
101, 103, 106, 108, 111, 112, 114, 115, 116, 119, 121, 124, 126, 127, 129, 130,
133, 136, 139, 140, 147, 148, 155, 159, 160, 162, 167, 169, 176, 181, 182, 183,
186, 192, 193, 196, 197, 198, 199, 200, 201, 205, 206, 207, 210, 211, 215, 216,
217, 221, 222, 226, 229, 231, 232, 233, 234, 237, 239, 240, 242, 243, 247, 251,
253, 256, 257, 259, 260, 261, 262, 263, 265, 266, 271, 272, 274, 276, 279, 281,
283, 284, 286, 287, 288, 290, 291, 293, 296, 298, 300, 302, 306, 307, 313, 314,
315, 318, 320, 323, 326, 327, 328, 329, 333, 335, 338, 344, 346, 350, 356, 357,
362, 364, 365, 366, 370, 373, 375, 380, 381, 382, 387, 388, 390, 392, 394, 395,
396, 401, 405, 406, 412, 415, 416, 418, 419, 420, 421, 425, 427, 430, 431, 432,
435, 438, 443, 445, 446, 447, 448, 450, 451, 452, 457, 458, 461, 462, 463, 464,
466, 469, 472, 473, 477, 478, 481, 482, 484, 485, 488, 489, 492, 494, 498, 501,
503, 505, 506, 512,
],
Outcome::Accept,
SPECIAL_PUBLIC,
),
];
fn is_low_order(test: &Test) -> bool {
test.flags
.iter()
.any(|f| f == "LowOrderPublic" || f == "ZeroSharedSecret")
}
fn check_agreement(test: &Test, result: &Result<Zeroizing<[u8; 32]>>, shared: &[u8]) {
let id = test.tc_id;
match result {
Ok(z) => assert_eq!(&z[..], shared, "tcId {id}: wrong shared secret"),
Err(err) => {
assert_eq!(*err, Error::InvalidKey("X25519 shared secret is all-zero"));
assert_eq!(shared, [0u8; 32], "tcId {id}: rejected a non-zero secret");
}
}
assert_eq!(
result.is_err(),
is_low_order(test),
"tcId {id} ({:?}, flags {:?})",
test.comment,
test.flags
);
}
const XDH_SUMMARY: Summary = Summary {
tests: 518,
accepted: 487,
rejected: 31,
deviations: 254,
};
#[test]
fn wycheproof_x25519() {
let suite = wycheproof::load("x25519");
let summary = wycheproof::assert_conforms(&suite, XDH, |_, test| {
let (public, private, shared) = test.agreement();
let secret = StaticSecret::from(<[u8; 32]>::try_from(private).unwrap());
let public = PublicKey::from(<[u8; 32]>::try_from(public).unwrap());
let result = agree(&secret, &public);
check_agreement(test, &result, shared);
Outcome::of(&result)
});
assert_eq!(summary, XDH_SUMMARY);
}
fn okp_x25519(jwk: &Jwk) -> Result<&OkpKey> {
if !jwk.material().serves_key_mgmt_alg(&KeyMgmtAlg::EcdhEs) {
return Err(Error::KeyAlgorithmMismatch);
}
match jwk.material() {
KeyMaterial::Okp(key) if key.crv() == OkpCurve::X25519 => Ok(key),
_ => Err(Error::KeyAlgorithmMismatch),
}
}
fn public_from_jwk(json: &str) -> Result<PublicKey> {
let jwk = Jwk::from_json(json.as_bytes())?;
let x = <[u8; 32]>::try_from(okp_x25519(&jwk)?.x())
.map_err(|_| Error::InvalidKey("X25519 x is not 32 octets"))?;
Ok(PublicKey::from(x))
}
fn secret_from_jwk(json: &str) -> Result<StaticSecret> {
let jwk = Jwk::from_json(json.as_bytes())?;
let d = okp_x25519(&jwk)?
.d()
.ok_or(Error::InvalidKey("X25519 d is missing"))?;
let d = Zeroizing::new(
<[u8; 32]>::try_from(d).map_err(|_| Error::InvalidKey("X25519 d is not 32 octets"))?,
);
Ok(StaticSecret::from(*d))
}
#[test]
fn wycheproof_x25519_jwk() {
let suite = wycheproof::load("x25519_jwk");
let summary = wycheproof::assert_conforms(&suite, XDH, |_, test| {
let (public, private, shared) = test.jwk_agreement();
let (result, agreements) = ecdh_agreements::count(|| {
let secret = secret_from_jwk(private)?;
agree(&secret, &public_from_jwk(public)?)
});
if test.flags.iter().any(|f| f == "InvalidPublic") {
assert!(result.is_err(), "tcId {}: invalid JWK accepted", test.tc_id);
assert_eq!(agreements, 0, "tcId {}: agreed before refusing", test.tc_id);
} else {
assert_eq!(agreements, 1, "tcId {}", test.tc_id);
check_agreement(test, &result, shared);
}
Outcome::of(&result)
});
assert_eq!(
summary,
Summary {
tests: XDH_SUMMARY.tests + 13,
rejected: XDH_SUMMARY.rejected + 13,
..XDH_SUMMARY
}
);
}
const LOW_ORDER_U: [&str; 7] = [
"0000000000000000000000000000000000000000000000000000000000000000",
"0100000000000000000000000000000000000000000000000000000000000000",
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
];
mod properties {
use proptest::prelude::*;
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn agreement_is_symmetric(a in any::<[u8; 32]>(), b in any::<[u8; 32]>()) {
let mut rng = FixedRng::new([a, b].concat());
let (alice, alice_public) = generate(&mut rng).unwrap();
let (bob, bob_public) = generate(&mut rng).unwrap();
let k = agree(&alice, &bob_public).unwrap();
prop_assert_eq!(*k, *agree(&bob, &alice_public).unwrap());
prop_assert_ne!(*k, [0u8; 32]);
}
#[test]
fn low_order_public_keys_are_always_rejected(
secret in any::<[u8; 32]>(),
which in 0..LOW_ORDER_U.len(),
top_bit in any::<bool>(),
) {
let mut u = hex32(LOW_ORDER_U[which]);
if top_bit {
u[31] |= 0x80;
}
prop_assert_eq!(
agree(&StaticSecret::from(secret), &PublicKey::from(u)),
Err(Error::InvalidKey("X25519 shared secret is all-zero"))
);
}
}
}
}