diff --git a/elliptic-curve/Cargo.toml b/elliptic-curve/Cargo.toml index c6a4f8f47..6f98bd994 100644 --- a/elliptic-curve/Cargo.toml +++ b/elliptic-curve/Cargo.toml @@ -23,7 +23,9 @@ subtle = { version = "2.2.2", default-features = false } zeroize = { version = "1", optional = true, default-features = false } [features] -default = [] +default = ["rand"] +ecdh = ["rand", "weierstrass", "zeroize"] +rand = ["rand_core"] weierstrass = [] std = [] diff --git a/elliptic-curve/src/ecdh.rs b/elliptic-curve/src/ecdh.rs new file mode 100644 index 000000000..d604ddbbc --- /dev/null +++ b/elliptic-curve/src/ecdh.rs @@ -0,0 +1,197 @@ +//! Elliptic Curve Diffie-Hellman (Ephemeral) Support. +//! +//! This module contains a generic ECDH implementation which is usable with +//! any elliptic curve which implements the [`Arithmetic`] trait (presently +//! the `k256` and `p256` crates) +//! +//! # Usage +//! +//! Have each participant generate an [`EphemeralSecret`] value, compute the +//! [`PublicKey`] for that value, exchange public keys, then each participant +//! uses their [`EphemeralSecret`] and the other participant's [`PublicKey`] +//! to compute a [`SharedSecret`] value. +//! +//! # ⚠️ SECURITY WARNING ⚠️ +//! +//! Ephemeral Diffie-Hellman exchanges are unauthenticated and without a +//! further authentication step are trivially vulnerable to man-in-the-middle +//! attacks! +//! +//! These exchanges should be performed in the context of a protocol which +//! takes further steps to authenticate the peers in a key exchange. + +use crate::{ + consts::U1, + point::Generator, + scalar::NonZeroScalar, + weierstrass::{ + point::{CompressedPoint, CompressedPointSize, UncompressedPoint, UncompressedPointSize}, + public_key::{FromPublicKey, PublicKey}, + Curve, + }, + Arithmetic, ElementBytes, Error, Generate, +}; +use core::ops::{Add, Mul}; +use generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +/// Ephemeral Diffie-Hellman Secret. +/// +/// These are ephemeral "secret key" values which are deliberately designed +/// to avoid being persisted. +pub struct EphemeralSecret +where + C: Curve + Arithmetic, + C::Scalar: Generate + Zeroize, +{ + scalar: NonZeroScalar, +} + +impl EphemeralSecret +where + C: Curve + Arithmetic, + C::Scalar: Clone + Generate + Zeroize, + C::AffinePoint: FromPublicKey + Mul, Output = C::AffinePoint> + Zeroize, + C::ElementSize: Add, + ::Output: Add, + CompressedPoint: From, + UncompressedPoint: From, + CompressedPointSize: ArrayLength, + UncompressedPointSize: ArrayLength, +{ + /// Generate a new [`EphemeralSecret`]. + pub fn generate(rng: impl CryptoRng + RngCore) -> Self { + Self { + scalar: NonZeroScalar::generate(rng), + } + } + + /// Get the public key associated with this ephemeral secret. + /// + /// The `compress` flag enables point compression. + pub fn public_key(&self, compress: bool) -> PublicKey { + let affine_point = C::AffinePoint::generator() * self.scalar.clone(); + + if compress { + PublicKey::Compressed(affine_point.into()) + } else { + PublicKey::Uncompressed(affine_point.into()) + } + } + + /// Compute a Diffie-Hellman shared secret from an ephemeral secret and the + /// [`PublicKey`] of the other participant in the exchange. + pub fn diffie_hellman(&self, public_key: &PublicKey) -> Result, Error> { + let affine_point = C::AffinePoint::from_public_key(public_key); + + if affine_point.is_some().into() { + let shared_secret = affine_point.unwrap() * self.scalar.clone(); + Ok(SharedSecret::new(shared_secret.into())) + } else { + Err(Error) + } + } +} + +impl From<&EphemeralSecret> for PublicKey +where + C: Curve + Arithmetic, + C::Scalar: Clone + Generate + Zeroize, + C::AffinePoint: FromPublicKey + Mul, Output = C::AffinePoint> + Zeroize, + C::ElementSize: Add, + ::Output: Add, + CompressedPoint: From, + UncompressedPoint: From, + CompressedPointSize: ArrayLength, + UncompressedPointSize: ArrayLength, +{ + fn from(ephemeral_secret: &EphemeralSecret) -> Self { + ephemeral_secret.public_key(C::COMPRESS_POINTS) + } +} + +impl Zeroize for EphemeralSecret +where + C: Curve + Arithmetic, + C::Scalar: Generate + Zeroize, +{ + fn zeroize(&mut self) { + self.scalar.zeroize() + } +} + +impl Drop for EphemeralSecret +where + C: Curve + Arithmetic, + C::Scalar: Generate + Zeroize, +{ + fn drop(&mut self) { + self.zeroize(); + } +} + +/// Shared secret value computed via ECDH key agreement. +/// +/// This value contains the raw serialized x-coordinate of the elliptic curve +/// point computed from a Diffie-Hellman exchange. +/// +/// # ⚠️ WARNING: NOT UNIFORMLY RANDOM! ⚠️ +/// +/// This value is not uniformly random and should not be used directly +/// as a cryptographic key for anything which requires that property +/// (e.g. symmetric ciphers). +/// +/// Instead, the resulting value should be used as input to a Key Derivation +/// Function (KDF) or cryptographic hash function to produce a symmetric key. +pub struct SharedSecret { + /// Computed secret value + secret_bytes: ElementBytes, +} + +impl SharedSecret +where + C: Curve + Arithmetic, + C::AffinePoint: Zeroize, + C::ElementSize: Add, + ::Output: Add, + UncompressedPointSize: ArrayLength, +{ + /// Create a new shared secret from the given uncompressed curve point + fn new(mut serialized_point: UncompressedPoint) -> Self { + let secret_bytes = GenericArray::clone_from_slice( + &serialized_point.as_ref()[1..(1 + C::ElementSize::to_usize())], + ); + + serialized_point.zeroize(); + Self { secret_bytes } + } + + /// Shared secret value, serialized as bytes. + /// + /// As noted in the comments for this struct, this value is non-uniform and + /// should not be used directly as a symmetric encryption key, but instead + /// as input to a KDF (or failing that, a hash function) used to produce + /// a symmetric key. + pub fn as_bytes(&self) -> &ElementBytes { + &self.secret_bytes + } +} + +impl Zeroize for SharedSecret +where + C: Curve + Arithmetic, +{ + fn zeroize(&mut self) { + self.secret_bytes.zeroize() + } +} + +impl Drop for SharedSecret +where + C: Curve + Arithmetic, +{ + fn drop(&mut self) { + self.zeroize(); + } +} diff --git a/elliptic-curve/src/lib.rs b/elliptic-curve/src/lib.rs index 9664ee8f3..618795245 100644 --- a/elliptic-curve/src/lib.rs +++ b/elliptic-curve/src/lib.rs @@ -27,6 +27,10 @@ pub mod point; pub mod scalar; pub mod secret_key; +#[cfg(feature = "ecdh")] +#[cfg_attr(docsrs, doc(cfg(feature = "ecdh")))] +pub mod ecdh; + // TODO(tarcieri): other curve forms #[cfg(feature = "weierstrass")] #[cfg_attr(docsrs, doc(cfg(feature = "weierstrass")))] @@ -39,7 +43,7 @@ pub use subtle; #[cfg(feature = "oid")] pub use oid; -#[cfg(feature = "rand_core")] +#[cfg(feature = "rand")] pub use rand_core; #[cfg(feature = "zeroize")] @@ -52,7 +56,7 @@ use core::{ use generic_array::{typenum::Unsigned, ArrayLength, GenericArray}; use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; -#[cfg(feature = "rand_core")] +#[cfg(feature = "rand")] use rand_core::{CryptoRng, RngCore}; /// Byte array containing a serialized scalar value (i.e. an integer) @@ -99,8 +103,8 @@ pub trait FromBytes: ConditionallySelectable + Sized { /// Randomly generate a value. /// /// Primarily intended for use with scalar types for a particular curve. -#[cfg(feature = "rand_core")] -#[cfg_attr(docsrs, doc(cfg(feature = "rand_core")))] +#[cfg(feature = "rand")] +#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub trait Generate { /// Generate a random element of this type using the provided [`CryptoRng`] fn generate(rng: impl CryptoRng + RngCore) -> Self; diff --git a/elliptic-curve/src/scalar.rs b/elliptic-curve/src/scalar.rs index 0237e8cf3..bf08b301a 100644 --- a/elliptic-curve/src/scalar.rs +++ b/elliptic-curve/src/scalar.rs @@ -1,8 +1,14 @@ //! Scalar types -use crate::{Arithmetic, Curve}; +use crate::{Arithmetic, Curve, FromBytes}; use subtle::{ConstantTimeEq, CtOption}; +#[cfg(feature = "rand")] +use crate::{ + rand_core::{CryptoRng, RngCore}, + Generate, +}; + #[cfg(feature = "zeroize")] use zeroize::Zeroize; @@ -14,6 +20,7 @@ use zeroize::Zeroize; /// /// In the context of ECC, it's useful for ensuring that scalar multiplication /// cannot result in the point at infinity. +#[derive(Clone)] pub struct NonZeroScalar { scalar: C::Scalar, } @@ -25,7 +32,8 @@ where /// Create a [`NonZeroScalar`] from a scalar, performing a constant-time /// check that it's non-zero. pub fn new(scalar: C::Scalar) -> CtOption { - let is_zero = scalar.ct_eq(&C::Scalar::default()); + let zero = C::Scalar::from_bytes(&Default::default()).unwrap(); + let is_zero = scalar.ct_eq(&zero); CtOption::new(Self { scalar }, !is_zero) } } @@ -39,6 +47,24 @@ where } } +#[cfg(feature = "rand")] +impl Generate for NonZeroScalar +where + C: Curve + Arithmetic, + C::Scalar: Generate, +{ + fn generate(mut rng: impl CryptoRng + RngCore) -> Self { + // Use rejection sampling to eliminate zeroes + loop { + let result = Self::new(C::Scalar::generate(&mut rng)); + + if result.is_some().into() { + break result.unwrap(); + } + } + } +} + #[cfg(feature = "zeroize")] impl Zeroize for NonZeroScalar where diff --git a/elliptic-curve/src/secret_key.rs b/elliptic-curve/src/secret_key.rs index c792560f4..f6afcfd45 100644 --- a/elliptic-curve/src/secret_key.rs +++ b/elliptic-curve/src/secret_key.rs @@ -14,7 +14,7 @@ use core::{ }; use generic_array::{typenum::Unsigned, GenericArray}; -#[cfg(feature = "rand_core")] +#[cfg(feature = "rand")] use { crate::{Arithmetic, Generate}, rand_core::{CryptoRng, RngCore}, @@ -68,8 +68,8 @@ impl Debug for SecretKey { } } -#[cfg(feature = "rand_core")] -#[cfg_attr(docsrs, doc(cfg(feature = "rand_core")))] +#[cfg(feature = "rand")] +#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] impl Generate for SecretKey where C: Curve + Arithmetic, diff --git a/elliptic-curve/src/weierstrass.rs b/elliptic-curve/src/weierstrass.rs index 36d4a01d3..2858176b8 100644 --- a/elliptic-curve/src/weierstrass.rs +++ b/elliptic-curve/src/weierstrass.rs @@ -9,4 +9,7 @@ pub use self::{ }; /// Marker trait for elliptic curves in short Weierstrass form -pub trait Curve: super::Curve {} +pub trait Curve: super::Curve { + /// Should point compression be applied by default? + const COMPRESS_POINTS: bool; +} diff --git a/elliptic-curve/src/weierstrass/point.rs b/elliptic-curve/src/weierstrass/point.rs index 050d266a3..e2b563eb0 100644 --- a/elliptic-curve/src/weierstrass/point.rs +++ b/elliptic-curve/src/weierstrass/point.rs @@ -14,6 +14,9 @@ use generic_array::{ ArrayLength, GenericArray, }; +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; + /// Size of a compressed elliptic curve point for the given curve when /// serialized using `Elliptic-Curve-Point-to-Octet-String` encoding /// (including leading `0x02` or `0x03` tag byte). @@ -32,8 +35,9 @@ pub type UncompressedPointSize = /// /// #[derive(Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct CompressedPoint +pub struct CompressedPoint where + C: Curve, C::ElementSize: Add, CompressedPointSize: ArrayLength, { @@ -41,8 +45,9 @@ where bytes: GenericArray>, } -impl CompressedPoint +impl CompressedPoint where + C: Curve, C::ElementSize: Add, CompressedPointSize: ArrayLength, { @@ -62,12 +67,10 @@ where B: Into>>, { let bytes = into_bytes.into(); - let tag_byte = bytes.as_ref()[0]; - if tag_byte == 0x02 || tag_byte == 0x03 { - Some(Self { bytes }) - } else { - None + match bytes[0] { + 0x02 | 0x03 => Some(Self { bytes }), + _ => None, } } @@ -84,8 +87,9 @@ where } } -impl AsRef<[u8]> for CompressedPoint +impl AsRef<[u8]> for CompressedPoint where + C: Curve, C::ElementSize: Add, CompressedPointSize: ArrayLength, { @@ -103,13 +107,28 @@ where { } -impl Clone for CompressedPoint +impl Clone for CompressedPoint where + C: Curve, C::ElementSize: Add, CompressedPointSize: ArrayLength, { fn clone(&self) -> Self { - Self::from_bytes(self.bytes.clone()).unwrap() + Self { + bytes: self.bytes.clone(), + } + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for CompressedPoint +where + C: Curve, + C::ElementSize: Add, + CompressedPointSize: ArrayLength, +{ + fn zeroize(&mut self) { + self.bytes.zeroize() } } @@ -130,8 +149,9 @@ where bytes: GenericArray>, } -impl UncompressedPoint +impl UncompressedPoint where + C: Curve, C::ElementSize: Add, ::Output: Add, UncompressedPointSize: ArrayLength, @@ -183,8 +203,9 @@ where } } -impl AsRef<[u8]> for UncompressedPoint +impl AsRef<[u8]> for UncompressedPoint where + C: Curve, C::ElementSize: Add, ::Output: Add, UncompressedPointSize: ArrayLength, @@ -195,8 +216,9 @@ where } } -impl Copy for UncompressedPoint +impl Copy for UncompressedPoint where + C: Curve, C::ElementSize: Add, ::Output: Add, UncompressedPointSize: ArrayLength, @@ -204,13 +226,29 @@ where { } -impl Clone for UncompressedPoint +impl Clone for UncompressedPoint where + C: Curve, C::ElementSize: Add, ::Output: Add, UncompressedPointSize: ArrayLength, { fn clone(&self) -> Self { - Self::from_bytes(self.bytes.clone()).unwrap() + Self { + bytes: self.bytes.clone(), + } + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for UncompressedPoint +where + C: Curve, + C::ElementSize: Add, + ::Output: Add, + UncompressedPointSize: ArrayLength, +{ + fn zeroize(&mut self) { + self.bytes.zeroize() } } diff --git a/elliptic-curve/src/weierstrass/public_key.rs b/elliptic-curve/src/weierstrass/public_key.rs index 82c2d08cc..94614e507 100644 --- a/elliptic-curve/src/weierstrass/public_key.rs +++ b/elliptic-curve/src/weierstrass/public_key.rs @@ -7,6 +7,7 @@ use super::{ }; use crate::{point::Generator, scalar::NonZeroScalar, Arithmetic, Error, FromBytes, SecretKey}; use core::{ + convert::TryFrom, fmt::{self, Debug}, ops::{Add, Mul}, }; @@ -21,8 +22,9 @@ pub type UntaggedPointSize = <::ElementSize as Add>::Outpu /// Public keys for Weierstrass curves #[derive(Clone, Eq, PartialEq, PartialOrd, Ord)] -pub enum PublicKey +pub enum PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -35,8 +37,9 @@ where Uncompressed(UncompressedPoint), } -impl PublicKey +impl PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -49,7 +52,7 @@ where /// 2.3.3 (page 10). /// /// - pub fn from_bytes>(bytes: B) -> Option { + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Option { let slice = bytes.as_ref(); let length = slice.len(); @@ -66,19 +69,6 @@ where } } - /// Decode public key from an compressed elliptic curve point - /// encoded using the `Elliptic-Curve-Point-to-Octet-String` algorithm - /// described in SEC 1: Elliptic Curve Cryptography (Version 2.0) section - /// 2.3.3 (page 10). - /// - /// - pub fn from_compressed_point(into_bytes: B) -> Option - where - B: Into>>, - { - CompressedPoint::from_bytes(into_bytes).map(PublicKey::Compressed) - } - /// Decode public key from a raw uncompressed point serialized /// as a bytestring, without a `0x04`-byte tag. /// @@ -146,8 +136,9 @@ where } } -impl AsRef<[u8]> for PublicKey +impl AsRef<[u8]> for PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -159,8 +150,9 @@ where } } -impl Copy for PublicKey +impl Copy for PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -170,8 +162,9 @@ where { } -impl Debug for PublicKey +impl Debug for PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -182,8 +175,27 @@ where } } -impl From> for PublicKey +impl TryFrom<&SecretKey> for PublicKey +where + C: Curve + Arithmetic, + C::AffinePoint: Mul, Output = C::AffinePoint>, + C::ElementSize: Add, + ::Output: Add, + CompressedPoint: From, + UncompressedPoint: From, + CompressedPointSize: ArrayLength, + UncompressedPointSize: ArrayLength, +{ + type Error = Error; + + fn try_from(secret_key: &SecretKey) -> Result { + Self::from_secret_key(secret_key, C::COMPRESS_POINTS) + } +} + +impl From> for PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -194,8 +206,9 @@ where } } -impl From> for PublicKey +impl From> for PublicKey where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength, @@ -209,8 +222,9 @@ where /// Trait for deserializing a value from a public key. /// /// This is intended for use with the `AffinePoint` type for a given elliptic curve. -pub trait FromPublicKey: Sized +pub trait FromPublicKey: Sized where + C: Curve, C::ElementSize: Add, ::Output: Add, CompressedPointSize: ArrayLength,