use subtle::ConstantTimeEq as _;
use x25519_dalek::{PublicKey, StaticSecret};
use zeroize::Zeroizing;
pub(crate) const X25519_PRIVATE_MISMATCH: &str = "X25519 private key d does not match x";
pub(crate) fn x25519_private_matches(x: &[u8], d: &[u8]) -> bool {
if x.len() != 32 || d.len() != 32 {
return false;
}
let mut seed = Zeroizing::new([0u8; 32]);
seed.copy_from_slice(d);
let secret = StaticSecret::from(*seed);
bool::from(PublicKey::from(&secret).as_bytes()[..].ct_eq(x))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
const ALICE_PRIVATE: &str = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a";
const ALICE_PUBLIC: &str = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a";
const BOB_PRIVATE: &str = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb";
const BOB_PUBLIC: &str = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f";
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)) {
*byte = u8::from_str_radix(core::str::from_utf8(pair).unwrap(), 16).unwrap();
}
out
}
#[test]
fn rfc7748_6_1_key_pairs_match() {
for (d, x) in [(ALICE_PRIVATE, ALICE_PUBLIC), (BOB_PRIVATE, BOB_PUBLIC)] {
assert!(x25519_private_matches(&hex32(x), &hex32(d)), "{x}");
}
}
#[test]
fn another_keys_public_key_does_not_match() {
assert!(!x25519_private_matches(
&hex32(BOB_PUBLIC),
&hex32(ALICE_PRIVATE)
));
assert!(!x25519_private_matches(
&hex32(ALICE_PUBLIC),
&hex32(BOB_PRIVATE)
));
}
#[test]
fn clamped_bits_of_d_are_ignored() {
let mut d = hex32(ALICE_PRIVATE);
d[0] ^= 0x07;
d[31] ^= 0xc0;
assert!(x25519_private_matches(&hex32(ALICE_PUBLIC), &d));
}
#[test]
fn the_top_bit_of_x_is_compared() {
let mut x = hex32(ALICE_PUBLIC);
x[31] |= 0x80;
assert!(!x25519_private_matches(&x, &hex32(ALICE_PRIVATE)));
}
#[test]
fn wrong_lengths_do_not_match() {
let (x, d) = (hex32(ALICE_PUBLIC), hex32(ALICE_PRIVATE));
assert!(!x25519_private_matches(&x[..31], &d));
assert!(!x25519_private_matches(&x, &d[..31]));
assert!(!x25519_private_matches(&[x.as_slice(), &[0]].concat(), &d));
assert!(!x25519_private_matches(&x, &[d.as_slice(), &[0]].concat()));
assert!(!x25519_private_matches(&[], &[]));
}
#[test]
fn the_secret_copies_are_wiped_on_drop() {
fn wiped_on_drop<T: zeroize::ZeroizeOnDrop>() {}
wiped_on_drop::<StaticSecret>();
wiped_on_drop::<Zeroizing<[u8; 32]>>();
}
}