Skip to content

Commit

Permalink
ecdsa: remove SignPrimitive and VerifyPrimitive traits (#793)
Browse files Browse the repository at this point in the history
The backstory of these traits was once upon a time we didn't yet have
the trait structure in place to express algorithms like ECDSA signing
and verification generically, so each crate (at the time just `k256` and
`p256`) had a nearly duplicated implementation of ECDSA, with `k256`
including tweaks for low-S normalization.

Now the `ecdsa` crate contains fully generic implementations of both
algorithms, and with the `EcdsaCurve` trait, carries a `NORMALIZE_S`
preference, so these traits are just needless indirection at this point.

This removes the traits, converting non-trivial methods into static
functions in the `hazmat` module, namely `sign_prehashed_rfc6979`.
  • Loading branch information
tarcieri committed Jan 18, 2024
1 parent c5206d4 commit 3ed9867
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 222 deletions.
39 changes: 19 additions & 20 deletions ecdsa/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ macro_rules! new_signing_test {
array::{typenum::Unsigned, Array},
bigint::Encoding,
group::ff::PrimeField,
Curve, CurveArithmetic, Scalar,
Curve, CurveArithmetic, FieldBytes, Scalar,
},
hazmat::SignPrimitive,
hazmat::sign_prehashed,
};

fn decode_scalar(bytes: &[u8]) -> Option<Scalar<$curve>> {
if bytes.len() == <$curve as Curve>::FieldBytesSize::USIZE {
Scalar::<$curve>::from_repr(Array::clone_from_slice(bytes)).into()
Scalar::<$curve>::from_repr(bytes.try_into().unwrap()).into()
} else {
None
}
Expand All @@ -67,8 +67,10 @@ macro_rules! new_signing_test {
vector.m.len(),
"invalid vector.m (must be field-sized digest)"
);
let z = Array::clone_from_slice(vector.m);
let sig = d.try_sign_prehashed(k, &z).expect("ECDSA sign failed").0;
let z = FieldBytes::<$curve>::try_from(vector.m).unwrap();
let sig = sign_prehashed::<$curve>(&d, &k, &z)
.expect("ECDSA sign failed")
.0;

assert_eq!(vector.r, sig.r().to_bytes().as_slice());
assert_eq!(vector.s, sig.s().to_bytes().as_slice());
Expand All @@ -88,8 +90,8 @@ macro_rules! new_verification_test {
sec1::{EncodedPoint, FromEncodedPoint},
AffinePoint, CurveArithmetic, Scalar,
},
hazmat::VerifyPrimitive,
Signature,
signature::hazmat::PrehashVerifier,
Signature, VerifyingKey,
};

#[test]
Expand All @@ -101,16 +103,15 @@ macro_rules! new_verification_test {
false,
);

let q = AffinePoint::<$curve>::from_encoded_point(&q_encoded).unwrap();
let z = Array::clone_from_slice(vector.m);
let q = VerifyingKey::<$curve>::from_encoded_point(&q_encoded).unwrap();

let sig = Signature::from_scalars(
Array::clone_from_slice(vector.r),
Array::clone_from_slice(vector.s),
Array::try_from(vector.r).unwrap(),
Array::try_from(vector.s).unwrap(),
)
.unwrap();

let result = q.verify_prehashed(&z, &sig);
let result = q.verify_prehash(vector.m, &sig);
assert!(result.is_ok());
}
}
Expand All @@ -124,17 +125,15 @@ macro_rules! new_verification_test {
false,
);

let q = AffinePoint::<$curve>::from_encoded_point(&q_encoded).unwrap();
let z = Array::clone_from_slice(vector.m);
let q = VerifyingKey::<$curve>::from_encoded_point(&q_encoded).unwrap();
let r = Array::try_from(vector.r).unwrap();

// Flip a bit in `s`
let mut s_tweaked = Array::clone_from_slice(vector.s);
let mut s_tweaked = Array::try_from(vector.s).unwrap();
s_tweaked[0] ^= 1;

let sig =
Signature::from_scalars(Array::clone_from_slice(vector.r), s_tweaked).unwrap();

let result = q.verify_prehashed(&z, &sig);
let sig = Signature::from_scalars(r, s_tweaked).unwrap();
let result = q.verify_prehash(vector.m, &sig);
assert!(result.is_err());
}
}
Expand Down Expand Up @@ -169,7 +168,7 @@ macro_rules! new_wycheproof_test {
for v in data.iter().take(offset) {
assert_eq!(*v, 0, "EcdsaVerifier: point too large");
}
elliptic_curve::FieldBytes::<C>::clone_from_slice(&data[offset..])
elliptic_curve::FieldBytes::<C>::try_from(&data[offset..]).unwrap()
} else {
// Provided slice is too short and needs to be padded with zeros
// on the left. Build a combined exact iterator to do this.
Expand Down
189 changes: 66 additions & 123 deletions ecdsa/src/hazmat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ use {
ops::{Invert, LinearCombination, MulByGenerator, Reduce},
point::AffineCoordinates,
scalar::IsHigh,
subtle::CtOption,
CurveArithmetic, ProjectivePoint, Scalar,
},
};
Expand All @@ -38,120 +37,11 @@ use {
};

#[cfg(feature = "rfc6979")]
use elliptic_curve::{FieldBytesEncoding, ScalarPrimitive};
use elliptic_curve::FieldBytesEncoding;

#[cfg(any(feature = "arithmetic", feature = "digest"))]
use crate::{elliptic_curve::array::ArraySize, Signature};

/// Try to sign the given prehashed message using ECDSA.
///
/// This trait is intended to be implemented on a type with access to the
/// secret scalar via `&self`, such as particular curve's `Scalar` type.
#[cfg(feature = "arithmetic")]
pub trait SignPrimitive<C>:
AsRef<Self>
+ Into<FieldBytes<C>>
+ IsHigh
+ PrimeField<Repr = FieldBytes<C>>
+ Reduce<C::Uint, Bytes = FieldBytes<C>>
+ Sized
where
C: EcdsaCurve + CurveArithmetic<Scalar = Self>,
SignatureSize<C>: ArraySize,
{
/// Try to sign the prehashed message.
///
/// Accepts the following arguments:
///
/// - `k`: ephemeral scalar value. MUST BE UNIFORMLY RANDOM!!!
/// - `z`: message digest to be signed. MUST BE OUTPUT OF A CRYPTOGRAPHICALLY
/// SECURE DIGEST ALGORITHM!!!
///
/// # Returns
///
/// ECDSA [`Signature`] and, when possible/desired, a [`RecoveryId`]
/// which can be used to recover the verifying key for a given signature.
fn try_sign_prehashed<K>(
&self,
k: K,
z: &FieldBytes<C>,
) -> Result<(Signature<C>, Option<RecoveryId>)>
where
K: AsRef<Self> + Invert<Output = CtOption<Self>>,
{
sign_prehashed(self, k, z).map(|(sig, recid)| (sig, (Some(recid))))
}

/// Try to sign the given message digest deterministically using the method
/// described in [RFC6979] for computing ECDSA ephemeral scalar `k`.
///
/// Accepts the following parameters:
/// - `z`: message digest to be signed, i.e. `H(m)`. Does not have to be reduced in advance.
/// - `ad`: optional additional data, e.g. added entropy from an RNG
///
/// [RFC6979]: https://datatracker.ietf.org/doc/html/rfc6979
#[cfg(feature = "rfc6979")]
fn try_sign_prehashed_rfc6979<D>(
&self,
z: &FieldBytes<C>,
ad: &[u8],
) -> Result<(Signature<C>, Option<RecoveryId>)>
where
Self: From<ScalarPrimitive<C>> + Invert<Output = CtOption<Self>>,
D: Digest + BlockSizeUser + FixedOutput + FixedOutputReset,
{
// From RFC6979 § 2.4:
//
// H(m) is transformed into an integer modulo q using the bits2int
// transform and an extra modular reduction:
//
// h = bits2int(H(m)) mod q
let z2 = <Scalar<C> as Reduce<C::Uint>>::reduce_bytes(z);

let k = Scalar::<C>::from_repr(rfc6979::generate_k::<D, _>(
&self.to_repr(),
&C::ORDER.encode_field_bytes(),
&z2.to_repr(),
ad,
))
.unwrap();

self.try_sign_prehashed::<Self>(k, z)
}
}

/// Verify the given prehashed message using ECDSA.
///
/// This trait is intended to be implemented on type which can access
/// the affine point represeting the public key via `&self`, such as a
/// particular curve's `AffinePoint` type.
#[cfg(feature = "arithmetic")]
pub trait VerifyPrimitive<C>: AffineCoordinates<FieldRepr = FieldBytes<C>> + Copy + Sized
where
C: EcdsaCurve + CurveArithmetic<AffinePoint = Self>,
SignatureSize<C>: ArraySize,
{
/// Verify the prehashed message against the provided ECDSA signature.
///
/// Accepts the following arguments:
///
/// - `z`: message digest to be verified. MUST BE OUTPUT OF A
/// CRYPTOGRAPHICALLY SECURE DIGEST ALGORITHM!!!
/// - `sig`: signature to be verified against the key and message
fn verify_prehashed(&self, z: &FieldBytes<C>, sig: &Signature<C>) -> Result<()> {
verify_prehashed(&ProjectivePoint::<C>::from(*self), z, sig)
}

/// Verify message digest against the provided signature.
#[cfg(feature = "digest")]
fn verify_digest<D>(&self, msg_digest: D, sig: &Signature<C>) -> Result<()>
where
D: FixedOutput,
{
self.verify_prehashed(&bits2field::<C>(&msg_digest.finalize_fixed())?, sig)
}
}

/// Bind a preferred [`Digest`] algorithm to an elliptic curve type.
///
/// Generally there is a preferred variety of the SHA-2 family used with ECDSA
Expand Down Expand Up @@ -220,34 +110,37 @@ pub fn bits2field<C: EcdsaCurve>(bits: &[u8]) -> Result<FieldBytes<C>> {
/// - `z`: message digest to be signed. MUST BE OUTPUT OF A CRYPTOGRAPHICALLY
/// SECURE DIGEST ALGORITHM!!!
///
/// # Low-S Normalization
///
/// This function will apply low-S normalization if `<C as EcdsaCurve>::NORMALIZE_S` is true.
///
/// # Returns
///
/// ECDSA [`Signature`] and, when possible/desired, a [`RecoveryId`]
/// which can be used to recover the verifying key for a given signature.
/// ECDSA [`Signature`] and a [`RecoveryId`] which can be used to recover the verifying key for a
/// given signature.
#[cfg(feature = "arithmetic")]
#[allow(non_snake_case)]
pub fn sign_prehashed<C, K>(
pub fn sign_prehashed<C>(
d: &Scalar<C>,
k: K,
k: &Scalar<C>,
z: &FieldBytes<C>,
) -> Result<(Signature<C>, RecoveryId)>
where
C: EcdsaCurve + CurveArithmetic,
K: AsRef<Scalar<C>> + Invert<Output = CtOption<Scalar<C>>>,
SignatureSize<C>: ArraySize,
{
// TODO(tarcieri): use `NonZeroScalar<C>` for `k`.
if k.as_ref().is_zero().into() {
if k.is_zero().into() {
return Err(Error::new());
}

let z = <Scalar<C> as Reduce<C::Uint>>::reduce_bytes(z);

// Compute scalar inversion of 𝑘
let k_inv = Option::<Scalar<C>>::from(k.invert()).ok_or_else(Error::new)?;
let k_inv = Option::<Scalar<C>>::from(Invert::invert(k)).ok_or_else(Error::new)?;

// Compute 𝑹 = 𝑘×𝑮
let R = ProjectivePoint::<C>::mul_by_generator(k.as_ref()).to_affine();
let R = ProjectivePoint::<C>::mul_by_generator(k).to_affine();

// Lift x-coordinate of 𝑹 (element of base field) into a serialized big
// integer, then reduce it into an element of the scalar field
Expand All @@ -258,19 +151,69 @@ where
let s = k_inv * (z + (r * d));

// NOTE: `Signature::from_scalars` checks that both `r` and `s` are non-zero.
let signature = Signature::from_scalars(r, s)?;
let recovery_id = RecoveryId::new(R.y_is_odd().into(), x_is_reduced);
let mut signature = Signature::from_scalars(r, s)?;
let mut recovery_id = RecoveryId::new(R.y_is_odd().into(), x_is_reduced);

// Apply low-S normalization if the curve is configured for it
if C::NORMALIZE_S {
recovery_id.0 ^= s.is_high().unwrap_u8();
signature = signature.normalize_s();
}

Ok((signature, recovery_id))
}

/// Try to sign the given message digest deterministically using the method
/// described in [RFC6979] for computing ECDSA ephemeral scalar `k`.
///
/// Accepts the following parameters:
/// - `d`: signing key. MUST BE UNIFORMLY RANDOM!!!
/// - `z`: message digest to be signed, i.e. `H(m)`. Does not have to be reduced in advance.
/// - `ad`: optional additional data, e.g. added entropy from an RNG
///
/// [RFC6979]: https://datatracker.ietf.org/doc/html/rfc6979
#[cfg(feature = "rfc6979")]
pub fn sign_prehashed_rfc6979<C, D>(
d: &Scalar<C>,
z: &FieldBytes<C>,
ad: &[u8],
) -> Result<(Signature<C>, RecoveryId)>
where
C: EcdsaCurve + CurveArithmetic,
D: Digest + BlockSizeUser + FixedOutput + FixedOutputReset,
SignatureSize<C>: ArraySize,
{
// From RFC6979 § 2.4:
//
// H(m) is transformed into an integer modulo q using the bits2int
// transform and an extra modular reduction:
//
// h = bits2int(H(m)) mod q
let z2 = <Scalar<C> as Reduce<C::Uint>>::reduce_bytes(z);

let k = Scalar::<C>::from_repr(rfc6979::generate_k::<D, _>(
&d.to_repr(),
&C::ORDER.encode_field_bytes(),
&z2.to_repr(),
ad,
))
.unwrap();

sign_prehashed(d, &k, z)
}

/// Verify the prehashed message against the provided ECDSA signature.
///
/// Accepts the following arguments:
///
/// - `q`: public key with which to verify the signature.
/// - `z`: message digest to be verified. MUST BE OUTPUT OF A
/// CRYPTOGRAPHICALLY SECURE DIGEST ALGORITHM!!!
/// - `z`: message digest to be verified. MUST BE OUTPUT OF A CRYPTOGRAPHICALLY SECURE DIGEST
/// ALGORITHM!!!
/// - `sig`: signature to be verified against the key and message.
///
/// # Low-S Normalization
///
/// This is a low-level function that does *NOT* apply the `EcdsaCurve::NORMALIZE_S` checks.
#[cfg(feature = "arithmetic")]
pub fn verify_prehashed<C>(
q: &ProjectivePoint<C>,
Expand Down
Loading

0 comments on commit 3ed9867

Please sign in to comment.