Skip to content

Commit ed4b91c

Browse files
tarcieribaloo
andauthored
rfc6979: integrate with crypto-bigint; KGenerator API (#1395)
Implements functions which are a 1:1 mapping to the ones defined in RFC6979, operating over `U: Unsigned + Encoding`. Notably this also implements the modular reduction instead of leaving it to the caller to perform. As the RFC notes, it's easily implemented as conditional subtraction. The public API has been changed to a `KGenerator` struct which provides an iterator-like API that can be called repeatedly if needed, e.g. if the computed `r` is zero. This makes it possible to make the `ecdsa::hazmat` RFC6979-based functions infallible, because they can automatically retry with the next `k` until they find one that works. Co-authored-by: Arthur Gautier <baloo@superbaloo.net>
1 parent 04479d0 commit ed4b91c

14 files changed

Lines changed: 626 additions & 526 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dsa/src/generate/secret_number.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,22 @@ where
2929
{
3030
let q = signing_key.verifying_key().components().q();
3131
let size = (q.bits() / 8) as usize;
32+
33+
// Truncate hash and reduce mod q
34+
// TODO(tarcieri): `rfc6979` now truncates and reduces mod q. some of this may be redundant?
3235
let hash = BoxedUint::from_be_slice(&hash[..min(size, hash.len())], q.bits_precision())
3336
.map_err(|_| signature::Error::new())?;
34-
35-
// Reduce hash mod q
3637
let hash = (hash % q).to_be_bytes();
3738
let hash = truncate_hash(&hash, size);
3839

39-
let q_bytes = q.to_be_bytes();
40-
let q_bytes = truncate_hash(&q_bytes, size);
41-
4240
let x_bytes = Zeroizing::new(signing_key.x().to_be_bytes());
4341
let x_bytes = truncate_hash(&x_bytes, size);
4442

43+
let mut kgen = rfc6979::KGenerator::<D, BoxedUint>::new(x_bytes, hash, &[], q);
44+
4545
let mut buffer = vec![0; size];
4646
loop {
47-
rfc6979::generate_k_mut::<D>(x_bytes, q_bytes, hash, &[], &mut buffer);
47+
kgen.fill_next_k(&mut buffer);
4848

4949
let k = BoxedUint::from_be_slice(&buffer, q.bits_precision())
5050
.map_err(|_| signature::Error::new())?;

ecdsa/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,11 @@ default = ["digest"]
3939
alloc = ["elliptic-curve/alloc", "signature/alloc", "spki/alloc"]
4040
std = ["alloc", "elliptic-curve/std"]
4141

42-
algorithm = ["dep:rfc6979", "digest", "elliptic-curve/arithmetic", "hazmat"]
42+
algorithm = ["dep:rfc6979", "digest", "elliptic-curve/arithmetic"]
4343
dev = ["algorithm", "digest/dev", "elliptic-curve/dev"]
4444
der = ["dep:der"]
4545
digest = ["dep:digest", "elliptic-curve/digest", "signature/digest"]
4646
getrandom = ["elliptic-curve/getrandom"]
47-
hazmat = []
4847
pkcs8 = ["der", "digest", "elliptic-curve/pkcs8"]
4948
pem = ["elliptic-curve/pem", "pkcs8"]
5049
serde = ["dep:serdect", "elliptic-curve/serde", "pkcs8"]

ecdsa/src/dev.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ pub struct TestVector {
309309
mod tests {
310310
use super::*;
311311

312-
impl crate::hazmat::DigestAlgorithm for MockCurve {
312+
impl crate::DigestAlgorithm for MockCurve {
313313
type Digest = sha2::Sha256;
314314
}
315315

ecdsa/src/hazmat.rs

Lines changed: 41 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -12,72 +12,20 @@
1212
//! FULL PRIVATE KEY RECOVERY!
1313
//! </div>
1414
15-
use crate::{EcdsaCurve, Error, Result};
16-
use core::cmp;
17-
use elliptic_curve::{FieldBytes, array::typenum::Unsigned};
18-
19-
#[cfg(feature = "algorithm")]
20-
use {
21-
crate::{RecoveryId, Signature, SignatureSize},
22-
elliptic_curve::{
23-
CurveArithmetic, NonZeroScalar, ProjectivePoint, Scalar,
24-
array::ArraySize,
25-
ff::PrimeField,
26-
field,
27-
group::{Curve as _, Group},
28-
ops::{Invert, MulByGeneratorVartime, Reduce},
29-
point::AffineCoordinates,
30-
scalar::IsHigh,
31-
},
15+
use crate::{EcdsaCurve, Error, RecoveryId, Result, Signature, SignatureSize};
16+
use elliptic_curve::{
17+
CurveArithmetic, FieldBytes, NonZeroScalar, ProjectivePoint, Scalar,
18+
array::ArraySize,
19+
bigint::{BitOps, Encoding},
20+
ff::PrimeField,
21+
group::{Curve as _, Group},
22+
ops::{Invert, MulByGeneratorVartime, Reduce},
23+
point::AffineCoordinates,
24+
scalar::IsHigh,
3225
};
3326

3427
#[cfg(feature = "digest")]
35-
use digest::{Digest, FixedOutput, FixedOutputReset, common::BlockSizeUser};
36-
37-
/// Bind a preferred [`Digest`] algorithm to an elliptic curve type.
38-
///
39-
/// Generally there is a preferred variety of the SHA-2 family used with ECDSA
40-
/// for a particular elliptic curve.
41-
#[cfg(feature = "digest")]
42-
pub trait DigestAlgorithm: EcdsaCurve {
43-
/// Preferred digest to use when computing ECDSA signatures for this
44-
/// elliptic curve. This is typically a member of the SHA-2 family.
45-
type Digest: BlockSizeUser + Digest + FixedOutput + FixedOutputReset;
46-
}
47-
48-
/// Partial implementation of the `bits2int` function as defined in
49-
/// [RFC6979 § 2.3.2] as well as [SEC1] § 2.3.8.
50-
///
51-
/// This is used to convert a message digest whose size may be smaller or
52-
/// larger than the size of the curve's scalar field into a serialized
53-
/// (unreduced) field element.
54-
///
55-
/// [RFC6979 § 2.3.2]: https://datatracker.ietf.org/doc/html/rfc6979#section-2.3.2
56-
/// [SEC1]: https://www.secg.org/sec1-v2.pdf
57-
pub fn bits2field<C: EcdsaCurve>(bits: &[u8]) -> Result<FieldBytes<C>> {
58-
// Absolute 128-bit / 16-byte minimum to catch egregious digest misuse
59-
// without rejecting legitimate combinations like SHA-256 with P-521
60-
// (see e.g. XML Signature, which permits such pairings).
61-
if bits.len() < 16 {
62-
return Err(Error::new());
63-
}
64-
65-
let mut field_bytes = FieldBytes::<C>::default();
66-
67-
match bits.len().cmp(&C::FieldBytesSize::USIZE) {
68-
cmp::Ordering::Equal => field_bytes.copy_from_slice(bits),
69-
cmp::Ordering::Less => {
70-
// If bits is smaller than the field size, pad with zeroes on the left
71-
field_bytes[(C::FieldBytesSize::USIZE - bits.len())..].copy_from_slice(bits);
72-
}
73-
cmp::Ordering::Greater => {
74-
// If bits is larger than the field size, truncate
75-
field_bytes.copy_from_slice(&bits[..C::FieldBytesSize::USIZE]);
76-
}
77-
}
78-
79-
Ok(field_bytes)
80-
}
28+
use digest::{Digest, FixedOutputReset, block_api::BlockSizeUser};
8129

8230
/// Sign a prehashed message digest using the provided secret scalar and
8331
/// ephemeral scalar, returning an ECDSA signature.
@@ -102,18 +50,17 @@ pub fn bits2field<C: EcdsaCurve>(bits: &[u8]) -> Result<FieldBytes<C>> {
10250
///
10351
/// This will return an error if a zero-scalar was generated. It can be tried again with a
10452
/// different `k`.
105-
#[cfg(feature = "algorithm")]
10653
#[allow(non_snake_case)]
10754
pub fn sign_prehashed<C>(
10855
d: &NonZeroScalar<C>,
10956
k: &NonZeroScalar<C>,
110-
z: &FieldBytes<C>,
57+
z: &[u8],
11158
) -> Result<(Signature<C>, RecoveryId)>
11259
where
11360
C: EcdsaCurve + CurveArithmetic,
11461
SignatureSize<C>: ArraySize,
11562
{
116-
let z = Scalar::<C>::reduce(z);
63+
let z = bytes2scalar::<C>(z);
11764

11865
// Compute scalar inversion of 𝑘.
11966
let k_inv = k.invert();
@@ -123,7 +70,7 @@ where
12370

12471
// Lift x-coordinate of 𝑹 (element of base field) into a serialized big
12572
// integer, then reduce it into an element of the scalar field.
126-
let r = Scalar::<C>::reduce(&R.x());
73+
let r = bytes2scalar::<C>(&R.x());
12774

12875
// Compute 𝒔 as a signature over 𝒓 and 𝒛.
12976
let s = *k_inv * (z + (r * d.as_ref()));
@@ -153,40 +100,30 @@ where
153100
/// - `z`: message digest to be signed, i.e. `H(m)`. Does not have to be reduced in advance.
154101
/// - `ad`: optional additional data, e.g. added entropy from an RNG
155102
///
156-
/// # Errors
157-
///
158-
/// This will return an error if a zero-scalar was generated. It can be tried again with different
159-
/// entropy `ad`.
160-
///
161103
/// [RFC6979]: https://datatracker.ietf.org/doc/html/rfc6979
162-
#[cfg(feature = "algorithm")]
163104
pub fn sign_prehashed_rfc6979<C, D>(
164105
d: &NonZeroScalar<C>,
165-
z: &FieldBytes<C>,
106+
z: &[u8],
166107
ad: &[u8],
167-
) -> Result<(Signature<C>, RecoveryId)>
108+
) -> (Signature<C>, RecoveryId)
168109
where
169110
C: EcdsaCurve + CurveArithmetic,
170-
D: Digest + BlockSizeUser + FixedOutput + FixedOutputReset,
111+
D: Digest + BlockSizeUser + FixedOutputReset,
171112
SignatureSize<C>: ArraySize,
172113
{
173-
// From RFC6979 § 2.4:
174-
//
175-
// H(m) is transformed into an integer modulo q using the bits2int
176-
// transform and an extra modular reduction:
177-
//
178-
// h = bits2int(H(m)) mod q
179-
let z2 = Scalar::<C>::reduce(z);
114+
let order = C::ORDER;
115+
let mut kgen = rfc6979::KGenerator::<D, C::Uint>::new(&d.to_repr(), z, ad, &order);
180116

181-
let k = NonZeroScalar::<C>::from_repr(rfc6979::generate_k::<D, _>(
182-
&d.to_repr(),
183-
&field::uint_to_bytes::<C>(&C::ORDER),
184-
&z2.to_repr(),
185-
ad,
186-
))
187-
.unwrap();
117+
loop {
118+
let mut k_bytes = FieldBytes::<C>::default();
119+
kgen.fill_next_k(&mut k_bytes);
188120

189-
sign_prehashed(d, &k, z)
121+
if let Some(k) = NonZeroScalar::<C>::from_repr(k_bytes).into_option() {
122+
if let Ok(ret) = sign_prehashed(d, &k, z) {
123+
return ret;
124+
}
125+
}
126+
}
190127
}
191128

192129
/// Verify the prehashed message against the provided ECDSA signature.
@@ -197,12 +134,7 @@ where
197134
/// - `z`: message digest to be verified. MUST BE OUTPUT OF A CRYPTOGRAPHICALLY SECURE DIGEST
198135
/// ALGORITHM!!!
199136
/// - `sig`: signature to be verified against the key and message.
200-
#[cfg(feature = "algorithm")]
201-
pub fn verify_prehashed<C>(
202-
q: &ProjectivePoint<C>,
203-
z: &FieldBytes<C>,
204-
sig: &Signature<C>,
205-
) -> Result<()>
137+
pub fn verify_prehashed<C>(q: &ProjectivePoint<C>, z: &[u8], sig: &Signature<C>) -> Result<()>
206138
where
207139
C: EcdsaCurve + CurveArithmetic,
208140
SignatureSize<C>: ArraySize,
@@ -211,7 +143,7 @@ where
211143
return Err(Error::new());
212144
}
213145

214-
let z = Scalar::<C>::reduce(z);
146+
let z = bytes2scalar::<C>(z);
215147
let (r, s) = sig.split_scalars();
216148
let s_inv = *s.invert_vartime();
217149
let u1 = z * s_inv;
@@ -220,53 +152,23 @@ where
220152
.to_affine()
221153
.x();
222154

223-
if *r == Scalar::<C>::reduce(&x) {
155+
if *r == bytes2scalar::<C>(&x) {
224156
Ok(())
225157
} else {
226158
Err(Error::new())
227159
}
228160
}
229161

230-
#[cfg(all(test, feature = "dev"))]
231-
mod tests {
232-
use super::bits2field;
233-
use elliptic_curve::dev::MockCurve;
234-
use hex_literal::hex;
235-
236-
#[test]
237-
fn bits2field_too_small() {
238-
assert!(bits2field::<MockCurve>(b"").is_err());
239-
// 15 bytes is one short of the 128-bit absolute floor.
240-
let prehash = hex!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
241-
assert!(bits2field::<MockCurve>(&prehash).is_err());
242-
}
243-
244-
#[test]
245-
fn bits2field_size_less() {
246-
let prehash = hex!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
247-
let field_bytes = bits2field::<MockCurve>(&prehash).expect("field bytes");
248-
assert_eq!(
249-
field_bytes.as_slice(),
250-
&hex!("00000000000000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
251-
);
162+
/// Convert the provided bytestring into a `Scalar` for the given curve, interpreting it as big
163+
/// endian, zero-padding or truncating it to the bit length of `n` (curve order) if necessary,
164+
/// and then reducing it mod `n`.
165+
pub(crate) fn bytes2scalar<C: EcdsaCurve + CurveArithmetic>(mut bytes: &[u8]) -> Scalar<C> {
166+
// Compute number of bytes in `n` (curve order)
167+
let n_bits = C::ORDER.bits();
168+
let n_bytes = n_bits.div_ceil(8) as usize;
169+
if bytes.len() > n_bytes {
170+
bytes = &bytes[..n_bytes];
252171
}
253172

254-
#[test]
255-
fn bits2field_size_eq() {
256-
let prehash = hex!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
257-
let field_bytes = bits2field::<MockCurve>(&prehash).expect("field bytes");
258-
assert_eq!(field_bytes.as_slice(), &prehash);
259-
}
260-
261-
#[test]
262-
fn bits2field_size_greater() {
263-
let prehash = hex!(
264-
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
265-
);
266-
let field_bytes = bits2field::<MockCurve>(&prehash).expect("field bytes");
267-
assert_eq!(
268-
field_bytes.as_slice(),
269-
&hex!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
270-
);
271-
}
173+
<Scalar<C> as Reduce<C::Uint>>::reduce(&C::Uint::from_be_slice_truncated(bytes, n_bits))
272174
}

0 commit comments

Comments
 (0)