use p256::elliptic_curve::{CurveArithmetic, NonZeroScalar, PublicKey, SecretKey, ecdh};
use zeroize::{Zeroize, Zeroizing};
use super::random::RandomSource;
use crate::Result;
use crate::keys::generate::generate_ec_secret;
pub(crate) fn generate<C: CurveArithmetic>(
rng: &mut dyn RandomSource,
) -> Result<(SecretKey<C>, PublicKey<C>)> {
let secret = generate_ec_secret::<C, _>(&mut rng.crypto_rng())?;
let public = secret.public_key();
Ok((secret, public))
}
pub(crate) fn agree<C: CurveArithmetic>(
secret: &SecretKey<C>,
public: &PublicKey<C>,
) -> Result<Zeroizing<Vec<u8>>> {
#[cfg(test)]
crate::test_counters::ecdh_agreements::tick();
let mut scalar = secret.to_nonzero_scalar();
let borrowed: &NonZeroScalar<C> = &scalar;
let shared = ecdh::diffie_hellman(borrowed, public.as_affine());
scalar.zeroize();
Ok(Zeroizing::new(shared.raw_secret_bytes().to_vec()))
}
#[cfg(test)]
mod tests {
use p256::elliptic_curve::sec1::ToSec1Point;
use p256::elliptic_curve::{FieldBytes, sec1::FromSec1Point};
use super::*;
use crate::Error;
use crate::rng::insecure_test_vectors::FixedRng;
use crate::test_counters::ecdh_agreements;
use crate::util::b64url_decode;
fn b64(text: &str) -> Vec<u8> {
b64url_decode(text).unwrap_or_else(|e| panic!("{text}: {e}"))
}
fn public<C>(x: &str, y: &str) -> PublicKey<C>
where
C: CurveArithmetic,
C::AffinePoint: FromSec1Point<C> + ToSec1Point<C>,
p256::elliptic_curve::FieldBytesSize<C>: p256::elliptic_curve::sec1::ModulusSize,
{
let point = [vec![0x04], b64(x), b64(y)].concat();
PublicKey::<C>::from_sec1_bytes(&point).unwrap_or_else(|e| panic!("{e}"))
}
fn secret<C: CurveArithmetic>(d: &str) -> SecretKey<C> {
let d = b64(d);
let d = <&FieldBytes<C>>::try_from(d.as_slice()).unwrap_or_else(|e| panic!("{e}"));
SecretKey::<C>::from_bytes(d).unwrap_or_else(|e| panic!("{e}"))
}
fn xy<C>(public: &PublicKey<C>) -> (Vec<u8>, Vec<u8>)
where
C: CurveArithmetic,
C::AffinePoint: FromSec1Point<C> + ToSec1Point<C>,
p256::elliptic_curve::FieldBytesSize<C>: p256::elliptic_curve::sec1::ModulusSize,
{
let point = public.to_sec1_point(false);
let (Some(x), Some(y)) = (point.x(), point.y()) else {
panic!("identity");
};
(x.to_vec(), y.to_vec())
}
mod c54 {
pub(super) const D: &str =
"D5H4Y_5PSKZvhfVFbcCYJOtcGZygRgfZkpsBr59Icmmhe9sW6nkZ8WfwhinUfWJg";
pub(super) const X: &str =
"uBo4kHPw6kbjx5l0xowrd_oYzBmaz-GKFZu4xAFFkbYiWgutEK6iuEDsQ6wNdNg3";
pub(super) const Y: &str =
"sp3p5SGhZVC2faXumI-e9JU2Mo8KpoYrFDr5yPNVtW4PgEwZOyQTA-JdaY8tb7E0";
}
mod c55 {
pub(super) const D: &str = "AtH35vJsQ9SGjYfOsjUxYXQKrPH3FjZHmEtSKoSN8cM";
pub(super) const X: &str = "mPUKT_bAWGHIhg0TpjjqVsP1rXWQu_vwVOHHtNkdYoA";
pub(super) const Y: &str = "8BQAsImGeAS46fyWw5MhYfGTT0IjBpFw2SS34Dv4Irs";
}
mod app_c {
pub(super) const ALICE_D: &str = "0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo";
pub(super) const ALICE_X: &str = "gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0";
pub(super) const ALICE_Y: &str = "SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps";
pub(super) const BOB_D: &str = "VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw";
pub(super) const BOB_X: &str = "weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ";
pub(super) const BOB_Y: &str = "e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck";
pub(super) const Z: [u8; 32] = [
158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, 38, 156, 251, 49,
110, 163, 218, 128, 106, 72, 246, 218, 167, 121, 140, 254, 144, 196,
];
}
type Pair<C> = (SecretKey<C>, PublicKey<C>);
fn generate_from<C: CurveArithmetic>(bytes: &[u8]) -> (Result<Pair<C>>, usize) {
let mut fixed = FixedRng::new(bytes);
let pair = generate::<C>(&mut fixed);
(pair, fixed.remaining())
}
#[test]
fn rfc7520_ephemeral_d_values_are_accepted_on_the_first_draw() {
let (pair, left) = generate_from::<p384::NistP384>(&b64(c54::D));
let (secret, public) = pair.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(left, 0);
assert_eq!(secret.to_bytes().to_vec(), b64(c54::D));
assert_eq!(xy(&public), (b64(c54::X), b64(c54::Y)));
let (pair, left) = generate_from::<p256::NistP256>(&b64(c55::D));
let (secret, public) = pair.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(left, 0);
assert_eq!(secret.to_bytes().to_vec(), b64(c55::D));
assert_eq!(xy(&public), (b64(c55::X), b64(c55::Y)));
}
#[test]
fn generate_rejection_samples_through_the_crate_generator() {
let mut bytes = vec![0u8; 32];
bytes.extend([0xFFu8; 32]);
bytes.extend(b64(c55::D));
let (pair, left) = generate_from::<p256::NistP256>(&bytes);
let (secret, _) = pair.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(left, 0);
assert_eq!(secret.to_bytes().to_vec(), b64(c55::D));
let d521: Vec<u8> = (0..66u8).map(|i| if i == 0 { 1 } else { i }).collect();
let (pair, left) = generate_from::<p521::NistP521>(&d521);
let (secret, _) = pair.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(left, 0);
assert_eq!(secret.to_bytes().to_vec(), d521);
}
#[test]
fn generate_fails_closed_when_the_rng_runs_out() {
let (pair, _) = generate_from::<p384::NistP384>(&[7u8; 47]);
assert_eq!(pair.map(|_| ()), Err(Error::RandomnessUnavailable));
}
#[test]
fn consecutive_os_generations_differ() {
let mut os = super::super::random::OsRandom;
let a = generate::<p256::NistP256>(&mut os).unwrap_or_else(|e| panic!("{e}"));
let b = generate::<p256::NistP256>(&mut os).unwrap_or_else(|e| panic!("{e}"));
assert_ne!(a.0.to_bytes(), b.0.to_bytes());
assert_ne!(xy(&a.1), xy(&b.1));
}
#[test]
fn rfc7518_appendix_c_agreement_gives_z_both_ways() {
let alice = secret::<p256::NistP256>(app_c::ALICE_D);
let bob = secret::<p256::NistP256>(app_c::BOB_D);
let alice_public = public::<p256::NistP256>(app_c::ALICE_X, app_c::ALICE_Y);
let bob_public = public::<p256::NistP256>(app_c::BOB_X, app_c::BOB_Y);
assert_eq!(xy(&alice.public_key()), xy(&alice_public));
let (z, ticks) = ecdh_agreements::count(|| agree(&alice, &bob_public));
assert_eq!(z.unwrap_or_else(|e| panic!("{e}")).as_slice(), app_c::Z);
assert_eq!(ticks, 1);
let z = agree(&bob, &alice_public).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(z.as_slice(), app_c::Z);
}
#[test]
fn z_has_the_full_field_length_on_every_curve() {
fn check<C>(len: usize)
where
C: CurveArithmetic,
C::AffinePoint: FromSec1Point<C> + ToSec1Point<C>,
p256::elliptic_curve::FieldBytesSize<C>: p256::elliptic_curve::sec1::ModulusSize,
{
let mut os = super::super::random::OsRandom;
let (a, a_pub) = generate::<C>(&mut os).unwrap_or_else(|e| panic!("{e}"));
let (b, b_pub) = generate::<C>(&mut os).unwrap_or_else(|e| panic!("{e}"));
let ab = agree(&a, &b_pub).unwrap_or_else(|e| panic!("{e}"));
let ba = agree(&b, &a_pub).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(ab.len(), len);
assert_eq!(*ab, *ba);
}
check::<p256::NistP256>(32);
check::<p384::NistP384>(48);
check::<p521::NistP521>(66);
}
mod wycheproof_ecdh {
use super::super::super::ecdh_es::agree_with_epk;
use super::*;
use crate::json::Object;
use crate::jwa::{ContentEncAlg, KeyMgmtAlg};
use crate::jwe::key::{Direction, RecipientKey};
use crate::jwk::{EcCurve, Jwk};
use crate::test_support::wycheproof::{self, Deviation, Outcome, Summary, Test};
use crate::util::b64url_encode;
const ECPOINT: &[Deviation] = &[Deviation::tests(
&[2],
Outcome::Reject,
"RFC 7518 §6.2.1: an EC JWK (and so an epk) has no compressed form; x and y are both required",
)];
const NO_AGREEMENT: &[&str] = &[
"InvalidCurveAttack",
"WrongCurve",
"InvalidEncoding",
"CompressedPoint",
"InvalidPublic",
];
fn recipient(crv: EcCurve, private: &[u8]) -> Option<RecipientKey> {
let len = crv.coordinate_len();
let start = private
.iter()
.position(|&b| b != 0)
.unwrap_or(private.len());
let digits = &private[start..];
if digits.len() > len {
return None;
}
let mut d = vec![0u8; len - digits.len()];
d.extend_from_slice(digits);
match crv {
EcCurve::P256 => SecretKey::<p256::NistP256>::from_slice(&d)
.ok()
.map(RecipientKey::P256Secret),
EcCurve::P384 => SecretKey::<p384::NistP384>::from_slice(&d)
.ok()
.map(RecipientKey::P384Secret),
EcCurve::P521 => SecretKey::<p521::NistP521>::from_slice(&d)
.ok()
.map(RecipientKey::P521Secret),
EcCurve::Secp256k1 => None,
}
}
fn epk(crv: EcCurve, public: &[u8]) -> Option<Object> {
let len = crv.coordinate_len();
if public.len() != 1 + 2 * len || public[0] != 0x04 {
return None;
}
let mut epk = Object::new();
epk.insert("kty", "EC");
epk.insert("crv", crv.as_str());
epk.insert("x", b64url_encode(&public[1..=len]));
epk.insert("y", b64url_encode(&public[1 + len..]));
Some(epk)
}
fn outcome(
test: &Test,
result: &Result<Zeroizing<Vec<u8>>>,
shared: &[u8],
agreements: u64,
) -> Outcome {
let id = test.tc_id;
if let Ok(z) = result {
assert_eq!(z.as_slice(), shared, "tcId {id}: wrong shared secret");
}
if NO_AGREEMENT.iter().any(|flag| test.has_flag(flag)) {
assert!(result.is_err(), "tcId {id}: attack case accepted");
assert_eq!(agreements, 0, "tcId {id}: agreed before refusing");
}
Outcome::of(result)
}
fn ecpoint(name: &str, crv: EcCurve) -> Summary {
let suite = wycheproof::load(name);
wycheproof::assert_conforms(&suite, ECPOINT, |_, test| {
let (public, private, shared) = test.agreement();
let (result, agreements) = ecdh_agreements::count(|| {
let key = recipient(crv, private).ok_or(Error::InvalidKey("scalar"))?;
let epk = epk(crv, public).ok_or(Error::DecryptionFailed)?;
agree_with_epk(&key, &epk)
});
outcome(test, &result, shared, agreements)
})
}
fn webcrypto(name: &str) -> Summary {
let suite = wycheproof::load(name);
wycheproof::assert_conforms(&suite, &[], |_, test| {
let (public, private, shared) = test.jwk_agreement();
let private = Jwk::from_json(private.as_bytes()).unwrap_or_else(|e| panic!("{e}"));
let key = RecipientKey::from_jwk(
&private,
&KeyMgmtAlg::EcdhEs,
&ContentEncAlg::A128GCM,
Direction::Decrypt,
)
.unwrap_or_else(|e| panic!("tcId {}: {e}", test.tc_id));
let epk = crate::json::parse(public.as_bytes())
.ok()
.and_then(crate::json::Value::into_object)
.unwrap_or_else(|| panic!("tcId {}: public is not an object", test.tc_id));
let (result, agreements) = ecdh_agreements::count(|| agree_with_epk(&key, &epk));
outcome(test, &result, shared, agreements)
})
}
fn summary(tests: usize, accepted: usize, deviations: usize) -> Summary {
Summary {
tests,
accepted,
rejected: tests - accepted,
deviations,
}
}
#[test]
fn wycheproof_ecdh_ecpoint() {
assert_eq!(
ecpoint("ecdh_secp256r1_ecpoint", EcCurve::P256),
summary(355, 330, 1)
);
assert_eq!(
ecpoint("ecdh_secp384r1_ecpoint", EcCurve::P384),
summary(790, 771, 1)
);
assert_eq!(
ecpoint("ecdh_secp521r1_ecpoint", EcCurve::P521),
summary(661, 632, 1)
);
}
#[test]
fn wycheproof_ecdh_webcrypto() {
assert_eq!(webcrypto("ecdh_secp256r1_webcrypto"), summary(353, 330, 0));
assert_eq!(webcrypto("ecdh_secp384r1_webcrypto"), summary(794, 771, 0));
assert_eq!(webcrypto("ecdh_secp521r1_webcrypto"), summary(655, 632, 0));
}
}
}