From f68492412d7b2bcfb1182819edbe834d1f81fcd0 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Tue, 1 Sep 2020 19:18:24 -0700 Subject: [PATCH] ecdsa: add RFC6979 test (plus `dev::curve` module) Adds a test that the RFC6979 implementation produces the correct ephemeral scalar (`k`) for the test vector in RFC 6979 Appendix 2.5: https://tools.ietf.org/html/rfc6979#appendix-A.2.5 This unfortunately requires basic scalar support, as RFC6979 uses rejection sampling to select a `k` value, so this commit also contains the rudiments of a P-256 scalar implementation necessary to implement the test. Hopefully this will be useful for testing other aspects of ECDSA, or potentially ensuring that the scalars of an ECDSA signature are in-range generically, even if no curve arithmetic backend is available. --- Cargo.lock | 50 ++++++++- ecdsa/Cargo.toml | 6 +- ecdsa/src/dev.rs | 2 + ecdsa/src/dev/curve.rs | 198 ++++++++++++++++++++++++++++++++++++ ecdsa/src/signer/rfc6979.rs | 27 +++++ 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 ecdsa/src/dev/curve.rs diff --git a/Cargo.lock b/Cargo.lock index 248a4ac5..21011518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,12 +10,33 @@ dependencies = [ "serde", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + [[package]] name = "crypto-mac" version = "0.9.1" @@ -40,7 +61,9 @@ name = "ecdsa" version = "0.7.2" dependencies = [ "elliptic-curve", + "hex-literal", "hmac", + "sha2", "signature", ] @@ -56,7 +79,7 @@ dependencies = [ [[package]] name = "elliptic-curve" version = "0.5.0" -source = "git+https://github.com/RustCrypto/traits#fc56c1b4f649f2126b0f42931d2a7d19cbcdbb0e" +source = "git+https://github.com/RustCrypto/traits#abff234bfe0ced9254615dc608ece09619a8db38" dependencies = [ "digest", "generic-array", @@ -75,6 +98,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "hex-literal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af1f635ef1bc545d78392b136bfe1c9809e029023c84a3638a864a10b8819c8" + [[package]] name = "hmac" version = "0.9.0" @@ -85,6 +114,12 @@ dependencies = [ "digest", ] +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "rand_core" version = "0.5.1" @@ -97,6 +132,19 @@ version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +[[package]] +name = "sha2" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +dependencies = [ + "block-buffer", + "cfg-if", + "cpuid-bool", + "digest", + "opaque-debug", +] + [[package]] name = "signature" version = "1.2.2" diff --git a/ecdsa/Cargo.toml b/ecdsa/Cargo.toml index abd853e4..0d0545a9 100644 --- a/ecdsa/Cargo.toml +++ b/ecdsa/Cargo.toml @@ -18,9 +18,13 @@ elliptic-curve = { version = "0.5", default-features = false, features = ["weier hmac = { version = "0.9", optional = true, default-features = false } signature = { version = ">= 1.2.2, < 1.3.0", default-features = false } +[dev-dependencies] +hex-literal = "0.3" +sha2 = { version = "0.9", default-features = false } + [features] default = ["digest"] -dev = [] +dev = ["digest", "zeroize"] digest = ["elliptic-curve/digest", "signature/digest-preview"] hazmat = [] rand = ["elliptic-curve/rand", "signature/rand-preview"] diff --git a/ecdsa/src/dev.rs b/ecdsa/src/dev.rs index 20221de0..b3889297 100644 --- a/ecdsa/src/dev.rs +++ b/ecdsa/src/dev.rs @@ -1,5 +1,7 @@ //! Development-related functionality +pub mod curve; + // TODO(tarcieri): implement full set of tests from ECDSA2VS // diff --git a/ecdsa/src/dev/curve.rs b/ecdsa/src/dev/curve.rs new file mode 100644 index 00000000..1a80823c --- /dev/null +++ b/ecdsa/src/dev/curve.rs @@ -0,0 +1,198 @@ +//! Minimalist example curve implementation for testing. +//! +//! Modeled after NIST P-256. + +use core::{convert::TryInto, ops::Mul}; +use elliptic_curve::{ + consts::U32, + digest::Digest, + ops::Invert, + point::Generator, + subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}, + util::{adc64, sbb64}, + zeroize::Zeroize, + FromBytes, FromDigest, +}; + +/// Example NIST P-256-like elliptic curve. +/// Implements only the features needed for testing the implementation. +#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] +pub struct ExampleCurve; + +impl elliptic_curve::Curve for ExampleCurve { + type ElementSize = U32; +} + +impl elliptic_curve::weierstrass::Curve for ExampleCurve { + const COMPRESS_POINTS: bool = false; +} + +impl elliptic_curve::Arithmetic for ExampleCurve { + type Scalar = Scalar; + type AffinePoint = AffinePoint; +} + +const LIMBS: usize = 4; + +type U256 = [u64; LIMBS]; + +const MODULUS: U256 = [ + 0xf3b9_cac2_fc63_2551, + 0xbce6_faad_a717_9e84, + 0xffff_ffff_ffff_ffff, + 0xffff_ffff_0000_0000, +]; + +/// Example scalar type +#[derive(Clone, Copy, Debug, Default)] +pub struct Scalar([u64; LIMBS]); + +impl ConditionallySelectable for Scalar { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + Scalar([ + u64::conditional_select(&a.0[0], &b.0[0], choice), + u64::conditional_select(&a.0[1], &b.0[1], choice), + u64::conditional_select(&a.0[2], &b.0[2], choice), + u64::conditional_select(&a.0[3], &b.0[3], choice), + ]) + } +} + +impl ConstantTimeEq for Scalar { + fn ct_eq(&self, other: &Self) -> Choice { + self.0[0].ct_eq(&other.0[0]) + & self.0[1].ct_eq(&other.0[1]) + & self.0[2].ct_eq(&other.0[2]) + & self.0[3].ct_eq(&other.0[3]) + } +} + +impl FromBytes for Scalar { + type Size = U32; + + fn from_bytes(bytes: &ElementBytes) -> CtOption { + let mut w = [0u64; LIMBS]; + + // Interpret the bytes as a big-endian integer w. + w[3] = u64::from_be_bytes(bytes[0..8].try_into().unwrap()); + w[2] = u64::from_be_bytes(bytes[8..16].try_into().unwrap()); + w[1] = u64::from_be_bytes(bytes[16..24].try_into().unwrap()); + w[0] = u64::from_be_bytes(bytes[24..32].try_into().unwrap()); + + // If w is in the range [0, n) then w - n will overflow, resulting in a borrow + // value of 2^64 - 1. + let (_, borrow) = sbb64(w[0], MODULUS[0], 0); + let (_, borrow) = sbb64(w[1], MODULUS[1], borrow); + let (_, borrow) = sbb64(w[2], MODULUS[2], borrow); + let (_, borrow) = sbb64(w[3], MODULUS[3], borrow); + let is_some = (borrow as u8) & 1; + + CtOption::new(Scalar(w), Choice::from(is_some)) + } +} + +impl From for ElementBytes { + fn from(scalar: Scalar) -> Self { + let mut ret = ElementBytes::default(); + ret[0..8].copy_from_slice(&scalar.0[3].to_be_bytes()); + ret[8..16].copy_from_slice(&scalar.0[2].to_be_bytes()); + ret[16..24].copy_from_slice(&scalar.0[1].to_be_bytes()); + ret[24..32].copy_from_slice(&scalar.0[0].to_be_bytes()); + ret + } +} + +impl FromDigest for Scalar { + fn from_digest(digest: D) -> Self + where + D: Digest, + { + let bytes = digest.finalize(); + + Self::sub_inner( + u64::from_be_bytes(bytes[24..32].try_into().unwrap()), + u64::from_be_bytes(bytes[16..24].try_into().unwrap()), + u64::from_be_bytes(bytes[8..16].try_into().unwrap()), + u64::from_be_bytes(bytes[0..8].try_into().unwrap()), + 0, + MODULUS[0], + MODULUS[1], + MODULUS[2], + MODULUS[3], + 0, + ) + } +} + +impl Invert for Scalar { + type Output = Self; + + fn invert(&self) -> CtOption { + unimplemented!(); + } +} + +impl Zeroize for Scalar { + fn zeroize(&mut self) { + self.0.as_mut().zeroize() + } +} + +impl Scalar { + #[allow(clippy::too_many_arguments)] + const fn sub_inner( + l0: u64, + l1: u64, + l2: u64, + l3: u64, + l4: u64, + r0: u64, + r1: u64, + r2: u64, + r3: u64, + r4: u64, + ) -> Self { + let (w0, borrow) = sbb64(l0, r0, 0); + let (w1, borrow) = sbb64(l1, r1, borrow); + let (w2, borrow) = sbb64(l2, r2, borrow); + let (w3, borrow) = sbb64(l3, r3, borrow); + let (_, borrow) = sbb64(l4, r4, borrow); + + let (w0, carry) = adc64(w0, MODULUS[0] & borrow, 0); + let (w1, carry) = adc64(w1, MODULUS[1] & borrow, carry); + let (w2, carry) = adc64(w2, MODULUS[2] & borrow, carry); + let (w3, _) = adc64(w3, MODULUS[3] & borrow, carry); + + Scalar([w0, w1, w2, w3]) + } +} + +/// Field element bytes; +pub type ElementBytes = elliptic_curve::ElementBytes; + +/// Non-zero scalar value. +pub type NonZeroScalar = elliptic_curve::scalar::NonZeroScalar; + +/// Example affine point type +#[derive(Clone, Copy, Debug)] +pub struct AffinePoint {} + +impl ConditionallySelectable for AffinePoint { + fn conditional_select(_a: &Self, _b: &Self, _choice: Choice) -> Self { + unimplemented!(); + } +} + +impl Mul for AffinePoint { + type Output = AffinePoint; + + fn mul(self, _scalar: NonZeroScalar) -> Self { + unimplemented!(); + } +} + +impl Generator for AffinePoint { + fn generator() -> AffinePoint { + unimplemented!(); + } +} diff --git a/ecdsa/src/signer/rfc6979.rs b/ecdsa/src/signer/rfc6979.rs index df75d549..68a77823 100644 --- a/ecdsa/src/signer/rfc6979.rs +++ b/ecdsa/src/signer/rfc6979.rs @@ -98,3 +98,30 @@ where t } } + +#[cfg(all(feature = "dev", test))] +mod tests { + use super::generate_k; + use crate::dev::curve::NonZeroScalar; + use elliptic_curve::FromBytes; + use hex_literal::hex; + use sha2::{Digest, Sha256}; + + /// Test vector from RFC 6979 Appendix 2.5 (NIST P-256 + SHA-256) + /// + #[test] + fn appendix_2_5_test_vector() { + let x = NonZeroScalar::from_bytes( + &hex!("c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721").into(), + ) + .unwrap(); + + let digest = Sha256::new().chain("sample"); + let k = generate_k(&x, digest, &[]); + + assert_eq!( + k.to_bytes().as_slice(), + &hex!("a6e3c57dd01abe90086538398355dd4c3b17aa873382b0f24d6129493d8aad60")[..] + ); + } +}