From def9b6b1a9efb8775ee1d6ed1ed332f044011369 Mon Sep 17 00:00:00 2001 From: Luis Ruiz Lopez Date: Wed, 14 Oct 2026 08:50:45 -0400 Subject: [PATCH 01/16] ec: pin secp256k1's third reduction fold, and put its bound proof where it lives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - reduce()'s third fold had no test, and its bound proof pointed at P-384 text that no longer exists - reduce() now derives its own bounds: one bit left after two folds, and folding it in cannot carry - p256k1_field_tests.rs pins a product that needs the fold; without it the result is short by C - comments and a test only; no code change in reduce() ====================================================================== [For agents] p256k1::reduce folds a 512-bit product twice with 2^256 ≡ C (mod p), then folds the single remaining bit above 2^256 in under a mask. Both the module docs and the in-function comment deferred the proof that two folds leave at most one bit -- and that the third fold cannot carry -- to crate::p384::reduce, "identical shape". Since 57d7417 P-384's reduce is the SP 800-186 G.1.3 term sum, which has no second fold and no such argument: secp256k1's is the only two-fold reduction left in the crate, and its correctness argument pointed at text that no longer exists. The argument is now written out in reduce() itself: hi_1 <= C after fold 1, acc_2 < 2^256 + C^2 < 2^256 + 2^66 after fold 2, and when the bit is set lo_2 < C^2, so lo_2 + C < p. That last bound also shows `carry1`, the carry out of the third fold, is identically zero; its comment said the opposite. The third fold had no test. The old comment noted that 2,000,000 random trials and (p-1)^2 never reached it and that an adversarial suite would -- but none was added for this curve, and the seven reduction-bound products in p256k1_field_tests.rs do not reach it either: (p-1)^2 gives fold 1 a high half of C - 1, one short of the bound, and is still ~2^190 away, because what matters is the low half after fold 1 landing within hi_1*C <= 2^66 of 2^256. One product is now pinned: a = 2^255 and b = 2 * ((2^256 - 1) * C^-1 mod 2^256), so that a*b = hi_0 * 2^256 with hi_0 * C ≡ 2^256 - 1; the two folds leave 2^256 + 0x3642e89815569618 and the third adds C. With the third fold dropped (`let extra_bit = 0`) reduce returns a value short by exactly C and every other test in the ec crate still passes; the new test fails. (In the workspace, wycheproof's ecdsa_secp256k1_sha256 tcId 135 / 364, "smallish r and s^-1", also fails on that mutant -- the P-384 history repeating -- so until now the fold was covered only from another crate.) No code change in reduce(): comments and a test only. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/src/p256k1.rs | 49 +++++++++++++++++++-------- crypto/ec/tests/p256k1_field_tests.rs | 36 ++++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/crypto/ec/src/p256k1.rs b/crypto/ec/src/p256k1.rs index 7399d53c5..803c56597 100644 --- a/crypto/ec/src/p256k1.rs +++ b/crypto/ec/src/p256k1.rs @@ -9,12 +9,19 @@ //! //! # Reduction algorithm //! -//! Same fold as [`crate::p256`]/[`crate::p384`]: `2^256 = p + C` where `C = 2^32 + 2^9 + 2^8 + 2^7 -//! + 2^6 + 2^4 + 1` (33 bits -- even smaller relative to the modulus than P-384's `C < 2^129`). -//! Two folds bound the result to within a single extra bit of `2^256`, exactly like -//! [`crate::p384::reduce`]: that function's own doc comment has the full derivation and the -//! wycheproof-caught bug an earlier, `debug_assert`-based version of this shape produced by -//! assuming that extra bit was always `0`. This module folds it in explicitly from the start. +//! `2^256 = p + C` with `C = 2^32 + 2^9 + 2^8 + 2^7 + 2^6 + 2^4 + 1` (33 bits), so a 512-bit +//! product `t = hi * 2^256 + lo` is congruent mod `p` to `lo + hi * C`, a value only about 33 bits +//! wider than `lo`. The private `reduce` applies that fold twice, which leaves at most a single +//! bit above `2^256`, then folds that bit in under a mask; the bounds that make exactly those +//! steps sufficient are derived in `reduce`'s own comment. This is the only reduction of its +//! shape in the crate -- [`crate::p256`], [`crate::p384`] and [`crate::sm2`] evaluate the term +//! tables of their respective standards, and the Brainpool curves use [`crate::montgomery`] -- +//! so the argument is written out here rather than by reference to another curve. +//! +//! The masked third fold is not a formality. An earlier P-384 version of this same two-fold shape +//! `debug_assert`ed that the bit was always `0`, which held for every random trial and was wrong: +//! wycheproof's `ecdsa_secp384r1` suite found a product that sets it. `reduce`'s comment gives +//! the operands that set it on this curve, and `p256k1_field_tests.rs` pins one such product. use crate::nat; use bouncycastle_utils::ct; @@ -287,13 +294,25 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { debug_assert_eq!(carry, 0, "secp256k1 reduction fold overflowed 512 bits"); acc = sum; } - // After exactly 2 folds, `acc`'s high 4 limbs are at most a single bit -- see - // crate::p384::reduce's doc comment for the general proof (identical shape: fold 1 bounds - // `hi < 2^34ish` since `C < 2^33` here, fold 2 then bounds the result to within a single bit - // of `2^256`). Folded in explicitly rather than assumed zero, per that function's own - // wycheproof-caught lesson (verified here too, not checked in, against 2,000,000 random - // trials plus the `(p-1)*(p-1)` worst case, none of which happened to hit the rare case -- - // exactly the kind of gap a real-world adversarial test suite, not random sampling, catches). + // After exactly two folds the high half of `acc` is a single bit. The bounds, with `C < 2^33` + // and `acc_i = hi_i * 2^256 + lo_i` the value after fold `i`: + // + // (1) `t < 2^512` splits as `hi_0 * 2^256 + lo_0` with `hi_0, lo_0 < 2^256`, so fold 1's + // `acc_1 = lo_0 + hi_0 * C < 2^256 + 2^256 * C < 2^290`: it fits in 8 limbs (the + // `carry == 0` asserted in the loop) and its high half is `hi_1 <= C`. + // (2) fold 2's `acc_2 = lo_1 + hi_1 * C < 2^256 + C^2 < 2^256 + 2^66`, so `acc[4]` is `0` or + // `1` and `acc[5..8]` are `0`. + // (3) when `acc[4]` is `1`, `lo_2 = acc_2 - 2^256 < C^2`, so `lo_2 + C < 2^66 + 2^33 < p`: + // folding the bit in can neither carry out of 256 bits nor produce a value `>= p`. + // + // The bit is folded in explicitly rather than assumed to be `0`. It is reachable from + // canonical operands, but only by construction, not by sampling: by (2), fold 2 overflows iff + // `lo_1` lies within `hi_1 * C <= 2^66` of `2^256`, a probability of about `2^-190` for a + // pseudo-random product, which is why 2,000,000 random trials and the `(p-1)*(p-1)` worst case + // never reached it. `a = 2^255` with `b = 2 * ((2^256 - 1) * C^-1 mod 2^256)` does reach it: + // `a * b = hi_0 * 2^256` with `hi_0 * C ≡ 2^256 - 1 (mod 2^256)`, so `lo_1 = 2^256 - 1`. + // `p256k1_field_tests.rs` pins that product; without this fold its result is short by exactly + // `C`, which is the P-384 bug the module docs describe. let extra_bit = acc[4]; let low: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; let mut c_or_zero = [0u64; 4]; @@ -304,8 +323,8 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { &mut c_or_zero, ); let (folded, carry1) = nat::add(&low, &c_or_zero); - let (folded_plus_c, _) = nat::add(&folded, &C_LIMBS); // valid when carry1 == 1: folded < 2^33 there - let (diff, borrow) = nat::sub(&folded, &P_LIMBS); // valid when carry1 == 0 + let (folded_plus_c, _) = nat::add(&folded, &C_LIMBS); // never selected: carry1 == 0 by bound (3) + let (diff, borrow) = nat::sub(&folded, &P_LIMBS); // the live path: carry1 == 0 let mut when_no_carry = [0u64; 4]; ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut when_no_carry); let mut result = [0u64; 4]; diff --git a/crypto/ec/tests/p256k1_field_tests.rs b/crypto/ec/tests/p256k1_field_tests.rs index 12aa1dae6..90968ea84 100644 --- a/crypto/ec/tests/p256k1_field_tests.rs +++ b/crypto/ec/tests/p256k1_field_tests.rs @@ -330,3 +330,39 @@ fn known_answer_mul_at_the_reduction_bounds() { assert_eq!(fe(a).square(), fe(a).mul(&fe(a)), "square at the same extreme: a = {a:x?}"); } } + +/// The one product shape the reduction-bound cases above cannot reach: a product whose two folds +/// leave the accumulator at or above `2^256`, so that `reduce`'s masked third fold (see its +/// comment in `p256k1.rs`) is the step that produces the right answer. None of the cases above +/// reach it -- `(p-1)^2` gives the first fold a high half of `C - 1`, one short of its bound, and +/// still falls about `2^190` short -- and neither does random sampling: it needs the low half +/// after the first fold to lie within `hi_1 * C <= 2^66` of `2^256`, probability about `2^-190` +/// per product. The operands are constructed instead, with `a = 2^255` so that `a * b = (b/2) * +/// 2^256` has an all-zero low half and `b/2` chosen so that `(b/2) * C` has every low bit set; +/// computed in Python, independently of this crate's arithmetic: +/// +/// ```text +/// p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +/// C = 2**256 - p +/// hi0 = (2**256 - 1) * pow(C, -1, 2**256) % 2**256 # hi0 * C == 2**256 - 1 (mod 2**256) +/// a, b = 2**255, 2 * hi0 # a * b == hi0 * 2**256, and b < p +/// expected = a * b % p +/// ``` +/// +/// After fold 1 the low half is `2^256 - 1` and the high half `0x3642e7c9`; after fold 2 the +/// value is `2^256 + 0x3642e89815569618`; the third fold adds `C`. A reduction that assumed the +/// bit above `2^256` to be zero after two folds returns `0x3642e89815569618`, short by exactly +/// `C`, and passes every other test in this file. +#[test] +fn known_answer_mul_reaching_the_third_fold() { + let a = [0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x8000000000000000]; + let b = [0x4f8fedc45bb5959e, 0x869bb80247b6bf4c, 0xc7727a7ad41a913c, 0x6c85cdf5d558f8cc]; + let expected = [0x3642e899155699e9, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + let short_by_c = + [0x3642e89815569618, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000]; + + assert_eq!(fe(a).mul(&fe(b)), fe(expected), "2^255 * b, third fold required"); + assert_eq!(fe(b).mul(&fe(a)), fe(expected), "commuted: b * 2^255, third fold required"); + assert_ne!(fe(a).mul(&fe(b)), fe(short_by_c), "the result a two-fold-only reduction gives"); + assert_eq!(fe(expected).sub(&fe(short_by_c)).to_limbs(), [0x00000001000003d1, 0, 0, 0], "C"); +} From b11afd6e9323e864085879878343c07422d6520c Mon Sep 17 00:00:00 2001 From: Luis Ruiz Lopez Date: Sat, 24 Oct 2026 15:33:19 -0400 Subject: [PATCH 02/16] ecdsa: say the brainpool rfc6979 rejection loop is a normal path, and pin it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - the brainpool rfc6979 docs called the step h.3 rejection loop near-dead, by reference to P-256 - on these curves 34 % / 45 % / 33 % of signatures reject a candidate: a visible timing signal - each module now carries its own argument: the rejection count is independent of k and d (PRF) - the vectors annotate the rejecting cases and add a CASE_5 per curve with five or six rejections - all 18 vectors re-derived and reproduced by python-ecdsa 0.19.2 and OpenSSL 4.0.2 - docs and tests only ====================================================================== [For agents] The three brainpool rfc6979 modules inherited P-256's rejection-loop text in three places, and on these curves all three say the opposite of what happens. generate_k's doc deferred to crate::rfc6979::generate_k's argument "which applies unchanged here": its quantitative half ("rejected with negligible probability") assumes n within 2^-32 of 2^256, where the brainpool moduli give P[candidate >= n] = 1 - n/2^qlen of 0.336 (bp256r1), 0.4503 (bp384r1) and 0.3326 (bp512r1) -- so 34 % / 45 % / 34 % of signatures run step h.3's "otherwise" branch at least once (mean extra iterations 0.51 / 0.82 / 0.50, each two more HMAC calls), a timing signal a remote observer can see. Its structural half ("a rejected candidate is discarded entirely and nothing derived from it is ever used") is not true of RFC 6979 on any curve: the accepted k is an HMAC of the rejected V. The test-module comment claimed the reject path is never reached by realistic inputs and took the QUALITY_AND_STYLE.md private-function exception on that basis -- on these curves a third to a half of real messages reach it, and the checked-in vectors already did (bp256r1 CASE_0 once; bp384r1 CASE_0 twice, CASE_4 once; bp512r1 CASE_0 and CASE_1 once each), by accident of which (d, msg) the generator drew. And the module docs did not say the loop is a normal control path at all. Each module now carries its own argument instead of the reference: the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF outputs under a key derived from (d, h1), so under the PRF assumption it is independent of the accepted k -- which, conditioned on the count, is uniform on [1, n-1] -- and reveals nothing about d; the count is deterministic per (d, msg), so the timing pattern over chosen messages is a fingerprint worth about a bit per message, usable for confirming a candidate d an attacker already holds but not for recovering one; and what must be constant time is everything done with the accepted k. The module docs state the rejection rate and that RFC 6979 §3.4's "utterly improbable" assumes n close to 2^qlen, which qlen == hlen alone does not give; the test-module comment now states the coverage instead of denying it. The vectors files pin the loop deliberately rather than by accident: the rejecting cases are annotated with their rejection counts, and each curve gains a CASE_5 searched to need several rejections in a row -- five (bp256r1), six (bp384r1), five (bp512r1) -- so the K = HMAC_K(V || 0x00) re-key is exercised repeatedly and a mutant that drops it is killed by these files' own generate_k and sign tests. The bp384r1 and bp512r1 pins are the PR 143 review's; bp256r1's is new: d is SHA-512("bc-rust brainpoolP256r1 h.3 rejection pin") reduced into [1, n-1] and the message is the first index of the form "brainpoolP256r1 rejection test {i}" whose rejection count reaches five. All 18 vectors (15 existing, 3 pins) were re-derived this session from an instrumented RFC 6979 port and reproduced by python-ecdsa 0.19.2 (its own pure-Python RFC 6979) and OpenSSL 4.0.2 (deterministic ECDSA through the cryptography 50.0.1 bindings): k, r, s and Q agree on all 18, which also anchors the h.3 continue-chain against two implementations that share no code with this crate. crate::rfc6979 (P-256) is deliberately untouched: its structural sentence misdescribes RFC 6979 on every curve and its handling of an r = 0 candidate differs from the RFC's step h.3, but that is a cross-curve item (secp256k1 review F6), not a brainpool one. Assisted-by: Claude:claude-fable-5 --- crypto/ecdsa/src/rfc6979_bp256r1.rs | 38 ++++++++++++++---- crypto/ecdsa/src/rfc6979_bp384r1.rs | 39 +++++++++++++++---- crypto/ecdsa/src/rfc6979_bp512r1.rs | 38 ++++++++++++++---- .../tests/rfc6979_bp256r1_vectors_tests.rs | 25 +++++++++++- .../tests/rfc6979_bp384r1_vectors_tests.rs | 25 +++++++++++- .../tests/rfc6979_bp512r1_vectors_tests.rs | 24 +++++++++++- 6 files changed, 165 insertions(+), 24 deletions(-) diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index 4c3169f7a..0e6f97c03 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -9,6 +9,13 @@ //! close `n` is to `2^256` itself (unlike secp256k1, brainpoolP256r1's `n` is not close to a power //! of two at all; only the top-bit-set property matters here). //! +//! `n` not being close to `2^256` has a second consequence: the step h.3 rejection loop is a +//! normal control path on this curve, not the near-dead branch it is for P-256 and secp256k1 -- a +//! uniform 256-bit candidate is `>= n` with probability `1 - n/2^256 = 0.336`, so about a third +//! of all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly +//! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) +//! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. +//! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves (its appendix //! only covers the NIST curves P-192 through P-521); `rfc6979_bp256r1_vectors_tests.rs` //! cross-checks this implementation against an independent Python implementation of the same RFC @@ -69,8 +76,24 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and -/// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs for the rejection-loop -/// constant-time argument, which applies unchanged here. +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on this curve +/// +/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate +/// is rejected not with negligible probability but with `1 - n/2^256 = 0.336`, so 34 % of +/// signatures run step h.3's "otherwise" branch at least once (mean 0.51 extra iterations), and +/// each extra iteration is two more HMAC-SHA-256 calls -- a timing difference a remote observer +/// can see. The argument that holds instead (and does not lean on the rejected candidate being +/// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the +/// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF +/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of +/// the accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals +/// nothing about `d`. Two consequences worth knowing: the count is deterministic per `(d, msg)`, +/// so the timing pattern over chosen messages is a fingerprint worth about a bit per message -- +/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds -- +/// and what must be constant time is everything done with the accepted `k`, which is the rest of +/// this crate's concern. pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -105,11 +128,12 @@ pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { } } -// `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on -// candidates that -- for any real message/key input -- are essentially always in range, so no -// realistic integration test exercises its reject path; the QUALITY_AND_STYLE.md private-function -// exception applies. Known answers: `0` and `n` are the immediate out-of-range neighbours of the -// valid interval `[1, n-1]`. +// `candidate_in_range`'s reject path is ordinary control flow on this curve -- a third of real +// messages take it (see the module docs) -- and the integration tests exercise it deliberately: +// in `rfc6979_bp256r1_vectors_tests.rs`, CASE_0 rejects one candidate before its `k` and CASE_5 +// five in a row. What integration tests cannot pin are the exact boundaries, which no realistic +// input lands on: `0` and `n`, the immediate out-of-range neighbours of the valid interval +// `[1, n-1]` -- the known answers below. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 38e24c182..6d64b90c1 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -7,6 +7,14 @@ //! brainpoolP256r1's own case: brainpoolP384r1's `n` has its top bit set (`n > 2^383`), so `2n > //! 2^384` and any 384-bit input is already `< 2n`, despite `n` not being close to `2^384` itself. //! +//! `n` not being close to `2^384` has a second consequence: the step h.3 rejection loop is a +//! normal control path on this curve, not the near-dead branch it is on the NIST curves -- and +//! the most-taken in the crate: a uniform 384-bit candidate is `>= n` with probability `1 - +//! n/2^384 = 0.4503`, so nearly half of all messages take at least one extra HMAC round and a +//! fifth take two or more. (RFC 6979 §3.4 calls rejection "utterly improbable"; that assumes `n` +//! close to `2^qlen`, which `qlen == hlen` alone does not give.) [`generate_k`]'s docs give the +//! constant-time argument that holds at this rejection rate. +//! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; //! `rfc6979_bp384r1_vectors_tests.rs` cross-checks this implementation against an independent //! Python implementation of the same RFC instead. @@ -66,8 +74,24 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and -/// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs for the rejection-loop -/// constant-time argument, which applies unchanged here. +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on this curve +/// +/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate +/// is rejected not with negligible probability but with `1 - n/2^384 = 0.4503`, so 45 % of +/// signatures run step h.3's "otherwise" branch at least once and a fifth at least twice (mean +/// 0.82 extra iterations), and each extra iteration is two more HMAC-SHA-384 calls -- a timing +/// difference a remote observer can see. The argument that holds instead (and does not lean on +/// the rejected candidate being discarded, which RFC 6979's chaining never satisfies: the +/// accepted `k` is an HMAC of the rejected `V`): the iteration count is a function of the +/// HMAC-DRBG output stream, i.e. of PRF outputs under a key derived from `(d, h1)`, so under the +/// PRF assumption it is independent of the accepted `k` -- which, conditioned on the count, is +/// uniform on `[1, n-1]` -- and reveals nothing about `d`. Two consequences worth knowing: the +/// count is deterministic per `(d, msg)`, so the timing pattern over chosen messages is a +/// fingerprint worth about a bit per message -- useless for recovering `d`, usable for confirming +/// a candidate `d` an attacker already holds -- and what must be constant time is everything done +/// with the accepted `k`, which is the rest of this crate's concern. pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -102,11 +126,12 @@ pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { } } -// `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on -// candidates that -- for any real message/key input -- are essentially always in range, so no -// realistic integration test exercises its reject path; the QUALITY_AND_STYLE.md private-function -// exception applies. Known answers: `0` and `n` are the immediate out-of-range neighbours of the -// valid interval `[1, n-1]`. +// `candidate_in_range`'s reject path is ordinary control flow on this curve -- nearly half of +// real messages take it (see the module docs) -- and the integration tests exercise it +// deliberately: in `rfc6979_bp384r1_vectors_tests.rs`, CASE_0 rejects two candidates before its +// `k`, CASE_4 one, and CASE_5 six in a row. What integration tests cannot pin are the exact +// boundaries, which no realistic input lands on: `0` and `n`, the immediate out-of-range +// neighbours of the valid interval `[1, n-1]` -- the known answers below. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 218540194..8d078ff41 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -8,6 +8,13 @@ //! other brainpool curves' own case: brainpoolP512r1's `n` has its top bit set (`n > 2^511`), so //! `2n > 2^512` and any 512-bit input is already `< 2n`, despite `n` not being close to `2^512`. //! +//! `n` not being close to `2^512` has a second consequence: the step h.3 rejection loop is a +//! normal control path on this curve, not the near-dead branch it is on the NIST curves -- a +//! uniform 512-bit candidate is `>= n` with probability `1 - n/2^512 = 0.3326`, so about a third +//! of all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly +//! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) +//! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. +//! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; //! `rfc6979_bp512r1_vectors_tests.rs` cross-checks this implementation against an independent //! Python implementation of the same RFC instead. @@ -67,8 +74,24 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and -/// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs for the rejection-loop -/// constant-time argument, which applies unchanged here. +/// message hash `h1 = H(m)`. +/// +/// # The rejection loop is a normal path on this curve +/// +/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate +/// is rejected not with negligible probability but with `1 - n/2^512 = 0.3326`, so 34 % of +/// signatures run step h.3's "otherwise" branch at least once (mean 0.50 extra iterations), and +/// each extra iteration is two more HMAC-SHA-512 calls -- a timing difference a remote observer +/// can see. The argument that holds instead (and does not lean on the rejected candidate being +/// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the +/// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF +/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of +/// the accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals +/// nothing about `d`. Two consequences worth knowing: the count is deterministic per `(d, msg)`, +/// so the timing pattern over chosen messages is a fingerprint worth about a bit per message -- +/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds -- +/// and what must be constant time is everything done with the accepted `k`, which is the rest of +/// this crate's concern. pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -103,11 +126,12 @@ pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { } } -// `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on -// candidates that -- for any real message/key input -- are essentially always in range, so no -// realistic integration test exercises its reject path; the QUALITY_AND_STYLE.md private-function -// exception applies. Known answers: `0` and `n` are the immediate out-of-range neighbours of the -// valid interval `[1, n-1]`. +// `candidate_in_range`'s reject path is ordinary control flow on this curve -- a third of real +// messages take it (see the module docs) -- and the integration tests exercise it deliberately: +// in `rfc6979_bp512r1_vectors_tests.rs`, CASE_0 and CASE_1 each reject one candidate before their +// `k` and CASE_5 five in a row. What integration tests cannot pin are the exact boundaries, which +// no realistic input lands on: `0` and `n`, the immediate out-of-range neighbours of the valid +// interval `[1, n-1]` -- the known answers below. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs index a12409d3d..e24df0404 100644 --- a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs @@ -5,6 +5,14 @@ //! msg)` pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the //! deterministic `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine //! group law (general `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on a third of real messages (see +//! `rfc6979_bp256r1`'s module docs), and these vectors pin it deliberately: CASE_0 rejects one +//! candidate before its `k`, and a sixth pair, CASE_5, is chosen so that five in a row are +//! rejected, exercising the `K = HMAC_K(V || 0x00)` re-key five times. Every vector, including +//! both rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own pure-Python +//! RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography 50.0.1 +//! bindings), which also anchors the curve constants a second way. use bouncycastle_core::traits::Hash; use bouncycastle_core::traits::{ @@ -28,6 +36,8 @@ struct Vector { uy: &'static str, } +// The first h.3 candidate for this (d, msg) is >= n: the accepted k is the second, after one +// re-key of the HMAC chain. const CASE_0: Vector = Vector { d: "1201AF823A0A4FE05170CFF294DC13FC115AC6CB3C58FD846EC5C07FF6908E54", msg: b"", @@ -78,7 +88,20 @@ const CASE_4: Vector = Vector { uy: "62F10FDBD0CDE940DD72CEBD0A3C8FB1D734ECB5C6B287CDEB2E50CA650D67C2", }; -const CASES: [&Vector; 5] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4]; +// Pins the h.3 continue-chain deliberately: five candidates in a row are >= n, so the accepted k +// follows five `K = HMAC_K(V || 0x00)` re-keys. d is SHA-512("bc-rust brainpoolP256r1 h.3 +// rejection pin") reduced into [1, n-1]; the message index is the first to reach five rejections. +const CASE_5: Vector = Vector { + d: "07C1C10F28FB620F5ABFC2231E5CF77931E282BFCC735F96DE13FDCCFB51F95E", + msg: b"brainpoolP256r1 rejection test 363", + k: "753584443CB1744D143C4E19D1DE051F2DB89DEA3F208C1089EFAF093C459CA9", + r: "2228C1F981B3F9514F15D1D2CFFCF96E5F6C93ABF5208872611B5A69E3B6CC2D", + s: "64B2088C315AB6158001B09B6F15EC2FE314F1B5CA971E92453A1FCDF414D472", + ux: "8CBDEA6139F062041BDF604D0C80750AC944D23BFD5406A1EFA70A5FC19B290C", + uy: "46A4AE6AA2E4182CC865D49169994AD26DA55207D4E517DC68780EC25A263DC1", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; fn bytes32(hex: &str) -> [u8; 32] { hex_decode(hex).unwrap().try_into().unwrap() diff --git a/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs index baf2fc7be..7b4e1fc84 100644 --- a/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs @@ -4,6 +4,14 @@ //! pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the deterministic //! `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine group law (general //! `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on nearly half of real messages (see +//! `rfc6979_bp384r1`'s module docs), and these vectors pin it deliberately: CASE_0 rejects two +//! candidates before its `k`, CASE_4 one, and a sixth pair, CASE_5, is chosen so that six in a +//! row are rejected, exercising the `K = HMAC_K(V || 0x00)` re-key six times. Every vector, +//! including all three rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own +//! pure-Python RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography +//! 50.0.1 bindings), which also anchors the curve constants a second way. use bouncycastle_core::traits::Hash; use bouncycastle_core::traits::{ @@ -27,6 +35,8 @@ struct Vector { uy: &'static str, } +// The first two h.3 candidates for this (d, msg) are >= n: the accepted k is the third, after +// two re-keys of the HMAC chain. const CASE_0: Vector = Vector { d: "0A3ECED49C8BE3E693AF8A68F77282BD1201AF823A0A4FE05170CFF294DC13FC115AC6CB3C58FD846EC5C07FF6908E54", msg: b"", @@ -67,6 +77,7 @@ const CASE_3: Vector = Vector { uy: "776FB4EA44FE91B2CD416CA75D3F115D0B044826D949E129F7A5B5008C6CB069925D86B6EEBD5EDCF813AC2B5D417305", }; +// One h.3 rejection: the accepted k is the second candidate. const CASE_4: Vector = Vector { d: "3AC2CE16303249B912258C63556A44E17CEE4B4D567578F26A89C3FC0098EFE577CBF8FCAC22AAE164F74D7169CEB006", msg: &[0xffu8; 10], @@ -77,7 +88,19 @@ const CASE_4: Vector = Vector { uy: "64DE68D75FA78C5E6F9B37E297163E77EFFABB263D957D5C017EAF2AD4F9F9567664BB1E701959E6302BE803300CE40F", }; -const CASES: [&Vector; 5] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4]; +// Pins the h.3 continue-chain deliberately: six candidates in a row are >= n, so the accepted k +// follows six `K = HMAC_K(V || 0x00)` re-keys -- the (d, msg) pair was searched for exactly that. +const CASE_5: Vector = Vector { + d: "65410D0A7B55AC6BE4151D747AF74AF138BC0BBDEE4345D0551AA055D0F995E9FAAA3692CCD92C41A178481E5EB2CB03", + msg: b"brainpoolP384r1 rejection test 3", + k: "1FD7200F56175EE6562B30503B474C69CAE25E83B924CCB619726AE707684E23820BEEB3BCCBBE9DFEA2FF805B26FB68", + r: "1AB76CFE4E93C696421D80EFC50C7F026B5C12125488970EFCF9FBA40D6734F2323F57BCE809923C886ADACFB51F105C", + s: "2B260A5C092C38E580E01EDE836CBA95D9C9C0F4A82CDE497B1603AFF0BE1E7141FE57177D940EE41F89A8F383C9D8C2", + ux: "87086428290CD23F878DE8250334D9A67EFCBF9DC90161B6A956CCF48AF45B3989FD7785A95B669080D6D3F7514E785B", + uy: "3E7F94B4B7826F07898649827C215A147C71A1DFDC0B42592998135F148B6EB53AB0E23C4DDF2D0BF795FB275B09A656", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; fn bytes48(hex: &str) -> [u8; 48] { hex_decode(hex).unwrap().try_into().unwrap() diff --git a/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs index 3b815107f..c6d7cd381 100644 --- a/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs @@ -4,6 +4,14 @@ //! pairs (empty, short ASCII, longer ASCII, all-zero, all-`0xff`), each with the deterministic //! `k`, the resulting `(r, s)`, and `Q = [d]G` computed via the standard affine group law (general //! `a`), `random.seed 1234567`. +//! +//! On this curve the §3.2 step h.3 rejection loop runs on a third of real messages (see +//! `rfc6979_bp512r1`'s module docs), and these vectors pin it deliberately: CASE_0 and CASE_1 +//! each reject one candidate before their `k`, and a sixth pair, CASE_5, is chosen so that five +//! in a row are rejected, exercising the `K = HMAC_K(V || 0x00)` re-key five times. Every vector, +//! including all three rejecting ones, is additionally reproduced by python-ecdsa 0.19.2 (its own +//! pure-Python RFC 6979) and by OpenSSL 4.0.2's deterministic ECDSA (through the cryptography +//! 50.0.1 bindings), which also anchors the curve constants a second way. use bouncycastle_core::traits::Hash; use bouncycastle_core::traits::{ @@ -27,6 +35,7 @@ struct Vector { uy: &'static str, } +// One h.3 rejection: the accepted k is the second candidate, after one re-key of the HMAC chain. const CASE_0: Vector = Vector { d: "330EC367DE3D130E41B64A8D9CA805E18BE822EDD22F87F9914BFAF280DD7C1531A01101800F36D7D7F978F0A674894EAF61ACFE376CEAE9AE8692ADE621E465", msg: b"", @@ -37,6 +46,7 @@ const CASE_0: Vector = Vector { uy: "3B7A607F3E359062492F5D078FEBD595B14A898C4117CFF633CAFB6A57EC5F38390BE97E618B482F10C657D89CC47A16FA93F73A8C99FE050907DDC758DA1BA1", }; +// One h.3 rejection: the accepted k is the second candidate, after one re-key of the HMAC chain. const CASE_1: Vector = Vector { d: "17BAA6642A507BD5336ED46492D516FEE5A381A0C9D32A03D4CCAF167412C30CA67E90EC30BC65D57594E68E791FA9ADF8E3DD706ECB52454C5082AF09E88A09", msg: b"sample", @@ -77,7 +87,19 @@ const CASE_4: Vector = Vector { uy: "073A8842B4732D73B6F7213F2DAAC046316A3CDA5D9C4B81B894FEF063340379103826C1CB111DA3161B630FCB6952B49E709903286B6914C1043F75450C1EEB", }; -const CASES: [&Vector; 5] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4]; +// Pins the h.3 continue-chain deliberately: five candidates in a row are >= n, so the accepted k +// follows five `K = HMAC_K(V || 0x00)` re-keys -- the (d, msg) pair was searched for exactly that. +const CASE_5: Vector = Vector { + d: "2ACDAFAA028C371E193290ECD1F609D3D83EE2CEA56DFBB70023B9242AF6DF75D55ABB2A9EC6C836EE206890D5425531336AC8A86FB7A41E074AA01D2C395E5C", + msg: b"brainpoolP512r1 rejection test 113", + k: "4ECFD8C45E6ED73F9B59A4F4BE2ACB4398D1CD73FAFD46CD5AF37A7E30B711195F9D6B12D6E8237CDC5F6B67AB7CC36A381335F3811935E1F76BA1E02363EB12", + r: "006EAB155B1A7883362C9FE0A0E84A47A8F4086D594F5F12A3BAC0A0628255DC6860A0649095F365C5B0F571144D00F044188B0DD2471E41BDF100A6FFA85762", + s: "92A29C2A899A8F513957A66A0D42F4F7B8E330ECF44A0272AF82568A3EB981169EBB81716A6AE8B395699E64F7C63FAA06C80514717E6F72E8D31536796D0117", + ux: "048760C2E0AC7AE2D36049ACB2406A37C6CC4160871B6E0BD69C0F92D95DD626CCFD90717F180EFF435820E8817756AB03D741480C80322C370FA9B8C3C2DBD2", + uy: "686B835D888512359FD2EBF73E233E0CFD5C82EDCE1FC9E891BD253C981D55A36D8FEAB7FC1EAAE1DE6E9D4ADD1A6A79A38FC553EA92A75A326ED6FAE1BEF499", +}; + +const CASES: [&Vector; 6] = [&CASE_0, &CASE_1, &CASE_2, &CASE_3, &CASE_4, &CASE_5]; fn bytes64(hex: &str) -> [u8; 64] { hex_decode(hex).unwrap().try_into().unwrap() From e955a8f174886ec4a2f7ebf1272e8fe5d3d5d048 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 14:09:32 +0000 Subject: [PATCH 03/16] ecdsa, ec: name the rfc6979 loop in the crate's timing contract, and fix two doc numbers [For humans] - the ecdsa crate docs claimed d and k are never handled in variable time; the rfc6979 loop is - the crate docs now name two exceptions: the rfc6979 candidate loop and the r = 0 / s = 0 error - brainpool generate_k docs: a rejection costs three more HMAC calls, not two - secp256k1 third-fold test doc: (p-1)^2 misses the fold by C - 1, not by about 2^190 - docs and comments only ====================================================================== [For agents] The ecdsa crate docs' Security Considerations opened with "d and k are never handled in variable time ... every operation performed on it or on k is branch-free". e8acb3b documents, in the three brainpool rfc6979 modules, a timing signal that 34 % to 45 % of signatures on those curves emit: the RFC 6979 step h.3 loop iterates a data-dependent number of times. The crate-level sentence contradicted that, and was not literally true on any curve, since the loop exists everywhere. The bullet now states what holds, that everything done with d and the accepted k is branch-free, and lists the two exceptions this crate adds to the ec crate's: the candidate loop (the rates, the PRF-assumption argument by reference to rfc6979_bp256r1::generate_k, and the per-message timing fingerprint it leaves) and the r = 0 / s = 0 error return, a branch on a public outcome. Two numbers from the last two commits were wrong. The brainpool generate_k docs said each extra iteration costs two more HMAC calls; it costs three: the re-key is two calls and the next candidate's HMAC_K(V) is the third. And the secp256k1 third-fold test's doc said (p-1)^2 "still falls about 2^190 short" of the third fold; it misses by C - 1, since its two folds leave exactly p + 1 (hi_0 = 2^256 - 2C - 2, lo_0 = (C+1)^2, acc_1 = C * 2^256 - C^2 + 1, acc_2 = 2^256 - C + 1). The 2^-190 figure is the probability for a pseudo-random product, which reduce()'s comment states correctly; that comment no longer files (p-1)^2 under the same probability either. Docs and comments only; no code change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/src/p256k1.rs | 5 +++-- crypto/ec/tests/p256k1_field_tests.rs | 10 +++++----- crypto/ecdsa/src/lib.rs | 19 ++++++++++++++----- crypto/ecdsa/src/rfc6979_bp256r1.rs | 5 +++-- crypto/ecdsa/src/rfc6979_bp384r1.rs | 5 +++-- crypto/ecdsa/src/rfc6979_bp512r1.rs | 5 +++-- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/crypto/ec/src/p256k1.rs b/crypto/ec/src/p256k1.rs index 803c56597..89c86a9fa 100644 --- a/crypto/ec/src/p256k1.rs +++ b/crypto/ec/src/p256k1.rs @@ -308,8 +308,9 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { // The bit is folded in explicitly rather than assumed to be `0`. It is reachable from // canonical operands, but only by construction, not by sampling: by (2), fold 2 overflows iff // `lo_1` lies within `hi_1 * C <= 2^66` of `2^256`, a probability of about `2^-190` for a - // pseudo-random product, which is why 2,000,000 random trials and the `(p-1)*(p-1)` worst case - // never reached it. `a = 2^255` with `b = 2 * ((2^256 - 1) * C^-1 mod 2^256)` does reach it: + // pseudo-random product, which is why 2,000,000 random trials never reached it; `(p-1)*(p-1)` + // misses it too, by `C - 1`: its two folds leave exactly `p + 1`. `a = 2^255` with + // `b = 2 * ((2^256 - 1) * C^-1 mod 2^256)` does reach it: // `a * b = hi_0 * 2^256` with `hi_0 * C ≡ 2^256 - 1 (mod 2^256)`, so `lo_1 = 2^256 - 1`. // `p256k1_field_tests.rs` pins that product; without this fold its result is short by exactly // `C`, which is the P-384 bug the module docs describe. diff --git a/crypto/ec/tests/p256k1_field_tests.rs b/crypto/ec/tests/p256k1_field_tests.rs index 90968ea84..79f48238e 100644 --- a/crypto/ec/tests/p256k1_field_tests.rs +++ b/crypto/ec/tests/p256k1_field_tests.rs @@ -335,11 +335,11 @@ fn known_answer_mul_at_the_reduction_bounds() { /// leave the accumulator at or above `2^256`, so that `reduce`'s masked third fold (see its /// comment in `p256k1.rs`) is the step that produces the right answer. None of the cases above /// reach it -- `(p-1)^2` gives the first fold a high half of `C - 1`, one short of its bound, and -/// still falls about `2^190` short -- and neither does random sampling: it needs the low half -/// after the first fold to lie within `hi_1 * C <= 2^66` of `2^256`, probability about `2^-190` -/// per product. The operands are constructed instead, with `a = 2^255` so that `a * b = (b/2) * -/// 2^256` has an all-zero low half and `b/2` chosen so that `(b/2) * C` has every low bit set; -/// computed in Python, independently of this crate's arithmetic: +/// its two folds leave exactly `p + 1`, short of `2^256` by `C - 1` -- and neither does random +/// sampling: it needs the low half after the first fold to lie within `hi_1 * C <= 2^66` of +/// `2^256`, probability about `2^-190` per product. The operands are constructed instead, with +/// `a = 2^255` so that `a * b = (b/2) * 2^256` has an all-zero low half and `b/2` chosen so that +/// `(b/2) * C` has every low bit set; computed in Python, independently of this crate's arithmetic: /// /// ```text /// p = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index 5a34147e1..8b30edef5 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -119,11 +119,20 @@ //! //! # Security Considerations //! -//! - **The private key `d` and the per-message secret `k` are never handled in variable time.** -//! `d` lives in [`bouncycastle_ec::p256_scalar::P256Scalar`] (a [`bouncycastle_utils::secret::Secret`]); -//! every operation performed on it or on `k` -- inversion, multiplication, the fixed-base -//! scalar multiplication `[k]G` -- is branch-free and does not index memory by their value. See -//! [`bouncycastle_ec`]'s own module docs for the constant-time discipline this crate builds on. +//! - **Everything done with the private key `d` and the accepted per-message secret `k` is +//! branch-free and does not index memory by their value**: inversion, multiplication, the +//! fixed-base scalar multiplication `[k]G`. `d` lives in +//! [`bouncycastle_ec::p256_scalar::P256Scalar`] (a [`bouncycastle_utils::secret::Secret`]); see +//! [`bouncycastle_ec`]'s own module docs for the constant-time discipline this crate builds on +//! and the exceptions it documents. This crate adds two exceptions of its own: +//! - RFC 6979's candidate loop runs a data-dependent number of iterations. On the NIST curves +//! and secp256k1 a candidate is rejected with negligible probability; on the brainpool curves +//! 34 % to 45 % of signatures reject at least one. Under the PRF assumption the count is +//! independent of the accepted `k` and reveals nothing about `d`, but it is deterministic per +//! `(d, message)`, so a remote observer gets a per-message timing fingerprint. See +//! [`rfc6979_bp256r1::generate_k`]'s docs for the argument and the rates. +//! - Signing returns an error on `r = 0` or `s = 0` instead of drawing another `k`; that branch +//! is on a public outcome. //! - **`k` must never repeat across two signatures under the same key with different messages.** //! A repeated `k` leaks `d` directly from two signatures' `(r, s)` pairs (elementary algebra on //! the ECDSA equation). This is why deterministic generation (RFC 6979) is the default here: `k` diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index 0e6f97c03..cc73d40ae 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -83,8 +83,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate /// is rejected not with negligible probability but with `1 - n/2^256 = 0.336`, so 34 % of /// signatures run step h.3's "otherwise" branch at least once (mean 0.51 extra iterations), and -/// each extra iteration is two more HMAC-SHA-256 calls -- a timing difference a remote observer -/// can see. The argument that holds instead (and does not lean on the rejected candidate being +/// each extra iteration is three more HMAC-SHA-256 calls (two for the re-key, one for the next +/// candidate) -- a timing difference a remote observer can see. The argument that holds instead +/// (and does not lean on the rejected candidate being /// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the /// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF /// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 6d64b90c1..97515d22c 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -81,8 +81,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate /// is rejected not with negligible probability but with `1 - n/2^384 = 0.4503`, so 45 % of /// signatures run step h.3's "otherwise" branch at least once and a fifth at least twice (mean -/// 0.82 extra iterations), and each extra iteration is two more HMAC-SHA-384 calls -- a timing -/// difference a remote observer can see. The argument that holds instead (and does not lean on +/// 0.82 extra iterations), and each extra iteration is three more HMAC-SHA-384 calls (two for the +/// re-key, one for the next candidate) -- a timing difference a remote observer can see. The +/// argument that holds instead (and does not lean on /// the rejected candidate being discarded, which RFC 6979's chaining never satisfies: the /// accepted `k` is an HMAC of the rejected `V`): the iteration count is a function of the /// HMAC-DRBG output stream, i.e. of PRF outputs under a key derived from `(d, h1)`, so under the diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 8d078ff41..3917ed94c 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -81,8 +81,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate /// is rejected not with negligible probability but with `1 - n/2^512 = 0.3326`, so 34 % of /// signatures run step h.3's "otherwise" branch at least once (mean 0.50 extra iterations), and -/// each extra iteration is two more HMAC-SHA-512 calls -- a timing difference a remote observer -/// can see. The argument that holds instead (and does not lean on the rejected candidate being +/// each extra iteration is three more HMAC-SHA-512 calls (two for the re-key, one for the next +/// candidate) -- a timing difference a remote observer can see. The argument that holds instead +/// (and does not lean on the rejected candidate being /// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the /// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF /// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of From 0e2bb4209f731ec9399fb983c169553a6a41d834 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 15:14:48 +0000 Subject: [PATCH 04/16] utils: Condition::to_bool_opaque, a bool conversion that passes the optimisation barrier [For humans] - to_bool is `self.0 != 0`: LLVM can fold the mask away and branch on the inputs that built it - to_bool_opaque runs the mask through the volatile barrier first, so the one branch is on the mask - added to all four widths (u64, u32, i64, i32), with a test per width in ct_tests.rs - to_bool is unchanged: it is const and has about 100 callers ====================================================================== [For agents] Condition's consumers that do masked arithmetic (select, mov, swap, conditional_select) all pass the mask through barrier(), a volatile store and load, before using it. to_bool does not: it is a const fn returning `self.0 != 0`, and when the mask was built from arithmetic the compiler can see (an OR-chain over limbs, a subtraction's borrow) LLVM folds the mask away and emits one branch per input comparison. Observed in release asm of bouncycastle_ecdsa's rfc6979::generate_k: a mask built from nat::is_zero and from_lsb(borrow) and converted with to_bool() still produced two conditional jumps, one on the all-zero test and one on the borrow; the same mask converted with select(1, 0) == 1, which passes the barrier, produced exactly one jump on the barrier-loaded value. to_bool_opaque is that second form as a method: `self.barrier().0 != 0`. It is for the case where the outcome of a decision is public but the inputs are secret, so the decision must be made on the whole mask and not on its constituents. It is not const because barrier() is not. to_bool is left as it is: const, about 100 call sites across the workspace, and whether it should itself pass the barrier is a separate question that needs a census of those callers first. Per CLAUDE.md the four `impl Condition` blocks are hand-written copies and a change to one width is applied to every width; the method is added to all four, and ct_tests.rs gains one test per width module that checks TRUE and FALSE and agreement with to_bool on masks built from the boundary values and their negations. cargo test -p bouncycastle-utils: all pass. Assisted-by: Claude:claude-fable-5-1 --- crypto/utils/src/ct.rs | 36 +++++++++++++++++++++++ crypto/utils/tests/ct_tests.rs | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 6c104445d..1c1ca7131 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -226,6 +226,15 @@ impl Condition { pub const fn to_bool(self) -> bool { self.0 != 0 } + + /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is + /// made on the whole mask, and the compiler cannot split it into tests on the inputs that + /// built the mask. + pub fn to_bool_opaque(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 + } } impl Condition { @@ -365,6 +374,15 @@ impl Condition { pub const fn to_bool(self) -> bool { self.0 != 0 } + + /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is + /// made on the whole mask, and the compiler cannot split it into tests on the inputs that + /// built the mask. + pub fn to_bool_opaque(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 + } } // TODO: We should do Condition. @@ -456,6 +474,15 @@ impl Condition { pub const fn to_bool(self) -> bool { self.0 != 0 } + + /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is + /// made on the whole mask, and the compiler cannot split it into tests on the inputs that + /// built the mask. + pub fn to_bool_opaque(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 + } } impl Condition { @@ -536,6 +563,15 @@ impl Condition { pub const fn to_bool(self) -> bool { self.0 != 0 } + + /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is + /// made on the whole mask, and the compiler cannot split it into tests on the inputs that + /// built the mask. + pub fn to_bool_opaque(self) -> bool { + // The barrier (section comment above) is what stops LLVM from folding the mask away and + // branching on its constituents; it is volatile, hence this is not `const`. + self.barrier().0 != 0 + } } impl BitAnd for Condition diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index fcda2b02d..8ea378e5f 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -125,6 +125,19 @@ mod unsigned_u64_tests { assert_canonical(Condition::::FALSE, false); } + /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on + /// both canonical masks and on every mask the constructors build from the boundary values. + #[test] + fn to_bool_opaque() { + assert!(Condition::::TRUE.to_bool_opaque()); + assert!(!Condition::::FALSE.to_bool_opaque()); + for &v in &BOUNDARY { + let cond = Condition::::is_zero(v); + assert_eq!(cond.to_bool_opaque(), cond.to_bool()); + assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); + } + } + #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -274,6 +287,19 @@ mod unsigned_u32_tests { assert_canonical(Condition::::FALSE, false); } + /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on + /// both canonical masks and on every mask the constructors build from the boundary values. + #[test] + fn to_bool_opaque() { + assert!(Condition::::TRUE.to_bool_opaque()); + assert!(!Condition::::FALSE.to_bool_opaque()); + for &v in &BOUNDARY { + let cond = Condition::::is_zero(v); + assert_eq!(cond.to_bool_opaque(), cond.to_bool()); + assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); + } + } + #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -426,6 +452,19 @@ mod signed_i64_tests { assert_canonical(Condition::::FALSE, false); } + /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on + /// both canonical masks and on every mask the constructors build from the boundary values. + #[test] + fn to_bool_opaque() { + assert!(Condition::::TRUE.to_bool_opaque()); + assert!(!Condition::::FALSE.to_bool_opaque()); + for &v in &BOUNDARY { + let cond = Condition::::is_zero(v); + assert_eq!(cond.to_bool_opaque(), cond.to_bool()); + assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); + } + } + #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -627,6 +666,19 @@ mod signed_i32_tests { assert_canonical(Condition::::FALSE, false); } + /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on + /// both canonical masks and on every mask the constructors build from the boundary values. + #[test] + fn to_bool_opaque() { + assert!(Condition::::TRUE.to_bool_opaque()); + assert!(!Condition::::FALSE.to_bool_opaque()); + for &v in &BOUNDARY { + let cond = Condition::::is_zero(v); + assert_eq!(cond.to_bool_opaque(), cond.to_bool()); + assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); + } + } + #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); From 040f1b2fe58e65be178eb24907fa8062e305d22d Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 15:40:58 +0000 Subject: [PATCH 05/16] ecdsa: compare the rfc6979 candidate against [1, n-1] under a mask, on all seven curves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - candidate_in_range returned a bool from `limbs == [0; L]` and `nat::sub(..).1 == 1` - release asm: `limbs == [0; L]` became a bcmp libcall over the candidate, then limb compares and jumps - it is now two masks (nat::is_zero, from_lsb(borrow)) joined and converted with to_bool_opaque - every generate_k now has one data-dependent conditional jump, on the barrier-loaded mask, and no bcmp - cargo test -p bouncycastle-ecdsa, all 33 test binaries pass (CAVP and wycheproof included) ====================================================================== [For agents] RFC 6979 step h.3 decides whether a candidate is in [1, n-1], and this crate's constant-time rules (plan §7, and the crate docs' Security Considerations) permit exactly one branch on that decision: the loop's. The function that made the decision computed `limbs == [0; L]` with an early return and then `borrow == 1`, both plain bools. In release asm (cargo rustc --release --lib --emit=asm, with codegen-units=1) that compiled, in every one of the seven generate_k bodies, to a call to bcmp over the candidate's L*8 bytes (`movq bcmp@GOTPCREL(%rip), %rbx ; callq *%rbx ; testl %eax, %eax ; je`, P-521 calling `*bcmp@GOTPCREL` directly), i.e. a variable-time memcmp with early exit, followed by compares of the candidate's limbs against n's. For P-256: movabsq $-4294967296, %rdx ; cmpq %rdx, 88(%rsp) ; sete %dl ; jb .LBB75_19 orb %al, %cl ; andb %cl, %dl ; jne .LBB75_19 a compare of the top limb against n's top limb 0xFFFFFFFF00000000 with a jump taken when it is smaller, and a second jump on the combined zero test and borrow. secp256k1, P-384 and P-521 had the same shape with `cmpq $-1, ` on the all-ones limbs of their n; the brainpool curves had the top-limb compare and the combined jump. The leaked information is small (how far the memcmp got, which limb decided the comparison), but it is a branch on k's bits that the rules and the docs said did not exist. The body is now let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); (is_nonzero & is_below_n).to_bool_opaque() to_bool_opaque (added to Condition in the previous commit) passes the mask through the volatile barrier before testing it; with plain to_bool() LLVM still split the test into two jumps, one on the OR-chain and one on the borrow. Conditional jumps per generate_k, before -> after, from the census script over the demangled release asm: rfc6979 (P-256) 3 -> 1 rfc6979_bp256r1 3 -> 1 rfc6979_p256k1 4 -> 1 rfc6979_bp384r1 3 -> 1 rfc6979_p384 4 -> 1 rfc6979_bp512r1 3 -> 1 rfc6979_p521 4 -> 2 (the second is the `cmpq $66` byte-copy loop counter) After: the bcmp call is gone (one call fewer per function) and the one data-dependent jump is `movq %rax, ; cmpq $0, ; jne`, the mask reloaded through the barrier. hmac_k is a separate symbol in both builds, so its expect on do_final_out's Result never appears here. rfc6979.rs's doc comment on candidate_in_range gains one sentence saying the comparison is done as masks and why; the other six copies are not given the same sentence (QUALITY_AND_STYLE.md "Proportion": one home per fact). The seven files differ only in limb count and sec1 module name; the body edit was applied to all seven with one regex and the diff checked by eye. Tests: the existing candidate_in_range_boundary_cases unit tests (0, 1, n-1, n, 0xff..ff) in each module kill the `!` and `&`->`|` mutants; the brainpool vectors suites also exercise the reject path (five and six rejections in a row since b11afd6). Full suite with /tmp/bc-test-data and /tmp/wycheproof symlinked: 33 binaries, all pass. Assisted-by: Claude:claude-fable-5-1 --- crypto/ecdsa/src/rfc6979.rs | 11 +++++++---- crypto/ecdsa/src/rfc6979_bp256r1.rs | 7 +++---- crypto/ecdsa/src/rfc6979_bp384r1.rs | 7 +++---- crypto/ecdsa/src/rfc6979_bp512r1.rs | 7 +++---- crypto/ecdsa/src/rfc6979_p256k1.rs | 7 +++---- crypto/ecdsa/src/rfc6979_p384.rs | 7 +++---- crypto/ecdsa/src/rfc6979_p521.rs | 7 +++---- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/crypto/ecdsa/src/rfc6979.rs b/crypto/ecdsa/src/rfc6979.rs index dc6eb441c..9ee8aef5b 100644 --- a/crypto/ecdsa/src/rfc6979.rs +++ b/crypto/ecdsa/src/rfc6979.rs @@ -67,13 +67,16 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// `true` iff the raw (unreduced) candidate bytes, read as a big-endian integer, are in `[1, /// n-1]` -- RFC 6979 step h.3's comparison against `q`, "not reduced modulo q" (§3.2, footnote to /// step h). +/// +/// The comparison is on the candidate's own bits, so it is done as masks and converted with +/// `Condition::to_bool_opaque`: the one branch is `generate_k`'s loop decision, which the crate +/// docs list as a timing exception. The other curves' copies of this function do the same. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = p256_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index cc73d40ae..f75e6a552 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -68,11 +68,10 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// n-1]` -- RFC 6979 step h.3's comparison against `q`. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = bp256r1_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 97515d22c..6d66d85ce 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -66,11 +66,10 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// n-1]` -- RFC 6979 step h.3's comparison against `q`. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = bp384r1_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 3917ed94c..1a9f5a34e 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -66,11 +66,10 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// n-1]` -- RFC 6979 step h.3's comparison against `q`. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = bp512r1_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0, 0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p256k1.rs b/crypto/ecdsa/src/rfc6979_p256k1.rs index 6158e4e13..6488a0978 100644 --- a/crypto/ecdsa/src/rfc6979_p256k1.rs +++ b/crypto/ecdsa/src/rfc6979_p256k1.rs @@ -56,11 +56,10 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// n-1]` -- RFC 6979 step h.3's comparison against `q`. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = p256k1_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p384.rs b/crypto/ecdsa/src/rfc6979_p384.rs index 6730e3a0a..2ebe808a5 100644 --- a/crypto/ecdsa/src/rfc6979_p384.rs +++ b/crypto/ecdsa/src/rfc6979_p384.rs @@ -48,11 +48,10 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// n-1]`. See [`crate::rfc6979::candidate_in_range`]'s docs. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = p384_sec1::limbs_from_be_bytes(candidate); - if limbs == [0, 0, 0, 0, 0, 0] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p521.rs b/crypto/ecdsa/src/rfc6979_p521.rs index ac8e05646..c62aeff5a 100644 --- a/crypto/ecdsa/src/rfc6979_p521.rs +++ b/crypto/ecdsa/src/rfc6979_p521.rs @@ -103,11 +103,10 @@ fn bits2int_521(v1: &[u8; HLEN], v2: &[u8; HLEN]) -> Secret<[u8; RLEN]> { /// n-1]`. See [`crate::rfc6979::candidate_in_range`]'s docs. fn candidate_in_range(candidate: &[u8; RLEN]) -> bool { let limbs = p521_sec1::limbs_from_be_bytes(candidate); - if limbs == [0; 9] { - return false; - } + let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); - borrow == 1 + let is_below_n = Condition::::from_lsb(borrow); + (is_nonzero & is_below_n).to_bool_opaque() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and From 432ee7441c935d7773ff1a6e8041b44af4be18b6 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 15:41:56 +0000 Subject: [PATCH 06/16] ecdsa: one home for the rfc6979 rejection-loop argument, and a rate for every curve [For humans] - P-256's generate_k said a rejected candidate is "discarded entirely"; RFC 6979 chains, so it is not - P-256 now carries the PRF-stream argument once; the three brainpool copies shrink to their rates - secp256k1, P-384 and P-521 state their rejection probability (2^-128, 2^-194, 2^-262) - P-256 says the loop retries only out-of-range candidates, not an r = 0, where signing errors - bp512r1's rate is 33 %, not 34 %; the fingerprint is one to two bits per message, not "about a bit" - docs only ====================================================================== [For agents] b11afd6 fixed the brainpool rfc6979 modules' rejection-loop text and deferred crate::rfc6979's as a cross-curve item: its structural sentence ("a rejected candidate is discarded entirely and nothing derived from it is ever used, because the only thing its iteration count can reveal is how many candidates were rejected") misdescribes RFC 6979 on every curve. After a candidate V is rejected the code computes K' = HMAC_K(V || 0x00) and V' = HMAC_K'(V), and the next candidate is HMAC_K'(V'): the accepted k is derived from the rejected V through two HMAC applications under a key that is itself derived from V. (b11afd6's shorthand "the accepted k is an HMAC of the rejected V" is inexact in the same direction and is replaced by that formula.) The argument that does hold is the one b11afd6 wrote three times, once per brainpool module: the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF outputs under a key derived from (d, h1), so under the PRF assumption it is independent of the accepted k (uniform on [1, n-1] conditioned on the count) and reveals nothing about d; what must be constant time is everything done with the accepted k. That argument is curve-independent, so per QUALITY_AND_STYLE.md "Proportion" (each fact has one home) it now lives once, in crate::rfc6979::generate_k's docs under a "The rejection loop" heading, which also states P-256's rate (n within 2^-32 of 2^256). The three brainpool generate_k docs keep what is specific to them: the rate (0.336, 0.4503, 0.3326, i.e. 34 %, 45 % and 33 % of signatures; the last was rounded to 34 % before), the mean extra iterations, the three HMAC calls per extra iteration, and the practical consequence of a deterministic per-(d, msg) count: a fingerprint worth one to two bits per message (the entropy of the geometric rejection count is 1.39, 1.81 and 1.38 bits; "about a bit" undersold it), usable for confirming a candidate d, not for recovering one. They link to P-256 for the argument. Their module docs said the loop is near-dead "for P-256 and secp256k1" in one file and "on the NIST curves" in the other two; all three now say "on the NIST curves and secp256k1", the phrase the crate docs use. rfc6979_p256k1 and rfc6979_p521 already deferred to P-256; they, and rfc6979_p384, which said nothing about the loop, now state their own rejection probability, computed from N_LIMBS as (2^qlen - n) / 2^qlen: 2^-127.65 (secp256k1, so "n within 2^-127 of 2^256", rejection about 2^-128), 2^-194.18 and 2^-262.49 (P-521 after the 521-bit truncation). The crate docs' Security Considerations link for the argument moves from rfc6979_bp256r1::generate_k to rfc6979::generate_k, with the brainpool modules named for the rates, and its range becomes 33 % to 45 %. The P-256 docs also say what the loop does not do: it rejects only out-of-range candidates, and a k whose r would be 0 is not retried, signing returns an error instead (FIPS 186-5 step 11, documented in ecdsa_p256 and in the crate's Security Considerations). This is stated in terms of the code's behaviour only. b11afd6's message notes that RFC 6979's step h.3 treats such a k as rejected; the RFC text could not be fetched from this sandbox (rfc-editor.org and ietf.org are not reachable through its proxy), so the quote and a sentence on the deviation are left for a session with access to the document. For the same reason the existing quotes ("otherwise ... loop", "not reduced modulo q", "utterly improbable") are carried over unchanged, not re-verified. cargo doc -p bouncycastle-ecdsa: no new warnings (the rfc6979_p521 private-item links predate this change). cargo test -p bouncycastle-ecdsa --doc and --lib pass. Docs only; no code change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ecdsa/src/lib.rs | 4 ++-- crypto/ecdsa/src/rfc6979.rs | 26 ++++++++++++++------- crypto/ecdsa/src/rfc6979_bp256r1.rs | 31 ++++++++++--------------- crypto/ecdsa/src/rfc6979_bp384r1.rs | 36 ++++++++++++----------------- crypto/ecdsa/src/rfc6979_bp512r1.rs | 31 ++++++++++--------------- crypto/ecdsa/src/rfc6979_p256k1.rs | 3 ++- crypto/ecdsa/src/rfc6979_p384.rs | 4 +++- crypto/ecdsa/src/rfc6979_p521.rs | 2 ++ 8 files changed, 66 insertions(+), 71 deletions(-) diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index 8b30edef5..d984cafe5 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -127,10 +127,10 @@ //! and the exceptions it documents. This crate adds two exceptions of its own: //! - RFC 6979's candidate loop runs a data-dependent number of iterations. On the NIST curves //! and secp256k1 a candidate is rejected with negligible probability; on the brainpool curves -//! 34 % to 45 % of signatures reject at least one. Under the PRF assumption the count is +//! 33 % to 45 % of signatures reject at least one. Under the PRF assumption the count is //! independent of the accepted `k` and reveals nothing about `d`, but it is deterministic per //! `(d, message)`, so a remote observer gets a per-message timing fingerprint. See -//! [`rfc6979_bp256r1::generate_k`]'s docs for the argument and the rates. +//! [`rfc6979::generate_k`]'s docs for the argument and the brainpool modules' for the rates. //! - Signing returns an error on `r = 0` or `s = 0` instead of drawing another `k`; that branch //! is on a public outcome. //! - **`k` must never repeat across two signatures under the same key with different messages.** diff --git a/crypto/ecdsa/src/rfc6979.rs b/crypto/ecdsa/src/rfc6979.rs index 9ee8aef5b..45be57d91 100644 --- a/crypto/ecdsa/src/rfc6979.rs +++ b/crypto/ecdsa/src/rfc6979.rs @@ -95,15 +95,25 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// whose own docs note it "momentarily holds the plain value ... the caller asked for the bytes"), /// so that transient is copied into the `Secret` above but the transient itself is not erased. /// +/// # The rejection loop +/// /// The rejection loop (step h.3, "otherwise ... loop") is the one place this crate's own -/// constant-time rules permit branching on a value derived from a secret. The position, which -/// applies equally to FIPS 186-5 Appendix A.3.2's and A.4.2's identical loops: a rejection loop is -/// acceptable when a rejected candidate is discarded entirely and nothing derived from it is ever -/// used, because the only thing its iteration count can reveal is how many candidates were -/// rejected -- and that is independent of the `k` finally returned. What must be constant time is -/// everything subsequently done *with* the accepted `k`, which is the rest of this crate's -/// concern. For P-256 the question is close to moot anyway: `n` is within `2^-32` of `2^256`, so a -/// candidate is rejected with negligible probability. +/// constant-time rules permit branching on a value derived from a secret; the crate docs list it as +/// an exception. The argument, which applies equally to FIPS 186-5 Appendix A.3.2's and A.4.2's +/// identical loops: the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF +/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of the +/// accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals nothing +/// about `d`. It does not rely on a rejected candidate being discarded, which RFC 6979's chaining +/// never satisfies: after `V` is rejected, `K' = HMAC_K(V || 0x00)` and `V' = HMAC_K'(V)`, and the +/// accepted `k` is `bits2int(HMAC_K'(V'))`. What must be constant time is everything subsequently +/// done *with* the accepted `k`, which is the rest of this crate's concern. The same argument +/// covers every curve in this crate; what differs is the rate. For P-256 the loop is close to dead: +/// `n` is within `2^-32` of `2^256`, so a candidate is rejected with probability about `2^-32`. The +/// brainpool modules, where it is a normal path, state their rates and what the count then amounts +/// to in practice. +/// +/// The loop rejects only out-of-range candidates. A `k` whose `r` would be `0` is not retried +/// here: signing returns an error instead (see the crate docs' Security Considerations). pub fn generate_k(d: &P256Scalar, h1: &[u8; HLEN]) -> P256Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index f75e6a552..e05084e0b 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -9,10 +9,10 @@ //! close `n` is to `2^256` itself (unlike secp256k1, brainpoolP256r1's `n` is not close to a power //! of two at all; only the top-bit-set property matters here). //! -//! `n` not being close to `2^256` has a second consequence: the step h.3 rejection loop is a -//! normal control path on this curve, not the near-dead branch it is for P-256 and secp256k1 -- a -//! uniform 256-bit candidate is `>= n` with probability `1 - n/2^256 = 0.336`, so about a third -//! of all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly +//! `n` not being close to `2^256` has a second consequence: the step h.3 rejection loop is a normal +//! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- a +//! uniform 256-bit candidate is `>= n` with probability `1 - n/2^256 = 0.336`, so about a third of +//! all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly //! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) //! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. //! @@ -79,21 +79,14 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// /// # The rejection loop is a normal path on this curve /// -/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate -/// is rejected not with negligible probability but with `1 - n/2^256 = 0.336`, so 34 % of -/// signatures run step h.3's "otherwise" branch at least once (mean 0.51 extra iterations), and -/// each extra iteration is three more HMAC-SHA-256 calls (two for the re-key, one for the next -/// candidate) -- a timing difference a remote observer can see. The argument that holds instead -/// (and does not lean on the rejected candidate being -/// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the -/// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF -/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of -/// the accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals -/// nothing about `d`. Two consequences worth knowing: the count is deterministic per `(d, msg)`, -/// so the timing pattern over chosen messages is a fingerprint worth about a bit per message -- -/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds -- -/// and what must be constant time is everything done with the accepted `k`, which is the rest of -/// this crate's concern. +/// A candidate is rejected with probability `1 - n/2^256 = 0.336`, so 34 % of signatures run step +/// h.3's "otherwise" branch at least once (mean 0.51 extra iterations). Each extra iteration is +/// three more HMAC-SHA-256 calls (two for the re-key, one for the next candidate) -- a timing +/// difference a remote observer can see. [`crate::rfc6979::generate_k`]'s docs give the argument +/// for why the count reveals nothing about `d` or the accepted `k`; it does not depend on the rate. +/// What the rate changes is the practical consequence: the count is deterministic per `(d, msg)`, +/// so the timing pattern over chosen messages is a fingerprint worth one to two bits per message -- +/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds. pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 6d66d85ce..85a041af9 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -7,12 +7,12 @@ //! brainpoolP256r1's own case: brainpoolP384r1's `n` has its top bit set (`n > 2^383`), so `2n > //! 2^384` and any 384-bit input is already `< 2n`, despite `n` not being close to `2^384` itself. //! -//! `n` not being close to `2^384` has a second consequence: the step h.3 rejection loop is a -//! normal control path on this curve, not the near-dead branch it is on the NIST curves -- and -//! the most-taken in the crate: a uniform 384-bit candidate is `>= n` with probability `1 - -//! n/2^384 = 0.4503`, so nearly half of all messages take at least one extra HMAC round and a -//! fifth take two or more. (RFC 6979 §3.4 calls rejection "utterly improbable"; that assumes `n` -//! close to `2^qlen`, which `qlen == hlen` alone does not give.) [`generate_k`]'s docs give the +//! `n` not being close to `2^384` has a second consequence: the step h.3 rejection loop is a normal +//! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- +//! and the most-taken in the crate: a uniform 384-bit candidate is `>= n` with probability `1 - +//! n/2^384 = 0.4503`, so nearly half of all messages take at least one extra HMAC round and a fifth +//! take two or more. (RFC 6979 §3.4 calls rejection "utterly improbable"; that assumes `n` close to +//! `2^qlen`, which `qlen == hlen` alone does not give.) [`generate_k`]'s docs give the //! constant-time argument that holds at this rejection rate. //! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; @@ -77,21 +77,15 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// /// # The rejection loop is a normal path on this curve /// -/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate -/// is rejected not with negligible probability but with `1 - n/2^384 = 0.4503`, so 45 % of -/// signatures run step h.3's "otherwise" branch at least once and a fifth at least twice (mean -/// 0.82 extra iterations), and each extra iteration is three more HMAC-SHA-384 calls (two for the -/// re-key, one for the next candidate) -- a timing difference a remote observer can see. The -/// argument that holds instead (and does not lean on -/// the rejected candidate being discarded, which RFC 6979's chaining never satisfies: the -/// accepted `k` is an HMAC of the rejected `V`): the iteration count is a function of the -/// HMAC-DRBG output stream, i.e. of PRF outputs under a key derived from `(d, h1)`, so under the -/// PRF assumption it is independent of the accepted `k` -- which, conditioned on the count, is -/// uniform on `[1, n-1]` -- and reveals nothing about `d`. Two consequences worth knowing: the -/// count is deterministic per `(d, msg)`, so the timing pattern over chosen messages is a -/// fingerprint worth about a bit per message -- useless for recovering `d`, usable for confirming -/// a candidate `d` an attacker already holds -- and what must be constant time is everything done -/// with the accepted `k`, which is the rest of this crate's concern. +/// A candidate is rejected with probability `1 - n/2^384 = 0.4503`, so 45 % of signatures run step +/// h.3's "otherwise" branch at least once and a fifth at least twice (mean 0.82 extra iterations). +/// Each extra iteration is three more HMAC-SHA-384 calls (two for the re-key, one for the next +/// candidate) -- a timing difference a remote observer can see. [`crate::rfc6979::generate_k`]'s +/// docs give the argument for why the count reveals nothing about `d` or the accepted `k`; it does +/// not depend on the rate. What the rate changes is the practical consequence: the count is +/// deterministic per `(d, msg)`, so the timing pattern over chosen messages is a fingerprint worth +/// one to two bits per message -- useless for recovering `d`, usable for confirming a candidate `d` +/// an attacker already holds. pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 1a9f5a34e..0f19139c3 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -8,10 +8,10 @@ //! other brainpool curves' own case: brainpoolP512r1's `n` has its top bit set (`n > 2^511`), so //! `2n > 2^512` and any 512-bit input is already `< 2n`, despite `n` not being close to `2^512`. //! -//! `n` not being close to `2^512` has a second consequence: the step h.3 rejection loop is a -//! normal control path on this curve, not the near-dead branch it is on the NIST curves -- a -//! uniform 512-bit candidate is `>= n` with probability `1 - n/2^512 = 0.3326`, so about a third -//! of all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly +//! `n` not being close to `2^512` has a second consequence: the step h.3 rejection loop is a normal +//! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- a +//! uniform 512-bit candidate is `>= n` with probability `1 - n/2^512 = 0.3326`, so about a third of +//! all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly //! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) //! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. //! @@ -77,21 +77,14 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// /// # The rejection loop is a normal path on this curve /// -/// [`crate::rfc6979::generate_k`]'s rejection-loop argument does not transfer here: a candidate -/// is rejected not with negligible probability but with `1 - n/2^512 = 0.3326`, so 34 % of -/// signatures run step h.3's "otherwise" branch at least once (mean 0.50 extra iterations), and -/// each extra iteration is three more HMAC-SHA-512 calls (two for the re-key, one for the next -/// candidate) -- a timing difference a remote observer can see. The argument that holds instead -/// (and does not lean on the rejected candidate being -/// discarded, which RFC 6979's chaining never satisfies: the accepted `k` is an HMAC of the -/// rejected `V`): the iteration count is a function of the HMAC-DRBG output stream, i.e. of PRF -/// outputs under a key derived from `(d, h1)`, so under the PRF assumption it is independent of -/// the accepted `k` -- which, conditioned on the count, is uniform on `[1, n-1]` -- and reveals -/// nothing about `d`. Two consequences worth knowing: the count is deterministic per `(d, msg)`, -/// so the timing pattern over chosen messages is a fingerprint worth about a bit per message -- -/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds -- -/// and what must be constant time is everything done with the accepted `k`, which is the rest of -/// this crate's concern. +/// A candidate is rejected with probability `1 - n/2^512 = 0.3326`, so 33 % of signatures run step +/// h.3's "otherwise" branch at least once (mean 0.50 extra iterations). Each extra iteration is +/// three more HMAC-SHA-512 calls (two for the re-key, one for the next candidate) -- a timing +/// difference a remote observer can see. [`crate::rfc6979::generate_k`]'s docs give the argument +/// for why the count reveals nothing about `d` or the accepted `k`; it does not depend on the rate. +/// What the rate changes is the practical consequence: the count is deterministic per `(d, msg)`, +/// so the timing pattern over chosen messages is a fingerprint worth one to two bits per message -- +/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds. pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] diff --git a/crypto/ecdsa/src/rfc6979_p256k1.rs b/crypto/ecdsa/src/rfc6979_p256k1.rs index 6488a0978..2b34975a0 100644 --- a/crypto/ecdsa/src/rfc6979_p256k1.rs +++ b/crypto/ecdsa/src/rfc6979_p256k1.rs @@ -64,7 +64,8 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and /// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs for the rejection-loop -/// constant-time argument, which applies unchanged here. +/// constant-time argument, which applies unchanged here; `n` is within `2^-127` of `2^256`, so a +/// candidate is rejected with probability about `2^-128`. pub fn generate_k(d: &P256K1Scalar, h1: &[u8; HLEN]) -> P256K1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] diff --git a/crypto/ecdsa/src/rfc6979_p384.rs b/crypto/ecdsa/src/rfc6979_p384.rs index 2ebe808a5..0fee0acc5 100644 --- a/crypto/ecdsa/src/rfc6979_p384.rs +++ b/crypto/ecdsa/src/rfc6979_p384.rs @@ -55,7 +55,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and -/// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs. +/// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs, including the +/// rejection-loop constant-time argument, which applies unchanged here; `n` is within `2^-194` of +/// `2^384`, so a candidate is rejected with probability about `2^-194`. pub fn generate_k(d: &P384Scalar, h1: &[u8; HLEN]) -> P384Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); diff --git a/crypto/ecdsa/src/rfc6979_p521.rs b/crypto/ecdsa/src/rfc6979_p521.rs index c62aeff5a..f71dbe76a 100644 --- a/crypto/ecdsa/src/rfc6979_p521.rs +++ b/crypto/ecdsa/src/rfc6979_p521.rs @@ -112,6 +112,8 @@ fn candidate_in_range(candidate: &[u8; RLEN]) -> bool { /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and /// message hash `h1 = H(m)`. See [`crate::rfc6979::generate_k`]'s docs; step h differs as /// described in the module docs (two `HMAC_K(V)` rounds per candidate, then a 521-bit truncation). +/// The rejection-loop constant-time argument there applies unchanged here; `n` is within `2^-262` +/// of `2^521`, so a 521-bit candidate is rejected with probability about `2^-262`. pub fn generate_k(d: &P521Scalar, h1: &[u8; HLEN]) -> P521Scalar { let mut int2octets_d = Secret::<[u8; RLEN]>::new(); *int2octets_d = d.to_be_bytes(); From 868e28b36da978e0987860bcd848e6728974d7fa Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 16:07:11 +0000 Subject: [PATCH 07/16] sm2: generate and accept the private key in [1, n-2], per GB/T 32918.1-2016 S6.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - GB/T 32918.1-2016 S6.1 a) generates dA in [1, n-2]; the crate used [1, n-1], which is k's range - dA = n - 1 loaded, and keygen could draw it (2^-256), but it cannot sign: (1 + dA)^-1 is of 0 - keygen now reduces the DRBG output mod n - 2, and from_bytes rejects n - 1 and above - extra_bits gains reduce_wide_bits_mod_n_minus_2 beside the n - 1 reduction that k keeps using - tests: seven n - 2 known answers from Python, the bit-serial reference on both moduli, n - 2 signs - fallibility counts unchanged; bc-rust sm2 sign and pk-from-sk refuse d = n - 1 and accept n - 2 ====================================================================== [For agents] GB/T 32918.1-2016 S6.1 (密钥对的生成, key pair generation) reads, in full: input, a valid set of elliptic curve system parameters; output, a key pair (d, P) for them; a) 用随机数发生器产生整数 d∈[1,n-2] ("generate an integer d in [1, n-2] with a random number generator"); b) P = [d]G; c) the key pair is (d, P), d the private key and P the public key. GB/T 32918.2-2016 S5.3 (用户密钥对) says the signer's key pair "shall conform to GB/T 32918.1-2016 chapter 6", and its S6.1 step A3 draws the per-message secret from [1, n-1] (用随机数发生器产生随机数k∈[1,n-1]), so the two ranges differ by one on purpose: step A6 divides by 1 + dA, and dA = n - 1 would make that a division by 0. draft-shen-sm2-ecdsa-02, which the crate cited for the key range, states none; its key generation section is a placeholder. The two GB/T parts were read for this change from their PDFs; the draft is not reachable from this sandbox and its lack of a range is as the review found it. The OpenSSL data point was reproduced here: OpenSSL 3.0.22's sm2_sig_gen computes (1 + dA)^-1 once and loops on s = 0, so pkeyutl -sign with d = n - 1 never returns (the review had the same result on 3.0.13), while d = n - 2 signs and the signature verifies. The crate's from_bytes compared the raw encoding against 0 and n, so d = n - 1 loaded and round-tripped (the old test asserted exactly that), and keygen reduced the 320-bit DRBG output mod n - 1 and added 1, which reaches n - 1 with probability 2^-256. Such a key's cached (1 + dA)^-1 is Fermat's inverse of 0, which is 0, so every signature came out s = 0 and sign_with_k returned its "regenerate k and retry" error forever. extra_bits now has a private reduce_wide_bits(bytes, m, mu_low) and two one-line public wrappers: reduce_wide_bits_mod_n_minus_1 for k, unchanged in behaviour, and reduce_wide_bits_mod_n_minus_2 for dA. The new constants, computed in Python from n and checked against the n - 1 values already in the file: N_MINUS_2_LIMBS = [0x53bbf40939d54121, 0x7203df6b21c6052b, 0xffffffffffffffff, 0xfffffffeffffffff] MU_LOW_LIMBS (n-2) = [0x12ac6361f15149a2, 0x8dfc2096fa323c01, 0x0000000100000001, 0x0000000100000001] mu = floor(2^512 / (n-2)) is 2^256 + mu_low with only the low limb differing from the n - 1 value (...49a1). Barrett's preconditions hold for the new modulus as for the old: (n-2) >> 255 = 1 and 2^504 < (n-2) * 2^256, so the 63-byte input limit is the same. FIPS 186-5 A.4.1's bias bound (2rho(1-rho)m <= 2^-64 * 2^l, rho = (2^l mod m)/m) re-evaluated with m = n - 2: l = 256 fails, l = 319 is the smallest passing length, l = 320 passes with the ratio at 2^-31, the same three numbers as for n - 1, so EXTRA_BITS_DRBG_OUTPUT_LEN = 40 serves both. N_MINUS_1_LIMBS becomes pub(crate) so from_bytes can compare against it: borrow out of limbs - (n - 1) is 1 iff limbs < n - 1 iff limbs <= n - 2. The check is still a load-time branch on public constants, the argument in the comment above it is unchanged. Known answers for the n - 2 reduction, each int.from_bytes(x, 'big') % (n - 2) + 1 in Python: 01020304 repeated to 40 bytes -> 0306090c...0e773203ec4d33fd; 40 bytes of 0xff -> 00000001000000008dfc20956c361b6a187a276050a8c5aa726ecad5c62abedf; 40 zero bytes -> 1; n - 2 (left-padded) -> 1, n - 3 -> n - 2, n - 1 -> 2; 63 bytes of 0xff -> a21eb5e3...3783fc97. The existing n - 1 answers reproduce from the same one-liner with m = n - 1. reference_bit_serial takes the modulus as a parameter and the pseudorandom agreement test and the 63-byte test run both functions. sm2_tests' range test is renamed private_key_rejects_values_outside_1_to_n_minus_2: n - 1, n, n + 1 and 2^256 - 1 are rejected, n - 2 loads, encodes back unchanged, signs and verifies (1 + dA = n - 1, the inverse of -1, a corner the old range never reached). Left alone: k's range and reduction (GB/T 32918.2-2016 S6.1 A3 is [1, n-1], as before), Sm2Scalar, sign_with_k, from_validated_scalar's expect message ([dA]G is never infinity for dA in [1, n-1] still holds a fortiori), the memory tables, the CLI (it loads through from_bytes, which is where the new behaviour was checked: bc-rust sm2 pk-from-sk and sign refuse d = n - 1 and accept d = n - 2, whose signature verify accepts). quality_stats.sh before and after: the fallibility counts are identical. cargo test -p bouncycastle-sm2: 5 binaries, all pass. Assisted-by: Claude:claude-fable-5-1 --- crypto/sm2/src/extra_bits.rs | 100 +++++++++++++-------- crypto/sm2/src/keys.rs | 57 ++++++------ crypto/sm2/src/lib.rs | 2 +- crypto/sm2/tests/extra_bits_tests.rs | 130 ++++++++++++++++++++------- crypto/sm2/tests/sm2_tests.rs | 31 ++++--- 5 files changed, 214 insertions(+), 106 deletions(-) diff --git a/crypto/sm2/src/extra_bits.rs b/crypto/sm2/src/extra_bits.rs index 6170d6d8b..42efdf956 100644 --- a/crypto/sm2/src/extra_bits.rs +++ b/crypto/sm2/src/extra_bits.rs @@ -1,59 +1,87 @@ //! "Extra random bits" reduction: turns a DRBG output wider than `n` into a value in `[1, n-1]` -//! with negligible bias, the same technique `bouncycastle-ecdsa`'s `extra_bits` module uses for -//! P-256 (and every brainpool curve on this branch), applied here for the private key `dA` and the -//! per-message secret `k` `draft-shen-sm2-ecdsa-02` §5.1.3/§5.2.1 step A3 both need. The draft -//! itself only says "pick a random number ... via a random number generator" without specifying a -//! DRBG-to-scalar conversion, so this reuses FIPS 186-5 Appendix A.4.1's method (the same -//! bias-bound methodology this workspace already applies to every other curve) rather than a plain -//! modular reduction, which would be measurably biased for a `DRBG output length == n's bit +//! (the per-message secret `k`) or `[1, n-2]` (the private key `dA`) with negligible bias, the +//! same technique `bouncycastle-ecdsa`'s `extra_bits` module uses for P-256 (and every brainpool +//! curve on this branch). GB/T 32918.2-2016 §6.1 step A3 draws `k` from `[1, n-1]` and GB/T +//! 32918.1-2016 §6.1 a) draws `dA` from `[1, n-2]`; both say only to use "a random number +//! generator" (`draft-shen-sm2-ecdsa-02` likewise, and it states no range for `dA`) without +//! specifying a DRBG-to-scalar conversion, so this reuses FIPS 186-5 Appendix A.4.1's method (the +//! same bias-bound methodology this workspace already applies to every other curve) rather than a +//! plain modular reduction, which would be measurably biased for a `DRBG output length == n's bit //! length` request. //! //! # Why SM2 needs this at all //! -//! SM2's `n` sits about `2^225` below `2^256` -- the same distance P-256's `n` keeps from its -//! power of two (about `2^224`), and for the same reason not close enough: a 256-bit DRBG output -//! reduced mod `n - 1` would land in `[0, 2^256 - n)` about twice as often as anywhere else, -//! which is the bias FIPS 186-5's Table A.2 gives P-256 its 288/352-bit entries to dilute. (The -//! brainpool curves need the extra bits for a different reason -- their `n` is nowhere near a -//! power of two -- while P-384, P-521 and secp256k1 need none for bias, their `n` being within a -//! relative `2^-128` or closer of one, and draw them only because FIPS 186-5 Appendix A.3.1 -//! requires `t >= 64` for a per-message secret.) FIPS 186-5 Appendix A.4.1 step 2's bias-bound check -//! (`2ρ(1-ρ)(n-1) > ε·N`, `ε = 2^-64`) was evaluated directly (in Python, against `n`'s actual -//! value from `draft-shen-sm2-ecdsa-02` Appendix D) for `N = 2^l` at increasing `l`: `l = 256` -//! (matching `n`'s own bit length) fails the check, and the smallest passing `l` is `319`; this -//! crate uses `l = 320` (40 bytes, byte-aligned) as `crate::keys::EXTRA_BITS_DRBG_OUTPUT_LEN`. +//! SM2's `n` sits `2^224` below `2^256` (`2^256 - n = 2^224 * 1.000000`, to six places) -- the +//! same distance P-256's `n` keeps from its power of two (`2^224 * 0.99999999977`), and for the +//! same reason not close enough: a 256-bit DRBG output reduced mod `n - 1` would land in `[0, +//! 2^256 - n)` about twice as often as anywhere else, which is the bias FIPS 186-5's Table A.2 +//! gives P-256 its 288/352-bit entries to dilute. (The brainpool curves need the extra bits for a +//! different reason -- their `n` is nowhere near a power of two -- while P-384, P-521 and +//! secp256k1 need none for bias, their `n` being within a relative `2^-128` or closer of one, and +//! draw them only because FIPS 186-5 Appendix A.3.1 requires `t >= 64` for a per-message +//! secret.) FIPS 186-5 Appendix A.4.1 step 2's bias-bound check (`2ρ(1-ρ)(n-1) > ε·N`, `ε = +//! 2^-64`) was evaluated directly (in Python, against `n`'s actual value from +//! `draft-shen-sm2-ecdsa-02` Appendix D) for `N = 2^l` at increasing `l`: `l = 256` (matching +//! `n`'s own bit length) fails the check, and the smallest passing `l` is `319`; this crate uses +//! `l = 320` (40 bytes, byte-aligned) as `crate::keys::EXTRA_BITS_DRBG_OUTPUT_LEN`. //! -//! The reduction itself is [`bouncycastle_ec::barrett`]'s constant-time Barrett reduction with -//! `m = n - 1` (see `bouncycastle_ecdsa`'s `extra_bits` docs for the constant-time argument); `extra_bits_tests.rs` -//! pins known answers computed in Python and cross-checks it against the bit-serial algorithm this -//! module used to implement, kept there as an independent reference. +//! # Two moduli +//! +//! Appendix A.4.1 steps 3-5 are `x mod (n-1)`, then `+ 1`: an output in `[1, n-1]`, the range +//! `k` needs. `dA`'s range is one smaller, so [`reduce_wide_bits_mod_n_minus_2`] runs the same +//! steps with modulus `n - 2` and lands in `[1, n-2]`. The bias-bound check above re-evaluated +//! with `n - 2` in place of `n - 1` gives the same answer -- `l = 319` is the smallest passing +//! length and `l = 320` passes with the same `2^-31` margin -- so one DRBG output length serves +//! both. +//! +//! The reduction itself is [`bouncycastle_ec::barrett`]'s constant-time Barrett reduction (see +//! `bouncycastle_ecdsa`'s `extra_bits` docs for the constant-time argument); `extra_bits_tests.rs` +//! pins known answers computed in Python for both moduli and cross-checks each against the +//! bit-serial algorithm this module used to implement, kept there as an independent reference. use bouncycastle_ec::barrett; use bouncycastle_ec::nat; use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2Scalar}; -/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` is odd (`n` is prime), so the subtraction -/// never borrows out of the low limb. -const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; +/// `n - 1`, little-endian `u64` limbs. `N_LIMBS[0]` ends in `0x23`, so neither this subtraction +/// nor [`N_MINUS_2_LIMBS`]'s borrows out of the low limb. +pub(crate) const N_MINUS_1_LIMBS: [u64; 4] = [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + +/// `n - 2`, little-endian `u64` limbs. +const N_MINUS_2_LIMBS: [u64; 4] = [N_LIMBS[0] - 2, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; /// `floor(2^512 / (n-1)) - 2^256`: the low limbs of Barrett's `mu` for `m = n - 1` (whose top /// limb is `mu`'s only other bit -- see [`bouncycastle_ec::barrett`]), computed in Python from /// draft-shen-sm2-ecdsa-02 Appendix D's `n`. -const MU_LOW_LIMBS: [u64; 4] = +const MU_LOW_LIMBS_N_MINUS_1: [u64; 4] = [0x12ac6361f15149a1, 0x8dfc2096fa323c01, 0x0000000100000001, 0x0000000100000001]; +/// `floor(2^512 / (n-2)) - 2^256`, the same for `m = n - 2`: only the low limb differs. +const MU_LOW_LIMBS_N_MINUS_2: [u64; 4] = + [0x12ac6361f15149a2, 0x8dfc2096fa323c01, 0x0000000100000001, 0x0000000100000001]; + /// FIPS 186-5 Appendix A.4.1 steps 3-5 for `n` = the curve order: reduces the big-endian bit string -/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]`. `bytes` may be up to `63` bytes -/// -- `2^504 < (n-1) * 2^256`, Barrett's precondition -- which covers the 40-byte DRBG -/// output this crate draws with room to spare; a longer input is a programming error and panics. +/// `bytes` modulo `n-1` and adds `1`, landing in `[1, n-1]` -- the range of the per-message +/// secret `k` (GB/T 32918.2-2016 §6.1 step A3). `bytes` may be up to `63` bytes -- `2^504 < (n-1) +/// * 2^256`, Barrett's precondition -- which covers the 40-byte DRBG output this crate draws with +/// room to spare; a longer input is a programming error and panics. pub fn reduce_wide_bits_mod_n_minus_1(bytes: &[u8]) -> Sm2Scalar { - assert!( - bytes.len() < 64, - "reduce_wide_bits_mod_n_minus_1 given {} bytes, more than 63", - bytes.len() - ); + reduce_wide_bits(bytes, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS_N_MINUS_1) +} + +/// The same steps with modulus `n-2`: `bytes mod (n-2)`, plus `1`, landing in `[1, n-2]` -- the +/// range of the private key `dA` (GB/T 32918.1-2016 §6.1 a)). Same input width limit as +/// [`reduce_wide_bits_mod_n_minus_1`], since `2^504 < (n-2) * 2^256` too. +pub fn reduce_wide_bits_mod_n_minus_2(bytes: &[u8]) -> Sm2Scalar { + reduce_wide_bits(bytes, &N_MINUS_2_LIMBS, &MU_LOW_LIMBS_N_MINUS_2) +} + +/// `bytes mod m + 1` for either modulus, as a scalar. `m`'s top bit is set and `2^504 < m * 2^256` +/// for both, so the 63-byte limit is Barrett's `t < m * 2^256` precondition in both cases. +fn reduce_wide_bits(bytes: &[u8], m: &[u64; 4], mu_low: &[u64; 4]) -> Sm2Scalar { + assert!(bytes.len() < 64, "reduce_wide_bits given {} bytes, more than 63", bytes.len()); let t = barrett::limbs_from_be_bytes::<8>(bytes); - let reduced = barrett::reduce::<4, 8, 5>(&t, &N_MINUS_1_LIMBS, &MU_LOW_LIMBS); + let reduced = barrett::reduce::<4, 8, 5>(&t, m, mu_low); let (plus_one, _) = nat::add(&reduced, &[1, 0, 0, 0]); Sm2Scalar::from_limbs(plus_one) } diff --git a/crypto/sm2/src/keys.rs b/crypto/sm2/src/keys.rs index 498a73be4..289d680b8 100644 --- a/crypto/sm2/src/keys.rs +++ b/crypto/sm2/src/keys.rs @@ -1,20 +1,21 @@ -//! SM2 key types (`draft-shen-sm2-ecdsa-02` §4, "Key Generation") and key-pair generation. Shaped -//! identically to `bouncycastle-ecdsa`'s per-curve `keys_*.rs` modules -- see that crate's -//! `keys_bp256r1` module's docs for the full reasoning -- with SM2's own field/scalar -//! types and the wide-DRBG-output reduction from [`crate::extra_bits`] substituted. The draft's §4 -//! only says the private key `dA` is "a random number ... selected via a random number generator", -//! without specifying a DRBG-to-scalar conversion; this crate uses the same FIPS 186-5 Appendix -//! A.4.1 "extra random bits" method as every curve in `bouncycastle-ecdsa`, for the same reason (see -//! [`crate::extra_bits`]'s docs for the bias-bound derivation specific to SM2's `n`). - -use crate::extra_bits::reduce_wide_bits_mod_n_minus_1; +//! SM2 key types and key-pair generation (GB/T 32918.1-2016 §6.1, which GB/T 32918.2-2016 §5.3 +//! defers to for the signer's key pair). Shaped identically to `bouncycastle-ecdsa`'s per-curve +//! `keys_*.rs` modules -- see that crate's `keys_bp256r1` module's docs for the full reasoning -- +//! with SM2's own field/scalar types and the wide-DRBG-output reduction from +//! [`crate::extra_bits`] substituted. §6.1 a) says only that `dA` is an integer in `[1, n-2]` +//! produced "with a random number generator" (`draft-shen-sm2-ecdsa-02` says the same without a +//! range), without specifying a DRBG-to-scalar conversion; this crate uses the same FIPS 186-5 +//! Appendix A.4.1 "extra random bits" method as every curve in `bouncycastle-ecdsa`, for the same +//! reason (see [`crate::extra_bits`]'s docs for the bias-bound derivation specific to SM2's `n`). + +use crate::extra_bits::{N_MINUS_1_LIMBS, reduce_wide_bits_mod_n_minus_2}; use bouncycastle_core::errors::{RNGError, SignatureError}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{RNG, SignaturePrivateKey, SignaturePublicKey}; use bouncycastle_ec::nat; use bouncycastle_ec::sm2::Sm2FieldElement; use bouncycastle_ec::sm2_comb::comb_multiply_base_point; -use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2Scalar, Sm2ScalarField}; +use bouncycastle_ec::sm2_scalar::{Sm2Scalar, Sm2ScalarField}; use bouncycastle_ec::sm2_sec1; use bouncycastle_rng::DefaultRNG; use bouncycastle_utils::secret::Secret; @@ -34,8 +35,8 @@ pub const PK_LEN: usize = 65; /// docs), not copied from any other curve's table entry. pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; -/// An SM2 private key: `draft-shen-sm2-ecdsa-02` §4's `dA`, `dA` in `[1, n-1]`, held in -/// [`bouncycastle_utils::secret::Secret`] via [`Sm2Scalar`]. +/// An SM2 private key: `dA` in `[1, n-2]` (GB/T 32918.1-2016 §6.1 a); the draft states no range), +/// held in [`bouncycastle_utils::secret::Secret`] via [`Sm2Scalar`]. /// /// # Why the public key is carried alongside `dA` /// @@ -74,19 +75,18 @@ pub struct SM2PrivateKey { } impl SM2PrivateKey { - /// Builds the key pair from an already-validated `dA` in `[1, n-1]`, computing `PA = [dA]G`. + /// Builds the key pair from an already-validated `dA` in `[1, n-2]`, computing `PA = [dA]G`. /// The single place `SM2PrivateKey`'s invariant (`pk` really is `[d]G`) is established. fn from_validated_scalar(d: Sm2Scalar) -> Self { let q = comb_multiply_base_point(&d); - // dA is in [1, n-1] by construction (every caller validates first) and G has prime order n + // dA is in [1, n-2] by construction (every caller validates first) and G has prime order n // (h = 1), so [dA]G is never the identity. let (x, y) = q.to_affine().expect("[dA]G is never infinity for dA in [1, n-1]"); // (1 + dA)^-1, once per key rather than once per signature (see the type's docs). The // three scalar-field intermediates reveal dA and are scrubbed, the same way sign_with_k - // scrubs its own; the cached value itself goes into a Secret. (For dA = n - 1 the inverse - // is of 0 and comes out 0, which makes every signature's s = 0 and fail step A6 exactly as - // it did when the inverse was computed per signature.) + // scrubs its own; the cached value itself goes into a Secret. 1 + dA is at most n - 1, + // since dA <= n - 2, so the inverse is never of 0. let mut d_field = Sm2ScalarField::from_secret(&d); let mut one_plus_d = Sm2ScalarField::ONE.add(&d_field); let mut inverse = one_plus_d.invert(); @@ -136,24 +136,25 @@ impl SignaturePrivateKey for SM2PrivateKey { let array: [u8; SK_LEN] = bytes .try_into() .map_err(|_| SignatureError::DecodingError("SM2 private key must be 32 bytes"))?; - // draft-shen-sm2-ecdsa-02 S4: dA is in [1, n-1], checked on the raw big-endian value + // GB/T 32918.1-2016 S6.1 a): dA is in [1, n-2], checked on the raw big-endian value // *before* any reduction. `Sm2Scalar::from_be_bytes` reduces mod n, so checking afterwards // would accept an out-of-range encoding as a different, perfectly valid key: `dA = n + 1` // would load as `dA = 1`, giving one key two encodings and silently treating malformed - // input as well-formed. Comparing against the fixed public values 0 and n is a one-time - // validation of caller-supplied bytes at load time, not a computation performed repeatedly - // on a secret intermediate value, so branching on it leaks nothing beyond what the caller - // already knows from having supplied these exact bytes. + // input as well-formed. Comparing against the fixed public values 0 and n - 1 is a + // one-time validation of caller-supplied bytes at load time, not a computation performed + // repeatedly on a secret intermediate value, so branching on it leaks nothing beyond what + // the caller already knows from having supplied these exact bytes. let limbs = sm2_sec1::limbs_from_be_bytes(&array); - let (_, borrow) = nat::sub(&limbs, &N_LIMBS); + // borrow == 1 iff limbs < n - 1, i.e. limbs <= n - 2. + let (_, borrow) = nat::sub(&limbs, &N_MINUS_1_LIMBS); if limbs == [0; 4] || borrow != 1 { - return Err(SignatureError::DecodingError("SM2 private key must be in [1, n-1]")); + return Err(SignatureError::DecodingError("SM2 private key must be in [1, n-2]")); } Ok(Self::from_validated_scalar(Sm2Scalar::from_limbs(limbs))) } } -/// An SM2 public key: `draft-shen-sm2-ecdsa-02` §4's `PA = [dA]G`, stored as affine coordinates. +/// An SM2 public key: `PA = [dA]G` (GB/T 32918.1-2016 §6.1 b)), stored as affine coordinates. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SM2PublicKey { pub(crate) x: Sm2FieldElement, @@ -218,9 +219,9 @@ pub fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(SM2PublicKey, SM2PrivateKey // `Secret` so it is scrubbed when this function returns rather than left on the stack. let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let d = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + let d = reduce_wide_bits_mod_n_minus_2(&*extra_bits); - // d is in [1, n-1] by construction (see reduce_wide_bits_mod_n_minus_1), which is what + // d is in [1, n-2] by construction (see reduce_wide_bits_mod_n_minus_2), which is what // `from_validated_scalar` assumes; it computes PA = [d]G once, and the pair shares that value. let sk = SM2PrivateKey::from_validated_scalar(d); diff --git a/crypto/sm2/src/lib.rs b/crypto/sm2/src/lib.rs index 57f82f36c..f5f2fef44 100644 --- a/crypto/sm2/src/lib.rs +++ b/crypto/sm2/src/lib.rs @@ -69,7 +69,7 @@ //! //! Keys use the same SEC 1 encodings as `bouncycastle-ecdsa` -- `04 || X || Y` for a public key //! (the 33-byte compressed form is also accepted on decode), a 32-byte big-endian integer for a -//! private key, with `dA` required to be in `[1, n-1]`: +//! private key, with `dA` required to be in `[1, n-2]`: //! //! ``` //! use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; diff --git a/crypto/sm2/tests/extra_bits_tests.rs b/crypto/sm2/tests/extra_bits_tests.rs index 88c288bdd..f429443f6 100644 --- a/crypto/sm2/tests/extra_bits_tests.rs +++ b/crypto/sm2/tests/extra_bits_tests.rs @@ -1,6 +1,7 @@ //! Known-answer tests for [`reduce_wide_bits_mod_n_minus_1`] (FIPS 186-5 Appendix A.4.1 steps -//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently -//! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from +//! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`, and for +//! [`reduce_wide_bits_mod_n_minus_2`], the same steps with modulus `n - 2`. Expected values +//! computed independently in Python via `(int.from_bytes(bytes, 'big') % m) + 1` with `n` from //! `draft-shen-sm2-ecdsa-02` Appendix D, not derived from this crate's own reduction code -- //! existing round-trip tests elsewhere (`keygen`/`sign_randomized`) only ever check that the //! reduced value is self-consistent with itself, which can't distinguish a correct reduction from @@ -9,13 +10,34 @@ //! `N_MINUS_1_LIMBS`/`TWO_POW_256_MOD_N_MINUS_1_LIMBS` constants surfaced several bit-level and //! off-by-one mutants surviving because nothing pinned them against an independent reference. +use bouncycastle_ec::nat; +use bouncycastle_ec::sm2_scalar::N_LIMBS; +use bouncycastle_ec::sm2_sec1::be_bytes_from_limbs; use bouncycastle_hex::decode as hex_decode; -use bouncycastle_sm2::extra_bits::reduce_wide_bits_mod_n_minus_1; +use bouncycastle_sm2::extra_bits::{ + reduce_wide_bits_mod_n_minus_1, reduce_wide_bits_mod_n_minus_2, +}; fn bytes32(hex: &str) -> [u8; 32] { hex_decode(hex).unwrap().try_into().unwrap() } +/// `n - 1` and `n - 2` as limbs: `n` is odd and `N_LIMBS[0]` ends `0x23`, so neither borrows. +fn n_minus_1() -> [u64; 4] { + [N_LIMBS[0] - 1, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]] +} + +fn n_minus_2() -> [u64; 4] { + [N_LIMBS[0] - 2, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]] +} + +/// `value`, big-endian, left-padded to the 40-byte DRBG output width this module is fed with. +fn padded_to_40(value: &[u64; 4]) -> [u8; 40] { + let mut input = [0u8; 40]; + input[8..].copy_from_slice(&be_bytes_from_limbs(value)); + input +} + #[test] fn known_answer_repeating_pattern() { let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); @@ -41,37 +63,71 @@ fn known_answer_all_zero_yields_one() { /// The two boundaries of Appendix A.4.1's own arithmetic: `n - 1` is the modulus of step 4, so it /// reduces to `0` and step 5 makes it `1`; `n - 2` is the largest residue, so it comes out as -/// `n - 1`, the top of the `[1, n-1]` output interval. Both inputs are left-padded to the DRBG -/// output width this module is fed with. +/// `n - 1`, the top of the `[1, n-1]` output interval. #[test] fn known_answer_at_the_n_minus_1_boundary() { - let n = bouncycastle_ec::sm2_sec1::be_bytes_from_limbs(&bouncycastle_ec::sm2_scalar::N_LIMBS); - let mut n_minus_1 = n; - n_minus_1[31] -= 1; // n is odd, so neither of these subtractions borrows - let mut n_minus_2 = n; - n_minus_2[31] -= 2; - - let mut input = [0u8; 40]; - input[8..].copy_from_slice(&n_minus_1); let mut one = [0u8; 32]; one[31] = 1; + let input = padded_to_40(&n_minus_1()); assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), one, "n - 1 -> 1"); - input[8..].copy_from_slice(&n_minus_2); + let input = padded_to_40(&n_minus_2()); + let n_minus_1 = be_bytes_from_limbs(&n_minus_1()); assert_eq!(reduce_wide_bits_mod_n_minus_1(&input).to_be_bytes(), n_minus_1, "n - 2 -> n - 1"); } +#[test] +fn known_answer_repeating_pattern_mod_n_minus_2() { + let input: Vec = [0x01u8, 0x02, 0x03, 0x04].iter().cloned().cycle().take(40).collect(); + let expected = bytes32("0306090c0102030401911cc9047a6deaf6e477a96f45da1d0e773203ec4d33fd"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_ff_mod_n_minus_2() { + let input = [0xffu8; 40]; + let expected = bytes32("00000001000000008dfc20956c361b6a187a276050a8c5aa726ecad5c62abedf"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +#[test] +fn known_answer_all_zero_yields_one_mod_n_minus_2() { + let input = [0u8; 40]; + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), expected); +} + +/// The same boundaries one lower: `n - 2` is the modulus, so it reduces to `0` and comes out `1`; +/// `n - 3` is the largest residue and comes out as `n - 2`, the top of the `[1, n-2]` output +/// interval; and `n - 1`, the value the `[1, n-1]` range used to admit as a private key, is `1` +/// above the modulus and comes out `2`. +#[test] +fn known_answer_at_the_n_minus_2_boundary() { + let mut one = [0u8; 32]; + one[31] = 1; + let mut two = [0u8; 32]; + two[31] = 2; + let n_minus_3 = [N_LIMBS[0] - 3, N_LIMBS[1], N_LIMBS[2], N_LIMBS[3]]; + + let input = padded_to_40(&n_minus_2()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), one, "n - 2 -> 1"); + + let input = padded_to_40(&n_minus_3); + let n_minus_2 = be_bytes_from_limbs(&n_minus_2()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), n_minus_2, "n - 3 -> n - 2"); + + let input = padded_to_40(&n_minus_1()); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&input).to_be_bytes(), two, "n - 1 -> 2"); +} + /// The bit-serial algorithm this module implemented before switching to Barrett reduction, kept /// here as an independent reference: Horner's rule over the input bits, doubling the running -/// remainder and subtracting `n - 1` when it is exceeded, with the carry out of the top limb folded -/// back in as `2^256 mod (n-1) = 2^256 - (n-1)`. Slow, obviously correct, and sharing no -/// code with the Barrett path beyond `nat`'s add and sub. -fn reference_bit_serial(bytes: &[u8]) -> [u8; 32] { - use bouncycastle_ec::nat; - let n = bouncycastle_ec::sm2_scalar::N_LIMBS; - let mut m = n; - m[0] -= 1; // n is odd - let two_pow_mod_m = nat::sub(&[0u64; 4], &m).0; // 2^256 - m, wrapping +/// remainder and subtracting the modulus `m` when it is exceeded, with the carry out of the top +/// limb folded back in as `2^256 mod m = 2^256 - m`. Slow, obviously correct, and sharing no code +/// with the Barrett path beyond `nat`'s add and sub. +fn reference_bit_serial(bytes: &[u8], m: &[u64; 4]) -> [u8; 32] { + let two_pow_mod_m = nat::sub(&[0u64; 4], m).0; // 2^256 - m, wrapping let mut acc = [0u64; 4]; for &byte in bytes { for bit_idx in (0..8).rev() { @@ -80,12 +136,12 @@ fn reference_bit_serial(bytes: &[u8]) -> [u8; 32] { if carry == 1 { doubled = nat::add(&doubled, &two_pow_mod_m).0; } - let (reduced, borrow) = nat::sub(&doubled, &m); + let (reduced, borrow) = nat::sub(&doubled, m); acc = if borrow == 1 { doubled } else { reduced }; } } let plus_one = nat::add(&acc, &[1u64, 0, 0, 0]).0; - bouncycastle_ec::sm2_sec1::be_bytes_from_limbs(&plus_one) + be_bytes_from_limbs(&plus_one) } /// xorshift64*, fixed seed, as in the `ec` crate's property tests. @@ -103,9 +159,9 @@ impl Xorshift64 { } /// Barrett against the bit-serial reference over pseudorandom inputs at every width from one byte -/// up to the 63-byte maximum the function accepts, biased towards the widths actually drawn +/// up to the 63-byte maximum the functions accept, biased towards the widths actually drawn /// (40 bytes) and towards values with long runs of set bits, where Barrett's quotient estimate is -/// furthest off and the most conditional subtractions are needed. +/// furthest off and the most conditional subtractions are needed. Both moduli, on the same inputs. #[test] fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { let mut rng = Xorshift64(0xBA77E770_0000_0004); @@ -127,15 +183,27 @@ fn agrees_with_the_bit_serial_reference_over_many_pseudorandom_inputs() { } assert_eq!( reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), - reference_bit_serial(&bytes), - "len = {len}, bytes = {bytes:02x?}" + reference_bit_serial(&bytes, &n_minus_1()), + "mod n - 1: len = {len}, bytes = {bytes:02x?}" + ); + assert_eq!( + reduce_wide_bits_mod_n_minus_2(&bytes).to_be_bytes(), + reference_bit_serial(&bytes, &n_minus_2()), + "mod n - 2: len = {len}, bytes = {bytes:02x?}" ); } } +/// `2^504 - 1`, the largest input accepted: Barrett's precondition holds with room to spare. The +/// `n - 2` answer is pinned from Python as well as against the reference. #[test] fn maximum_width_all_ones_input() { - // 2^504 - 1, the largest input accepted: Barrett's precondition holds with room to spare. let bytes = [0xffu8; 63]; - assert_eq!(reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), reference_bit_serial(&bytes)); + assert_eq!( + reduce_wide_bits_mod_n_minus_1(&bytes).to_be_bytes(), + reference_bit_serial(&bytes, &n_minus_1()) + ); + let expected = bytes32("a21eb5e374a22b3d3d620fc84e3affe07e92c8346dee9756110ae2df3783fc97"); + assert_eq!(reduce_wide_bits_mod_n_minus_2(&bytes).to_be_bytes(), expected); + assert_eq!(expected, reference_bit_serial(&bytes, &n_minus_2())); } diff --git a/crypto/sm2/tests/sm2_tests.rs b/crypto/sm2/tests/sm2_tests.rs index 80dab1189..785b54df8 100644 --- a/crypto/sm2/tests/sm2_tests.rs +++ b/crypto/sm2/tests/sm2_tests.rs @@ -298,26 +298,37 @@ fn private_key_rejects_zero() { } } -/// `draft-shen-sm2-ecdsa-02` §4 puts `dA` in `[1, n-1]`, so an encoding of `n` or above is not a -/// key. Reducing such an encoding mod `n` rather than rejecting it would give one key many -/// valid-looking encodings: `dA = n + 1` would load as `dA = 1`. +/// GB/T 32918.1-2016 §6.1 a) puts `dA` in `[1, n-2]`, so an encoding of `n - 1` or above is not +/// a key. `n - 1` cannot sign (step A6 would invert `1 + dA = n`, i.e. `0`), and reducing an +/// encoding of `n` or above mod `n` rather than rejecting it would give one key many +/// valid-looking encodings: `dA = n + 1` would load as `dA = 1`. `n - 2` is the top of the range +/// and the one value for which step A6 divides by `n - 1`, i.e. by `-1`. #[test] -fn private_key_rejects_values_at_or_above_the_group_order() { +fn private_key_rejects_values_outside_1_to_n_minus_2() { let n = be_bytes_from_limbs(&N_LIMBS); + // N_LIMBS[0] ends 0x23, so none of the +-1/-2 adjustments below carries or borrows. + let mut n_minus_1 = n; + n_minus_1[31] -= 1; let mut n_plus_1 = n; - n_plus_1[31] += 1; // N_LIMBS[0] ends 0x23, so this never carries - for (name, bytes) in [("n", n), ("n + 1", n_plus_1), ("2^256 - 1", [0xffu8; 32])] { + n_plus_1[31] += 1; + for (name, bytes) in + [("n - 1", n_minus_1), ("n", n), ("n + 1", n_plus_1), ("2^256 - 1", [0xffu8; 32])] + { match SM2PrivateKey::from_bytes(&bytes) { Err(SignatureError::DecodingError(_)) => {} other => panic!("dA = {name} should have been rejected, got {other:?}"), } } - let mut n_minus_1 = n; - n_minus_1[31] -= 1; - let sk = SM2PrivateKey::from_bytes(&n_minus_1).expect("dA = n - 1 is in range"); - assert_eq!(sk.encode(), n_minus_1, "dA = n - 1 must round-trip unchanged"); + let mut n_minus_2 = n; + n_minus_2[31] -= 2; + let sk = SM2PrivateKey::from_bytes(&n_minus_2).expect("dA = n - 2 is in range"); + assert_eq!(sk.encode(), n_minus_2, "dA = n - 2 must round-trip unchanged"); + + let msg = b"dA = n - 2: step A6 divides by 1 + dA = n - 1"; + let sig = SM2::sign(&sk, msg, Some(ID)).expect("dA = n - 2 must sign"); + SM2::verify(&sk.derive_pk(), msg, Some(ID), &sig).expect("and its signature must verify"); } #[test] From 4b445c09de261ad04432318cd8e04c0bee7e77c0 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 16:08:36 +0000 Subject: [PATCH 08/16] ec: pin sm2 REDC's final subtraction with a known answer [For humans] - sm2_scalar's redc ends with a select of high - n when high >= n; no test reached that select - cargo mutants' "select forced to high" survived both crates' suites: a non-canonical u = c + n - downstream, negate, == and is_zero assume canonical form, so s = n would pass s == ZERO - three constructed products now land there, in both operand orders, and are pinned from Python - the test asserts canonical output (add zero is a no-op, negate gives n - expected), not just value - tests only; no code change ====================================================================== [For agents] redc (sm2_scalar.rs) reduces the 8-limb product with four SOS rounds, then, when the 9th limb is 0, selects high - n over high if high >= n. The operands that reach that select need the Montgomery-form product abR mod n below 2^256 - n, about 2^224, and the pre-subtraction value u = (T + m n) / R to be the representative above n, which happens for about 2^-32 of products; 3000 pseudorandom pairs do not find one, and the (n-1)^2 and round-trip tests do not either. The mutant that forces the select to high survived cargo mutants on both bouncycastle-ec and bouncycastle-sm2. It is load-bearing: Sm2ScalarField's negate (n - x, selecting 0 when is_zero), PartialEq (limb compare) and is_zero all read the limbs as canonical, so a product that came out as n would compare equal to ZERO and a signing s equal to n would be emitted as 0. The three cases were constructed in the review rather than searched: pick the Montgomery operand a and the target pre-subtraction value u in [n, 2^256), solve m = uR n^-1 mod a and b = (uR - mn)/a, which makes REDC(a b) land exactly on u. Each case was re-checked here in Python before being committed: a b mod n equals the expected limbs, the SOS pre-subtraction value is in [n, 2^256) in both operand orders, u - n equals abR mod n, and abR mod n is below 2^224. from_limbs(expected) was also checked to pre-reduce to a value at or above n, which is why mul(a, b) == from_limbs(expected) alone would pass under the mutant (both sides would hold the same non-canonical limbs) and why the test asserts prod == prod.add(ZERO) and prod.negate() == n - expected as well as prod.to_limbs() == expected. The mutant was applied by hand to confirm: with the select forced to high, the test fails at the add-zero assertion on the first case; on the unmutated code the sm2_scalar_tests binary passes, 18 tests. The 5p -> 4p mutant in sm2.rs's base-field reduce is a different matter: the four D words sit at bits 64-95, so V > -4 * 2^96 and any bias of 2^98 or more keeps the accumulator non-negative; that mutant is equivalent and its documentation is corrected in the following docs commit, not here. redc's doc comment is left as it is; the test's comment carries the argument. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/tests/sm2_scalar_tests.rs | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crypto/ec/tests/sm2_scalar_tests.rs b/crypto/ec/tests/sm2_scalar_tests.rs index 18a1a15f4..9ed00b013 100644 --- a/crypto/ec/tests/sm2_scalar_tests.rs +++ b/crypto/ec/tests/sm2_scalar_tests.rs @@ -9,6 +9,7 @@ //! # for each i: (-vals[i]) % n, pow(vals[i], n-2, n) if vals[i] != 0 else 0 //! ``` +use bouncycastle_ec::nat; use bouncycastle_ec::sm2_scalar::{N_LIMBS, Sm2PublicScalar, Sm2Scalar, Sm2ScalarField}; const VALS_0: [u64; 4] = @@ -412,6 +413,45 @@ fn known_answer_at_n_minus_1() { assert_eq!(Sm2ScalarField::ONE.invert(), Sm2ScalarField::ONE, "1^-1 == 1"); } +/// Products whose REDC pre-reduction value lands in `[n, 2^256)` -- `acc[8] = 0` and `high >= n` +/// -- so the final conditional subtraction is the step that produces the canonical result. That +/// needs the canonical result below `2^224` and the wrong representative drawn, `2^-32` per +/// reduction, so no pseudorandom or round-trip test reaches it; these were constructed, not +/// searched (pick the Montgomery operand `a` and the target `u`, solve `m = uR * n^-1 mod a`, +/// `b = (uR - mn) / a`). A plain `mul(..) == from_limbs(expected)` would NOT detect the +/// subtraction's absence: `from_limbs` of the expected value takes the same step and would land +/// on the same non-canonical representative. The two extra assertions are what pin it. +#[test] +fn known_answer_mul_reaching_the_final_subtraction() { + let cases: [([u64; 4], [u64; 4], [u64; 4]); 3] = [ + ( + [0xcc0763d7a2a61e2e, 0xe3e7e0e51c470ea0, 0x64d4afaca15a0fe5, 0xd39e2a14c5fad104], + [0x6fed21e2d993738d, 0xb256903f893f641e, 0x800c9d58bd0ed69d, 0xb3abdcfe03d26162], + [0xf7632108662761bb, 0x2ea1b6cc5a05df86, 0xf85446494d6ec949, 0x61814d697467fe93], + ), + ( + [0x220bc6677cac3348, 0xbd56b3bef29ea6cf, 0xbc548b1d35fb72ca, 0x5436dcde311c8535], + [0x5fa32a181cc2adc6, 0x990944d795a06fd1, 0x256b1f9f049ff59a, 0x5c49581b3bcf99da], + [0xf7bb16069a9fbdd1, 0xa9b6720591dff91a, 0xcfb254d884a3a3df, 0x94aa57d7cb811c82], + ), + ( + [0x91bafd07c6d8e402, 0x150e9d662c62dc0d, 0x0f9c4d1c16d2c93f, 0x52ac4d55133a8232], + [0xfba938138feff62d, 0xacbf42b139fd6682, 0xf78407c4c7c96c45, 0x521b875c9611d4fd], + [0xd6f27669b7a11bd9, 0x6c88bca9c0b127c4, 0x2f33d68c2e78a389, 0x2e4612ca50df6aac], + ), + ]; + for (x, y, expected) in cases { + for (a, b) in [(x, y), (y, x)] { + let prod = fe(a).mul(&fe(b)); + assert_eq!(prod.to_limbs(), expected, "a = {a:x?}"); + // The output must be canonical: unchanged by adding zero, and negating to n - expected. + assert_eq!(prod, prod.add(&Sm2ScalarField::ZERO), "non-canonical output, a = {a:x?}"); + let (n_minus_expected, _) = nat::sub(&N_LIMBS, &expected); + assert_eq!(prod.negate().to_limbs(), n_minus_expected, "a = {a:x?}"); + } + } +} + /// The variable-time inverse on the public scalar type must agree with the constant-time /// [`Sm2ScalarField::invert`] everywhere: on the known-answer values (whose inverses are pinned above /// from Python), on `0` (both return `0` by convention), `1`, `n - 1`, and on pseudorandom From 558472405f9d2e886e2f02e66e44d2ece3de731b Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 16:13:03 +0000 Subject: [PATCH 09/16] sm2, ec: cite GB/T 32918 and the draft's real sections, and describe SM2 rather than ECDSA [For humans] - every section and step cited in the sm2 crate now names GB/T 32918.1/.2-2016, the draft in parentheses - the verification steps use GB/T S7.1's numbering (B1-B2 range, B3-B4 hash, B5 t, B6 point, B7 R) - the ec crate's sm2_* modules describe SM2's own equations, not ECDSA's k^-1, u, v or FIPS 186-5 - sm2 base-field reduce: the D words sit at bits 64-95, so V > -4 * 2^96; 5p is for the limb bound - docs and comments only; no code change ====================================================================== [For agents] GB/T 32918.2-2016 was read from its PDF for this change: S5.3 defers the key pair to part 1 chapter 6; S5.4.2 admits any approved hash "such as SM3"; S5.5 defines ZA and IDA; S6.1 is signing A1-A7 (A1 prepends ZA, A2 hashes, A3 draws k in [1, n-1], A4 [k]G, A5 r, A6 s, A7 output); S7.1 is verification B1-B7 (B1 r' range, B2 s' range, B3 prepends ZA, B4 hashes, B5 t = r' + s' with t = 0 failing, B6 [s']G + [t]PA, B7 R = e' + x1' compared with r'). The crate's old B labels followed the draft (B1 for both range checks, B4 for t, B5 for the point, B6 for R); they are renumbered to the standard's, which is what the code now cites first. The draft's section numbers given in parentheses (S5.1.4.2 hash, S5.1.4.4 ZA, S5.2.1 signing, S5.3 verification) are taken from the review's mapping: the draft is not reachable from this sandbox, so they were not re-verified here and should be spot-checked against draft-shen-sm2-ecdsa-02 before merge. The two ctx-required error strings now cite S5.5, where IDA is defined, rather than the signing and verification sections. In the ec crate, sm2_scalar.rs's module and item docs described ECDSA's k^-1, u and v and FIPS 186-5 S6.4.2; they now describe the SM2 quantities the sm2 crate computes ((1 + dA)^-1 once per key, r = e + x1, s, t = r' + s') and point at bouncycastle_sm2's sign_with_k. invert_vartime has no SM2 caller; its docs say so and that it is kept for parity with the ECDSA curves' public scalar types (removing it is an API decision left to the crate's owner). sm2_wnaf.rs names [s']G + [t]PA and S7.1 B6. sm2_sec1.rs says SM2 and GB/T 32918.5-2017 where it said P-256 and SP 800-186 S3.2.1.3. In sm2.rs, mul_small's "operand-independent count" is corrected: acc[4] is a function of the operands, in [4, 18], and only a multiplicand; reduce's precondition paragraph now says mul and square are the two call sites and that the bounds hold for any 8-limb input; and the bias argument is corrected: the four D words occupy column 2 (bits 64-95), so V > -4 * 2^96, any bias of 2^98 or more keeps the accumulator non-negative, and 5p is chosen for the top-limb bound of 18. Checked in Python against the column table: the all-ones 512-bit input reaches top limb 18, and no product sampled (random, near-p, and (p-1)^2 at 14) exceeded 16, so the field tests' "4 through 17" is the product range. This is why cargo mutants' 5p -> 4p mutant survives: it is equivalent. Docs and comments only; no code change. cargo fmt and cargo doc clean on the touched crates (the two pre-existing private-item link warnings in bouncycastle-sm2 are unchanged). Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/src/sm2.rs | 33 ++++++++-------- crypto/ec/src/sm2_scalar.rs | 42 ++++++++++----------- crypto/ec/src/sm2_sec1.rs | 6 +-- crypto/ec/src/sm2_wnaf.rs | 7 ++-- crypto/sm2/src/keys.rs | 14 ++++--- crypto/sm2/src/lib.rs | 17 +++++---- crypto/sm2/src/sm2.rs | 63 ++++++++++++++++--------------- crypto/sm2/src/za.rs | 11 +++--- crypto/sm2/tests/sm2_kat_tests.rs | 5 ++- crypto/sm2/tests/sm2_tests.rs | 11 +++--- 10 files changed, 111 insertions(+), 98 deletions(-) diff --git a/crypto/ec/src/sm2.rs b/crypto/ec/src/sm2.rs index b45215ed8..aadf8ee3a 100644 --- a/crypto/ec/src/sm2.rs +++ b/crypto/ec/src/sm2.rs @@ -274,8 +274,9 @@ fn widen(v: &[u64; 4]) -> [u64; 5] { [v[0], v[1], v[2], v[3], 0] } -/// `small * v`, one limb times a 4-limb value, as 5 limbs. Only ever called with a `small` that is -/// a bounded, operand-independent count (see [`reduce`]), never a secret. +/// `small * v`, one limb times a 4-limb value, as 5 limbs. `small` is [`reduce`]'s accumulator top +/// limb: a function of the operands, bounded in `[4, 18]`, and only ever a multiplicand here -- +/// the loop's trip count and memory accesses do not depend on it. fn mul_small(small: u64, v: &[u64; 4]) -> [u64; 5] { let mut out = [0u64; 5]; let mut carry: u128 = 0; @@ -369,17 +370,19 @@ fn widening_square(a: &[u64; 4]) -> [u64; 8] { /// D4 = ( 0 || 0 || 0 || 0 || 0 || A14 || 0 || 0 ) /// ``` /// -/// **Precondition: `t < p^2`**, matching SP 800-186 §G.1's own precondition for the analogous NIST -/// tables ("given an integer A less than m^2") and holding for every call site, since `mul` is the -/// only one and it passes the product of two canonical (`< p`) field elements. The bounds below -/// rest on it; a raw 512-bit value near `2^512` is *not* reduced correctly by this function. +/// Both call sites, `mul` and `square`, pass the product of two canonical (`< p`) field elements, +/// so `t < p^2` as in SP 800-186 §G.1's analogous NIST tables ("given an integer A less than +/// m^2"); the bounds below do not need that, and hold for any 8-limb `t`. /// -/// Bounds. Each term is a 256-bit value, so `T + S1 + ... + S13 < 14*2^256` and `D1 + ... + D4 < -/// 4*2^256`. Starting the accumulator at `5p` keeps it non-negative throughout (`5p > 4*2^256`, -/// since `5p - 4*2^256 = 2^256 - 5*2^224 - ... > 0`) and bounded by `5p + 14*2^256 < 19*2^256`, so -/// five limbs always suffice and the top limb is at most 18. Folding that limb back in via -/// `2^256 ≡ C` adds `u_hi*C < 19*2^225 < 2^230`, leaving `V < 2^256 + 2^230`; since `p > 2^256 - -/// 2^225`, `V - p < 2^230 + 2^225 < p`, so exactly one conditional subtraction finishes the job. +/// Bounds. Each term is a 256-bit value, so `T + S1 + ... + S13 < 14*2^256`; the four `D` words +/// all sit at bits 64-95, so `D1 + ... + D4 < 4*2^96`. Any bias of `2^98` or more therefore keeps +/// the accumulator non-negative; `5p` is used because it also keeps the top-limb bound a clean +/// 18: `5p + 14*2^256 < 19*2^256`, so five limbs always suffice. (`sm2_field_tests.rs` drives the +/// limb through 4 to 17 on products; only non-product input, such as all ones, reaches 18.) +/// Folding that limb +/// back in via `2^256 ≡ C` adds `u_hi*C < 19*2^225 < 2^230`, leaving `V < 2^256 + 2^230`; since +/// `p > 2^256 - 2^225`, `V - p < 2^230 + 2^225 < p`, so exactly one conditional subtraction +/// finishes the job. fn reduce(t: &[u64; 8]) -> [u64; 4] { let a = |i: usize| word(t, i) as i64; @@ -410,8 +413,8 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { // `V = sum(columns[w] * 2^(32 w))`, assembled as a two's-complement 5-limb value with one // signed carry pass: `cur >> 32` is an arithmetic shift, so a negative running sum carries a // negative amount into the next word, and the final carry (which can be negative) becomes the - // sign-extended top limb. `V` itself may be negative here -- the `D` terms can exceed the - // `S` terms -- which is what the `5p` bias is for. + // sign-extended top limb. `V` itself may be negative here -- the `D` words can exceed the + // `S` words in column 2 -- which is what the `5p` bias is for. let mut v = [0u64; 5]; let mut carry: i64 = 0; for w in 0..8 { @@ -420,7 +423,7 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { carry = cur >> 32; } v[4] = carry as u64; - // `V + 5p`, wrapping: the docs prove `V > -4 * 2^256`, so the true sum is non-negative and + // `V + 5p`, wrapping: the docs prove `V > -4 * 2^96`, so the true sum is non-negative and // below `19 * 2^256`, and the wrapping add of two 5-limb values recovers it exactly. let (acc, _) = nat::add(&v, &FIVE_P_LIMBS); diff --git a/crypto/ec/src/sm2_scalar.rs b/crypto/ec/src/sm2_scalar.rs index 34aae2f43..e2b6b5475 100644 --- a/crypto/ec/src/sm2_scalar.rs +++ b/crypto/ec/src/sm2_scalar.rs @@ -1,5 +1,7 @@ //! The SM2 scalar field, arithmetic mod the curve order `n` (`draft-shen-sm2-ecdsa-02` Appendix -//! D), needed for `k⁻¹` and `s = k⁻¹(e + rd) mod n` in ECDSA-shaped signing. +//! D), for SM2 signing's `r = e + x1` and `s = (1 + dA)^-1 (k - r dA) mod n` (GB/T 32918.2-2016 +//! §6.1 steps A5-A6, with `(1 + dA)^-1` inverted once per key) and verification's `t = r' + s'` +//! (§7.1 step B5). SM2 has no `k^-1`. //! //! Unlike `p`, `n` is not a Solinas prime -- there is no special shape to exploit for reduction -- //! so this uses Montgomery arithmetic instead: [`Sm2ScalarField`] holds its value as `x*R mod n` @@ -126,13 +128,13 @@ impl Sm2ScalarField { /// Overwrites this value with zero, through a write the compiler may not elide. /// - /// For the signing path, where a value of this type holds `d`, `k` or `k^-1` and would + /// For the signing path, where a value of this type holds `d`, `k` or `(1 + d)^-1` and would /// otherwise stay legible on the stack after the signature is returned. This type is `Copy` - /// and carries public values too (verification's `u`, `v`, `e`, `r`, `s`), so it is not + /// and carries public values too (verification's `e`, `r'`, `s'`, `t`), so it is not /// wrapped in [`bouncycastle_utils::secret::Secret`] -- that would cost its `Copy`, its `const /// ZERO`/`ONE`, and a scrub on every intermediate including the public ones. The trade is that /// scrubbing is the caller's job, on every path out of the function; see - /// `bouncycastle_ecdsa`'s `sign_with_k` for the intended use. + /// `bouncycastle_sm2`'s `sign_with_k` for the intended use. /// /// This does not reach values the compiler kept in registers or spilled itself, and is /// defence in depth rather than a guarantee. @@ -143,19 +145,16 @@ impl Sm2ScalarField { /// `self^2`, via a dedicated squaring rather than `self.mul(self)` -- see /// [`widening_square`]. /// - /// Used by the exponentiation loops ([`Self::invert`] and friends), which call it hundreds of - /// times in a row with nothing else competing for registers. Deliberately *not* used by this - /// curve's point arithmetic, which squares by calling `mul` with equal arguments: that was - /// tried and measured on P-256, and despite `square` being ~9% cheaper than `mul` in - /// isolation it made the constant-time comb multiplier consistently slower, because inlining - /// a third wide-multiply routine into that loop costs more than the saved partial products - /// return. See [`crate::p256::P256FieldElement::square`] for the numbers. + /// Used only by [`Self::invert`]'s exponentiation loop, which calls it hundreds of times in + /// a row with nothing else competing for registers. (The point arithmetic is on the base + /// field, whose own `square` is likewise kept out of it; see + /// [`crate::p256::P256FieldElement::square`] for the measurements behind that.) pub fn square(&self) -> Self { Self(redc(&widening_square(&self.0))) } - /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic (`k⁻¹`, `r*d`, - /// ...). Reads the secret's limbs once into a plain value, the same "protects storage, not + /// Converts a secret scalar (`d` or `k`) into Montgomery form for field arithmetic (`r*d`, + /// `k - r*d`, ...). Reads the secret's limbs once into a plain value, the same "protects storage, not /// every derived computation" boundary [`crate::sm2_comb::comb_multiply_base_point`] already /// crosses to read `k`'s bits. pub fn from_secret(secret: &Sm2Scalar) -> Self { @@ -386,7 +385,7 @@ impl Sm2Scalar { Self::from_limbs(crate::sm2_sec1::limbs_from_be_bytes(bytes)) } - /// Encodes to SEC 1 §2.3.7 big-endian octets -- the wire form of an ECDSA private key. + /// Encodes to SEC 1 §2.3.7 big-endian octets -- the wire form of an SM2 private key. /// Momentarily holds the plain value in the returned array, same as any other `encode()` on a /// `SignaturePrivateKey`: the caller asked for the bytes. pub fn to_be_bytes(&self) -> [u8; 32] { @@ -402,9 +401,9 @@ impl Sm2Scalar { } /// A scalar mod `n`, `< n`, held as a plain (non-secret) value -- for a value that is public by -/// construction: the `u`, `v` scalars ECDSA verification computes from a signature and public -/// data, or a message-derived value. See [`Sm2Scalar`]'s docs for why there is no conversion the -/// other way. +/// construction: the `s'` and `t = r' + s'` scalars SM2 verification computes from a signature +/// and public data, or a message-derived value. See [`Sm2Scalar`]'s docs for why there is no +/// conversion the other way. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Sm2PublicScalar([u64; 4]); @@ -420,10 +419,11 @@ impl Sm2PublicScalar { } /// `self^-1 mod n` in **variable time**, as a [`Sm2ScalarField`] ready for the arithmetic - /// that follows, or `0` for `0`. For ECDSA verification's `s` (FIPS 186-5 §6.4.2 step 4), - /// which is part of the signature and so already public; nothing derived from a private key - /// or a per-message secret may reach this, which is why it lives on the public scalar type - /// and not on [`Sm2ScalarField`], whose `invert` is the constant-time one. See + /// that follows, or `0` for `0`. SM2 verification (GB/T 32918.2-2016 §7.1) inverts nothing, + /// so `bouncycastle-sm2` never calls this; it is kept for parity with the ECDSA curves' + /// public scalar types, for a value that is public by construction. Nothing derived from a + /// private key or a per-message secret may reach this, which is why it lives on the public + /// scalar type and not on [`Sm2ScalarField`], whose `invert` is the constant-time one. See /// [`crate::inverse_vartime`]. pub fn invert_vartime(&self) -> Sm2ScalarField { Sm2ScalarField::from_limbs(crate::inverse_vartime::mod_inverse(&self.0, &N_LIMBS)) diff --git a/crypto/ec/src/sm2_sec1.rs b/crypto/ec/src/sm2_sec1.rs index d6668fe7c..b50e85507 100644 --- a/crypto/ec/src/sm2_sec1.rs +++ b/crypto/ec/src/sm2_sec1.rs @@ -14,7 +14,7 @@ //! [`encode_uncompressed`] is SEC 1 §2.3.3 case 3 (`04 || X || Y`) and [`encode_compressed`] is //! case 2 (`02 || X` or `03 || X`, tag selected by `y mod 2`). Both take affine coordinates //! directly rather than a [`Sm2JacobianPoint`], not a `[u8; 1]` "point at infinity" case: SEC 1 -//! §2.3.3 case 1 encodes `∅` as a single `0x00` octet, but an ECDSA-shaped public key -- the only +//! §2.3.3 case 1 encodes `∅` as a single `0x00` octet, but an SM2 public key -- the only //! caller -- can never legitimately be `∅`, so pushing the infinity check to the type (a caller //! must have already gotten `Some(x, y)` out of [`Sm2JacobianPoint::to_affine`]) makes encoding //! infinity a compile error instead of a runtime one. @@ -127,7 +127,7 @@ pub fn encode_compressed(x: &Sm2FieldElement, y: &Sm2FieldElement) -> [u8; 33] { /// Decodes and validates a SEC 1 §2.3.4 point encoding: 33 bytes (compressed, tag `0x02`/`0x03`) /// or 65 bytes (uncompressed, tag `0x04`). See the module docs for exactly which SP 800-186 /// Appendix D.1.1.1 checks this performs and why it constitutes full (not merely partial) -/// validation for P-256. Returns `None` for anything invalid. +/// validation for SM2. Returns `None` for anything invalid. pub fn decode(bytes: &[u8]) -> Option<(Sm2FieldElement, Sm2FieldElement)> { match bytes.len() { 33 => { @@ -187,7 +187,7 @@ pub fn decode_point(bytes: &[u8]) -> Option { // check, so no integration test through the public API can pin either one's pass/fail boundary // independent of that coincidence -- the QUALITY_AND_STYLE.md "private function, known-answer, // can't be reached cleanly from outside the crate" exception applies to both. Known answers for -// `limbs_less_than_p`: SP 800-186 S3.2.1.3 gives `p`; 0 and `p - 1` are its immediate neighbours +// `limbs_less_than_p`: GB/T 32918.5-2017 (draft Appendix D) gives `p`; 0 and `p - 1` are its immediate neighbours // inside `[0, p)`, and `p` itself plus a value with only a non-adjacent limb bumped above `p`'s // are its immediate neighbours outside it. `either_out_of_range` is exercised over all four // in-range/out-of-range combinations of its two arguments, which is what pins OR against AND. diff --git a/crypto/ec/src/sm2_wnaf.rs b/crypto/ec/src/sm2_wnaf.rs index ccdfbbf21..fb4cb36f2 100644 --- a/crypto/ec/src/sm2_wnaf.rs +++ b/crypto/ec/src/sm2_wnaf.rs @@ -1,6 +1,7 @@ -//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for ECDSA -//! verification's `R' = [u]G + [v]Q` (FIPS 186-5 §6.4.2). Both scalars and both points are public -//! -- `G` is the curve's fixed base point and `Q` a signer's public key -- so unlike +//! Interleaved windowed-NAF (wNAF) Shamir's-trick multiplication: `[u]G + [v]Q`, for SM2 +//! verification's `(x1', y1') = [s']G + [t]PA` (GB/T 32918.2-2016 §7.1 step B6). Both scalars and +//! both points are public -- `G` is the curve's fixed base point and `Q` a signer's public key -- +//! so unlike //! [`crate::sm2_comb`], this multiplier takes [`Sm2PublicScalar`], not [`crate::sm2_scalar::Sm2Scalar`], //! and is written for speed, not constant time: it branches freely on the digits it computes, //! and adds points with `add_vartime` rather than the constant-time `add` (see that method's diff --git a/crypto/sm2/src/keys.rs b/crypto/sm2/src/keys.rs index 289d680b8..aa56b6ce4 100644 --- a/crypto/sm2/src/keys.rs +++ b/crypto/sm2/src/keys.rs @@ -30,7 +30,7 @@ pub const SK_LEN: usize = 32; pub const PK_LEN: usize = 65; /// Requested output length, in bytes, from the DRBG for key generation and for the per-signature -/// secret `k` (`draft-shen-sm2-ecdsa-02` §5.1.3 step A3): `320` bits (40 bytes) -- derived directly +/// secret `k` (GB/T 32918.2-2016 §6.1 step A3): `320` bits (40 bytes) -- derived directly /// from FIPS 186-5 Appendix A.4.1's own bias-bound check for SM2's `n` (see [`crate::extra_bits`]'s /// docs), not copied from any other curve's table entry. pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; @@ -40,8 +40,9 @@ pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; /// /// # Why the public key is carried alongside `dA` /// -/// Unlike ECDSA, *every* SM2 signing operation needs the signer's own public key: §5.1.2's `ZA = -/// SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)` mixes `PA`'s affine coordinates into the +/// Unlike ECDSA, *every* SM2 signing operation needs the signer's own public key: GB/T +/// 32918.2-2016 §5.5's `ZA = SM3(ENTLA || IDA || a || b || xG || yG || xA || yA)` mixes `PA`'s +/// affine coordinates into the /// message hash, so `sign` cannot proceed without them. Deriving `PA` on each call means a second /// fixed-base scalar multiplication per signature -- as expensive as the `[k]G` the signature /// actually needs, i.e. double the cost of signing, and measurably so: signing used to cost about @@ -57,7 +58,7 @@ pub(crate) const EXTRA_BITS_DRBG_OUTPUT_LEN: usize = 40; /// /// # Why `(1 + dA)^-1` is carried too /// -/// `draft-shen-sm2-ecdsa-02` §5.1.3 step A6 divides by `1 + dA`, and that inverse depends on the +/// GB/T 32918.2-2016 §6.1 step A6 divides by `1 + dA`, and that inverse depends on the /// key alone, not on the message or `k`. Recomputing it per signature was a constant-time /// Fermat inversion each time -- roughly the same cost as `to_affine`'s field inversion, about a /// tenth of a signature on P-256-class hardware -- for a value that never changes. It is computed @@ -203,8 +204,9 @@ pub fn keygen() -> Result<(SM2PublicKey, SM2PrivateKey), SignatureError> { } /// As [`keygen`], but sources the DRBG output from the caller-provided RNG, which must offer -/// a security strength of at least 128 bits: the draft leaves this unstated (see the module -/// docs on why key generation borrows FIPS 186-5's method wholesale), so this follows that +/// a security strength of at least 128 bits: GB/T 32918.2-2016 §5.4.3 asks only for an approved +/// random number generator and the draft says no more (see the module docs on why key generation +/// borrows FIPS 186-5's method wholesale), so this follows that /// method's own Appendix A.2.1 step 3, requiring a DRBG strength "not less than" the one SP /// 800-57 Part 1 Rev. 5's Table 2 associates with SM2's 256-bit curve order (`f = 256-383` in /// that table's ECC column, giving 128-bit security). diff --git a/crypto/sm2/src/lib.rs b/crypto/sm2/src/lib.rs index f5f2fef44..ba454a765 100644 --- a/crypto/sm2/src/lib.rs +++ b/crypto/sm2/src/lib.rs @@ -1,9 +1,9 @@ //! SM2: the Chinese national-standard elliptic-curve Digital Signature Algorithm (GB/T -//! 32918.2-2016, mirrored by the IETF informational draft `draft-shen-sm2-ecdsa-02`). This crate -//! implements only the Digital Signature Algorithm ([`sm2`]'s §5) -- key generation ([`keys`]) and -//! signing/verification ([`sm2::SM2`]) -- built on [`bouncycastle_ec::sm2`]'s field/scalar/point -//! arithmetic, SEC 1 encoding, and comb/wNAF multipliers, and [`bouncycastle_sm3`] for the hash the -//! draft mandates throughout. +//! 32918.2-2016, mirrored by the IETF informational draft `draft-shen-sm2-ecdsa-02`, whose §5 +//! is the same algorithm). This crate implements only the Digital Signature Algorithm -- key +//! generation ([`keys`]) and signing/verification ([`sm2::SM2`]) -- built on +//! [`bouncycastle_ec::sm2`]'s field/scalar/point arithmetic, SEC 1 encoding, and comb/wNAF +//! multipliers, and [`bouncycastle_sm3`] for the hash (see [`sm2`]'s docs on why only SM3). //! //! # Scope //! @@ -15,8 +15,9 @@ //! //! # `ctx` carries the signer's identity //! -//! Every SM2 signing/verification operation needs the signer's identity octet string `IDA` (the -//! draft's §5.1.2/§5.2.2 step 1). This crate repurposes [`bouncycastle_core::traits::Signer`]'s +//! Every SM2 signing/verification operation needs the signer's identity octet string `IDA`, which +//! `ZA` binds into the hash (GB/T 32918.2-2016 §5.5; the draft's §5.1.4.4). This crate +//! repurposes [`bouncycastle_core::traits::Signer`]'s //! `ctx` parameter to carry it -- see [`sm2`]'s module docs for the full reasoning and the //! resulting requirement that `ctx` must be `Some`. //! @@ -119,7 +120,7 @@ //! - **`k` must never repeat across two signatures under the same key with different messages.** As //! with ECDSA, a repeated `k` leaks `dA` directly from two signatures' `(r, s)` pairs. Unlike //! `bouncycastle-ecdsa`'s curves, there is no deterministic (RFC 6979-style) default here: the -//! draft only specifies randomised `k` generation (§5.1.3 step A3), so +//! standard only specifies randomised `k` generation (GB/T 32918.2-2016 §6.1 step A3), so //! [`bouncycastle_core::traits::Signer::sign`] sources `k` from the library's default OS-backed //! RNG on every call -- see [`sm2`]'s module docs. diff --git a/crypto/sm2/src/sm2.rs b/crypto/sm2/src/sm2.rs index 571cbbc5d..08e795e7a 100644 --- a/crypto/sm2/src/sm2.rs +++ b/crypto/sm2/src/sm2.rs @@ -1,14 +1,16 @@ -//! `SM2`: `draft-shen-sm2-ecdsa-02` §5.1 (signature generation) and §5.2 (verification), the actual -//! SM2 Digital Signature Algorithm -- distinct from ECDSA, despite both being built on Weierstrass -//! elliptic-curve arithmetic: SM2's signing equation `s = (1+dA)^-1 * (k - r*dA) mod n` and its -//! identity-binding `ZA` digest (see [`crate::za`]) have no ECDSA equivalent. Fixed to SM3 (the -//! draft's own hash, §5's worked example and every citation of "the cryptographic hash function" -//! throughout use SM3; `draft-shen-sm2-ecdsa-02` does not sanction substituting another hash). +//! `SM2`: GB/T 32918.2-2016 §6.1 (signature generation) and §7.1 (verification), the draft's §5.2 +//! and §5.3 -- the actual SM2 Digital Signature Algorithm, distinct from ECDSA despite both being +//! built on Weierstrass elliptic-curve arithmetic: SM2's signing equation `s = (1+dA)^-1 * (k - +//! r*dA) mod n` and its identity-binding `ZA` digest (see [`crate::za`]) have no ECDSA +//! equivalent. Fixed to SM3: GB/T 32918.2-2016 §5.4.2 (the draft's §5.1.4.2) admits any approved +//! hash "such as SM3", but SM3 is what the standard's own Appendix A example, the draft's, and +//! every deployed implementation use, so no other hash is offered. //! //! # `ctx` carries the signer's identity `IDA` //! -//! Every operation here (§5.1 step 1 / §5.2 step 1) needs `IDA`, the signer's identity octet -//! string, to compute `ZA`. [`bouncycastle_core::traits::Signer::sign`]'s own documentation sanctions using +//! Every operation here (`ZA` is prepended to the message in §6.1 step A1 / §7.1 step B3) needs +//! `IDA`, the signer's identity octet string (§5.5), to compute `ZA`. +//! [`bouncycastle_core::traits::Signer::sign`]'s own documentation sanctions using //! `ctx` "in a non-standard way" when a primitive doesn't natively support a context-binding value //! the way the trait envisions: this crate treats `ctx: Option<&[u8]>` as literally `IDA`, and //! returns [`SignatureError::GenericError`] if `ctx` is `None` -- there is no default identity in @@ -17,8 +19,9 @@ //! //! # Randomness, not determinism //! -//! The draft's §5.1.3 step A3 says only to "pick a random number `k`... using a random number -//! generator"; there is no SM2 analogue of RFC 6979 in this draft, so (unlike every curve in +//! GB/T 32918.2-2016 §6.1 step A3 says only to produce `k` in `[1, n-1]` with a random number +//! generator (the draft's §5.2.1: "pick a random number `k`... using a random number +//! generator"); there is no SM2 analogue of RFC 6979 in either, so (unlike every curve in //! `bouncycastle-ecdsa`) there is no deterministic default here. [`bouncycastle_core::traits::Signer`]'s //! own docs say its trait is "assumed to source all its randomness from bouncycastle's default //! os-backed RNG", so [`Signer::sign`]/[`Signer::do_sign_final`] do exactly that (via [`bouncycastle_rng::DefaultRNG`]); @@ -32,16 +35,16 @@ //! draw a new `k`. Matching every curve's `sign_with_k` in `bouncycastle-ecdsa` (which returns `Err` //! with a "regenerate k and retry" message rather than looping internally), this crate does the //! same rather than retrying in a loop -- a deliberate consistency choice with this workspace's -//! established pattern, not mandated by the draft itself. +//! established pattern, not mandated by the standard itself. //! //! # No DER encoding //! -//! The draft has no ASN.1/DER encoding for `(r, s)`; [`SIG_LEN`] is raw `r || s`, 64 bytes, each a +//! GB/T 32918.2-2016 has no ASN.1/DER encoding for `(r, s)`; [`SIG_LEN`] is raw `r || s`, 64 bytes, each a //! 32-byte big-endian integer -- the same convention `bouncycastle-ecdsa` uses as its own default. //! //! # Extracting `x1` from a secret-derived `[k]G` //! -//! `[k]G` (§5.1.3 step A4) is Jacobian-coordinate output of a *secret* scalar multiplication, so +//! `[k]G` (§6.1 step A4) is Jacobian-coordinate output of a *secret* scalar multiplication, so //! converting it to affine cannot use [`Sm2JacobianPoint::to_affine`] -- that function's own docs //! say it branches on infinity and is "not for use on a secret intermediate value". //! [`x_affine_of_signing_point`] instead always computes `X * Z^-2`, branch-free; see its docs for @@ -73,14 +76,14 @@ pub const SIG_LEN: usize = 64; /// `bouncycastle-ecdsa`'s curves, the hash absorbed here already has `ZA` prepended by /// [`Signer::do_sign_init`]/[`SignatureVerifier::do_verify_init`], so a later [`Signer::do_sign_final`]/ /// [`SignatureVerifier::do_verify_final`] need only finish hashing `M` to get `e = SM3(ZA || M)` -/// (`draft-shen-sm2-ecdsa-02` §5.1.3 step 1 / §5.2.3 step 1). +/// (GB/T 32918.2-2016 §6.1 steps A1-A2 / §7.1 steps B3-B4). pub struct SM2 { hash: SM3, sk: Option, pk: Option, } -/// `e` from the (already `ZA`-prefixed) running hash, per §5.1.3 step 1 / §5.2.3 step 1. +/// `e` from the (already `ZA`-prefixed) running hash, per §6.1 step A2 / §7.1 step B4. fn e_from_hash(hash: SM3) -> Sm2ScalarField { let mut e_bytes = [0u8; 32]; hash.do_final_out(&mut e_bytes); @@ -102,7 +105,7 @@ fn x_affine_of_signing_point(point: &Sm2JacobianPoint) -> Sm2FieldElement { point.x.mul(&z_inv.mul(&z_inv)) } -/// §5.1.3 steps A4-A7, given `k` already generated (step A3) by one of `SM2`'s randomised methods. +/// §6.1 steps A4-A7, given `k` already generated (step A3) by one of `SM2`'s randomised methods. /// Returns `Err` for step A5's `r = 0`/`r + k = n` or step A6's `s = 0` -- see the module docs on /// this crate's retry convention. fn sign_with_k( @@ -157,7 +160,7 @@ fn sign_with_k( Ok(sig) // step A7 } -/// §5.1.3 step A5's `r = 0` or `r + k = n` check, named so it has something to unit test directly: +/// §6.1 step A5's `r = 0` or `r + k = n` check, named so it has something to unit test directly: /// reaching either branch through `sign_with_k` needs an `r`/`r + k` value that is astronomically /// unlikely for any real `k`/`e` (see `sign_with_k`'s call site). fn r_is_zero_or_r_plus_k_is_zero(r: &Sm2ScalarField, r_plus_k: &Sm2ScalarField) -> bool { @@ -165,7 +168,7 @@ fn r_is_zero_or_r_plus_k_is_zero(r: &Sm2ScalarField, r_plus_k: &Sm2ScalarField) } impl SM2 { - /// §5.1 with `k` drawn from the caller-provided RNG -- an additional capability alongside + /// §6.1 with `k` drawn from the caller-provided RNG -- an additional capability alongside /// [`Signer::sign`] (which sources `k` from the library's default RNG instead). See the module /// docs for why there is no deterministic alternative to offer here, unlike /// `bouncycastle-ecdsa`'s curves. @@ -219,8 +222,8 @@ impl Signer for SM2 { fn do_sign_init(sk: &SM2PrivateKey, ctx: Option<&[u8]>) -> Result { let id = ctx.ok_or(SignatureError::GenericError( - "SM2 requires ctx to carry the signer's identity IDA (draft-shen-sm2-ecdsa-02 S5.1.2); \ - see bouncycastle-sm2's crate docs", + "SM2 requires ctx to carry the signer's identity IDA (GB/T 32918.2-2016 S5.5); see \ + bouncycastle-sm2's crate docs", ))?; // PA is carried by the key, not recomputed: see SM2PrivateKey's docs. let (x, y) = sk.derive_pk().affine(); @@ -272,8 +275,8 @@ impl SignatureVerifier for SM2 { fn do_verify_init(pk: &SM2PublicKey, ctx: Option<&[u8]>) -> Result { let id = ctx.ok_or(SignatureError::GenericError( - "SM2 requires ctx to carry the signer's identity IDA (draft-shen-sm2-ecdsa-02 S5.2.2); \ - see bouncycastle-sm2's crate docs", + "SM2 requires ctx to carry the signer's identity IDA (GB/T 32918.2-2016 S5.5); see \ + bouncycastle-sm2's crate docs", ))?; let (x, y) = pk.affine(); let za = za::compute(id, &x, &y)?; @@ -302,7 +305,7 @@ impl SignatureVerifier for SM2 { return Err(SignatureError::SignatureVerificationFailed); } - // step B1: r', s' in [1, n-1], rejected (not reduced) if out of range. + // steps B1-B2: r', s' in [1, n-1], rejected (not reduced) if out of range. let r_limbs = sm2_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); let s_limbs = sm2_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); if either_out_of_range(&r_limbs, &s_limbs) { @@ -311,9 +314,9 @@ impl SignatureVerifier for SM2 { let r = Sm2ScalarField::from_limbs(r_limbs); let s = Sm2ScalarField::from_limbs(s_limbs); - let e = e_from_hash(self.hash); // steps B2-B3 + let e = e_from_hash(self.hash); // steps B3-B4 - let t = r.add(&s); // step B4 + let t = r.add(&s); // step B5 if t == Sm2ScalarField::ZERO { return Err(SignatureError::SignatureVerificationFailed); } @@ -321,20 +324,20 @@ impl SignatureVerifier for SM2 { let q = Sm2JacobianPoint::from_affine(pk.x, pk.y); let s_pub = Sm2PublicScalar::from_limbs(s.to_limbs()); let t_pub = Sm2PublicScalar::from_limbs(t.to_limbs()); - let point = shamir_multiply(&s_pub, &t_pub, &q); // step B5 + let point = shamir_multiply(&s_pub, &t_pub, &q); // step B6 if point.is_infinity().to_bool() { return Err(SignatureError::SignatureVerificationFailed); } let (x1, _) = point.to_affine().expect("just checked point is not the point at infinity"); - let r1 = e.add(&Sm2ScalarField::from_limbs(x1.to_limbs())); // step B6 + let r1 = e.add(&Sm2ScalarField::from_limbs(x1.to_limbs())); // step B7's R if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step B7 } } -/// `true` iff `limbs`, read as a big-endian-decoded integer, is in `[1, n-1]` -- §5.2.2 step B1's -/// range check on `r'` and `s'`. On public signature data, so plain (not branch-free). +/// `true` iff `limbs`, read as a big-endian-decoded integer, is in `[1, n-1]` -- §7.1 steps +/// B1-B2's range check on `r'` and `s'`. On public signature data, so plain (not branch-free). fn in_range(limbs: &[u64; 4]) -> bool { if *limbs == [0, 0, 0, 0] { return false; @@ -343,7 +346,7 @@ fn in_range(limbs: &[u64; 4]) -> bool { borrow == 1 } -/// `true` iff either `r'` or `s'` is outside `[1, n-1]` -- §5.2.2 step B1's range check, named so +/// `true` iff either `r'` or `s'` is outside `[1, n-1]` -- §7.1 steps B1-B2's range check, named so /// the OR-of-two-checks combination has something to unit test directly (mirroring /// `bouncycastle_ec::sm2_sec1::either_out_of_range`'s own naming and reasoning). fn either_out_of_range(r_limbs: &[u64; 4], s_limbs: &[u64; 4]) -> bool { diff --git a/crypto/sm2/src/za.rs b/crypto/sm2/src/za.rs index 8f32da83c..a9931a9b6 100644 --- a/crypto/sm2/src/za.rs +++ b/crypto/sm2/src/za.rs @@ -1,7 +1,8 @@ -//! `ZA` computation: `draft-shen-sm2-ecdsa-02` §5.1.2, `ZA = SM3(ENTLA || IDA || a || b || xG || yG -//! || xA || yA)` -- the identity-and-domain-parameter digest that binds an SM2 signature to a -//! specific signer's identity `IDA` and public key `(xA, yA)`, distinct from §5.1.3's `e = SM3(ZA || -//! M)` hash of the actual message. `ENTLA` is `IDA`'s bit length as a 2-byte big-endian integer; `a`, +//! `ZA` computation: GB/T 32918.2-2016 §5.5 (the draft's §5.1.4.4), `ZA = SM3(ENTLA || IDA || a || +//! b || xG || yG || xA || yA)` -- the identity-and-domain-parameter digest that binds an SM2 +//! signature to a specific signer's identity `IDA` and public key `(xA, yA)`, distinct from §6.1 +//! steps A1-A2's `e = SM3(ZA || M)` hash of the actual message. `ENTLA` is `IDA`'s bit length as a +//! 2-byte big-endian integer; `a`, //! `b`, `xG`, `yG`, `xA`, `yA` are each encoded as fixed-width 32-byte big-endian octets, matching //! this curve's field size. //! @@ -33,7 +34,7 @@ fn a_limbs() -> [u64; 4] { } /// Computes `ZA` for the given identity `id` and public key affine coordinates -/// (`draft-shen-sm2-ecdsa-02` §5.1.2). Returns [`SignatureError::GenericError`] if `id`'s bit length +/// (GB/T 32918.2-2016 §5.5). Returns [`SignatureError::GenericError`] if `id`'s bit length /// doesn't fit in 16 bits, the limit `ENTLA`'s 2-byte encoding imposes. pub fn compute( id: &[u8], diff --git a/crypto/sm2/tests/sm2_kat_tests.rs b/crypto/sm2/tests/sm2_kat_tests.rs index 8f8e6ab76..b56461944 100644 --- a/crypto/sm2/tests/sm2_kat_tests.rs +++ b/crypto/sm2/tests/sm2_kat_tests.rs @@ -21,7 +21,8 @@ //! `SM2SignerTest.doSignerTestFpStandardSM3` case (`core/src/test/java/org/bouncycastle/crypto/ //! test/SM2SignerTest.java`), on the same `sm2p256v1` curve with SM3. A separate implementation //! produced it, so it cross-checks this crate's domain constants, `ZA`, `e = SM3(ZA || M)` and the -//! §5.2.3 verification equation against something external -- which the Python-generated vectors +//! GB/T 32918.2-2016 §7.1 verification equation against something external -- which the +//! Python-generated vectors //! above, sharing this crate's own `ZA` and signing logic, cannot do. It was additionally //! reproduced from scratch (in Python, not checked in: affine group law, `hashlib.new("sm3")`, //! and the draft's published domain parameters) before being written here, so it rests on two @@ -120,7 +121,7 @@ fn known_answer_vectors_reject_wrong_id() { #[test] fn known_answer_za_matches_independent_python_computation() { // Cross-checks crate::za::compute directly, not just the end-to-end verify result -- pins the - // intermediate ZA value itself (draft-shen-sm2-ecdsa-02 S5.1.2), independent of e/r/s. + // intermediate ZA value itself (GB/T 32918.2-2016 S5.5), independent of e/r/s. let expected: [(&str, &str, &[u8], &str); 3] = [ (V1.qx, V1.qy, V1.id, "bab69c9204aadd3d49ff244e8fdffb204e1d4249e36855f06a912abd81ddf167"), ( diff --git a/crypto/sm2/tests/sm2_tests.rs b/crypto/sm2/tests/sm2_tests.rs index 785b54df8..9977a9f25 100644 --- a/crypto/sm2/tests/sm2_tests.rs +++ b/crypto/sm2/tests/sm2_tests.rs @@ -42,7 +42,7 @@ fn sign_then_verify_round_trips() { #[test] fn sign_is_non_deterministic() { - // draft-shen-sm2-ecdsa-02 S5.1.3 step A3: k is drawn fresh from randomness each call, unlike + // GB/T 32918.2-2016 S6.1 step A3: k is drawn fresh from randomness each call, unlike // bouncycastle-ecdsa's RFC-6979-deterministic default. let (_, sk) = keygen().unwrap(); let msg = b"same message, different k"; @@ -422,7 +422,7 @@ fn keygen_from_rng_rejects_an_rng_below_the_required_strength() { )); } -/// `draft-shen-sm2-ecdsa-02` §5.2.2 step B1: `r'` and `s'` must each be in `[1, n-1]`, rejected +/// GB/T 32918.2-2016 §7.1 steps B1-B2: `r'` and `s'` must each be in `[1, n-1]`, rejected /// rather than reduced. Each half of a valid signature is replaced in turn by `0`, by `n`, and by /// the all-ones pattern; the untouched half stays valid, so only the range check can be what /// rejects the result. @@ -446,8 +446,8 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// `draft-shen-sm2-ecdsa-02` §5.2.2 step B4: `t = (r' + s') mod n`, and `t = 0` fails verification -/// outright. `s' = n - r'` is in `[1, n-1]` (so it passes step B1) and is the one value that makes +/// GB/T 32918.2-2016 §7.1 step B5: `t = (r' + s') mod n`, and `t = 0` fails verification +/// outright. `s' = n - r'` is in `[1, n-1]` (so it passes step B2) and is the one value that makes /// `t` zero -- a signature that would otherwise reach the point multiplication with `[0]PA`. #[test] fn verify_rejects_s_equal_to_n_minus_r() { @@ -467,7 +467,8 @@ fn verify_rejects_s_equal_to_n_minus_r() { } } -/// `ZA` encodes `IDA`'s bit length as the two-byte `ENTLA` (§5.1.2), so an identity of 8191 bytes +/// `ZA` encodes `IDA`'s bit length as the two-byte `ENTLA` (GB/T 32918.2-2016 §5.5), so an +/// identity of 8191 bytes /// (65528 bits) is the longest that fits and 8192 bytes (65536 bits) is the shortest that does /// not. The limit applies on both sides, since verification computes `ZA` too. #[test] From eb0bca4ab6bc3cbe06ac1746cfe766128ea26de5 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 25 Oct 2026 17:47:06 +0000 Subject: [PATCH 10/16] sm2: name the step A5 and A6 retry branches in the crate's timing contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - the crate docs said every operation on dA and k is branch-free; sign_with_k has two branches - step A5 (r = 0 or r + k = n) and step A6 (s = 0) each branch, then discard the k they branched on - the A5 comment called the check a branch on the public output r; r + k = n is a function of k - the Security Considerations bullet now names both checks as the exceptions, with the probability - the heading becomes the `# 🚨 Security Considerations 🚨` form QUALITY_AND_STYLE.md requires - docs and comments only; no code change ====================================================================== [For agents] The same move as the ecdsa crate's commit that named the rfc6979 loop in its timing contract. sign_with_k's comment above the step A5 check said the comparison was "the draft's own mandated success/failure branch on the just-computed public output r". Only r = 0 is a condition on the public output; (r + k) mod n = 0, which for k in [1, n-1] and r in [0, n-1] means r + k = n, is a function of k. The comment now says what is true: both disjuncts are compared in constant time (Sm2ScalarField's PartialEq compares every limb through Condition masks), the if is one of two data-dependent branches on the signing path, the other being step A6's s = 0, each disjunct is taken with probability 1/(n-1) per signature, and a taken branch discards that k, so neither reveals anything about a k that is used. The crate docs' first Security Considerations bullet claimed "every operation performed on it or on k ... is branch-free". It now says everything done with dA and the used k is branch-free and indexes no memory by their value, and names GB/T 32918.2-2016 S6.1's two retry checks as the exceptions, with their probability (about 2^-255 each, since each is one value out of n) and why they are harmless. The heading is changed to the exact form QUALITY_AND_STYLE.md's Docs section requires; thirteen crates in the workspace already use it, and the ec and ecdsa crates, which this branch does not touch for that purpose, still use the plain heading. Docs and comments only; no code change. cargo test -p bouncycastle-sm2 passes; cargo doc shows the same two pre-existing private-item link warnings and nothing new. Assisted-by: Claude:claude-fable-5-1 --- crypto/sm2/src/lib.rs | 16 ++++++++++------ crypto/sm2/src/sm2.rs | 9 ++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crypto/sm2/src/lib.rs b/crypto/sm2/src/lib.rs index ba454a765..936a6a93d 100644 --- a/crypto/sm2/src/lib.rs +++ b/crypto/sm2/src/lib.rs @@ -111,12 +111,16 @@ //! constant-time inversion for a value that never changes. See [`keys::SM2PrivateKey`]'s own //! docs. //! -//! # Security Considerations -//! -//! - **The private key `dA` and the per-message secret `k` are never handled in variable time.** -//! `dA` lives in [`bouncycastle_ec::sm2_scalar::Sm2Scalar`] (a [`bouncycastle_utils::secret::Secret`]); -//! every operation performed on it or on `k` -- inversion, multiplication, the fixed-base scalar -//! multiplication `[k]G` -- is branch-free and does not index memory by their value. +//! # 🚨 Security Considerations 🚨 +//! +//! - **Everything done with the private key `dA` and the used per-message secret `k` is +//! branch-free and does not index memory by their value**: the inversion of `1 + dA`, +//! multiplication, the fixed-base scalar multiplication `[k]G`. `dA` lives in +//! [`bouncycastle_ec::sm2_scalar::Sm2Scalar`] (a [`bouncycastle_utils::secret::Secret`]). The +//! exceptions are GB/T 32918.2-2016 §6.1's two retry checks, step A5's `r = 0` or `r + k = n` +//! and step A6's `s = 0`: each is a branch, taken with probability about `2^-255`, and a taken +//! branch discards that `k` (the signer returns an error and the caller draws a fresh one), so +//! neither reveals anything about a `k` that is used. //! - **`k` must never repeat across two signatures under the same key with different messages.** As //! with ECDSA, a repeated `k` leaks `dA` directly from two signatures' `(r, s)` pairs. Unlike //! `bouncycastle-ecdsa`'s curves, there is no deterministic (RFC 6979-style) default here: the diff --git a/crypto/sm2/src/sm2.rs b/crypto/sm2/src/sm2.rs index 08e795e7a..c9fee4f7d 100644 --- a/crypto/sm2/src/sm2.rs +++ b/crypto/sm2/src/sm2.rs @@ -123,9 +123,12 @@ fn sign_with_k( // `r + k` is as sensitive as `k`: `r` is published in the signature, so anyone holding both // recovers `k` by subtraction. It is scrubbed on both ways out of the check below. let mut r_plus_k = r.add(&k_field); - // Astronomically unlikely for any real k/e (r, r+k range over ~2^256 values each); this - // comparison is the draft's own mandated success/failure branch on the just-computed public - // output r, not a constant-time violation on a secret intermediate. + // Step A5's check. Both disjuncts are compared in constant time (`PartialEq` on the field + // type is), and the branch below is one of the two data-dependent branches on the signing + // path (step A6's `s = 0` is the other). `r = 0` is a condition on the public output; + // `(r + k) mod n = 0`, i.e. `r + k = n` for `k` in `[1, n-1]` and `r` in `[0, n-1]`, is a + // function of `k`, but a taken branch discards that `k`, so it reveals nothing about a `k` + // that is used. Each disjunct is taken with probability `1/(n-1)` per signature. let r_out_of_range = r_is_zero_or_r_plus_k_is_zero(&r, &r_plus_k); r_plus_k.zeroize(); if r_out_of_range { From 73e42eedd4d67adba74af9fa8d29f82233347312 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Tue, 27 Oct 2026 13:49:36 +0000 Subject: [PATCH 11/16] utils, ecdsa: Condition::to_bool passes the optimisation barrier, and to_bool_opaque goes [For humans] - to_bool was `self.0 != 0`: on a mask built from secret values LLVM may branch on each value - to_bool now tests the mask after the volatile barrier, on all four widths; to_bool_opaque goes - the seven rfc6979 candidate_in_range sites go back to to_bool; its 99 other callers change nothing - to_bool is no longer const; nothing in the workspace called it in const context - release asm: every generate_k keeps one conditional jump (two on P-521), as with to_bool_opaque - no ecdsa function changes its conditional-jump count; sign_with_k gains 11 instructions - rejection-loop shape, same outcome, different failing check: 9 ticks apart before, 0.00 after ====================================================================== [For agents] Condition's consumers that do masked arithmetic (select, mov, swap, conditional_select) pass the mask through barrier(), a volatile store and load, before using it. to_bool did not: it was a const fn returning `self.0 != 0`, and when the mask was built from arithmetic the compiler can see (an OR-chain over limbs, a subtraction's borrow) LLVM may fold the mask away and emit one conditional branch per input comparison. The outcome of the decision is the same, but the timing then reveals which condition decided it. PR 182's earlier commit added to_bool_opaque, `self.barrier().0 != 0`, as an opt-in second method and left to_bool alone pending a census of its callers. The review that followed (its findings are summarised here, since its documents are not in the repo) read LLVM 22's SelectionDAGBuilder::visitBr: it splits a branch on an and/or of conditions whenever jumps are cheap, which on x86 means whenever the second condition costs more than two cycles of latency, and on AArch64, ARM, RISC-V, WebAssembly, AVR and MSP430 whenever it can. So every to_bool caller whose mask came from secret values was exposed, and an opt-in method only protects the callers that know to opt in. This commit makes the barriered form the only one. The census: 99 to_bool() calls across ec, ecdsa, sm2 and utils before this change. 64 are in the eight *_point.rs files (add_vartime and add_vartime_affine, on public data, plus to_affine's infinity check on secret-dependent points), 16 are the PartialEq limb comparisons on the field and scalar types, which are documented as constant time, 8 are comb-table asserts, 7 are the r = 0 and s = 0 checks in sign_with_k, 2 are ct_eq_bytes and ct_eq_zero_bytes in ct.rs, and one each are in inverse_vartime and the SM2 verifier. None is in const context: `cargo check --workspace --all-targets` passes with the four to_bool no longer const, and the plans directory's prototype text that shows a `const fn to_bool` is a historical document, not code. ct_eq_bytes and ct_eq_zero_bytes now pass a second barrier after acc_is_zero's own; harmless, and left as they are. Per CLAUDE.md the four `impl Condition` blocks are hand-written copies and a change to one width is applied to every width, so to_bool is changed and to_bool_opaque deleted in all four. The rustdoc states the contract only (the test is made on the whole mask, best-effort, and a condition whose value must stay secret should not be converted at all); the mechanism and its pointer to the "Optimisation barrier" section comment stay in a `//` comment, as the review of to_bool_opaque's docs settled. That section comment gains one sentence naming the branch-splitting form of the risk, and barrier()'s doc names to_bool as a consumer. The four to_bool_opaque tests in ct_tests.rs are deleted and no test is added: to_bool is already checked by assert_canonical (21, 21, 39 and 39 calls in the u64, u32, i64 and i32 modules) and the operator tests, and whether the barrier is effective is not visible from a unit test; it is checked in the assembly, as for select and swap. cargo mutants -p bouncycastle-utils --re to_bool (with examine_globs dropped from the config so the filter applies): 12 mutants, 12 caught. Evidence, all on x86_64 release builds with codegen-units=1. Assembly of bouncycastle_ecdsa before (eb0bca4, to_bool_opaque) and after: every generate_k has exactly one conditional jump, the loop decision on the barrier-loaded mask (`jne`), and P-521 has two, the second being its public 66-byte copy-loop counter; both builds identical. A diff of every function in the crate, with labels and hashes normalised, shows no function changing its conditional-jump count; the only bodies that change length are the seven sign_with_k (+10 or +11 instructions, the r = 0 and s = 0 checks now loading the mask through the barrier) and the seven do_verify_final (+12). A scratch rejection loop in the shape of generate_k, built against the previous utils and this one: two conditional jumps (`jne` on the all-zero test, `je` on the borrow) with the old to_bool, one with the new; this is the finding that started PR 182's candidate_in_range work, reproduced on plain to_bool. A scratch OAEP-style check (Y zero, lHash' equal, 0x01 separator present) compiled to one jump with both versions, so the three-jump form the review observed on that shape depends on how the mask is written and was not reproduced here. Timing on the rejection loop, 1,000,000 interleaved samples per class with the order shuffled each round, rdtscp, top decile trimmed, Welch's t: with the old to_bool a zero candidate and an all-ones candidate (both rejected once, then accept the same value) sit 9.14 ticks apart, |t| = 492, while the control pair (two different candidates that both fail the below-n test) is 0.01 ticks apart; with this change the same pair is 0.00 ticks apart, |t| = 0.1, and the control is 0.00. The OAEP shape shows no difference between failure causes in either build, as its single jump predicts. cargo test --workspace: all 251 test binaries pass with bc-test-data and wycheproof present (no "not found" skips); cargo test --workspace --all-targets --no-run compiles the benches and mem_usage_benches; cargo fmt --all --check passes; cargo doc for utils and ecdsa reports the same 15 pre-existing warnings as before, nothing new. quality_stats.sh on crypto/utils and crypto/ecdsa is unchanged apart from one more docstring line in ecdsa. No to_bool_vartime escape hatch is added: nothing needs an unbarriered or const conversion, and the `_vartime` name is available if one ever does. Criterion benches for ecdsa (keygen, sign and verify on all seven curves), before against after on the shared 2-vCPU VM: the 21 differences run from -9 % to +3 % in both directions, and the largest are speedups (P-384 sign -9 %, P-521 verify -8 %) where this change can only add instructions, so the run resolves nothing below the machine's drift between the two passes; no slowdown is attributable to the change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ecdsa/src/rfc6979.rs | 7 +-- crypto/ecdsa/src/rfc6979_bp256r1.rs | 2 +- crypto/ecdsa/src/rfc6979_bp384r1.rs | 2 +- crypto/ecdsa/src/rfc6979_bp512r1.rs | 2 +- crypto/ecdsa/src/rfc6979_p256k1.rs | 2 +- crypto/ecdsa/src/rfc6979_p384.rs | 2 +- crypto/ecdsa/src/rfc6979_p521.rs | 2 +- crypto/utils/src/ct.rs | 74 ++++++++++++----------------- crypto/utils/tests/ct_tests.rs | 52 -------------------- 9 files changed, 40 insertions(+), 105 deletions(-) diff --git a/crypto/ecdsa/src/rfc6979.rs b/crypto/ecdsa/src/rfc6979.rs index 45be57d91..be57f774e 100644 --- a/crypto/ecdsa/src/rfc6979.rs +++ b/crypto/ecdsa/src/rfc6979.rs @@ -69,14 +69,15 @@ fn bits2octets(h1: &[u8; HLEN]) -> [u8; HLEN] { /// step h). /// /// The comparison is on the candidate's own bits, so it is done as masks and converted with -/// `Condition::to_bool_opaque`: the one branch is `generate_k`'s loop decision, which the crate -/// docs list as a timing exception. The other curves' copies of this function do the same. +/// `Condition::to_bool`, which tests the combined mask after the optimisation barrier: the one +/// branch is `generate_k`'s loop decision, which the crate docs list as a timing exception. The +/// other curves' copies of this function do the same. fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let limbs = p256_sec1::limbs_from_be_bytes(candidate); let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index e05084e0b..472bb78e1 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -71,7 +71,7 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 85a041af9..6f1c8180d 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -69,7 +69,7 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 0f19139c3..412a3c053 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -69,7 +69,7 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p256k1.rs b/crypto/ecdsa/src/rfc6979_p256k1.rs index 2b34975a0..2ac234560 100644 --- a/crypto/ecdsa/src/rfc6979_p256k1.rs +++ b/crypto/ecdsa/src/rfc6979_p256k1.rs @@ -59,7 +59,7 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p384.rs b/crypto/ecdsa/src/rfc6979_p384.rs index 0fee0acc5..fa635fa58 100644 --- a/crypto/ecdsa/src/rfc6979_p384.rs +++ b/crypto/ecdsa/src/rfc6979_p384.rs @@ -51,7 +51,7 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/ecdsa/src/rfc6979_p521.rs b/crypto/ecdsa/src/rfc6979_p521.rs index f71dbe76a..240ab667d 100644 --- a/crypto/ecdsa/src/rfc6979_p521.rs +++ b/crypto/ecdsa/src/rfc6979_p521.rs @@ -106,7 +106,7 @@ fn candidate_in_range(candidate: &[u8; RLEN]) -> bool { let is_nonzero = !nat::is_zero(&limbs); let (_, borrow) = nat::sub(&limbs, &N_LIMBS); let is_below_n = Condition::::from_lsb(borrow); - (is_nonzero & is_below_n).to_bool_opaque() + (is_nonzero & is_below_n).to_bool() } /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 1c1ca7131..e5f3dccdf 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -38,8 +38,10 @@ where // could otherwise prove to be one of a small number of constants (a `Condition` mask is all-ones // or all-zeros; the accumulator of a comparison loop is zero until the first difference). Given // that knowledge the compiler is free to lower an expression like `(t & m) | (f & !m)` to a branch -// or a conditional move on the secret, or to leave a comparison loop early. To stop that, the value -// is routed through a volatile store and load. +// or a conditional move on the secret, or to leave a comparison loop early. A branch on a mask is +// the same risk in another form: the compiler may replace one test of a combined mask with a test +// per condition that built it. To stop that, the value is routed through a volatile store and +// load. // // This is still a best-effort, not a guarantee. // The language guarantees less than is relied on here. The documentation (as of rust 1.99.0) of @@ -77,8 +79,8 @@ where MaskType: SupportedMaskType, { /// The mask after the optimisation barrier (section comment above), applied at the point of - /// use by every consumer that does masked arithmetic. Not `const`: volatile accesses are not - /// allowed in const context. + /// use by every consumer that does masked arithmetic, and by `to_bool`. Not `const`: volatile + /// accesses are not allowed in const context. #[inline(always)] fn barrier(self) -> Self { Self(value_barrier(self.0)) @@ -221,16 +223,12 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 - } - - /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is - /// made on the whole mask, and the compiler cannot split it into tests on the inputs that - /// built the mask. - pub fn to_bool_opaque(self) -> bool { + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { // The barrier (section comment above) is what stops LLVM from folding the mask away and // branching on its constituents; it is volatile, hence this is not `const`. self.barrier().0 != 0 @@ -369,16 +367,12 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 - } - - /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is - /// made on the whole mask, and the compiler cannot split it into tests on the inputs that - /// built the mask. - pub fn to_bool_opaque(self) -> bool { + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { // The barrier (section comment above) is what stops LLVM from folding the mask away and // branching on its constituents; it is volatile, hence this is not `const`. self.barrier().0 != 0 @@ -469,16 +463,12 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 - } - - /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is - /// made on the whole mask, and the compiler cannot split it into tests on the inputs that - /// built the mask. - pub fn to_bool_opaque(self) -> bool { + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { // The barrier (section comment above) is what stops LLVM from folding the mask away and // branching on its constituents; it is volatile, hence this is not `const`. self.barrier().0 != 0 @@ -558,16 +548,12 @@ impl Condition { let t = (lhs ^ rhs) & self.barrier().0; (lhs ^ t, rhs ^ t) } - /// Convert the mask to a runtime boolean. Only use this at genuine public - /// decision points: branching on the result leaks the condition's value. - pub const fn to_bool(self) -> bool { - self.0 != 0 - } - - /// [`to_bool`](Self::to_bool) for a public decision computed from secret inputs: the test is - /// made on the whole mask, and the compiler cannot split it into tests on the inputs that - /// built the mask. - pub fn to_bool_opaque(self) -> bool { + /// Convert the mask to a runtime boolean, at a decision whose outcome may be public. The test + /// is made on the whole mask, so the compiler is not given the values that built the mask to + /// branch on separately (best-effort, like every constant-time construction in this crate). If + /// the condition's value itself must stay secret, do not convert it: keep the mask and use + /// [`Self::select`]. + pub fn to_bool(self) -> bool { // The barrier (section comment above) is what stops LLVM from folding the mask away and // branching on its constituents; it is volatile, hence this is not `const`. self.barrier().0 != 0 diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index 8ea378e5f..fcda2b02d 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -125,19 +125,6 @@ mod unsigned_u64_tests { assert_canonical(Condition::::FALSE, false); } - /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on - /// both canonical masks and on every mask the constructors build from the boundary values. - #[test] - fn to_bool_opaque() { - assert!(Condition::::TRUE.to_bool_opaque()); - assert!(!Condition::::FALSE.to_bool_opaque()); - for &v in &BOUNDARY { - let cond = Condition::::is_zero(v); - assert_eq!(cond.to_bool_opaque(), cond.to_bool()); - assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); - } - } - #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -287,19 +274,6 @@ mod unsigned_u32_tests { assert_canonical(Condition::::FALSE, false); } - /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on - /// both canonical masks and on every mask the constructors build from the boundary values. - #[test] - fn to_bool_opaque() { - assert!(Condition::::TRUE.to_bool_opaque()); - assert!(!Condition::::FALSE.to_bool_opaque()); - for &v in &BOUNDARY { - let cond = Condition::::is_zero(v); - assert_eq!(cond.to_bool_opaque(), cond.to_bool()); - assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); - } - } - #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -452,19 +426,6 @@ mod signed_i64_tests { assert_canonical(Condition::::FALSE, false); } - /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on - /// both canonical masks and on every mask the constructors build from the boundary values. - #[test] - fn to_bool_opaque() { - assert!(Condition::::TRUE.to_bool_opaque()); - assert!(!Condition::::FALSE.to_bool_opaque()); - for &v in &BOUNDARY { - let cond = Condition::::is_zero(v); - assert_eq!(cond.to_bool_opaque(), cond.to_bool()); - assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); - } - } - #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); @@ -666,19 +627,6 @@ mod signed_i32_tests { assert_canonical(Condition::::FALSE, false); } - /// The barrier is a volatile round trip, so `to_bool_opaque` must agree with `to_bool` on - /// both canonical masks and on every mask the constructors build from the boundary values. - #[test] - fn to_bool_opaque() { - assert!(Condition::::TRUE.to_bool_opaque()); - assert!(!Condition::::FALSE.to_bool_opaque()); - for &v in &BOUNDARY { - let cond = Condition::::is_zero(v); - assert_eq!(cond.to_bool_opaque(), cond.to_bool()); - assert_eq!((!cond).to_bool_opaque(), !cond.to_bool()); - } - } - #[test] fn from_bool() { assert_canonical(Condition::::from_bool_const::(), true); From aa38bcbf9c65d0d8f034c74409d26348ec097f63 Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 04:01:44 +1100 Subject: [PATCH 12/16] ecdsa: split each curve's signer into ECDSA* (random k) and ECDetDSA* (RFC 6979), both reached through Signer::sign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Until now one type per curve (ECDSAP256 and the other six) implemented Signer with the deterministic per-message secret of FIPS 186-5 §6.3.2 / Appendix A.3.3 (RFC 6979), and the randomised method of §6.3.1 / Appendix A.3.1 was only reachable through an inherent sign_randomized that took the caller's RNG -- so the two algorithms could not be chosen through the trait. FIPS 186-5 §6 treats them as two variants with one verifier: Deterministic ECDSA "does not impact the signature verification process". Each ecdsa_ module now has two signer types, both implementing Signer and SignatureVerifier over the same key types: - ECDSA (the existing name) is ECDSA proper: Signer::sign and do_sign_final draw k by Appendix A.3.1 from the library's default RNG (DefaultRNG, instantiated per signature, as bouncycastle-mldsa and bouncycastle-sm2 already do inside their sign), and the inherent sign_rng(sk, msg, ctx, rng) / do_sign_final_rng(self, rng) take the caller's RNG instead -- the `_rng` twins QUALITY_AND_STYLE.md's suffix rules call for, replacing sign_randomized (which also gains the trait's ctx parameter, ignored as everywhere in this crate). - ECDetDSA is Deterministic ECDSA: Signer::sign and do_sign_final derive k by Appendix A.3.3 through the existing rfc6979*::generate_k, and signing draws no randomness. Verification is one private verify_final per module that both types' do_verify_final call, and sign_der / verify_der exist on both. The signing core (sign_with_k, e_from_hash, x_affine_of_signing_point) is unchanged; the new random_k collects the A.3.1 draw-and-reduce that sign_randomized did inline. The seven modules keep their identical shape; ecdsa_p256.rs was written by hand and the other six were derived from it mechanically, then formatted. SM2 is untouched apart from a doc sentence that pointed at sign_randomized. Tests: each curve's conformance suite now runs twice, with TestFrameworkSignature::new(false, ..) over ECDSA* (two signatures of one message must differ) and new(true, ..) over ECDetDSA* (must match), plus a cross-verification test (either type's signature verifies under the other), a DER round trip through either verify_der, and the streaming-misuse errors on the deterministic type. The RFC 6979 vector suites pin ECDetDSA*, the CAVP SigGen suites inject k through sign_rng, and the sign_der-equals-sign test moved onto ECDetDSA*, the only type for which that equality holds. cargo test -p bouncycastle-ecdsa -p bouncycastle-sm2 -p cli: all green, including the crate's doctests. The bc-rust CLI reproduces RFC 6979 A.2.5's P-256 "sample" signature with `ecdsa-p256 sign --deterministic`. CLI: the seven ecdsa-* subcommands take --deterministic, which instantiates the generic ecdsa_cmd over ECDetDSA* instead of ECDSA*; verification accepts either flag setting. Benches: the criterion series "sign_randomized" becomes "sign_rng" and gains a sibling "sign_deterministic" (ECDetDSA*::sign); the existing "sign" series now measures ECDSA*::sign, i.e. the randomised path including the DefaultRNG instantiation, where it measured RFC 6979 before, so its gh-pages history breaks at this commit. The stack harness gains bench__sign_deterministic beside each bench__sign. ./dev_scripts/quality_stats.sh ./crypto/ecdsa: unwraps in core code 28 -> 21 (sign_randomized's one-shot hash `try_into().unwrap()` per curve is gone, the streaming path having no such conversion), Err() in core code unchanged at 49. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/ecdsa_cmd.rs | 269 +++++++++++---- cli/src/main.rs | 81 ++++- crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs | 12 +- crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs | 12 +- crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs | 12 +- crypto/ecdsa/benches/ecdsa_p256_benches.rs | 11 +- crypto/ecdsa/benches/ecdsa_p256k1_benches.rs | 12 +- crypto/ecdsa/benches/ecdsa_p384_benches.rs | 11 +- crypto/ecdsa/benches/ecdsa_p521_benches.rs | 11 +- crypto/ecdsa/src/ecdsa_bp256r1.rs | 299 ++++++++++++----- crypto/ecdsa/src/ecdsa_bp384r1.rs | 299 ++++++++++++----- crypto/ecdsa/src/ecdsa_bp512r1.rs | 299 ++++++++++++----- crypto/ecdsa/src/ecdsa_p256.rs | 305 +++++++++++++----- crypto/ecdsa/src/ecdsa_p256k1.rs | 295 ++++++++++++----- crypto/ecdsa/src/ecdsa_p384.rs | 286 +++++++++++----- crypto/ecdsa/src/ecdsa_p521.rs | 292 ++++++++++++----- crypto/ecdsa/src/extra_bits_p256k1.rs | 2 +- crypto/ecdsa/src/extra_bits_p384.rs | 2 +- crypto/ecdsa/src/keys_p521.rs | 6 +- crypto/ecdsa/src/lib.rs | 69 ++-- crypto/ecdsa/tests/cavp_common/mod.rs | 2 +- crypto/ecdsa/tests/cavp_p256.rs | 4 +- crypto/ecdsa/tests/cavp_p384.rs | 4 +- crypto/ecdsa/tests/cavp_p521.rs | 4 +- crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs | 82 +++-- crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs | 82 +++-- crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs | 82 +++-- crypto/ecdsa/tests/ecdsa_p256_tests.rs | 77 +++-- crypto/ecdsa/tests/ecdsa_p256k1_tests.rs | 82 +++-- crypto/ecdsa/tests/ecdsa_p384_tests.rs | 72 ++++- crypto/ecdsa/tests/ecdsa_p521_tests.rs | 72 ++++- .../ecdsa/tests/extra_bits_bp256r1_tests.rs | 2 +- crypto/ecdsa/tests/extra_bits_tests.rs | 2 +- .../tests/rfc6979_bp256r1_vectors_tests.rs | 8 +- .../tests/rfc6979_bp384r1_vectors_tests.rs | 8 +- .../tests/rfc6979_bp512r1_vectors_tests.rs | 8 +- .../tests/rfc6979_p256k1_vectors_tests.rs | 8 +- .../ecdsa/tests/rfc6979_p384_vectors_tests.rs | 8 +- .../ecdsa/tests/rfc6979_p521_vectors_tests.rs | 8 +- crypto/ecdsa/tests/rfc6979_vectors_tests.rs | 8 +- crypto/sm2/src/sm2.rs | 5 +- .../src/bench_ecdsa_mem_usage.rs | 96 +++++- 42 files changed, 2463 insertions(+), 846 deletions(-) diff --git a/cli/src/ecdsa_cmd.rs b/cli/src/ecdsa_cmd.rs index 3d608b6ba..93098aed1 100644 --- a/cli/src/ecdsa_cmd.rs +++ b/cli/src/ecdsa_cmd.rs @@ -8,6 +8,9 @@ //! over those traits, monomorphised once per curve below, covers all seven without repeating the //! same match block. //! +//! Each curve has two signer types, `ECDSA*` (random `k`) and `ECDetDSA*` (RFC 6979), identical in +//! trait shape; `--deterministic` picks which one [`ecdsa_cmd`] is instantiated over. +//! //! `ctx` (accepted by `Signer`/`SignatureVerifier` for trait conformance) is not exposed as a CLI //! flag here: `bouncycastle_ecdsa`'s own crate docs say ECDSA "accepts but ignores" it (FIPS //! 186-5/SEC 1 have no context-string input), so a `--ctxfile` flag would silently do nothing -- @@ -23,10 +26,20 @@ use bouncycastle::core::traits::{ }; use bouncycastle::ecdsa::keys_common::DerivePublicKey; use bouncycastle::ecdsa::{ - ecdsa_bp256r1, ecdsa_bp256r1::ECDSABp256r1, ecdsa_bp384r1, ecdsa_bp384r1::ECDSABp384r1, - ecdsa_bp512r1, ecdsa_bp512r1::ECDSABp512r1, ecdsa_p256, ecdsa_p256::ECDSAP256, ecdsa_p256k1, - ecdsa_p256k1::ECDSASecp256K1, ecdsa_p384, ecdsa_p384::ECDSAP384, ecdsa_p521, - ecdsa_p521::ECDSAP521, + ecdsa_bp256r1, + ecdsa_bp256r1::{ECDSABp256r1, ECDetDSABp256r1}, + ecdsa_bp384r1, + ecdsa_bp384r1::{ECDSABp384r1, ECDetDSABp384r1}, + ecdsa_bp512r1, + ecdsa_bp512r1::{ECDSABp512r1, ECDetDSABp512r1}, + ecdsa_p256, + ecdsa_p256::{ECDSAP256, ECDetDSAP256}, + ecdsa_p256k1, + ecdsa_p256k1::{ECDSASecp256K1, ECDetDSASecp256K1}, + ecdsa_p384, + ecdsa_p384::{ECDSAP384, ECDetDSAP384}, + ecdsa_p521, + ecdsa_p521::{ECDSAP521, ECDetDSAP521}, }; use bouncycastle::ecdsa::{ keys as keys_p256, keys_bp256r1, keys_bp384r1, keys_bp512r1, keys_p256k1, keys_p384, keys_p521, @@ -196,15 +209,27 @@ pub(crate) fn ecdsa_p256_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSAP256, - { keys_p256::SK_LEN }, - { keys_p256::PK_LEN }, - { ecdsa_p256::SIG_LEN }, - >(action, keys_p256::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-256") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSAP256, + { keys_p256::SK_LEN }, + { keys_p256::PK_LEN }, + { ecdsa_p256::SIG_LEN }, + >(action, keys_p256::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-256") + } else { + ecdsa_cmd::< + _, + _, + ECDSAP256, + { keys_p256::SK_LEN }, + { keys_p256::PK_LEN }, + { ecdsa_p256::SIG_LEN }, + >(action, keys_p256::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-256") + } } pub(crate) fn ecdsa_p384_cmd( @@ -213,15 +238,27 @@ pub(crate) fn ecdsa_p384_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSAP384, - { keys_p384::SK_LEN }, - { keys_p384::PK_LEN }, - { ecdsa_p384::SIG_LEN }, - >(action, keys_p384::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-384") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSAP384, + { keys_p384::SK_LEN }, + { keys_p384::PK_LEN }, + { ecdsa_p384::SIG_LEN }, + >(action, keys_p384::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-384") + } else { + ecdsa_cmd::< + _, + _, + ECDSAP384, + { keys_p384::SK_LEN }, + { keys_p384::PK_LEN }, + { ecdsa_p384::SIG_LEN }, + >(action, keys_p384::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-384") + } } pub(crate) fn ecdsa_p521_cmd( @@ -230,15 +267,27 @@ pub(crate) fn ecdsa_p521_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSAP521, - { keys_p521::SK_LEN }, - { keys_p521::PK_LEN }, - { ecdsa_p521::SIG_LEN }, - >(action, keys_p521::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-521") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSAP521, + { keys_p521::SK_LEN }, + { keys_p521::PK_LEN }, + { ecdsa_p521::SIG_LEN }, + >(action, keys_p521::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-521") + } else { + ecdsa_cmd::< + _, + _, + ECDSAP521, + { keys_p521::SK_LEN }, + { keys_p521::PK_LEN }, + { ecdsa_p521::SIG_LEN }, + >(action, keys_p521::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA P-521") + } } pub(crate) fn ecdsa_secp256k1_cmd( @@ -247,15 +296,31 @@ pub(crate) fn ecdsa_secp256k1_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSASecp256K1, - { keys_p256k1::SK_LEN }, - { keys_p256k1::PK_LEN }, - { ecdsa_p256k1::SIG_LEN }, - >(action, keys_p256k1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA secp256k1") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSASecp256K1, + { keys_p256k1::SK_LEN }, + { keys_p256k1::PK_LEN }, + { ecdsa_p256k1::SIG_LEN }, + >( + action, keys_p256k1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA secp256k1" + ) + } else { + ecdsa_cmd::< + _, + _, + ECDSASecp256K1, + { keys_p256k1::SK_LEN }, + { keys_p256k1::PK_LEN }, + { ecdsa_p256k1::SIG_LEN }, + >( + action, keys_p256k1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA secp256k1" + ) + } } pub(crate) fn ecdsa_bp256r1_cmd( @@ -264,15 +329,43 @@ pub(crate) fn ecdsa_bp256r1_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSABp256r1, - { keys_bp256r1::SK_LEN }, - { keys_bp256r1::PK_LEN }, - { ecdsa_bp256r1::SIG_LEN }, - >(action, keys_bp256r1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA brainpoolP256r1") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSABp256r1, + { keys_bp256r1::SK_LEN }, + { keys_bp256r1::PK_LEN }, + { ecdsa_bp256r1::SIG_LEN }, + >( + action, + keys_bp256r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP256r1", + ) + } else { + ecdsa_cmd::< + _, + _, + ECDSABp256r1, + { keys_bp256r1::SK_LEN }, + { keys_bp256r1::PK_LEN }, + { ecdsa_bp256r1::SIG_LEN }, + >( + action, + keys_bp256r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP256r1", + ) + } } pub(crate) fn ecdsa_bp384r1_cmd( @@ -281,15 +374,43 @@ pub(crate) fn ecdsa_bp384r1_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSABp384r1, - { keys_bp384r1::SK_LEN }, - { keys_bp384r1::PK_LEN }, - { ecdsa_bp384r1::SIG_LEN }, - >(action, keys_bp384r1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA brainpoolP384r1") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSABp384r1, + { keys_bp384r1::SK_LEN }, + { keys_bp384r1::PK_LEN }, + { ecdsa_bp384r1::SIG_LEN }, + >( + action, + keys_bp384r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP384r1", + ) + } else { + ecdsa_cmd::< + _, + _, + ECDSABp384r1, + { keys_bp384r1::SK_LEN }, + { keys_bp384r1::PK_LEN }, + { ecdsa_bp384r1::SIG_LEN }, + >( + action, + keys_bp384r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP384r1", + ) + } } pub(crate) fn ecdsa_bp512r1_cmd( @@ -298,13 +419,41 @@ pub(crate) fn ecdsa_bp512r1_cmd( pkfile: &Option, sigfile: &Option, output_hex: bool, + deterministic: bool, ) { - ecdsa_cmd::< - _, - _, - ECDSABp512r1, - { keys_bp512r1::SK_LEN }, - { keys_bp512r1::PK_LEN }, - { ecdsa_bp512r1::SIG_LEN }, - >(action, keys_bp512r1::keygen, skfile, pkfile, sigfile, output_hex, "ECDSA brainpoolP512r1") + if deterministic { + ecdsa_cmd::< + _, + _, + ECDetDSABp512r1, + { keys_bp512r1::SK_LEN }, + { keys_bp512r1::PK_LEN }, + { ecdsa_bp512r1::SIG_LEN }, + >( + action, + keys_bp512r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP512r1", + ) + } else { + ecdsa_cmd::< + _, + _, + ECDSABp512r1, + { keys_bp512r1::SK_LEN }, + { keys_bp512r1::PK_LEN }, + { ecdsa_bp512r1::SIG_LEN }, + >( + action, + keys_bp512r1::keygen, + skfile, + pkfile, + sigfile, + output_hex, + "ECDSA brainpoolP512r1", + ) + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 4aba9b241..4dee676b4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1014,8 +1014,8 @@ enum Subcommands { /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. Base64Encode, - /// The ECDSA signature algorithm over NIST P-256 (FIPS 186-5), deterministic (RFC 6979) by - /// default. `ctx` has no effect: ECDSA has no context-string input. + /// The ECDSA signature algorithm over NIST P-256 (FIPS 186-5); `--deterministic` selects + /// Deterministic ECDSA (RFC 6979). `ctx` has no effect: ECDSA has no context-string input. ECDSA_P256 { action: ECDSAAction, @@ -1034,6 +1034,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over NIST P-384 (FIPS 186-5). See ecdsa-p256. @@ -1055,6 +1061,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over NIST P-521 (FIPS 186-5). See ecdsa-p256. @@ -1076,6 +1088,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over secp256k1 (SEC 2). See ecdsa-p256. @@ -1097,6 +1115,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over brainpoolP256r1 (RFC 5639). See ecdsa-p256. @@ -1118,6 +1142,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over brainpoolP384r1 (RFC 5639). See ecdsa-p256. @@ -1139,6 +1169,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The ECDSA signature algorithm over brainpoolP512r1 (RFC 5639). See ecdsa-p256. @@ -1160,6 +1196,12 @@ enum Subcommands { #[arg(short)] /// Output in hex format. x: bool, + + #[arg(long)] + /// Deterministic ECDSA (FIPS 186-5 Section 6.3.2, RFC 6979): `k` is derived from the key + /// and the message rather than drawn from the RNG, so signing is repeatable. Verification + /// is the same either way. + deterministic: bool, }, /// The HashML-DSA-44 signature algorithm. @@ -2079,26 +2121,33 @@ fn run() { Some(Subcommands::Base64Encode) => { encoders_cmd::base64_encode_cmd(); } - Some(Subcommands::ECDSA_P256 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_p256_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_P256 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p256_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_P384 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_p384_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_P384 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p384_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_P521 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_p521_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_P521 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_p521_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_SECP256K1 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_secp256k1_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_SECP256K1 { + action, + skfile, + pkfile, + sigfile, + x, + deterministic, + }) => { + ecdsa_cmd::ecdsa_secp256k1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_BP256R1 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_bp256r1_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_BP256R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp256r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_BP384R1 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_bp384r1_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_BP384R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp384r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } - Some(Subcommands::ECDSA_BP512R1 { action, skfile, pkfile, sigfile, x }) => { - ecdsa_cmd::ecdsa_bp512r1_cmd(action, skfile, pkfile, sigfile, *x); + Some(Subcommands::ECDSA_BP512R1 { action, skfile, pkfile, sigfile, x, deterministic }) => { + ecdsa_cmd::ecdsa_bp512r1_cmd(action, skfile, pkfile, sigfile, *x, *deterministic); } Some(Subcommands::HashMLDSA44 { action, ctxfile, skfile, pkfile, sigfile, x }) => { mldsa_cmd::hash_mldsa44_sha512_cmd(action, ctxfile, skfile, pkfile, sigfile, *x); diff --git a/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs index 79251fdc6..1185cc949 100644 --- a/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_bp256r1_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_bp256r1::ECDSABp256r1; +use bouncycastle_ecdsa::ecdsa_bp256r1::{ECDSABp256r1, ECDetDSABp256r1}; use bouncycastle_ecdsa::keys_bp256r1::keygen; use bouncycastle_rng::DefaultRNG; @@ -18,18 +18,24 @@ fn bench_ecdsa_bp256r1(c: &mut Criterion) { bencher .iter(|| black_box(ECDSABp256r1::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSABp256r1::sign_randomized( + ECDSABp256r1::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| { + black_box(ECDetDSABp256r1::sign(black_box(&sk), black_box(MSG), None).unwrap()) + }) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSABp256r1::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs index 58393bc11..783b0ea8f 100644 --- a/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_bp384r1_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_bp384r1::ECDSABp384r1; +use bouncycastle_ecdsa::ecdsa_bp384r1::{ECDSABp384r1, ECDetDSABp384r1}; use bouncycastle_ecdsa::keys_bp384r1::keygen; use bouncycastle_rng::DefaultRNG; @@ -18,18 +18,24 @@ fn bench_ecdsa_bp384r1(c: &mut Criterion) { bencher .iter(|| black_box(ECDSABp384r1::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSABp384r1::sign_randomized( + ECDSABp384r1::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| { + black_box(ECDetDSABp384r1::sign(black_box(&sk), black_box(MSG), None).unwrap()) + }) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSABp384r1::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs b/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs index e20c0fc97..0213e1ded 100644 --- a/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_bp512r1_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_bp512r1::ECDSABp512r1; +use bouncycastle_ecdsa::ecdsa_bp512r1::{ECDSABp512r1, ECDetDSABp512r1}; use bouncycastle_ecdsa::keys_bp512r1::keygen; use bouncycastle_rng::DefaultRNG; @@ -18,18 +18,24 @@ fn bench_ecdsa_bp512r1(c: &mut Criterion) { bencher .iter(|| black_box(ECDSABp512r1::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSABp512r1::sign_randomized( + ECDSABp512r1::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| { + black_box(ECDetDSABp512r1::sign(black_box(&sk), black_box(MSG), None).unwrap()) + }) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSABp512r1::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_p256_benches.rs b/crypto/ecdsa/benches/ecdsa_p256_benches.rs index 825935f13..3307e23dd 100644 --- a/crypto/ecdsa/benches/ecdsa_p256_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_p256_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +use bouncycastle_ecdsa::ecdsa_p256::{ECDSAP256, ECDetDSAP256}; use bouncycastle_ecdsa::keys::keygen; use bouncycastle_rng::DefaultRNG; @@ -17,18 +17,23 @@ fn bench_ecdsa_p256(c: &mut Criterion) { group.bench_function("sign", |bencher| { bencher.iter(|| black_box(ECDSAP256::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSAP256::sign_randomized( + ECDSAP256::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher + .iter(|| black_box(ECDetDSAP256::sign(black_box(&sk), black_box(MSG), None).unwrap())) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSAP256::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs b/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs index 1f4a88229..bdc3594d3 100644 --- a/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_p256k1_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_p256k1::ECDSASecp256K1; +use bouncycastle_ecdsa::ecdsa_p256k1::{ECDSASecp256K1, ECDetDSASecp256K1}; use bouncycastle_ecdsa::keys_p256k1::keygen; use bouncycastle_rng::DefaultRNG; @@ -18,18 +18,24 @@ fn bench_ecdsa_secp256k1(c: &mut Criterion) { bencher .iter(|| black_box(ECDSASecp256K1::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSASecp256K1::sign_randomized( + ECDSASecp256K1::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher.iter(|| { + black_box(ECDetDSASecp256K1::sign(black_box(&sk), black_box(MSG), None).unwrap()) + }) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSASecp256K1::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_p384_benches.rs b/crypto/ecdsa/benches/ecdsa_p384_benches.rs index 0895c38db..be6b765ca 100644 --- a/crypto/ecdsa/benches/ecdsa_p384_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_p384_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_p384::ECDSAP384; +use bouncycastle_ecdsa::ecdsa_p384::{ECDSAP384, ECDetDSAP384}; use bouncycastle_ecdsa::keys_p384::keygen; use bouncycastle_rng::DefaultRNG; @@ -17,18 +17,23 @@ fn bench_ecdsa_p384(c: &mut Criterion) { group.bench_function("sign", |bencher| { bencher.iter(|| black_box(ECDSAP384::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSAP384::sign_randomized( + ECDSAP384::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher + .iter(|| black_box(ECDetDSAP384::sign(black_box(&sk), black_box(MSG), None).unwrap())) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSAP384::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/benches/ecdsa_p521_benches.rs b/crypto/ecdsa/benches/ecdsa_p521_benches.rs index 40f8aa577..acf648136 100644 --- a/crypto/ecdsa/benches/ecdsa_p521_benches.rs +++ b/crypto/ecdsa/benches/ecdsa_p521_benches.rs @@ -2,7 +2,7 @@ use criterion::{Criterion, criterion_group, criterion_main}; use std::hint::black_box; use bouncycastle_core::traits::{SignatureVerifier, Signer}; -use bouncycastle_ecdsa::ecdsa_p521::ECDSAP521; +use bouncycastle_ecdsa::ecdsa_p521::{ECDSAP521, ECDetDSAP521}; use bouncycastle_ecdsa::keys_p521::keygen; use bouncycastle_rng::DefaultRNG; @@ -17,18 +17,23 @@ fn bench_ecdsa_p521(c: &mut Criterion) { group.bench_function("sign", |bencher| { bencher.iter(|| black_box(ECDSAP521::sign(black_box(&sk), black_box(MSG), None).unwrap())) }); - group.bench_function("sign_randomized", |bencher| { + group.bench_function("sign_rng", |bencher| { bencher.iter(|| { black_box( - ECDSAP521::sign_randomized( + ECDSAP521::sign_rng( black_box(&sk), black_box(MSG), + None, &mut DefaultRNG::default(), ) .unwrap(), ) }) }); + group.bench_function("sign_deterministic", |bencher| { + bencher + .iter(|| black_box(ECDetDSAP521::sign(black_box(&sk), black_box(MSG), None).unwrap())) + }); group.bench_function("verify", |bencher| { bencher.iter(|| { ECDSAP521::verify(black_box(&pk), black_box(MSG), None, black_box(&sig)).unwrap() diff --git a/crypto/ecdsa/src/ecdsa_bp256r1.rs b/crypto/ecdsa/src/ecdsa_bp256r1.rs index a506dc436..f8cf365a3 100644 --- a/crypto/ecdsa/src/ecdsa_bp256r1.rs +++ b/crypto/ecdsa/src/ecdsa_bp256r1.rs @@ -1,10 +1,12 @@ -//! `ECDSABp256r1`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix -//! A.3.3 by default) and §6.4.2 (verification), fixed to SHA-256 (the pairing wycheproof's -//! `ecdsa_brainpoolP256r1_sha256_*` vector files use). Identical in shape to [`crate::ecdsa_p256`] -//! -- see that module's docs for the full reasoning (`e = bits2int(H)` needs no truncation since -//! `len(n) == hashlen == 256`; `x_affine_of_signing_point`'s branch-free-on-a-secret-value -//! argument; both hold unchanged here) -- with brainpoolP256r1's types, [`crate::keys_bp256r1`], -//! [`crate::extra_bits_bp256r1`], and [`crate::rfc6979_bp256r1`] substituted. +//! `ECDSABp256r1` and `ECDetDSABp256r1`: FIPS 186-5 §6.4.1 (signature generation, with `k` drawn at +//! random for `ECDSABp256r1` per §6.3.1 or derived from `d` and the hash for `ECDetDSABp256r1` per +//! §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), fixed to SHA-256 (the +//! pairing wycheproof's `ecdsa_brainpoolP256r1_sha256_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 256`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP256r1's +//! types, [`crate::keys_bp256r1`], [`crate::extra_bits_bp256r1`], and [`crate::rfc6979_bp256r1`] +//! substituted. use crate::extra_bits_bp256r1::reduce_wide_bits_mod_n_minus_1; use crate::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey, PK_LEN, SK_LEN}; @@ -20,6 +22,7 @@ use bouncycastle_ec::bp256r1_scalar::{ use bouncycastle_ec::bp256r1_sec1; use bouncycastle_ec::bp256r1_wnaf::shamir_multiply; use bouncycastle_ec::nat; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA256; use bouncycastle_utils::secret::Secret; @@ -32,14 +35,23 @@ pub const SIG_LEN: usize = 64; /// [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); -/// Streaming state for both `ECDSABp256r1`'s [`Signer`] and [`SignatureVerifier`] impls, mirroring -/// [`crate::ecdsa_p256::ECDSAP256`]'s shape. +/// ECDSA over brainpoolP256r1 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); +/// see [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSABp256r1`] is the deterministic variant. pub struct ECDSABp256r1 { hash: SHA256, sk: Option, pk: Option, } +/// Deterministic ECDSA over brainpoolP256r1 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSABp256r1`]. +pub struct ECDetDSABp256r1 { + hash: SHA256, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't /// [`Bp256r1JacobianPoint::to_affine`]. fn x_affine_of_signing_point(point: &Bp256r1JacobianPoint) -> Bp256r1FieldElement { @@ -88,6 +100,16 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp256r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp256r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 32-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no /// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 32]) -> Bp256r1ScalarField { @@ -101,53 +123,138 @@ fn r_or_s_is_zero(r: &Bp256r1ScalarField, s: &Bp256r1ScalarField) -> bool { *r == Bp256r1ScalarField::ZERO || *s == Bp256r1ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA256, + pk: &ECDSABp256r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp256r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp256r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp256r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp256r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp256r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp256r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSABp256r1 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default -- an additional capability alongside [`Signer::sign`]. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs for the full reasoning. - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSABp256r1PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 32] = SHA256::default().hash(msg)[..32].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSABp256r1; call do_verify_final instead", + ))?; + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; crate::keys_bp256r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSABp256r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSABp256r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result (RFC 3279 §2.2.3 `Ecdsa-Sig-Value`) instead - /// of raw `r || s`. Returns the encoded length; unused trailing bytes of the fixed-size buffer - /// are left as written by [`crate::der::encode`] (zeroed only up to that length). +impl ECDetDSABp256r1 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSABp256r1PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature (see [`Self::sign_der`]) - /// instead of raw `r || s`. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSABp256r1PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(32); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -177,7 +284,50 @@ impl Signer for ECDSABp256r1 { sk: &ECDSABp256r1PrivateKey, _ctx: Option<&[u8]>, ) -> Result { - // ctx ignored -- see this crate's docs. + Ok(Self { hash: SHA256::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSABp256r1 { + fn sign( + sk: &ECDSABp256r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSABp256r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init( + sk: &ECDSABp256r1PrivateKey, + _ctx: Option<&[u8]>, + ) -> Result { Ok(Self { hash: SHA256::default(), sk: Some(sk.clone()), pk: None }) } @@ -187,13 +337,12 @@ impl Signer for ECDSABp256r1 { fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSABp256r1; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSABp256r1; call do_verify_final", ))?; let mut h = [0u8; 32]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_bp256r1::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_bp256r1::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -230,48 +379,38 @@ impl SignatureVerifier for ECDSABp256r1 let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSABp256r1; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. - let r_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); - let s_limbs = bp256r1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = Bp256r1ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 32]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); // steps 2-3 - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = Bp256r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 - let u = e.mul(&s_inv); // step 5 - let v = r.mul(&s_inv); - - let q = Bp256r1JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = Bp256r1PublicScalar::from_limbs(u.to_limbs()); - let v_pub = Bp256r1PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = Bp256r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +impl SignatureVerifier for ECDetDSABp256r1 { + fn verify( + pk: &ECDSABp256r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSABp256r1PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA256::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSABp256r1; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_bp384r1.rs b/crypto/ecdsa/src/ecdsa_bp384r1.rs index 4ce9656fd..d997d015f 100644 --- a/crypto/ecdsa/src/ecdsa_bp384r1.rs +++ b/crypto/ecdsa/src/ecdsa_bp384r1.rs @@ -1,10 +1,12 @@ -//! `ECDSABp384r1`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix -//! A.3.3 by default) and §6.4.2 (verification), fixed to SHA-384 (the pairing wycheproof's -//! `ecdsa_brainpoolP384r1_sha384_*` vector files use). Identical in shape to [`crate::ecdsa_p256`] -//! -- see that module's docs for the full reasoning (`e = bits2int(H)` needs no truncation since -//! `len(n) == hashlen == 384`; `x_affine_of_signing_point`'s branch-free-on-a-secret-value -//! argument; both hold unchanged here) -- with brainpoolP384r1's types, [`crate::keys_bp384r1`], -//! [`crate::extra_bits_bp384r1`], and [`crate::rfc6979_bp384r1`] substituted. +//! `ECDSABp384r1` and `ECDetDSABp384r1`: FIPS 186-5 §6.4.1 (signature generation, with `k` drawn at +//! random for `ECDSABp384r1` per §6.3.1 or derived from `d` and the hash for `ECDetDSABp384r1` per +//! §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), fixed to SHA-384 (the +//! pairing wycheproof's `ecdsa_brainpoolP384r1_sha384_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 384`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP384r1's +//! types, [`crate::keys_bp384r1`], [`crate::extra_bits_bp384r1`], and [`crate::rfc6979_bp384r1`] +//! substituted. use crate::extra_bits_bp384r1::reduce_wide_bits_mod_n_minus_1; use crate::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey, PK_LEN, SK_LEN}; @@ -20,6 +22,7 @@ use bouncycastle_ec::bp384r1_scalar::{ use bouncycastle_ec::bp384r1_sec1; use bouncycastle_ec::bp384r1_wnaf::shamir_multiply; use bouncycastle_ec::nat; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA384; use bouncycastle_utils::secret::Secret; @@ -32,14 +35,23 @@ pub const SIG_LEN: usize = 96; /// [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(48); -/// Streaming state for both `ECDSABp384r1`'s [`Signer`] and [`SignatureVerifier`] impls, mirroring -/// [`crate::ecdsa_p256::ECDSAP256`]'s shape. +/// ECDSA over brainpoolP384r1 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); +/// see [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSABp384r1`] is the deterministic variant. pub struct ECDSABp384r1 { hash: SHA384, sk: Option, pk: Option, } +/// Deterministic ECDSA over brainpoolP384r1 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSABp384r1`]. +pub struct ECDetDSABp384r1 { + hash: SHA384, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't /// [`Bp384r1JacobianPoint::to_affine`]. fn x_affine_of_signing_point(point: &Bp384r1JacobianPoint) -> Bp384r1FieldElement { @@ -88,6 +100,16 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp384r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp384r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 48-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no /// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 48]) -> Bp384r1ScalarField { @@ -101,53 +123,138 @@ fn r_or_s_is_zero(r: &Bp384r1ScalarField, s: &Bp384r1ScalarField) -> bool { *r == Bp384r1ScalarField::ZERO || *s == Bp384r1ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA384, + pk: &ECDSABp384r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); + let s_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp384r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 48]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp384r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp384r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp384r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp384r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp384r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(48); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSABp384r1 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default -- an additional capability alongside [`Signer::sign`]. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs for the full reasoning. - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSABp384r1PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 48] = SHA384::default().hash(msg)[..48].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSABp384r1; call do_verify_final instead", + ))?; + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; crate::keys_bp384r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSABp384r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSABp384r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result (RFC 3279 §2.2.3 `Ecdsa-Sig-Value`) instead - /// of raw `r || s`. Returns the encoded length; unused trailing bytes of the fixed-size buffer - /// are left as written by [`crate::der::encode`] (zeroed only up to that length). +impl ECDetDSABp384r1 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSABp384r1PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature (see [`Self::sign_der`]) - /// instead of raw `r || s`. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSABp384r1PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(48); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -177,7 +284,50 @@ impl Signer for ECDSABp384r1 { sk: &ECDSABp384r1PrivateKey, _ctx: Option<&[u8]>, ) -> Result { - // ctx ignored -- see this crate's docs. + Ok(Self { hash: SHA384::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSABp384r1 { + fn sign( + sk: &ECDSABp384r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSABp384r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init( + sk: &ECDSABp384r1PrivateKey, + _ctx: Option<&[u8]>, + ) -> Result { Ok(Self { hash: SHA384::default(), sk: Some(sk.clone()), pk: None }) } @@ -187,13 +337,12 @@ impl Signer for ECDSABp384r1 { fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSABp384r1; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSABp384r1; call do_verify_final", ))?; let mut h = [0u8; 48]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_bp384r1::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_bp384r1::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -230,48 +379,38 @@ impl SignatureVerifier for ECDSABp384r1 let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSABp384r1; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. - let r_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); - let s_limbs = bp384r1_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = Bp384r1ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 48]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); // steps 2-3 - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = Bp384r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 - let u = e.mul(&s_inv); // step 5 - let v = r.mul(&s_inv); - - let q = Bp384r1JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = Bp384r1PublicScalar::from_limbs(u.to_limbs()); - let v_pub = Bp384r1PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = Bp384r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +impl SignatureVerifier for ECDetDSABp384r1 { + fn verify( + pk: &ECDSABp384r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSABp384r1PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA384::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSABp384r1; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_bp512r1.rs b/crypto/ecdsa/src/ecdsa_bp512r1.rs index e281410ce..f70d53f17 100644 --- a/crypto/ecdsa/src/ecdsa_bp512r1.rs +++ b/crypto/ecdsa/src/ecdsa_bp512r1.rs @@ -1,10 +1,12 @@ -//! `ECDSABp512r1`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix -//! A.3.3 by default) and §6.4.2 (verification), fixed to SHA-512 (the pairing wycheproof's -//! `ecdsa_brainpoolP512r1_sha512_*` vector files use). Identical in shape to [`crate::ecdsa_p256`] -//! -- see that module's docs for the full reasoning (`e = bits2int(H)` needs no truncation since -//! `len(n) == hashlen == 512`; `x_affine_of_signing_point`'s branch-free-on-a-secret-value -//! argument; both hold unchanged here) -- with brainpoolP512r1's types, [`crate::keys_bp512r1`], -//! [`crate::extra_bits_bp512r1`], and [`crate::rfc6979_bp512r1`] substituted. +//! `ECDSABp512r1` and `ECDetDSABp512r1`: FIPS 186-5 §6.4.1 (signature generation, with `k` drawn at +//! random for `ECDSABp512r1` per §6.3.1 or derived from `d` and the hash for `ECDetDSABp512r1` per +//! §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), fixed to SHA-512 (the +//! pairing wycheproof's `ecdsa_brainpoolP512r1_sha512_*` vector files use). Identical in shape to +//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (`e = bits2int(H)` needs +//! no truncation since `len(n) == hashlen == 512`; `x_affine_of_signing_point`'s +//! branch-free-on-a-secret-value argument; both hold unchanged here) -- with brainpoolP512r1's +//! types, [`crate::keys_bp512r1`], [`crate::extra_bits_bp512r1`], and [`crate::rfc6979_bp512r1`] +//! substituted. use crate::extra_bits_bp512r1::reduce_wide_bits_mod_n_minus_1; use crate::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey, PK_LEN, SK_LEN}; @@ -20,6 +22,7 @@ use bouncycastle_ec::bp512r1_scalar::{ use bouncycastle_ec::bp512r1_sec1; use bouncycastle_ec::bp512r1_wnaf::shamir_multiply; use bouncycastle_ec::nat; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA512; use bouncycastle_utils::secret::Secret; @@ -32,14 +35,23 @@ pub const SIG_LEN: usize = 128; /// [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(64); -/// Streaming state for both `ECDSABp512r1`'s [`Signer`] and [`SignatureVerifier`] impls, mirroring -/// [`crate::ecdsa_p256::ECDSAP256`]'s shape. +/// ECDSA over brainpoolP512r1 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); +/// see [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSABp512r1`] is the deterministic variant. pub struct ECDSABp512r1 { hash: SHA512, sk: Option, pk: Option, } +/// Deterministic ECDSA over brainpoolP512r1 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSABp512r1`]. +pub struct ECDetDSABp512r1 { + hash: SHA512, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free -- see [`crate::ecdsa_p256`]'s module docs for why this isn't /// [`Bp512r1JacobianPoint::to_affine`]. fn x_affine_of_signing_point(point: &Bp512r1JacobianPoint) -> Bp512r1FieldElement { @@ -88,6 +100,16 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSABp512r1`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; crate::keys_bp512r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 64-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no /// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 64]) -> Bp512r1ScalarField { @@ -101,53 +123,138 @@ fn r_or_s_is_zero(r: &Bp512r1ScalarField, s: &Bp512r1ScalarField) -> bool { *r == Bp512r1ScalarField::ZERO || *s == Bp512r1ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA512, + pk: &ECDSABp512r1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[..64].try_into().unwrap()); + let s_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[64..128].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = Bp512r1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 64]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = Bp512r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = Bp512r1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = Bp512r1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = Bp512r1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = Bp512r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..64], &raw[64..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(64); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSABp512r1 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default -- an additional capability alongside [`Signer::sign`]. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs for the full reasoning. - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSABp512r1PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 64] = SHA512::default().hash(msg)[..64].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSABp512r1; call do_verify_final instead", + ))?; + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; crate::keys_bp512r1::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSABp512r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSABp512r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result (RFC 3279 §2.2.3 `Ecdsa-Sig-Value`) instead - /// of raw `r || s`. Returns the encoded length; unused trailing bytes of the fixed-size buffer - /// are left as written by [`crate::der::encode`] (zeroed only up to that length). +impl ECDetDSABp512r1 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSABp512r1PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..64], &raw[64..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature (see [`Self::sign_der`]) - /// instead of raw `r || s`. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSABp512r1PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(64); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -177,7 +284,50 @@ impl Signer for ECDSABp512r1 { sk: &ECDSABp512r1PrivateKey, _ctx: Option<&[u8]>, ) -> Result { - // ctx ignored -- see this crate's docs. + Ok(Self { hash: SHA512::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSABp512r1 { + fn sign( + sk: &ECDSABp512r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSABp512r1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init( + sk: &ECDSABp512r1PrivateKey, + _ctx: Option<&[u8]>, + ) -> Result { Ok(Self { hash: SHA512::default(), sk: Some(sk.clone()), pk: None }) } @@ -187,13 +337,12 @@ impl Signer for ECDSABp512r1 { fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSABp512r1; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSABp512r1; call do_verify_final", ))?; let mut h = [0u8; 64]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_bp512r1::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_bp512r1::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -230,48 +379,38 @@ impl SignatureVerifier for ECDSABp512r1 let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSABp512r1; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. - let r_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[..64].try_into().unwrap()); - let s_limbs = bp512r1_sec1::limbs_from_be_bytes(&sig[64..128].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = Bp512r1ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 64]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); // steps 2-3 - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = Bp512r1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 - let u = e.mul(&s_inv); // step 5 - let v = r.mul(&s_inv); - - let q = Bp512r1JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = Bp512r1PublicScalar::from_limbs(u.to_limbs()); - let v_pub = Bp512r1PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = Bp512r1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +impl SignatureVerifier for ECDetDSABp512r1 { + fn verify( + pk: &ECDSABp512r1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSABp512r1PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA512::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSABp512r1; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_p256.rs b/crypto/ecdsa/src/ecdsa_p256.rs index 71502dff4..944942b49 100644 --- a/crypto/ecdsa/src/ecdsa_p256.rs +++ b/crypto/ecdsa/src/ecdsa_p256.rs @@ -1,5 +1,10 @@ -//! `ECDSAP256`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix A.3.3 -//! by default) and §6.4.2 (verification), fixed to SHA-256. +//! `ECDSAP256` and `ECDetDSAP256`: FIPS 186-5 §6.4.1 (signature generation) and §6.4.2 +//! (verification), fixed to SHA-256. The two types differ only in §6.4.1 step 3, where the +//! per-message secret `k` comes from: [`ECDSAP256`] is ECDSA, drawing `k` at random (§6.3.1, +//! Appendix A.3.1); [`ECDetDSAP256`] is Deterministic ECDSA, deriving `k` from `d` and the +//! message hash (§6.3.2, Appendix A.3.3, RFC 6979). §6 says the variant "does not impact the +//! signature verification process", so the two share one verifier and a signature from either +//! verifies under either. //! //! # `r`, `s` and `e` //! @@ -23,7 +28,9 @@ //! holds unconditionally, so skipping the infinity branch leaks nothing about which `k` was drawn. use crate::extra_bits::reduce_wide_bits_mod_n_minus_1; -use crate::keys::{ECDSAP256PrivateKey, ECDSAP256PublicKey, PK_LEN, SK_LEN}; +use crate::keys::{ + ECDSAP256PrivateKey, ECDSAP256PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN, PK_LEN, SK_LEN, +}; use crate::rfc6979; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::traits::{Hash, RNG, SignatureVerifier, Signer}; @@ -34,27 +41,45 @@ use bouncycastle_ec::p256_point::P256JacobianPoint; use bouncycastle_ec::p256_scalar::{N_LIMBS, P256PublicScalar, P256Scalar, P256ScalarField}; use bouncycastle_ec::p256_sec1; use bouncycastle_ec::p256_wnaf::shamir_multiply; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA256; use bouncycastle_utils::secret::Secret; /// Raw `r || s` signature length: two 32-byte field-width integers, the default encoding. -/// [`ECDSAP256::sign_der`]/[`ECDSAP256::verify_der`] offer the RFC 3279 §2.2.3 +/// `sign_der`/`verify_der` on either signer type offer the RFC 3279 §2.2.3 /// DER `SEQUENCE { r, s }` alternative via [`crate::der`], for interop that needs it. pub const SIG_LEN: usize = 64; /// Upper bound on a DER-encoded P-256 signature (`r`, `s` each 32 bytes); see [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); -/// Streaming state for both `ECDSAP256`'s [`Signer`] and [`SignatureVerifier`] impls, mirroring -/// how `bouncycastle_mldsa::MLDSA` holds an optional signing key and an optional verification key -/// on one type: exactly one of `sk`/`pk` is `Some`, chosen by which trait's `_init` constructed -/// this value. +/// ECDSA over P-256 with a random per-message secret: FIPS 186-5 §6.3.1, `k` from Appendix A.3.1 +/// ("extra random bits"). [`Signer::sign`] and [`Signer::do_sign_final`] draw it from the +/// library's default RNG ([`DefaultRNG`], instantiated per signature); [`Self::sign_rng`] and +/// [`Self::do_sign_final_rng`] take the caller's. Two signatures of one message under one key +/// differ. [`ECDetDSAP256`] is the deterministic variant. +/// +/// Streaming state for both the [`Signer`] and [`SignatureVerifier`] impls, mirroring how +/// `bouncycastle_mldsa::MLDSA` holds an optional signing key and an optional verification key on +/// one type: exactly one of `sk`/`pk` is `Some`, chosen by which trait's `_init` constructed this +/// value. pub struct ECDSAP256 { hash: SHA256, sk: Option, pk: Option, } +/// Deterministic ECDSA over P-256: FIPS 186-5 §6.3.2, `k` from Appendix A.3.3 (RFC 6979) as a +/// function of `d` and the message hash, so the same key and message always give the same +/// signature and signing draws no randomness. Verification is ECDSA's, unchanged: a signature +/// from either this type or [`ECDSAP256`] verifies under either. Streaming state as for +/// [`ECDSAP256`]. +pub struct ECDetDSAP256 { + hash: SHA256, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free -- see the module docs for why this isn't [`P256JacobianPoint::to_affine`]. fn x_affine_of_signing_point(point: &P256JacobianPoint) -> P256FieldElement { let z_inv = point.z.invert(); @@ -102,6 +127,16 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1: `N + 64` bits from `rng`, reduced into `[1, n-1]` +/// by `reduce_wide_bits_mod_n_minus_1`. The one place [`ECDSAP256`]'s signing paths draw `k`. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 32-byte message hash: FIPS 186-5 §6.4.1 step 2 / §6.4.2 step 3, `E = H` (no /// truncation needed -- see the module docs) then Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 32]) -> P256ScalarField { @@ -115,42 +150,104 @@ fn r_or_s_is_zero(r: &P256ScalarField, s: &P256ScalarField) -> bool { *r == P256ScalarField::ZERO || *s == P256ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash: step 1's range check, then steps 3-9 (step 2 +/// is the hashing the caller has done). Both signer types' `do_verify_final` are this one +/// function, since how `k` was generated does not enter verification. +fn verify_final(hash: SHA256, pk: &ECDSAP256PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = p256_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = p256_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P256ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P256PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = P256JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P256PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P256PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P256ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; unused trailing +/// bytes of the fixed-size buffer are left as written by [`crate::der::encode`] (zeroed only up +/// to that length). +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSAP256 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default -- an additional capability alongside [`Signer::sign`], not part of - /// the [`Signer`] trait itself (the trait's own docs describe its `sign` as sourcing - /// randomness from the library's default RNG when a primitive needs one; ECDSA's default - /// `sign` instead needs none, being deterministic, so the randomised path is this separate - /// method). Two calls with the same key and message produce different, both valid, signatures. - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSAP256PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 32] = SHA256::default().hash(msg)[..32].try_into().unwrap(); - let e = e_from_hash(&h); - - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; crate::keys::EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } - sign_with_k(sk, &e, k) + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSAP256; call do_verify_final instead", + ))?; + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) } /// As [`Signer::sign`], but DER-encodes the result (RFC 3279 §2.2.3 `Ecdsa-Sig-Value`) instead - /// of raw `r || s`. Returns the encoded length; unused trailing bytes of the fixed-size buffer - /// are left as written by [`crate::der::encode`] (zeroed only up to that length). + /// of raw `r || s`. Returns the encoded length; see `der_encode` for the unused tail. pub fn sign_der( sk: &ECDSAP256PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature (see [`Self::sign_der`]) @@ -161,10 +258,29 @@ impl ECDSAP256 { ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(32); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) + } +} + +impl ECDetDSAP256 { + /// As [`Signer::sign`], but DER-encoded; see [`ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSAP256PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } + + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSAP256PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -199,15 +315,56 @@ impl Signer for ECDSAP256 { self.hash.do_update(msg_chunk); } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSAP256 { + fn sign( + sk: &ECDSAP256PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSAP256PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init(sk: &ECDSAP256PrivateKey, _ctx: Option<&[u8]>) -> Result { + // ctx ignored -- see this crate's docs. + Ok(Self { hash: SHA256::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSAP256; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSAP256; call do_verify_final", ))?; let mut h = [0u8; 32]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -244,48 +401,38 @@ impl SignatureVerifier for ECDSAP256 { let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSAP256; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. - let r_limbs = p256_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); - let s_limbs = p256_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = P256ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 32]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); // steps 2-3 - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = P256PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 - let u = e.mul(&s_inv); // step 5 - let v = r.mul(&s_inv); - - let q = P256JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = P256PublicScalar::from_limbs(u.to_limbs()); - let v_pub = P256PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = P256ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +impl SignatureVerifier for ECDetDSAP256 { + fn verify( + pk: &ECDSAP256PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSAP256PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA256::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSAP256; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_p256k1.rs b/crypto/ecdsa/src/ecdsa_p256k1.rs index c73311228..4e4b5a95b 100644 --- a/crypto/ecdsa/src/ecdsa_p256k1.rs +++ b/crypto/ecdsa/src/ecdsa_p256k1.rs @@ -1,8 +1,9 @@ -//! `ECDSASecp256K1`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix -//! A.3.3 by default) and §6.4.2 (verification), fixed to SHA-256. Identical in shape to -//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (why `e` needs no -//! truncation, why `x_affine_of_signing_point` exists, the `ctx`/ignore-it stance) -- with -//! secp256k1's types substituted. +//! `ECDSASecp256K1` and `ECDetDSASecp256K1`: FIPS 186-5 §6.4.1 (signature generation, with `k` +//! drawn at random for `ECDSASecp256K1` per §6.3.1 or derived from `d` and the hash for +//! `ECDetDSASecp256K1` per §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), +//! fixed to SHA-256. Identical in shape to [`crate::ecdsa_p256`] -- see that module's docs for the +//! full reasoning (why `e` needs no truncation, why `x_affine_of_signing_point` exists, the +//! `ctx`/ignore-it stance) -- with secp256k1's types substituted. use crate::extra_bits_p256k1::reduce_wide_bits_mod_n_minus_1; use crate::keys_p256k1::{ @@ -20,6 +21,7 @@ use bouncycastle_ec::p256k1_scalar::{ }; use bouncycastle_ec::p256k1_sec1; use bouncycastle_ec::p256k1_wnaf::shamir_multiply; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA256; use bouncycastle_utils::secret::Secret; @@ -30,14 +32,23 @@ pub const SIG_LEN: usize = 64; /// [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(32); -/// Streaming state for both `ECDSASecp256K1`'s [`Signer`] and [`SignatureVerifier`] impls. See -/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. +/// ECDSA over secp256k1 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); see +/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSASecp256K1`] is the deterministic variant. pub struct ECDSASecp256K1 { hash: SHA256, sk: Option, pk: Option, } +/// Deterministic ECDSA over secp256k1 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSASecp256K1`]. +pub struct ECDetDSASecp256K1 { + hash: SHA256, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. fn x_affine_of_signing_point(point: &P256K1JacobianPoint) -> P256K1FieldElement { let z_inv = point.z.invert(); @@ -80,6 +91,18 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSASecp256K1`]'s signing paths draw `k`. +/// Draws A.3.1 step 3's `N + t` bits with `t = 64` (see [`crate::extra_bits_p256k1`]'s docs for why +/// that is required here even though secp256k1's `n` would need none for bias alone). +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 32-byte message hash: `E = H` (no truncation needed, `len(n) == hashlen`), then /// Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 32]) -> P256K1ScalarField { @@ -92,53 +115,138 @@ fn r_or_s_is_zero(r: &P256K1ScalarField, s: &P256K1ScalarField) -> bool { *r == P256K1ScalarField::ZERO || *s == P256K1ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final( + hash: SHA256, + pk: &ECDSASecp256K1PublicKey, + sig: &[u8], +) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. + let r_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); + let s_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P256K1ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 32]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); // steps 2-3 + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P256K1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 + let u = e.mul(&s_inv); // step 5 + let v = r.mul(&s_inv); + + let q = P256K1JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P256K1PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P256K1PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P256K1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(32); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSASecp256K1 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default. See [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs. Draws - /// A.3.1 step 3's `N + t` bits with `t = 64` (see [`crate::extra_bits_p256k1`]'s docs for why - /// that is required here even though secp256k1's `n` would need none for bias alone). - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSASecp256K1PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 32] = SHA256::default().hash(msg)[..32].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSASecp256K1; call do_verify_final instead", + ))?; + let mut h = [0u8; 32]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSASecp256K1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSASecp256K1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_der`]'s docs. +impl ECDetDSASecp256K1 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSASecp256K1PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..32], &raw[32..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature. See - /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]'s docs. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSASecp256K1PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(32); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -175,15 +283,58 @@ impl Signer for ECDSASecp256K1 { self.hash.do_update(msg_chunk); } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSASecp256K1 { + fn sign( + sk: &ECDSASecp256K1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSASecp256K1PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init( + sk: &ECDSASecp256K1PrivateKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA256::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSASecp256K1; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSASecp256K1; call do_verify_final", ))?; let mut h = [0u8; 32]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_p256k1::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_p256k1::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -220,48 +371,38 @@ impl SignatureVerifier for ECDSASecp25 let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSASecp256K1; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - // step 1: r, s in [1, n-1], rejected (not reduced) if out of range. - let r_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[..32].try_into().unwrap()); - let s_limbs = p256k1_sec1::limbs_from_be_bytes(&sig[32..64].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = P256K1ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 32]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); // steps 2-3 - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = P256K1PublicScalar::from_limbs(s_limbs).invert_vartime(); // step 4 - let u = e.mul(&s_inv); // step 5 - let v = r.mul(&s_inv); - - let q = P256K1JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = P256K1PublicScalar::from_limbs(u.to_limbs()); - let v_pub = P256K1PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); // step 6 - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = P256K1ScalarField::from_limbs(x_r1.to_limbs()); // steps 7-8 - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } // step 9 +impl SignatureVerifier for ECDetDSASecp256K1 { + fn verify( + pk: &ECDSASecp256K1PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSASecp256K1PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA256::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSASecp256K1; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_p384.rs b/crypto/ecdsa/src/ecdsa_p384.rs index b976e7084..81f89d7fa 100644 --- a/crypto/ecdsa/src/ecdsa_p384.rs +++ b/crypto/ecdsa/src/ecdsa_p384.rs @@ -1,8 +1,9 @@ -//! `ECDSAP384`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix A.3.3 -//! by default) and §6.4.2 (verification), fixed to SHA-384. Identical in shape to -//! [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning (why `e` needs no -//! truncation, why `x_affine_of_signing_point` exists, the `ctx`/ignore-it stance) -- with P-384's -//! types and SHA-384 substituted. +//! `ECDSAP384` and `ECDetDSAP384`: FIPS 186-5 §6.4.1 (signature generation, with `k` drawn at +//! random for `ECDSAP384` per §6.3.1 or derived from `d` and the hash for `ECDetDSAP384` per +//! §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), fixed to SHA-384. +//! Identical in shape to [`crate::ecdsa_p256`] -- see that module's docs for the full reasoning +//! (why `e` needs no truncation, why `x_affine_of_signing_point` exists, the `ctx`/ignore-it +//! stance) -- with P-384's types and SHA-384 substituted. use crate::extra_bits_p384::reduce_wide_bits_mod_n_minus_1; use crate::keys_p384::{ @@ -18,6 +19,7 @@ use bouncycastle_ec::p384_point::P384JacobianPoint; use bouncycastle_ec::p384_scalar::{N_LIMBS, P384PublicScalar, P384Scalar, P384ScalarField}; use bouncycastle_ec::p384_sec1; use bouncycastle_ec::p384_wnaf::shamir_multiply; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA384; use bouncycastle_utils::secret::Secret; @@ -27,14 +29,23 @@ pub const SIG_LEN: usize = 96; /// Upper bound on a DER-encoded P-384 signature (`r`, `s` each 48 bytes); see [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(48); -/// Streaming state for both `ECDSAP384`'s [`Signer`] and [`SignatureVerifier`] impls. See -/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. +/// ECDSA over P-384 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); see +/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSAP384`] is the deterministic variant. pub struct ECDSAP384 { hash: SHA384, sk: Option, pk: Option, } +/// Deterministic ECDSA over P-384 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSAP384`]. +pub struct ECDetDSAP384 { + hash: SHA384, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. fn x_affine_of_signing_point(point: &P384JacobianPoint) -> P384FieldElement { let z_inv = point.z.invert(); @@ -77,6 +88,18 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSAP384`]'s signing paths draw `k`. Draws +/// A.3.1 step 3's `N + t` bits with `t = 64` (see [`crate::extra_bits_p384`]'s docs for why that is +/// required here even though P-384's `n` would need none for bias alone). +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 48-byte message hash: `E = H` (no truncation needed, `len(n) == hashlen`), then /// Appendix B.2.1's big-endian conversion. fn e_from_hash(h: &[u8; 48]) -> P384ScalarField { @@ -89,53 +112,133 @@ fn r_or_s_is_zero(r: &P384ScalarField, s: &P384ScalarField) -> bool { *r == P384ScalarField::ZERO || *s == P384ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final(hash: SHA384, pk: &ECDSAP384PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + let r_limbs = p384_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); + let s_limbs = p384_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P384ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 48]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P384PublicScalar::from_limbs(s_limbs).invert_vartime(); + let u = e.mul(&s_inv); + let v = r.mul(&s_inv); + + let q = P384JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P384PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P384PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P384ScalarField::from_limbs(x_r1.to_limbs()); + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(48); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSAP384 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default. See [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs. Draws - /// A.3.1 step 3's `N + t` bits with `t = 64` (see [`crate::extra_bits_p384`]'s docs for why - /// that is required here even though P-384's `n` would need none for bias alone). - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSAP384PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 48] = SHA384::default().hash(msg)[..48].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSAP384; call do_verify_final instead", + ))?; + let mut h = [0u8; 48]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSAP384PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSAP384PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_der`]'s docs. +impl ECDetDSAP384 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSAP384PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..48], &raw[48..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature. See - /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]'s docs. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSAP384PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(48); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -169,15 +272,55 @@ impl Signer for ECDSAP384 { self.hash.do_update(msg_chunk); } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSAP384 { + fn sign( + sk: &ECDSAP384PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSAP384PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init(sk: &ECDSAP384PrivateKey, _ctx: Option<&[u8]>) -> Result { + Ok(Self { hash: SHA384::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSAP384; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSAP384; call do_verify_final", ))?; let mut h = [0u8; 48]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_p384::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_p384::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -214,47 +357,38 @@ impl SignatureVerifier for ECDSAP384 { let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSAP384; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - let r_limbs = p384_sec1::limbs_from_be_bytes(&sig[..48].try_into().unwrap()); - let s_limbs = p384_sec1::limbs_from_be_bytes(&sig[48..96].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = P384ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 48]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = P384PublicScalar::from_limbs(s_limbs).invert_vartime(); - let u = e.mul(&s_inv); - let v = r.mul(&s_inv); - - let q = P384JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = P384PublicScalar::from_limbs(u.to_limbs()); - let v_pub = P384PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = P384ScalarField::from_limbs(x_r1.to_limbs()); - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +impl SignatureVerifier for ECDetDSAP384 { + fn verify( + pk: &ECDSAP384PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSAP384PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA384::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSAP384; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/ecdsa_p521.rs b/crypto/ecdsa/src/ecdsa_p521.rs index aa7d635d1..42bb19d05 100644 --- a/crypto/ecdsa/src/ecdsa_p521.rs +++ b/crypto/ecdsa/src/ecdsa_p521.rs @@ -1,10 +1,11 @@ -//! `ECDSAP521`: FIPS 186-5 §6.4.1 (signature generation, deterministic per §6.3.2/Appendix A.3.3 -//! by default) and §6.4.2 (verification), fixed to SHA-512. Identical in shape to -//! [`crate::ecdsa_p384`] -- see [`crate::ecdsa_p256`]'s docs for the shared reasoning (why -//! `x_affine_of_signing_point` exists, the `ctx`/ignore-it stance) -- with P-521's types, SHA-512, -//! and one difference `e_from_hash` documents: unlike P-256/SHA-256 and P-384/SHA-384, `len(n) = -//! 521` and `hashlen = 512` are not equal here, though `len(n) >= hashlen` still holds, so `E = H` -//! still needs no truncation (FIPS 186-5 §6.4.1 step 2a). +//! `ECDSAP521` and `ECDetDSAP521`: FIPS 186-5 §6.4.1 (signature generation, with `k` drawn at +//! random for `ECDSAP521` per §6.3.1 or derived from `d` and the hash for `ECDetDSAP521` per +//! §6.3.2, as [`crate::ecdsa_p256`] lays out) and §6.4.2 (verification), fixed to SHA-512. +//! Identical in shape to [`crate::ecdsa_p384`] -- see [`crate::ecdsa_p256`]'s docs for the shared +//! reasoning (why `x_affine_of_signing_point` exists, the `ctx`/ignore-it stance) -- with P-521's +//! types, SHA-512, and one difference `e_from_hash` documents: unlike P-256/SHA-256 and +//! P-384/SHA-384, `len(n) = 521` and `hashlen = 512` are not equal here, though `len(n) >= hashlen` +//! still holds, so `E = H` still needs no truncation (FIPS 186-5 §6.4.1 step 2a). use crate::keys_p521::{ ECDSAP521PrivateKey, ECDSAP521PublicKey, EXTRA_BITS_DRBG_OUTPUT_LEN, PK_LEN, SK_LEN, @@ -20,6 +21,7 @@ use bouncycastle_ec::p521_point::P521JacobianPoint; use bouncycastle_ec::p521_scalar::{N_LIMBS, P521PublicScalar, P521Scalar, P521ScalarField}; use bouncycastle_ec::p521_sec1; use bouncycastle_ec::p521_wnaf::shamir_multiply; +use bouncycastle_rng::DefaultRNG; use bouncycastle_sha2::SHA512; use bouncycastle_utils::secret::Secret; @@ -29,14 +31,23 @@ pub const SIG_LEN: usize = 132; /// Upper bound on a DER-encoded P-521 signature (`r`, `s` each 66 bytes); see [`crate::der::max_len`]. pub const DER_SIG_MAX_LEN: usize = crate::der::max_len(66); -/// Streaming state for both `ECDSAP521`'s [`Signer`] and [`SignatureVerifier`] impls. See -/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. +/// ECDSA over P-521 with a random per-message secret (FIPS 186-5 §6.3.1, Appendix A.3.1); see +/// [`crate::ecdsa_p256::ECDSAP256`]'s docs. Streaming state for both the [`Signer`] and +/// [`SignatureVerifier`] impls. [`ECDetDSAP521`] is the deterministic variant. pub struct ECDSAP521 { hash: SHA512, sk: Option, pk: Option, } +/// Deterministic ECDSA over P-521 (FIPS 186-5 §6.3.2, Appendix A.3.3, RFC 6979); see +/// [`crate::ecdsa_p256::ECDetDSAP256`]'s docs. Streaming state as for [`ECDSAP521`]. +pub struct ECDetDSAP521 { + hash: SHA512, + sk: Option, + pk: Option, +} + /// `x * z^-2`, branch-free. See [`crate::ecdsa_p256::x_affine_of_signing_point`]'s docs. fn x_affine_of_signing_point(point: &P521JacobianPoint) -> P521FieldElement { let z_inv = point.z.invert(); @@ -79,6 +90,19 @@ fn sign_with_k( Ok(sig) } +/// FIPS 186-5 §6.4.1 step 3 by Appendix A.3.1, reduced into `[1, n-1]` by +/// `reduce_wide_bits_mod_n_minus_1`; the one place [`ECDSAP521`]'s signing paths draw `k`. Draws +/// A.3.1 step 3's `N + t` bits (`t = 71`, the byte-aligned width at or above `t = 64`) and reduces +/// them with [`crate::keys_p521`]'s own copy of the wide-reduction machinery [`crate::extra_bits`] +/// implements for P-256 -- see that module's docs. +fn random_k(rng: &mut dyn RNG) -> Result { + // Raw DRBG output, reduced below into the per-message secret: held in `Secret` so it is + // scrubbed when this function returns rather than left on the stack. + let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); + rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; + Ok(reduce_wide_bits_mod_n_minus_1(&*extra_bits)) +} + /// `e` from a 64-byte (SHA-512) message hash: `E = H` (`len(n) = 521 >= hashlen = 512`, so no /// truncation -- see the module docs), zero-extended into a 9-limb value, then Appendix B.2.1's /// big-endian conversion. @@ -94,54 +118,133 @@ fn r_or_s_is_zero(r: &P521ScalarField, s: &P521ScalarField) -> bool { *r == P521ScalarField::ZERO || *s == P521ScalarField::ZERO } +/// FIPS 186-5 §6.4.2 on the finished message hash; both signer types' `do_verify_final` are +/// this one function. See `crate::ecdsa_p256`'s `verify_final`. +fn verify_final(hash: SHA512, pk: &ECDSAP521PublicKey, sig: &[u8]) -> Result<(), SignatureError> { + // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and + // nothing else, so trailing bytes make this a different, malformed encoding rather than a + // valid signature in a roomy buffer. Accepting them would let anyone turn one valid + // signature into unlimited distinct byte strings that all verify -- malleability that + // breaks any caller treating the signature as an opaque, comparable blob. This matches how + // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long + // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). + if sig.len() != SIG_LEN { + return Err(SignatureError::SignatureVerificationFailed); + } + + let r_limbs = p521_sec1::limbs_from_be_bytes(&sig[..66].try_into().unwrap()); + let s_limbs = p521_sec1::limbs_from_be_bytes(&sig[66..132].try_into().unwrap()); + if !in_range(&r_limbs) || !in_range(&s_limbs) { + return Err(SignatureError::SignatureVerificationFailed); + } + let r = P521ScalarField::from_limbs(r_limbs); + + let mut h = [0u8; 64]; + hash.do_final_out(&mut h); + let e = e_from_hash(&h); + + // `s` is part of the signature, so its inverse is taken in variable time on the public + // scalar type; the constant-time `invert` is for the signer's `k`. + let s_inv = P521PublicScalar::from_limbs(s_limbs).invert_vartime(); + let u = e.mul(&s_inv); + let v = r.mul(&s_inv); + + let q = P521JacobianPoint::from_affine(pk.x, pk.y); + let u_pub = P521PublicScalar::from_limbs(u.to_limbs()); + let v_pub = P521PublicScalar::from_limbs(v.to_limbs()); + let r1_point = shamir_multiply(&u_pub, &v_pub, &q); + + if r1_point.is_infinity().to_bool() { + return Err(SignatureError::SignatureVerificationFailed); + } + let (x_r1, _) = + r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); + let r1 = P521ScalarField::from_limbs(x_r1.to_limbs()); + + if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +} + +/// RFC 3279 §2.2.3 `Ecdsa-Sig-Value` from raw `r || s`, with the encoded length; see +/// `crate::ecdsa_p256`'s `der_encode`. +fn der_encode(raw: &[u8; SIG_LEN]) -> ([u8; DER_SIG_MAX_LEN], usize) { + let mut out = [0u8; DER_SIG_MAX_LEN]; + let len = crate::der::encode(&raw[..66], &raw[66..], &mut out); + (out, len) +} + +/// Raw `r || s` from a DER `Ecdsa-Sig-Value`; a malformed encoding is a verification failure. +fn der_decode(sig: &[u8]) -> Result<[u8; SIG_LEN], SignatureError> { + let mut raw = [0u8; SIG_LEN]; + let (r_out, s_out) = raw.split_at_mut(66); + crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; + Ok(raw) +} + impl ECDSAP521 { - /// FIPS 186-5 §6.4.1 with `k` generated by Appendix A.3.1 ("extra random bits") instead of the - /// deterministic default. See [`crate::ecdsa_p256::ECDSAP256::sign_randomized`]'s docs. Draws - /// A.3.1 step 3's `N + t` bits (`t = 71`, the byte-aligned width at or above `t = 64`) and - /// reduces them with [`crate::keys_p521`]'s own copy of the wide-reduction machinery - /// [`crate::extra_bits`] implements for P-256 -- see that module's docs. - pub fn sign_randomized( + /// As [`Signer::sign`], with `k` drawn from `rng` instead of the library's default RNG. + pub fn sign_rng( sk: &ECDSAP521PrivateKey, msg: &[u8], + ctx: Option<&[u8]>, rng: &mut dyn RNG, ) -> Result<[u8; SIG_LEN], SignatureError> { - let h: [u8; 64] = SHA512::default().hash(msg)[..64].try_into().unwrap(); - let e = e_from_hash(&h); + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final_rng(rng) + } + + /// As [`Signer::do_sign_final`], with `k` drawn from `rng` instead of the library's default + /// RNG. + pub fn do_sign_final_rng(self, rng: &mut dyn RNG) -> Result<[u8; SIG_LEN], SignatureError> { + let sk = self.sk.ok_or(SignatureError::GenericError( + "do_sign_final called on a verify-initialized ECDSAP521; call do_verify_final instead", + ))?; + let mut h = [0u8; 64]; + self.hash.do_final_out(&mut h); + let k = random_k(rng)?; + sign_with_k(&sk, &e_from_hash(&h), k) + } - // Raw DRBG output, reduced below into the private key / per-message secret: held in - // `Secret` so it is scrubbed when this function returns rather than left on the stack. - let mut extra_bits = Secret::<[u8; EXTRA_BITS_DRBG_OUTPUT_LEN]>::new(); - rng.next_bytes_out(&mut *extra_bits).map_err(SignatureError::RNGError)?; - let k = reduce_wide_bits_mod_n_minus_1(&*extra_bits); + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. + pub fn sign_der( + sk: &ECDSAP521PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) + } - sign_with_k(sk, &e, k) + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. + pub fn verify_der( + pk: &ECDSAP521PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + Self::verify(pk, msg, ctx, &der_decode(sig)?) } +} - /// As [`Signer::sign`], but DER-encodes the result. See - /// [`crate::ecdsa_p256::ECDSAP256::sign_der`]'s docs. +impl ECDetDSAP521 { + /// As [`Signer::sign`], but DER-encoded; see [`crate::ecdsa_p256::ECDSAP256::sign_der`]. pub fn sign_der( sk: &ECDSAP521PrivateKey, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<([u8; DER_SIG_MAX_LEN], usize), SignatureError> { - let raw = Self::sign(sk, msg, ctx)?; - let mut out = [0u8; DER_SIG_MAX_LEN]; - let len = crate::der::encode(&raw[..66], &raw[66..], &mut out); - Ok((out, len)) + Ok(der_encode(&Self::sign(sk, msg, ctx)?)) } - /// As [`SignatureVerifier::verify`], but expects a DER-encoded signature. See - /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]'s docs. + /// As [`SignatureVerifier::verify`], but for a DER-encoded signature; see + /// [`crate::ecdsa_p256::ECDSAP256::verify_der`]. pub fn verify_der( pk: &ECDSAP521PublicKey, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { - let mut raw = [0u8; SIG_LEN]; - let (r_out, s_out) = raw.split_at_mut(66); - crate::der::decode(sig, r_out, s_out).ok_or(SignatureError::SignatureVerificationFailed)?; - Self::verify(pk, msg, ctx, &raw) + Self::verify(pk, msg, ctx, &der_decode(sig)?) } } @@ -175,15 +278,55 @@ impl Signer for ECDSAP521 { self.hash.do_update(msg_chunk); } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + self.do_sign_final_rng(&mut DefaultRNG::default()) + } + + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + output.fill(0); + *output = self.do_sign_final()?; + Ok(SIG_LEN) + } +} + +impl Signer for ECDetDSAP521 { + fn sign( + sk: &ECDSAP521PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + ) -> Result<[u8; SIG_LEN], SignatureError> { + let mut s = Self::do_sign_init(sk, ctx)?; + s.do_sign_update(msg); + s.do_sign_final() + } + + fn sign_out( + sk: &ECDSAP521PrivateKey, + msg: &[u8], + ctx: Option<&[u8]>, + output: &mut [u8; SIG_LEN], + ) -> Result { + output.fill(0); + *output = Self::sign(sk, msg, ctx)?; + Ok(SIG_LEN) + } + + fn do_sign_init(sk: &ECDSAP521PrivateKey, _ctx: Option<&[u8]>) -> Result { + Ok(Self { hash: SHA512::default(), sk: Some(sk.clone()), pk: None }) + } + + fn do_sign_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let sk = self.sk.ok_or(SignatureError::GenericError( - "do_sign_final called on a verify-initialized ECDSAP521; call do_verify_final instead", + "do_sign_final on a verify-initialized ECDetDSAP521; call do_verify_final", ))?; let mut h = [0u8; 64]; self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - let k = rfc6979_p521::generate_k(sk.scalar(), &h); - sign_with_k(&sk, &e, k) + let k = rfc6979_p521::generate_k(sk.scalar(), &h); // step 3, Appendix A.3.3 + sign_with_k(&sk, &e_from_hash(&h), k) } fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { @@ -220,47 +363,38 @@ impl SignatureVerifier for ECDSAP521 { let pk = self.pk.ok_or(SignatureError::GenericError( "do_verify_final called on a sign-initialized ECDSAP521; call do_sign_final instead", ))?; - // Exactly SIG_LEN, not "at least": the raw encoding is two fixed-width integers and - // nothing else, so trailing bytes make this a different, malformed encoding rather than a - // valid signature in a roomy buffer. Accepting them would let anyone turn one valid - // signature into unlimited distinct byte strings that all verify -- malleability that - // breaks any caller treating the signature as an opaque, comparable blob. This matches how - // `SignaturePublicKey`/`SignaturePrivateKey::from_bytes` already reject an over-long - // encoding here (see `core-test-framework`'s own `test_boundary_conditions`). - if sig.len() != SIG_LEN { - return Err(SignatureError::SignatureVerificationFailed); - } - - let r_limbs = p521_sec1::limbs_from_be_bytes(&sig[..66].try_into().unwrap()); - let s_limbs = p521_sec1::limbs_from_be_bytes(&sig[66..132].try_into().unwrap()); - if !in_range(&r_limbs) || !in_range(&s_limbs) { - return Err(SignatureError::SignatureVerificationFailed); - } - let r = P521ScalarField::from_limbs(r_limbs); + verify_final(self.hash, &pk, sig) + } +} - let mut h = [0u8; 64]; - self.hash.do_final_out(&mut h); - let e = e_from_hash(&h); - - // `s` is part of the signature, so its inverse is taken in variable time on the public - // scalar type; the constant-time `invert` is for the signer's `k`. - let s_inv = P521PublicScalar::from_limbs(s_limbs).invert_vartime(); - let u = e.mul(&s_inv); - let v = r.mul(&s_inv); - - let q = P521JacobianPoint::from_affine(pk.x, pk.y); - let u_pub = P521PublicScalar::from_limbs(u.to_limbs()); - let v_pub = P521PublicScalar::from_limbs(v.to_limbs()); - let r1_point = shamir_multiply(&u_pub, &v_pub, &q); - - if r1_point.is_infinity().to_bool() { - return Err(SignatureError::SignatureVerificationFailed); - } - let (x_r1, _) = - r1_point.to_affine().expect("just checked r1_point is not the point at infinity"); - let r1 = P521ScalarField::from_limbs(x_r1.to_limbs()); - - if r1 == r { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) } +impl SignatureVerifier for ECDetDSAP521 { + fn verify( + pk: &ECDSAP521PublicKey, + msg: &[u8], + ctx: Option<&[u8]>, + sig: &[u8], + ) -> Result<(), SignatureError> { + let mut v = Self::do_verify_init(pk, ctx)?; + v.do_verify_update(msg); + v.do_verify_final(sig) + } + + fn do_verify_init( + pk: &ECDSAP521PublicKey, + _ctx: Option<&[u8]>, + ) -> Result { + Ok(Self { hash: SHA512::default(), sk: None, pk: Some(*pk) }) + } + + fn do_verify_update(&mut self, msg_chunk: &[u8]) { + self.hash.do_update(msg_chunk); + } + + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + let pk = self.pk.ok_or(SignatureError::GenericError( + "do_verify_final on a sign-initialized ECDetDSAP521; call do_sign_final", + ))?; + verify_final(self.hash, &pk, sig) } } diff --git a/crypto/ecdsa/src/extra_bits_p256k1.rs b/crypto/ecdsa/src/extra_bits_p256k1.rs index 88871a42d..3b75bceed 100644 --- a/crypto/ecdsa/src/extra_bits_p256k1.rs +++ b/crypto/ecdsa/src/extra_bits_p256k1.rs @@ -17,7 +17,7 @@ //! Appendix A.3.1 is stricter than that bound, though. Its step 2 returns FAILURE if `t < 64` and //! its step 3 says: *"Obtain a bit string of N+t returned_bits from the established DRBG"*. That //! is a requirement on the per-message secret's draw independent of how close `n` is to a power -//! of two, and 256 bits does not meet it. So `sign_randomized` now draws `N + 64 = 320` bits (40 +//! of two, and 256 bits does not meet it. So `ECDSASecp256K1` now draws `N + 64 = 320` bits (40 //! bytes, [`crate::keys_p256k1::EXTRA_BITS_DRBG_OUTPUT_LEN`]) and reduces them with this module. //! Key generation draws the same 40 bytes: Appendix A.2.1's lengths are minimums, so the wider //! draw is permitted there too, and one reduction path per curve is simpler than two. The extra diff --git a/crypto/ecdsa/src/extra_bits_p384.rs b/crypto/ecdsa/src/extra_bits_p384.rs index 44b38f737..f7e4c79d1 100644 --- a/crypto/ecdsa/src/extra_bits_p384.rs +++ b/crypto/ecdsa/src/extra_bits_p384.rs @@ -17,7 +17,7 @@ //! Appendix A.3.1 is stricter than Table A.2, though. Its step 2 returns FAILURE if `t < 64` and //! its step 3 says: *"Obtain a bit string of N+t returned_bits from the established DRBG"*. That //! is a requirement on the per-message secret's draw independent of how close `n` is to a power -//! of two, and 384 bits does not meet it. So `sign_randomized` now draws `N + 64 = 448` bits (56 +//! of two, and 384 bits does not meet it. So `ECDSAP384` now draws `N + 64 = 448` bits (56 //! bytes, [`crate::keys_p384::EXTRA_BITS_DRBG_OUTPUT_LEN`]) and reduces them with this module. //! Key generation draws the same 56 bytes: Table A.2's 384 is a minimum ("not less than"), so //! the wider draw is permitted there too, and one reduction path per curve is simpler than two. diff --git a/crypto/ecdsa/src/keys_p521.rs b/crypto/ecdsa/src/keys_p521.rs index aa8ffe06d..fdc086b1e 100644 --- a/crypto/ecdsa/src/keys_p521.rs +++ b/crypto/ecdsa/src/keys_p521.rs @@ -15,7 +15,7 @@ //! than `n`'s 521 (SEC 1 octets are byte-aligned and 521 is not a multiple of 8), so a raw draw //! reinterpreted as a 9-limb integer can be as large as `2^528 - 1`, far past the `< 2(n-1)` a //! single conditional subtraction of `n - 1` assumes, and one subtraction silently leaves most of -//! the value unreduced (a bug this module's own tests caught: `keygen`/`sign_randomized` produced +//! the value unreduced (a bug this module's own tests caught: `keygen`/`ECDSAP521::sign` produced //! keys/signatures too broken to round-trip through sign+verify, despite every fixed-input KAT -- //! which never exercises a raw, unbounded DRBG value -- passing). [`reduce_wide_bits_mod_n_minus_1`] //! is this module's own reduction, a single fold at bit 521 -- see its docs for why P-521 gets @@ -239,7 +239,7 @@ pub fn keygen_from_rng( Ok((ECDSAP521PublicKey { x, y }, ECDSAP521PrivateKey(d))) } -// `reduce_wide_bits_mod_n_minus_1` is private and, through `keygen`/`sign_randomized`, only ever +// `reduce_wide_bits_mod_n_minus_1` is private and, through `keygen`/`ECDSAP521::sign`, only ever // reached on genuinely random DRBG output, so there is no fixed public-API input to pin a KAT // against -- the QUALITY_AND_STYLE.md private-function exception applies. Expected values // computed independently in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1`, not from @@ -251,7 +251,7 @@ mod tests { #[test] fn known_answer_all_ff_at_the_drbg_width() { - // 74 bytes of 0xff: the width keygen and sign_randomized actually draw. Expected value is + // 74 bytes of 0xff: the width keygen and ECDSAP521 actually draw. Expected value is // Python's ((2^592 - 1) % (n - 1)) + 1. let input = [0xffu8; EXTRA_BITS_DRBG_OUTPUT_LEN]; let expected: [u64; 9] = [ diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index 5a34147e1..db79f3a04 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -1,29 +1,53 @@ //! ECDSA over seven curves: key generation (FIPS 186-5 Appendix A.2.1), signature generation -//! (§6.4.1, deterministic per §6.3.2/Appendix A.3.3/RFC 6979 by default, randomised per -//! §6.3.1/Appendix A.3.1 as an additional API), and verification (§6.4.2), built on -//! [`bouncycastle_ec`]'s field/scalar/point arithmetic, SEC 1 encodings, and comb/wNAF -//! multipliers. Raw `r || s` is the default signature encoding; [`der`] offers DER `SEQUENCE { r, -//! s }` (RFC 3279 §2.2.3) as an alternative for interop that needs it. +//! (§6.4.1) and verification (§6.4.2), built on [`bouncycastle_ec`]'s field/scalar/point +//! arithmetic, SEC 1 encodings, and comb/wNAF multipliers. Each curve has two signer types that +//! differ only in where the per-message secret `k` comes from (§6.3): `ECDSA*` draws it at random +//! (§6.3.1, Appendix A.3.1), `ECDetDSA*` derives it from the private key and the message hash +//! (§6.3.2, Appendix A.3.3, RFC 6979). Both implement [`Signer`](bouncycastle_core::traits::Signer) +//! and [`SignatureVerifier`](bouncycastle_core::traits::SignatureVerifier), and since the variant +//! "does not impact the signature verification process" (FIPS 186-5 §6), a signature from either +//! verifies under either. Raw `r || s` is the default signature encoding; [`der`] offers DER +//! `SEQUENCE { r, s }` (RFC 3279 §2.2.3) as an alternative for interop that needs it. //! //! # Usage Examples //! -//! Signing is deterministic (RFC 6979), so the same key and message always produce the same -//! signature: +//! `ECDSAP256::sign` draws a fresh `k` from the library's default RNG, so two signatures of one +//! message differ; `ECDetDSAP256::sign` reproduces the same signature every time: //! //! ``` //! use bouncycastle_core::traits::{SignatureVerifier, Signer}; -//! use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +//! use bouncycastle_ecdsa::ecdsa_p256::{ECDSAP256, ECDetDSAP256}; //! use bouncycastle_ecdsa::keys::keygen; //! //! let (pk, sk) = keygen()?; //! let message = b"the message to sign"; //! //! // ECDSA has no context string; the trait's `ctx` parameter is accepted and ignored. -//! let signature = ECDSAP256::sign(&sk, message, None)?; -//! assert_eq!(signature, ECDSAP256::sign(&sk, message, None)?, "RFC 6979 is deterministic"); +//! let random = ECDSAP256::sign(&sk, message, None)?; +//! assert_ne!(random, ECDSAP256::sign(&sk, message, None)?, "a fresh k each time"); +//! +//! let deterministic = ECDetDSAP256::sign(&sk, message, None)?; +//! assert_eq!(deterministic, ECDetDSAP256::sign(&sk, message, None)?, "RFC 6979"); //! -//! ECDSAP256::verify(&pk, message, None, &signature)?; -//! assert!(ECDSAP256::verify(&pk, b"a different message", None, &signature).is_err()); +//! // One verifier for both. +//! ECDSAP256::verify(&pk, message, None, &random)?; +//! ECDSAP256::verify(&pk, message, None, &deterministic)?; +//! assert!(ECDSAP256::verify(&pk, b"a different message", None, &random).is_err()); +//! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) +//! ``` +//! +//! To draw `k` from an RNG of your own instead of the default, use `sign_rng` (or +//! `do_sign_final_rng` when streaming): +//! +//! ``` +//! use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +//! use bouncycastle_ecdsa::keys::keygen; +//! use bouncycastle_rng::DefaultRNG; +//! +//! let (_pk, sk) = keygen()?; +//! let mut rng = DefaultRNG::default(); +//! let signature = ECDSAP256::sign_rng(&sk, b"the message to sign", None, &mut rng)?; +//! # let _ = signature; //! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) //! ``` //! @@ -31,17 +55,17 @@ //! //! ``` //! use bouncycastle_core::traits::{SignatureVerifier, Signer}; -//! use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +//! use bouncycastle_ecdsa::ecdsa_p256::ECDetDSAP256; //! use bouncycastle_ecdsa::keys::keygen; //! //! let (pk, sk) = keygen()?; //! -//! let mut signer = ECDSAP256::do_sign_init(&sk, None)?; +//! let mut signer = ECDetDSAP256::do_sign_init(&sk, None)?; //! signer.do_sign_update(b"the first chunk, "); //! signer.do_sign_update(b"then the second"); //! let signature = signer.do_sign_final()?; //! -//! let mut verifier = ECDSAP256::do_verify_init(&pk, None)?; +//! let mut verifier = ECDetDSAP256::do_verify_init(&pk, None)?; //! verifier.do_verify_update(b"the first chunk, then the second"); //! verifier.do_verify_final(&signature)?; //! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) @@ -70,7 +94,8 @@ //! # Ok::<(), bouncycastle_core::errors::SignatureError>(()) //! ``` //! -//! For interop that needs DER `SEQUENCE { r, s }` rather than raw `r || s`: +//! For interop that needs DER `SEQUENCE { r, s }` rather than raw `r || s`, both signer types +//! have `sign_der`/`verify_der`: //! //! ``` //! use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; @@ -126,15 +151,19 @@ //! [`bouncycastle_ec`]'s own module docs for the constant-time discipline this crate builds on. //! - **`k` must never repeat across two signatures under the same key with different messages.** //! A repeated `k` leaks `d` directly from two signatures' `(r, s)` pairs (elementary algebra on -//! the ECDSA equation). This is why deterministic generation (RFC 6979) is the default here: `k` -//! is a function of `d` and the message hash, so the same `(d, message)` pair always regenerates -//! the same `k` rather than depending on a caller's RNG being correctly seeded every time. +//! the ECDSA equation). `ECDSA*` therefore depends on its RNG being correctly seeded for every +//! signature -- the library default, or whatever the caller hands `sign_rng`. `ECDetDSA*` +//! removes that dependency: `k` is a function of `d` and the message hash, so the same +//! `(d, message)` pair always regenerates the same `k` and distinct messages give distinct `k` +//! (FIPS 186-5 §6.3.2: "This protects against attacks arising from generating signatures with +//! insufficient randomness in the per-message secret number"). //! - **Deterministic signatures are a fault-attack target** (FIPS 186-5 §6, first paragraph): an //! attacker who can induce a computational fault and observe the faulty signature may be able to //! recover `d`, because a deterministic scheme will reproduce the exact same `k` on a retry. This //! is a property of determinism itself, not a defect in this implementation; see FIPS 186-5's //! references \[17\]-\[22\] for mitigations at the deployment level (e.g. verifying a signature -//! in-device before releasing it). +//! in-device before releasing it). Where that attacker is in the threat model and the RNG is +//! trusted, `ECDSA*` is the one to use. //! - **`ctx` is accepted but ignored.** ECDSA (FIPS 186-5, SEC 1) has no context-string input; //! `Signer`/`SignatureVerifier`'s `ctx` parameter is accepted for trait conformance and silently //! discarded, per the sanctioned behaviour `core::traits::Signer`'s own docs list ("ignore the diff --git a/crypto/ecdsa/tests/cavp_common/mod.rs b/crypto/ecdsa/tests/cavp_common/mod.rs index 9047a677b..4985fb366 100644 --- a/crypto/ecdsa/tests/cavp_common/mod.rs +++ b/crypto/ecdsa/tests/cavp_common/mod.rs @@ -43,7 +43,7 @@ pub fn get_test_data(filename: &str) -> String { /// An [`RNG`] that hands out one fixed big-endian value, zero-padded on the left to whatever width /// is asked for. This is how `ECDSA_SigGen.txt`'s per-message secret `k` is injected: every -/// curve's `sign_randomized` draws its DRBG bytes and reduces them per FIPS 186-5 Appendix A.4.1 +/// curve's `sign_rng` draws its DRBG bytes and reduces them per FIPS 186-5 Appendix A.4.1 /// (`x mod (n-1)`, then `+ 1`), so an RNG returning `k - 1` -- which is `< n - 1`, making the /// reduction the identity -- produces exactly `k`, through the public API and with no test-only /// hook in the crate. Only `next_bytes_out`/`next_bytes` are meaningful; nothing in the signing diff --git a/crypto/ecdsa/tests/cavp_p256.rs b/crypto/ecdsa/tests/cavp_p256.rs index e8088499a..5a301e2e4 100644 --- a/crypto/ecdsa/tests/cavp_p256.rs +++ b/crypto/ecdsa/tests/cavp_p256.rs @@ -129,7 +129,7 @@ fn cavp_keypair_consistency() { /// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the /// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form -/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_randomized` +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` /// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins /// signing output against NIST's values through the public API. RFC 6979's own vectors pin the /// deterministic path; this is the only NIST check on the randomised one. @@ -157,7 +157,7 @@ fn cavp_siggen() { .expect("CAVP Q must be a valid public key"); let mut rng = FixedBytesRng::new(k_minus_1); - let sig = ECDSAP256::sign_randomized(&sk, &msg, &mut rng) + let sig = ECDSAP256::sign_rng(&sk, &msg, None, &mut rng) .expect("signing with a valid key and k must succeed"); assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); ECDSAP256::verify(&pk, &msg, None, &sig).expect("NIST's own signature must verify"); diff --git a/crypto/ecdsa/tests/cavp_p384.rs b/crypto/ecdsa/tests/cavp_p384.rs index 99afb78a7..3e2ca8815 100644 --- a/crypto/ecdsa/tests/cavp_p384.rs +++ b/crypto/ecdsa/tests/cavp_p384.rs @@ -114,7 +114,7 @@ fn cavp_keypair_consistency() { /// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the /// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form -/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_randomized` +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` /// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins /// signing output against NIST's values through the public API. RFC 6979's own vectors pin the /// deterministic path; this is the only NIST check on the randomised one. @@ -142,7 +142,7 @@ fn cavp_siggen() { .expect("CAVP Q must be a valid public key"); let mut rng = FixedBytesRng::new(k_minus_1); - let sig = ECDSAP384::sign_randomized(&sk, &msg, &mut rng) + let sig = ECDSAP384::sign_rng(&sk, &msg, None, &mut rng) .expect("signing with a valid key and k must succeed"); assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); ECDSAP384::verify(&pk, &msg, None, &sig).expect("NIST's own signature must verify"); diff --git a/crypto/ecdsa/tests/cavp_p521.rs b/crypto/ecdsa/tests/cavp_p521.rs index f00f65542..6c76f5c72 100644 --- a/crypto/ecdsa/tests/cavp_p521.rs +++ b/crypto/ecdsa/tests/cavp_p521.rs @@ -115,7 +115,7 @@ fn cavp_keypair_consistency() { /// `ECDSA_SigGen.txt` -- NIST's `SigGen.txt` from `186-4ecdsatestvectors.zip`, the form of the /// SigGen vectors that carries `d` and the per-message secret `k`, which the response-file form -/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_randomized` +/// `ECDSA_SigGen.rsp` omits. `k` is injected through [`FixedBytesRng`] and `sign_rng` /// (see that type's docs for why an RNG returning `k - 1` yields exactly `k`), so this pins /// signing output against NIST's values through the public API. RFC 6979's own vectors pin the /// deterministic path; this is the only NIST check on the randomised one. @@ -143,7 +143,7 @@ fn cavp_siggen() { .expect("CAVP Q must be a valid public key"); let mut rng = FixedBytesRng::new(k_minus_1); - let sig = ECDSAP521::sign_randomized(&sk, &msg, &mut rng) + let sig = ECDSAP521::sign_rng(&sk, &msg, None, &mut rng) .expect("signing with a valid key and k must succeed"); assert_eq!(sig, signature_bytes(&r, &s), "mismatch for record {:?}", record.fields); ECDSAP521::verify(&pk, &msg, None, &sig).expect("NIST's own signature must verify"); diff --git a/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs index 46f01c68c..40e8ab87c 100644 --- a/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_bp256r1_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_bp256r1::ECDSABp256r1; +use bouncycastle_ecdsa::ecdsa_bp256r1::{ECDSABp256r1, ECDetDSABp256r1}; use bouncycastle_ecdsa::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey, keygen}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_rng::DefaultRNG; @@ -18,17 +18,39 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } -/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so `sign` always -/// reproduces the same `(r, s)`; ECDSA has no `ctx` input at all, so it ignores the one the -/// [`Signer`]/[`SignatureVerifier`] traits offer. +/// [`ECDSABp256r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { + let framework = TestFrameworkSignature::new(false, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSABp256r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { let framework = TestFrameworkSignature::new(true, false); - framework - .test_signature::( - keygen, - true, - ); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSABp256r1::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSABp256r1::sign(&sk, msg, None).unwrap(); + ECDetDSABp256r1::verify(&pk, msg, None, &random).unwrap(); + ECDSABp256r1::verify(&pk, msg, None, &deterministic).unwrap(); } #[test] @@ -38,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSABp256r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSABp256r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSABp256r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSABp256r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSABp256r1::verify(&pk, msg, None, &sig1).unwrap(); @@ -51,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSABp256r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSABp256r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSABp256r1::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -67,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSABp256r1::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSABp256r1::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSABp256r1::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSABp256r1::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 32]; let mut s_out = [0u8; 32]; @@ -77,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); - ECDSABp256r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp256r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSABp256r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSABp256r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSABp256r1::sign_der(&sk, msg, None).unwrap(); + ECDSABp256r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp256r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -312,9 +343,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSABp256r1` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSABp256r1` (or `ECDetDSABp256r1`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -334,3 +365,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSABp256r1::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSABp256r1::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 64]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs index e73a98914..a604384dd 100644 --- a/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_bp384r1_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_bp384r1::ECDSABp384r1; +use bouncycastle_ecdsa::ecdsa_bp384r1::{ECDSABp384r1, ECDetDSABp384r1}; use bouncycastle_ecdsa::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey, keygen}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_rng::DefaultRNG; @@ -18,17 +18,39 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } -/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so `sign` always -/// reproduces the same `(r, s)`; ECDSA has no `ctx` input at all, so it ignores the one the -/// [`Signer`]/[`SignatureVerifier`] traits offer. +/// [`ECDSABp384r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { + let framework = TestFrameworkSignature::new(false, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSABp384r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { let framework = TestFrameworkSignature::new(true, false); - framework - .test_signature::( - keygen, - true, - ); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSABp384r1::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSABp384r1::sign(&sk, msg, None).unwrap(); + ECDetDSABp384r1::verify(&pk, msg, None, &random).unwrap(); + ECDSABp384r1::verify(&pk, msg, None, &deterministic).unwrap(); } #[test] @@ -38,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSABp384r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSABp384r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSABp384r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSABp384r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSABp384r1::verify(&pk, msg, None, &sig1).unwrap(); @@ -51,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSABp384r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSABp384r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSABp384r1::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -67,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSABp384r1::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSABp384r1::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSABp384r1::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSABp384r1::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 48]; let mut s_out = [0u8; 48]; @@ -77,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..48], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[48..], "DER-decoded s must match sign's raw s"); - ECDSABp384r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp384r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSABp384r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSABp384r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSABp384r1::sign_der(&sk, msg, None).unwrap(); + ECDSABp384r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp384r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -312,9 +343,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSABp384r1` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSABp384r1` (or `ECDetDSABp384r1`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -334,3 +365,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSABp384r1::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSABp384r1::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 96]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs b/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs index 21ac4e371..860f27ee5 100644 --- a/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_bp512r1_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_bp512r1::ECDSABp512r1; +use bouncycastle_ecdsa::ecdsa_bp512r1::{ECDSABp512r1, ECDetDSABp512r1}; use bouncycastle_ecdsa::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey, keygen}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_rng::DefaultRNG; @@ -18,17 +18,39 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } -/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so `sign` always -/// reproduces the same `(r, s)`; ECDSA has no `ctx` input at all, so it ignores the one the -/// [`Signer`]/[`SignatureVerifier`] traits offer. +/// [`ECDSABp512r1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { + let framework = TestFrameworkSignature::new(false, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSABp512r1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { let framework = TestFrameworkSignature::new(true, false); - framework - .test_signature::( - keygen, - true, - ); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSABp512r1::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSABp512r1::sign(&sk, msg, None).unwrap(); + ECDetDSABp512r1::verify(&pk, msg, None, &random).unwrap(); + ECDSABp512r1::verify(&pk, msg, None, &deterministic).unwrap(); } #[test] @@ -38,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSABp512r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSABp512r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSABp512r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSABp512r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSABp512r1::verify(&pk, msg, None, &sig1).unwrap(); @@ -51,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSABp512r1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSABp512r1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSABp512r1::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -67,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSABp512r1::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSABp512r1::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSABp512r1::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSABp512r1::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 64]; let mut s_out = [0u8; 64]; @@ -77,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..64], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[64..], "DER-decoded s must match sign's raw s"); - ECDSABp512r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp512r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSABp512r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSABp512r1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSABp512r1::sign_der(&sk, msg, None).unwrap(); + ECDSABp512r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSABp512r1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -312,9 +343,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSABp512r1` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSABp512r1` (or `ECDetDSABp512r1`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -334,3 +365,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSABp512r1::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSABp512r1::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 128]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_p256_tests.rs b/crypto/ecdsa/tests/ecdsa_p256_tests.rs index 817e87709..b1f8b31d3 100644 --- a/crypto/ecdsa/tests/ecdsa_p256_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_p256_tests.rs @@ -1,7 +1,7 @@ //! Behavioural tests for [`ECDSAP256`]: the shared `core-test-framework` conformance suite (per //! QUALITY_AND_STYLE's rule that trait conformance lives there, not per-implementation), key //! encode/decode boundary conditions, and this crate's own additions the generic suite doesn't -//! cover -- the randomised (Appendix A.3.1) signing path and public-key encoding round trips. +//! cover -- the `sign_rng` path and public-key encoding round trips. use bouncycastle_core::errors::SignatureError; use bouncycastle_core::traits::{ @@ -12,7 +12,7 @@ use bouncycastle_core_test_framework::signature::{ }; use bouncycastle_ec::p256_scalar::N_LIMBS; use bouncycastle_ec::p256_sec1::be_bytes_from_limbs; -use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +use bouncycastle_ecdsa::ecdsa_p256::{ECDSAP256, ECDetDSAP256}; use bouncycastle_ecdsa::keys::{ECDSAP256PrivateKey, ECDSAP256PublicKey, keygen}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_rng::DefaultRNG; @@ -23,18 +23,41 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } -/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so `sign` always -/// reproduces the same `(r, s)`; ECDSA has no `ctx` input at all, so it ignores the one the -/// [`Signer`]/[`SignatureVerifier`] traits offer. +/// [`ECDSAP256`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { - let framework = TestFrameworkSignature::new(true, false); + let framework = TestFrameworkSignature::new(false, false); framework.test_signature::( keygen, true, ); } +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSAP256`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(true, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSAP256::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSAP256::sign(&sk, msg, None).unwrap(); + ECDetDSAP256::verify(&pk, msg, None, &random).unwrap(); + ECDSAP256::verify(&pk, msg, None, &deterministic).unwrap(); +} + #[test] fn key_boundary_conditions() { TestFrameworkSignatureKeys::new() @@ -42,12 +65,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSAP256::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSAP256::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSAP256::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSAP256::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSAP256::verify(&pk, msg, None, &sig1).unwrap(); @@ -55,11 +78,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSAP256::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSAP256::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSAP256::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -71,8 +94,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSAP256::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSAP256::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSAP256::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSAP256::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 32]; let mut s_out = [0u8; 32]; @@ -81,16 +104,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); - ECDSAP256::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP256::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSAP256::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSAP256::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSAP256::sign_der(&sk, msg, None).unwrap(); + ECDSAP256::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP256::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -317,9 +349,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSAP256` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSAP256` (or `ECDetDSAP256`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -339,3 +371,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSAP256::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSAP256::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 64]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs b/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs index 847933d31..5fba8adb7 100644 --- a/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_p256k1_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_p256k1::ECDSASecp256K1; +use bouncycastle_ecdsa::ecdsa_p256k1::{ECDSASecp256K1, ECDetDSASecp256K1}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_ecdsa::keys_p256k1::{ECDSASecp256K1PrivateKey, ECDSASecp256K1PublicKey, keygen}; use bouncycastle_rng::DefaultRNG; @@ -18,17 +18,39 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } -/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so `sign` always -/// reproduces the same `(r, s)`; ECDSA has no `ctx` input at all, so it ignores the one the -/// [`Signer`]/[`SignatureVerifier`] traits offer. +/// [`ECDSASecp256K1`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { + let framework = TestFrameworkSignature::new(false, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSASecp256K1`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { let framework = TestFrameworkSignature::new(true, false); - framework - .test_signature::( - keygen, - true, - ); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSASecp256K1::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSASecp256K1::sign(&sk, msg, None).unwrap(); + ECDetDSASecp256K1::verify(&pk, msg, None, &random).unwrap(); + ECDSASecp256K1::verify(&pk, msg, None, &deterministic).unwrap(); } #[test] @@ -38,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSASecp256K1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSASecp256K1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSASecp256K1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSASecp256K1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSASecp256K1::verify(&pk, msg, None, &sig1).unwrap(); @@ -51,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSASecp256K1::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSASecp256K1::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSASecp256K1::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -67,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSASecp256K1::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSASecp256K1::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSASecp256K1::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSASecp256K1::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 32]; let mut s_out = [0u8; 32]; @@ -77,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..32], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[32..], "DER-decoded s must match sign's raw s"); - ECDSASecp256K1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSASecp256K1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSASecp256K1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSASecp256K1::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSASecp256K1::sign_der(&sk, msg, None).unwrap(); + ECDSASecp256K1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSASecp256K1::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -310,9 +341,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSASecp256K1` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSASecp256K1` (or `ECDetDSASecp256K1`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -332,3 +363,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSASecp256K1::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSASecp256K1::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 64]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_p384_tests.rs b/crypto/ecdsa/tests/ecdsa_p384_tests.rs index 73668e42d..da258d56d 100644 --- a/crypto/ecdsa/tests/ecdsa_p384_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_p384_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_p384::ECDSAP384; +use bouncycastle_ecdsa::ecdsa_p384::{ECDSAP384, ECDetDSAP384}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_ecdsa::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey, keygen}; use bouncycastle_rng::DefaultRNG; @@ -18,15 +18,41 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } +/// [`ECDSAP384`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { - let framework = TestFrameworkSignature::new(true, false); + let framework = TestFrameworkSignature::new(false, false); framework.test_signature::( keygen, true, ); } +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSAP384`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(true, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSAP384::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSAP384::sign(&sk, msg, None).unwrap(); + ECDetDSAP384::verify(&pk, msg, None, &random).unwrap(); + ECDSAP384::verify(&pk, msg, None, &deterministic).unwrap(); +} + #[test] fn key_boundary_conditions() { TestFrameworkSignatureKeys::new() @@ -34,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSAP384::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSAP384::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSAP384::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSAP384::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSAP384::verify(&pk, msg, None, &sig1).unwrap(); @@ -47,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSAP384::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSAP384::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSAP384::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -63,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSAP384::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSAP384::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSAP384::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSAP384::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 48]; let mut s_out = [0u8; 48]; @@ -73,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..48], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[48..], "DER-decoded s must match sign's raw s"); - ECDSAP384::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP384::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSAP384::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSAP384::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSAP384::sign_der(&sk, msg, None).unwrap(); + ECDSAP384::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP384::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -302,9 +337,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSAP384` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSAP384` (or `ECDetDSAP384`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -324,3 +359,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSAP384::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSAP384::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 96]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/ecdsa_p521_tests.rs b/crypto/ecdsa/tests/ecdsa_p521_tests.rs index c05e0a24e..965aee44d 100644 --- a/crypto/ecdsa/tests/ecdsa_p521_tests.rs +++ b/crypto/ecdsa/tests/ecdsa_p521_tests.rs @@ -7,7 +7,7 @@ use bouncycastle_core::traits::{ use bouncycastle_core_test_framework::signature::{ TestFrameworkSignature, TestFrameworkSignatureKeys, }; -use bouncycastle_ecdsa::ecdsa_p521::ECDSAP521; +use bouncycastle_ecdsa::ecdsa_p521::{ECDSAP521, ECDetDSAP521}; use bouncycastle_ecdsa::keys_common::DerivePublicKey; use bouncycastle_ecdsa::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey, keygen}; use bouncycastle_rng::DefaultRNG; @@ -18,15 +18,41 @@ fn derive_pk_matches_keygen() { assert_eq!(sk.derive_pk(), pk); } +/// [`ECDSAP521`] draws a fresh `k` per signature (FIPS 186-5 Appendix A.3.1), so the suite expects +/// two signatures of one message to differ. ECDSA has no `ctx` input at all, so it ignores the one +/// the [`Signer`]/[`SignatureVerifier`] traits offer. #[test] fn conformance_suite() { - let framework = TestFrameworkSignature::new(true, false); + let framework = TestFrameworkSignature::new(false, false); framework.test_signature::( keygen, true, ); } +/// RFC 6979 §3.4/§3.6: `k` is a deterministic function of `d` and the message, so [`ECDetDSAP521`] +/// always reproduces the same `(r, s)`. +#[test] +fn deterministic_conformance_suite() { + let framework = TestFrameworkSignature::new(true, false); + framework.test_signature::( + keygen, + true, + ); +} + +/// FIPS 186-5 §6: Deterministic ECDSA "does not impact the signature verification process", so +/// either type's signature verifies under the other. +#[test] +fn signatures_verify_across_the_two_types() { + let (pk, sk) = keygen().unwrap(); + let msg = b"one verifier for both kinds of k"; + let random = ECDSAP521::sign(&sk, msg, None).unwrap(); + let deterministic = ECDetDSAP521::sign(&sk, msg, None).unwrap(); + ECDetDSAP521::verify(&pk, msg, None, &random).unwrap(); + ECDSAP521::verify(&pk, msg, None, &deterministic).unwrap(); +} + #[test] fn key_boundary_conditions() { TestFrameworkSignatureKeys::new() @@ -34,12 +60,12 @@ fn key_boundary_conditions() { } #[test] -fn sign_randomized_produces_distinct_valid_signatures() { +fn sign_rng_produces_distinct_valid_signatures() { let (pk, sk) = keygen().unwrap(); let msg = b"randomised ECDSA signing"; - let sig1 = ECDSAP521::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); - let sig2 = ECDSAP521::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig1 = ECDSAP521::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); + let sig2 = ECDSAP521::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); assert_ne!(sig1, sig2, "Appendix A.3.1's k comes from fresh randomness each call"); ECDSAP521::verify(&pk, msg, None, &sig1).unwrap(); @@ -47,11 +73,11 @@ fn sign_randomized_produces_distinct_valid_signatures() { } #[test] -fn sign_randomized_signature_rejected_under_wrong_key() { +fn sign_rng_signature_rejected_under_wrong_key() { let (_, sk) = keygen().unwrap(); let (other_pk, _) = keygen().unwrap(); let msg = b"message"; - let sig = ECDSAP521::sign_randomized(&sk, msg, &mut DefaultRNG::default()).unwrap(); + let sig = ECDSAP521::sign_rng(&sk, msg, None, &mut DefaultRNG::default()).unwrap(); match ECDSAP521::verify(&other_pk, msg, None, &sig) { Err(SignatureError::SignatureVerificationFailed) => {} other => panic!("expected SignatureVerificationFailed, got {other:?}"), @@ -63,8 +89,8 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { let (pk, sk) = keygen().unwrap(); let msg = b"DER-encoded ECDSA signing"; - let raw = ECDSAP521::sign(&sk, msg, None).unwrap(); - let (der, der_len) = ECDSAP521::sign_der(&sk, msg, None).unwrap(); + let raw = ECDetDSAP521::sign(&sk, msg, None).unwrap(); + let (der, der_len) = ECDetDSAP521::sign_der(&sk, msg, None).unwrap(); let mut r_out = [0u8; 66]; let mut s_out = [0u8; 66]; @@ -73,16 +99,25 @@ fn sign_der_matches_raw_sign_and_round_trips_through_verify_der() { assert_eq!(r_out, raw[..66], "DER-decoded r must match sign's raw r"); assert_eq!(s_out, raw[66..], "DER-decoded s must match sign's raw s"); - ECDSAP521::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP521::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); let mut tampered = der; tampered[der_len - 1] ^= 0xFF; assert!( - ECDSAP521::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), + ECDetDSAP521::verify_der(&pk, msg, None, &tampered[..der_len]).is_err(), "a corrupted DER signature must not verify" ); } +#[test] +fn sign_der_round_trips_through_either_verify_der() { + let (pk, sk) = keygen().unwrap(); + let msg = b"DER-encoded ECDSA signing, random k"; + let (der, der_len) = ECDSAP521::sign_der(&sk, msg, None).unwrap(); + ECDSAP521::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); + ECDetDSAP521::verify_der(&pk, msg, None, &der[..der_len]).unwrap(); +} + #[test] fn public_key_uncompressed_and_compressed_round_trip() { let (pk, _) = keygen().unwrap(); @@ -302,9 +337,9 @@ fn verify_rejects_r_or_s_outside_1_to_n_minus_1() { } } -/// One `ECDSAP521` value serves both the [`Signer`] and [`SignatureVerifier`] streaming APIs, holding -/// whichever key its `_init` was given. Finishing with the other trait's `_final` is a caller -/// error and must be reported as one, not panic and not produce output. +/// One `ECDSAP521` (or `ECDetDSAP521`) value serves both the [`Signer`] and [`SignatureVerifier`] +/// streaming APIs, holding whichever key its `_init` was given. Finishing with the other trait's +/// `_final` is a caller error and must be reported as one, not panic and not produce output. #[test] fn sign_final_on_verify_initialized_state_errors() { let (pk, _) = keygen().unwrap(); @@ -324,3 +359,12 @@ fn verify_final_on_sign_initialized_state_errors() { other => panic!("expected GenericError, got {other:?}"), } } + +#[test] +fn deterministic_state_misuse_errors() { + let (pk, sk) = keygen().unwrap(); + let v = ECDetDSAP521::do_verify_init(&pk, None).unwrap(); + assert!(matches!(v.do_sign_final(), Err(SignatureError::GenericError(_)))); + let s = ECDetDSAP521::do_sign_init(&sk, None).unwrap(); + assert!(matches!(s.do_verify_final(&[0u8; 132]), Err(SignatureError::GenericError(_)))); +} diff --git a/crypto/ecdsa/tests/extra_bits_bp256r1_tests.rs b/crypto/ecdsa/tests/extra_bits_bp256r1_tests.rs index 385c5e74d..5ee3c62a4 100644 --- a/crypto/ecdsa/tests/extra_bits_bp256r1_tests.rs +++ b/crypto/ecdsa/tests/extra_bits_bp256r1_tests.rs @@ -2,7 +2,7 @@ //! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently //! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from RFC 5639 §3.4, not //! derived from this crate's own reduction code -- existing round-trip tests elsewhere -//! (`keygen`/`sign_randomized`) only ever check that the reduced value is self-consistent with +//! (`keygen`/`sign_rng`) only ever check that the reduced value is self-consistent with //! itself, which can't distinguish a correct reduction from a systematically wrong one. This is //! exactly what a `cargo mutants` run on `reduce_wide_bits_mod_n_minus_1` and its //! `N_MINUS_1_LIMBS` constant surfaced: several bit-level and off-by-one mutants survived because diff --git a/crypto/ecdsa/tests/extra_bits_tests.rs b/crypto/ecdsa/tests/extra_bits_tests.rs index cf51492b6..d9afb4a22 100644 --- a/crypto/ecdsa/tests/extra_bits_tests.rs +++ b/crypto/ecdsa/tests/extra_bits_tests.rs @@ -2,7 +2,7 @@ //! 3-5): `x = bits2int(X)`, `x = x mod (n-1)`, `x = x + 1`. Expected values computed independently //! in Python via `(int.from_bytes(bytes, 'big') % (n - 1)) + 1` with `n` from SP 800-186 §3.2.1.3, //! not derived from this crate's own reduction code -- existing round-trip tests elsewhere -//! (`keygen`/`sign_randomized`) only ever check that the reduced value is self-consistent with +//! (`keygen`/`sign_rng`) only ever check that the reduced value is self-consistent with //! itself, which can't distinguish a correct reduction from a systematically wrong one (this is //! exactly what a `cargo mutants` run on `reduce_wide_bits_mod_n_minus_1`'s bit-extraction and //! carry-correction surfaced: several bit-level mutants survived because nothing here pinned the diff --git a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs index a12409d3d..152bccba2 100644 --- a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs @@ -12,7 +12,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::bp256r1_comb::comb_multiply_base_point; use bouncycastle_ec::bp256r1_scalar::Bp256r1Scalar; -use bouncycastle_ecdsa::ecdsa_bp256r1::ECDSABp256r1; +use bouncycastle_ecdsa::ecdsa_bp256r1::ECDetDSABp256r1; use bouncycastle_ecdsa::keys_bp256r1::{ECDSABp256r1PrivateKey, ECDSABp256r1PublicKey}; use bouncycastle_ecdsa::rfc6979_bp256r1::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -127,7 +127,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { for v in CASES { let sk = ECDSABp256r1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); - let sig = ECDSABp256r1::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSABp256r1::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 64]; expected[..32].copy_from_slice(&bytes32(v.r)); expected[32..].copy_from_slice(&bytes32(v.s)); @@ -140,7 +140,7 @@ fn deterministic_signature_verifies() { for v in CASES { let sk = ECDSABp256r1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); let pk = public_key_for(v); - let sig = ECDSABp256r1::sign(&sk, v.msg, None).unwrap(); - ECDSABp256r1::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSABp256r1::sign(&sk, v.msg, None).unwrap(); + ECDetDSABp256r1::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs index baf2fc7be..dcc84e0ae 100644 --- a/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp384r1_vectors_tests.rs @@ -11,7 +11,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::bp384r1_comb::comb_multiply_base_point; use bouncycastle_ec::bp384r1_scalar::Bp384r1Scalar; -use bouncycastle_ecdsa::ecdsa_bp384r1::ECDSABp384r1; +use bouncycastle_ecdsa::ecdsa_bp384r1::ECDetDSABp384r1; use bouncycastle_ecdsa::keys_bp384r1::{ECDSABp384r1PrivateKey, ECDSABp384r1PublicKey}; use bouncycastle_ecdsa::rfc6979_bp384r1::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -126,7 +126,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { for v in CASES { let sk = ECDSABp384r1PrivateKey::from_bytes(&bytes48(v.d)).unwrap(); - let sig = ECDSABp384r1::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSABp384r1::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 96]; expected[..48].copy_from_slice(&bytes48(v.r)); expected[48..].copy_from_slice(&bytes48(v.s)); @@ -139,7 +139,7 @@ fn deterministic_signature_verifies() { for v in CASES { let sk = ECDSABp384r1PrivateKey::from_bytes(&bytes48(v.d)).unwrap(); let pk = public_key_for(v); - let sig = ECDSABp384r1::sign(&sk, v.msg, None).unwrap(); - ECDSABp384r1::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSABp384r1::sign(&sk, v.msg, None).unwrap(); + ECDetDSABp384r1::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs index 3b815107f..d462e927b 100644 --- a/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_bp512r1_vectors_tests.rs @@ -11,7 +11,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::bp512r1_comb::comb_multiply_base_point; use bouncycastle_ec::bp512r1_scalar::Bp512r1Scalar; -use bouncycastle_ecdsa::ecdsa_bp512r1::ECDSABp512r1; +use bouncycastle_ecdsa::ecdsa_bp512r1::ECDetDSABp512r1; use bouncycastle_ecdsa::keys_bp512r1::{ECDSABp512r1PrivateKey, ECDSABp512r1PublicKey}; use bouncycastle_ecdsa::rfc6979_bp512r1::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -126,7 +126,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { for v in CASES { let sk = ECDSABp512r1PrivateKey::from_bytes(&bytes64(v.d)).unwrap(); - let sig = ECDSABp512r1::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSABp512r1::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 128]; expected[..64].copy_from_slice(&bytes64(v.r)); expected[64..].copy_from_slice(&bytes64(v.s)); @@ -139,7 +139,7 @@ fn deterministic_signature_verifies() { for v in CASES { let sk = ECDSABp512r1PrivateKey::from_bytes(&bytes64(v.d)).unwrap(); let pk = public_key_for(v); - let sig = ECDSABp512r1::sign(&sk, v.msg, None).unwrap(); - ECDSABp512r1::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSABp512r1::sign(&sk, v.msg, None).unwrap(); + ECDetDSABp512r1::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs index 3348bdecb..60e041230 100644 --- a/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_p256k1_vectors_tests.rs @@ -12,7 +12,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::p256k1_comb::comb_multiply_base_point; use bouncycastle_ec::p256k1_scalar::P256K1Scalar; -use bouncycastle_ecdsa::ecdsa_p256k1::ECDSASecp256K1; +use bouncycastle_ecdsa::ecdsa_p256k1::ECDetDSASecp256K1; use bouncycastle_ecdsa::keys_p256k1::{ECDSASecp256K1PrivateKey, ECDSASecp256K1PublicKey}; use bouncycastle_ecdsa::rfc6979_p256k1::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -127,7 +127,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { for v in CASES { let sk = ECDSASecp256K1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); - let sig = ECDSASecp256K1::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSASecp256K1::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 64]; expected[..32].copy_from_slice(&bytes32(v.r)); expected[32..].copy_from_slice(&bytes32(v.s)); @@ -140,7 +140,7 @@ fn deterministic_signature_verifies() { for v in CASES { let sk = ECDSASecp256K1PrivateKey::from_bytes(&bytes32(v.d)).unwrap(); let pk = public_key_for(v); - let sig = ECDSASecp256K1::sign(&sk, v.msg, None).unwrap(); - ECDSASecp256K1::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSASecp256K1::sign(&sk, v.msg, None).unwrap(); + ECDetDSASecp256K1::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs index 5c022d524..3755f850a 100644 --- a/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_p384_vectors_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::p384_comb::comb_multiply_base_point; use bouncycastle_ec::p384_scalar::P384Scalar; -use bouncycastle_ecdsa::ecdsa_p384::ECDSAP384; +use bouncycastle_ecdsa::ecdsa_p384::ECDetDSAP384; use bouncycastle_ecdsa::keys_p384::{ECDSAP384PrivateKey, ECDSAP384PublicKey}; use bouncycastle_ecdsa::rfc6979_p384::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -70,7 +70,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { let sk = private_key(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP384::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSAP384::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 96]; expected[..48].copy_from_slice(&bytes48(v.r)); expected[48..].copy_from_slice(&bytes48(v.s)); @@ -88,7 +88,7 @@ fn deterministic_signature_verifies() { let pk = ECDSAP384PublicKey::from_bytes(&uncompressed).unwrap(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP384::sign(&sk, v.msg, None).unwrap(); - ECDSAP384::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSAP384::sign(&sk, v.msg, None).unwrap(); + ECDetDSAP384::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs index 3a0111a82..07c70ffe8 100644 --- a/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_p521_vectors_tests.rs @@ -11,7 +11,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::p521_comb::comb_multiply_base_point; use bouncycastle_ec::p521_scalar::P521Scalar; -use bouncycastle_ecdsa::ecdsa_p521::ECDSAP521; +use bouncycastle_ecdsa::ecdsa_p521::ECDetDSAP521; use bouncycastle_ecdsa::keys_p521::{ECDSAP521PrivateKey, ECDSAP521PublicKey}; use bouncycastle_ecdsa::rfc6979_p521::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -73,7 +73,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { let sk = private_key(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP521::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSAP521::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 132]; expected[..66].copy_from_slice(&bytes66(v.r)); expected[66..].copy_from_slice(&bytes66(v.s)); @@ -91,7 +91,7 @@ fn deterministic_signature_verifies() { let pk = ECDSAP521PublicKey::from_bytes(&uncompressed).unwrap(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP521::sign(&sk, v.msg, None).unwrap(); - ECDSAP521::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSAP521::sign(&sk, v.msg, None).unwrap(); + ECDetDSAP521::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/ecdsa/tests/rfc6979_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_vectors_tests.rs index 90829707a..a5dfcfd3a 100644 --- a/crypto/ecdsa/tests/rfc6979_vectors_tests.rs +++ b/crypto/ecdsa/tests/rfc6979_vectors_tests.rs @@ -8,7 +8,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_ec::p256_comb::comb_multiply_base_point; use bouncycastle_ec::p256_scalar::P256Scalar; -use bouncycastle_ecdsa::ecdsa_p256::ECDSAP256; +use bouncycastle_ecdsa::ecdsa_p256::ECDetDSAP256; use bouncycastle_ecdsa::keys::{ECDSAP256PrivateKey, ECDSAP256PublicKey}; use bouncycastle_ecdsa::rfc6979::generate_k; use bouncycastle_hex::decode as hex_decode; @@ -70,7 +70,7 @@ fn generate_k_matches_vectors() { fn deterministic_sign_matches_vectors() { let sk = private_key(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP256::sign(&sk, v.msg, None).unwrap(); + let sig = ECDetDSAP256::sign(&sk, v.msg, None).unwrap(); let mut expected = [0u8; 64]; expected[..32].copy_from_slice(&bytes32(v.r)); expected[32..].copy_from_slice(&bytes32(v.s)); @@ -88,7 +88,7 @@ fn deterministic_signature_verifies() { let pk = ECDSAP256PublicKey::from_bytes(&uncompressed).unwrap(); for v in [&SAMPLE, &TEST] { - let sig = ECDSAP256::sign(&sk, v.msg, None).unwrap(); - ECDSAP256::verify(&pk, v.msg, None, &sig).unwrap(); + let sig = ECDetDSAP256::sign(&sk, v.msg, None).unwrap(); + ECDetDSAP256::verify(&pk, v.msg, None, &sig).unwrap(); } } diff --git a/crypto/sm2/src/sm2.rs b/crypto/sm2/src/sm2.rs index 571cbbc5d..ebad5d815 100644 --- a/crypto/sm2/src/sm2.rs +++ b/crypto/sm2/src/sm2.rs @@ -22,9 +22,8 @@ //! `bouncycastle-ecdsa`) there is no deterministic default here. [`bouncycastle_core::traits::Signer`]'s //! own docs say its trait is "assumed to source all its randomness from bouncycastle's default //! os-backed RNG", so [`Signer::sign`]/[`Signer::do_sign_final`] do exactly that (via [`bouncycastle_rng::DefaultRNG`]); -//! [`SM2::sign_randomized`] is offered alongside for a caller-supplied RNG, the same "additional -//! capability" shape `bouncycastle-ecdsa`'s curves offer for their own randomised alternative to -//! RFC 6979. +//! [`SM2::sign_randomized`] is offered alongside for a caller-supplied RNG, as `bouncycastle-ecdsa`'s +//! randomised signers offer `sign_rng`. //! //! # Retry convention //! diff --git a/mem_usage_benches/src/bench_ecdsa_mem_usage.rs b/mem_usage_benches/src/bench_ecdsa_mem_usage.rs index 68fc2d489..4660f5573 100644 --- a/mem_usage_benches/src/bench_ecdsa_mem_usage.rs +++ b/mem_usage_benches/src/bench_ecdsa_mem_usage.rs @@ -29,9 +29,13 @@ use bouncycastle::core::traits::{ }; use bouncycastle::ecdsa::keys_common::DerivePublicKey; use bouncycastle::ecdsa::{ - ecdsa_bp256r1::ECDSABp256r1, ecdsa_bp384r1::ECDSABp384r1, ecdsa_bp512r1::ECDSABp512r1, - ecdsa_p256::ECDSAP256, ecdsa_p256k1::ECDSASecp256K1, ecdsa_p384::ECDSAP384, - ecdsa_p521::ECDSAP521, + ecdsa_bp256r1::{ECDSABp256r1, ECDetDSABp256r1}, + ecdsa_bp384r1::{ECDSABp384r1, ECDetDSABp384r1}, + ecdsa_bp512r1::{ECDSABp512r1, ECDetDSABp512r1}, + ecdsa_p256::{ECDSAP256, ECDetDSAP256}, + ecdsa_p256k1::{ECDSASecp256K1, ECDetDSASecp256K1}, + ecdsa_p384::{ECDSAP384, ECDetDSAP384}, + ecdsa_p521::{ECDSAP521, ECDetDSAP521}, }; use bouncycastle::ecdsa::{ keys as keys_p256, keys_bp256r1, keys_bp384r1, keys_bp512r1, keys_p256k1, keys_p384, keys_p521, @@ -140,6 +144,19 @@ fn bench_p256_sign() { println!("{:x?}", sig); } +fn bench_p256_sign_deterministic() { + eprintln!("ECDetDSA P-256/Sign"); + + // d = 42, well within [1, n-1] -- same fixed-key trick the crate's own tests use, so keygen's + // own stack cost is not folded into this measurement. + let mut bytes = [0u8; keys_p256::SK_LEN]; + bytes[keys_p256::SK_LEN - 1] = 0x2A; + let sk = keys_p256::ECDSAP256PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSAP256::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_p256_verify() { eprintln!("ECDSA P-256/Verify"); @@ -174,6 +191,17 @@ fn bench_p384_sign() { println!("{:x?}", sig); } +fn bench_p384_sign_deterministic() { + eprintln!("ECDetDSA P-384/Sign"); + + let mut bytes = [0u8; keys_p384::SK_LEN]; + bytes[keys_p384::SK_LEN - 1] = 0x2A; + let sk = keys_p384::ECDSAP384PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSAP384::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_p384_verify() { eprintln!("ECDSA P-384/Verify"); @@ -208,6 +236,17 @@ fn bench_p521_sign() { println!("{:x?}", sig); } +fn bench_p521_sign_deterministic() { + eprintln!("ECDetDSA P-521/Sign"); + + let mut bytes = [0u8; keys_p521::SK_LEN]; + bytes[keys_p521::SK_LEN - 1] = 0x2A; + let sk = keys_p521::ECDSAP521PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSAP521::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_p521_verify() { eprintln!("ECDSA P-521/Verify"); @@ -242,6 +281,17 @@ fn bench_secp256k1_sign() { println!("{:x?}", sig); } +fn bench_secp256k1_sign_deterministic() { + eprintln!("ECDetDSA secp256k1/Sign"); + + let mut bytes = [0u8; keys_p256k1::SK_LEN]; + bytes[keys_p256k1::SK_LEN - 1] = 0x2A; + let sk = keys_p256k1::ECDSASecp256K1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSASecp256K1::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_secp256k1_verify() { eprintln!("ECDSA secp256k1/Verify"); @@ -276,6 +326,17 @@ fn bench_bp256r1_sign() { println!("{:x?}", sig); } +fn bench_bp256r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP256r1/Sign"); + + let mut bytes = [0u8; keys_bp256r1::SK_LEN]; + bytes[keys_bp256r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp256r1::ECDSABp256r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSABp256r1::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_bp256r1_verify() { eprintln!("ECDSA brainpoolP256r1/Verify"); @@ -310,6 +371,17 @@ fn bench_bp384r1_sign() { println!("{:x?}", sig); } +fn bench_bp384r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP384r1/Sign"); + + let mut bytes = [0u8; keys_bp384r1::SK_LEN]; + bytes[keys_bp384r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp384r1::ECDSABp384r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSABp384r1::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_bp384r1_verify() { eprintln!("ECDSA brainpoolP384r1/Verify"); @@ -344,6 +416,17 @@ fn bench_bp512r1_sign() { println!("{:x?}", sig); } +fn bench_bp512r1_sign_deterministic() { + eprintln!("ECDetDSA brainpoolP512r1/Sign"); + + let mut bytes = [0u8; keys_bp512r1::SK_LEN]; + bytes[keys_bp512r1::SK_LEN - 1] = 0x2A; + let sk = keys_bp512r1::ECDSABp512r1PrivateKey::from_bytes(&bytes).unwrap(); + + let sig = ECDetDSABp512r1::sign(&sk, MSG, None).unwrap(); + println!("{:x?}", sig); +} + fn bench_bp512r1_verify() { eprintln!("ECDSA brainpoolP512r1/Verify"); @@ -365,23 +448,30 @@ fn main() { // bench_do_nothing() // bench_p256_keygen() // bench_p256_sign() + // bench_p256_sign_deterministic() // bench_p256_verify() // bench_p384_keygen() // bench_p384_sign() + // bench_p384_sign_deterministic() // bench_p384_verify() // bench_p521_keygen() // bench_p521_sign() + // bench_p521_sign_deterministic() // bench_p521_verify() // bench_secp256k1_keygen() // bench_secp256k1_sign() + // bench_secp256k1_sign_deterministic() // bench_secp256k1_verify() // bench_bp256r1_keygen() // bench_bp256r1_sign() + // bench_bp256r1_sign_deterministic() // bench_bp256r1_verify() // bench_bp384r1_keygen() // bench_bp384r1_sign() + // bench_bp384r1_sign_deterministic() // bench_bp384r1_verify() // bench_bp512r1_keygen() // bench_bp512r1_sign() + // bench_bp512r1_sign_deterministic() // bench_bp512r1_verify() } From b9d3e561a66255edc4a563b7919d47cae860bb21 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 1 Nov 2026 21:48:56 +0000 Subject: [PATCH 13/16] ec: drop the dead carry select in secp256k1's third reduction fold [For humans] - reduce()'s third fold re-added C and selected it on a carry-out that bound (3) proves is 0 - the second add and the select are gone; the carry is debug_asserted like the fold loop's own - the bounds proof stays in reduce(); the pinned product's construction lives only in the test file - no behaviour change: the borrow-selected value the live path already produced is now returned - ec tests green incl. the third-fold known answer; cargo mutants on p256k1.rs: 97 before and after - the 2 surviving mutants are documented equivalents on unchanged lines; reduce()'s own are caught ====================================================================== [For agents] p256k1::reduce folds a 512-bit product twice with 2^256 = p + C, then folds the one bit that can remain above 2^256 in under a mask. After that masked add of C it also computed folded + C a second time and selected it on carry1, the carry out of the masked add. Bound (3) in the same comment, lo_2 + C < 2^66 + 2^33 < p whenever the bit is set and nothing added when it is not, makes that carry identically zero, and the code's own comment said so ("never selected"). The add and the select were therefore dead code in the secret path: harmless for timing, but untestable, since no input makes the selected arm the result. cargo-mutants never saw it either: the two statements contain nothing it mutates (no operator, no function of this file), so the file lists the same 97 mutants before and after this change, which is why reasoning rather than a surviving mutant flagged it. The change: the second add and the second select are removed, the carry is debug_assert_eq!(carry, 0, ..) in the same style as the fold loop's carry assert and widening_square's two, and the borrow-selected value is returned directly. Release builds compile the assert out, so the function gains no branch on secret-derived data; in a debug build the assert can only fire if bound (3) is wrong. Behaviour is unchanged on every input: when the bit is set the old code's live path was already the borrow select of folded against p, and when it is not the fold adds zero. folded < 2^256 < 2p either way, so the single conditional subtraction suffices. The comment restructure, from the same review: the bounds (1)-(3) stay in reduce(), where (3) now also justifies the assert; the operand construction for the product that reaches the fold (a = 2^255, b = 2 * hi_0 with hi_0 * C = 2^256 - 1 mod 2^256, and the Python that computes it) and the argument that sampling never reaches it (about 2^-190 per product; (p-1)^2 falls short by C - 1) live once, in p256k1_field_tests.rs next to the pinned product. reduce() points at the test for the construction and the test at reduce() for the bounds, so each derivation has one home. The module docs no longer say reduce()'s comment gives the operands. Verification: cargo test -p bouncycastle-ec, 53 test binaries green including known_answer_mul_reaching_the_third_fold; cargo +nightly fmt --all --check clean. cargo mutants scoped to p256k1.rs (the checked-in config minus examine_globs, --test-package bouncycastle-ec, bc-test-data and wycheproof symlinked under /tmp): 97 mutants, 79 caught, 15 unviable, 2 missed, 1 timeout. reduce()'s two function-level mutants are caught. The two misses are on unchanged lines and are the equivalents their comments already accept: widening_mul's tail-loop `carry = s >> 64` mutated to `<<`, and widening_square's doubling `|` mutated to `^`. The timeout is widening_mul's tail-loop `k += 1` mutated to `k *= 1`, an infinite loop, so a kill rather than a false alarm. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/src/p256k1.rs | 34 ++++++++++----------------- crypto/ec/tests/p256k1_field_tests.rs | 5 ++-- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crypto/ec/src/p256k1.rs b/crypto/ec/src/p256k1.rs index 89c86a9fa..ad2d534fb 100644 --- a/crypto/ec/src/p256k1.rs +++ b/crypto/ec/src/p256k1.rs @@ -20,8 +20,8 @@ //! //! The masked third fold is not a formality. An earlier P-384 version of this same two-fold shape //! `debug_assert`ed that the bit was always `0`, which held for every random trial and was wrong: -//! wycheproof's `ecdsa_secp384r1` suite found a product that sets it. `reduce`'s comment gives -//! the operands that set it on this curve, and `p256k1_field_tests.rs` pins one such product. +//! wycheproof's `ecdsa_secp384r1` suite found a product that sets it. `p256k1_field_tests.rs` +//! constructs and pins a product that sets it on this curve. use crate::nat; use bouncycastle_utils::ct; @@ -305,15 +305,12 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { // (3) when `acc[4]` is `1`, `lo_2 = acc_2 - 2^256 < C^2`, so `lo_2 + C < 2^66 + 2^33 < p`: // folding the bit in can neither carry out of 256 bits nor produce a value `>= p`. // - // The bit is folded in explicitly rather than assumed to be `0`. It is reachable from - // canonical operands, but only by construction, not by sampling: by (2), fold 2 overflows iff - // `lo_1` lies within `hi_1 * C <= 2^66` of `2^256`, a probability of about `2^-190` for a - // pseudo-random product, which is why 2,000,000 random trials never reached it; `(p-1)*(p-1)` - // misses it too, by `C - 1`: its two folds leave exactly `p + 1`. `a = 2^255` with - // `b = 2 * ((2^256 - 1) * C^-1 mod 2^256)` does reach it: - // `a * b = hi_0 * 2^256` with `hi_0 * C ≡ 2^256 - 1 (mod 2^256)`, so `lo_1 = 2^256 - 1`. - // `p256k1_field_tests.rs` pins that product; without this fold its result is short by exactly - // `C`, which is the P-384 bug the module docs describe. + // The bit is folded in under a mask rather than assumed to be `0`: it is reachable from + // canonical operands, and `p256k1_field_tests.rs` constructs a product that sets it (the + // operands, and why sampling never finds them, are derived there). Without this fold that + // product's result is short by exactly `C`, the P-384 bug the module docs describe. By (3) + // the fold itself cannot carry, which the `debug_assert` pins, so `folded < 2^256 < 2p` and + // one conditional subtraction of `p` makes it canonical. let extra_bit = acc[4]; let low: [u64; 4] = [acc[0], acc[1], acc[2], acc[3]]; let mut c_or_zero = [0u64; 4]; @@ -323,17 +320,10 @@ fn reduce(t: &[u64; 8]) -> [u64; 4] { &[0u64; 4], &mut c_or_zero, ); - let (folded, carry1) = nat::add(&low, &c_or_zero); - let (folded_plus_c, _) = nat::add(&folded, &C_LIMBS); // never selected: carry1 == 0 by bound (3) - let (diff, borrow) = nat::sub(&folded, &P_LIMBS); // the live path: carry1 == 0 - let mut when_no_carry = [0u64; 4]; - ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut when_no_carry); + let (folded, carry) = nat::add(&low, &c_or_zero); + debug_assert_eq!(carry, 0, "secp256k1 third fold carried out of 256 bits"); + let (diff, borrow) = nat::sub(&folded, &P_LIMBS); let mut result = [0u64; 4]; - ct::conditional_select( - Condition::::from_lsb(carry1), - &folded_plus_c, - &when_no_carry, - &mut result, - ); + ct::conditional_select(Condition::::from_lsb(borrow), &folded, &diff, &mut result); result } diff --git a/crypto/ec/tests/p256k1_field_tests.rs b/crypto/ec/tests/p256k1_field_tests.rs index 79f48238e..b4b6db426 100644 --- a/crypto/ec/tests/p256k1_field_tests.rs +++ b/crypto/ec/tests/p256k1_field_tests.rs @@ -332,8 +332,9 @@ fn known_answer_mul_at_the_reduction_bounds() { } /// The one product shape the reduction-bound cases above cannot reach: a product whose two folds -/// leave the accumulator at or above `2^256`, so that `reduce`'s masked third fold (see its -/// comment in `p256k1.rs`) is the step that produces the right answer. None of the cases above +/// leave the accumulator at or above `2^256`, so that `reduce`'s masked third fold is the step +/// that produces the right answer (`reduce`'s comment in `p256k1.rs` derives the bounds: two folds +/// leave at most one bit above `2^256`, and folding it in cannot carry). None of the cases above /// reach it -- `(p-1)^2` gives the first fold a high half of `C - 1`, one short of its bound, and /// its two folds leave exactly `p + 1`, short of `2^256` by `C - 1` -- and neither does random /// sampling: it needs the low half after the first fold to lie within `hi_1 * C <= 2^66` of From 60bff452c5e3cf79422539c0596ad89d63528461 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 1 Nov 2026 21:48:56 +0000 Subject: [PATCH 14/16] ecdsa: fix the RFC 6979 attribution and give the brainpool rejection text one home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - four docs said RFC 6979 §3.4 calls candidate rejection "utterly improbable"; it says so of r = 0 - step h.3 attaches no probability to its loop; the parenthetical is dropped at all four sites - rfc6979_bp256r1 is the one home for the brainpool rejection text; bp384r1/bp512r1 point at it - the crate docs' timing bullet now names ECDetDSA*; ECDSA* draws k per FIPS 186-5 A.3.1, no loop - docs and comments only; cargo doc warning set identical before and after; all ecdsa suites green ====================================================================== [For agents] RFC 6979 uses "utterly improbable" twice, in §2.4 item 3 and in the §3.4 Usage Notes, both about r turning out to be 0 and a new k having to be selected. Step h.3 of §3.2 says only that bits2int's result is compared to q, not reduced modulo q, and that the process loops if the value is not between 1 and q-1; it makes no claim about how often. The three brainpool rfc6979 module docs attributed the phrase to the rejection loop and then explained it away with an assumption ("n close to 2^qlen") the RFC never states, and the P-256 module's #[cfg(test)] comment made the same misattribution. The RFC text was read first-hand on 2026-10-31 (rfc-editor.org is not reachable from the build container, so the quotes are from that reading, not from recall). rfc6979_bp256r1's module docs now carry the one correct sentence: no probability is attached to the loop, and the phrase is about r = 0, which this crate reports as an error rather than retrying. The P-256 test comment states the curve fact instead: 2^256 - n is about 2^224, so a uniform candidate is >= n with probability about 2^-32. generate_k's doc had "n is within 2^-32 of 2^256", which is loose (the absolute distance is about 2^224); it now says so directly. The brainpool rejection-loop text (rate, three HMAC calls per extra iteration, the fingerprint it amounts to for an observer) had been copied verbatim into all three generate_k docs, with a rate sentence plus pointer in each module doc and a rate recap in each test comment. Per QUALITY_AND_STYLE.md "Proportion" each fact gets one home: rfc6979_bp256r1::generate_k keeps the full text under a heading that names the brainpool curves, and its module doc keeps the normal-path-versus-near-dead-branch contrast; rfc6979_bp384r1 and rfc6979_bp512r1 state their own rate and mean in both places and link to bp256r1. The rate-independent PRF argument keeps its existing home in rfc6979::generate_k (P-256), which the brainpool text already cited. The three test-module comments shrink to the coverage statement (the reject path is reached from the rejecting cases in the vectors files; the boundaries 0 and n are what those cannot pin). The vectors files are untouched: their per-case rejection counts are file-local provenance and already point at the module docs. The crate docs listed "RFC 6979's candidate loop" as a timing exception without saying which signer it belongs to. ECDSA* draws k per FIPS 186-5 Appendix A.3.1 (extra random bits reduced mod n-1, plus 1), which has no rejection loop, so the bullet now names ECDetDSA*; the 33 % to 45 % summary stays as the crate-level one-liner, with pointers to the argument, the rates and the fingerprint text. Verification: cargo test -p bouncycastle-ecdsa, 33 test binaries green with the bc-test-data and wycheproof suites present (CAVP and wycheproof run, not skipped); the brainpool vectors suites with their rejection pins are untouched and pass. cargo doc --no-deps for ecdsa, ec and sm2 at 735fae31 and after this change emits the same 82 warnings, all pre-existing links to private items, so the new intra-doc links all resolve. cargo +nightly fmt --all --check clean. Docs and comments only; no code change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ecdsa/src/lib.rs | 6 ++++-- crypto/ecdsa/src/rfc6979.rs | 14 ++++++------ crypto/ecdsa/src/rfc6979_bp256r1.rs | 33 ++++++++++++++--------------- crypto/ecdsa/src/rfc6979_bp384r1.rs | 31 ++++++++++----------------- crypto/ecdsa/src/rfc6979_bp512r1.rs | 27 ++++++++--------------- 5 files changed, 47 insertions(+), 64 deletions(-) diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index ac3ca5363..a7e4a9dc7 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -150,12 +150,14 @@ //! [`bouncycastle_ec::p256_scalar::P256Scalar`] (a [`bouncycastle_utils::secret::Secret`]); see //! [`bouncycastle_ec`]'s own module docs for the constant-time discipline this crate builds on //! and the exceptions it documents. This crate adds two exceptions of its own: -//! - RFC 6979's candidate loop runs a data-dependent number of iterations. On the NIST curves +//! - `ECDetDSA*`'s RFC 6979 candidate loop runs a data-dependent number of iterations (`ECDSA*` +//! draws `k` per FIPS 186-5 Appendix A.3.1, which has no rejection loop). On the NIST curves //! and secp256k1 a candidate is rejected with negligible probability; on the brainpool curves //! 33 % to 45 % of signatures reject at least one. Under the PRF assumption the count is //! independent of the accepted `k` and reveals nothing about `d`, but it is deterministic per //! `(d, message)`, so a remote observer gets a per-message timing fingerprint. See -//! [`rfc6979::generate_k`]'s docs for the argument and the brainpool modules' for the rates. +//! [`rfc6979::generate_k`]'s docs for the argument, the brainpool modules' for the rates, and +//! [`rfc6979_bp256r1::generate_k`]'s for what the fingerprint is worth. //! - Signing returns an error on `r = 0` or `s = 0` instead of drawing another `k`; that branch //! is on a public outcome. //! - **`k` must never repeat across two signatures under the same key with different messages.** diff --git a/crypto/ecdsa/src/rfc6979.rs b/crypto/ecdsa/src/rfc6979.rs index 57e5a8271..016cfdb3c 100644 --- a/crypto/ecdsa/src/rfc6979.rs +++ b/crypto/ecdsa/src/rfc6979.rs @@ -111,9 +111,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// accepted `k` is `bits2int(HMAC_K'(V'))`. What must be constant time is everything subsequently /// done *with* the accepted `k`, which is the rest of this crate's concern. The same argument /// covers every curve in this crate; what differs is the rate. For P-256 the loop is close to dead: -/// `n` is within `2^-32` of `2^256`, so a candidate is rejected with probability about `2^-32`. The -/// brainpool modules, where it is a normal path, state their rates and what the count then amounts -/// to in practice. +/// `2^256 - n` is about `2^224`, so a candidate is rejected with probability about `2^-32`. The +/// brainpool modules, where it is a normal path, state their rates; +/// [`crate::rfc6979_bp256r1::generate_k`]'s docs say what the count then amounts to in practice. /// /// The loop rejects only out-of-range candidates. A `k` whose `r` would be `0` is not retried /// here: signing returns an error instead (see the crate docs' Security Considerations). @@ -152,10 +152,10 @@ pub fn generate_k(d: &P256Scalar, h1: &[u8; HLEN]) -> P256Scalar { } // `candidate_in_range` is private and, through `generate_k`'s rejection loop, only ever reached on -// candidates that -- for any real message/key input -- are essentially always in range (RFC 6979 -// §3.4: rejection is "utterly improbable"), so no realistic integration test exercises its reject -// path; the QUALITY_AND_STYLE.md private-function exception applies. Known answers: `0` and `n` -// are the immediate out-of-range neighbours of the valid interval `[1, n-1]`. +// candidates that -- for any real message/key input -- are essentially always in range (a uniform +// 256-bit candidate is `>= n` with probability about `2^-32`), so no realistic integration test +// exercises its reject path; the QUALITY_AND_STYLE.md private-function exception applies. Known +// answers: `0` and `n` are the immediate out-of-range neighbours of the valid interval `[1, n-1]`. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/src/rfc6979_bp256r1.rs b/crypto/ecdsa/src/rfc6979_bp256r1.rs index 39b7e42ce..6ab96a2a7 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -12,9 +12,10 @@ //! `n` not being close to `2^256` has a second consequence: the step h.3 rejection loop is a normal //! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- a //! uniform 256-bit candidate is `>= n` with probability `1 - n/2^256 = 0.336`, so about a third of -//! all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly -//! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) -//! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. +//! all messages take at least one extra HMAC round. RFC 6979 puts no probability on this loop +//! (§3.2 step h.3 says only that the process loops on an out-of-range candidate; its "utterly +//! improbable", §2.4 and §3.4, is about `r = 0`, which this crate reports as an error instead). +//! [`generate_k`]'s docs say what the loop costs and amounts to on the brainpool curves. //! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves (its appendix //! only covers the NIST curves P-192 through P-521); `rfc6979_bp256r1_vectors_tests.rs` @@ -79,16 +80,17 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// RFC 6979 §3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and /// message hash `h1 = H(m)`. /// -/// # The rejection loop is a normal path on this curve +/// # The rejection loop is a normal path on the brainpool curves /// /// A candidate is rejected with probability `1 - n/2^256 = 0.336`, so 34 % of signatures run step -/// h.3's "otherwise" branch at least once (mean 0.51 extra iterations). Each extra iteration is -/// three more HMAC-SHA-256 calls (two for the re-key, one for the next candidate) -- a timing -/// difference a remote observer can see. [`crate::rfc6979::generate_k`]'s docs give the argument -/// for why the count reveals nothing about `d` or the accepted `k`; it does not depend on the rate. -/// What the rate changes is the practical consequence: the count is deterministic per `(d, msg)`, -/// so the timing pattern over chosen messages is a fingerprint worth one to two bits per message -- -/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds. +/// h.3's "otherwise" branch at least once (mean 0.51 extra iterations); [`crate::rfc6979_bp384r1`] +/// and [`crate::rfc6979_bp512r1`] state their own rates. Each extra iteration is three more HMAC +/// calls (two for the re-key, one for the next candidate) -- a timing difference a remote observer +/// can see. [`crate::rfc6979::generate_k`]'s docs give the argument for why the count reveals +/// nothing about `d` or the accepted `k`; it does not depend on the rate. What the rate changes is +/// the practical consequence: the count is deterministic per `(d, msg)`, so the timing pattern over +/// chosen messages is a fingerprint worth one to two bits per message -- useless for recovering +/// `d`, usable for confirming a candidate `d` an attacker already holds. pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -123,12 +125,9 @@ pub fn generate_k(d: &Bp256r1Scalar, h1: &[u8; HLEN]) -> Bp256r1Scalar { } } -// `candidate_in_range`'s reject path is ordinary control flow on this curve -- a third of real -// messages take it (see the module docs) -- and the integration tests exercise it deliberately: -// in `rfc6979_bp256r1_vectors_tests.rs`, CASE_0 rejects one candidate before its `k` and CASE_5 -// five in a row. What integration tests cannot pin are the exact boundaries, which no realistic -// input lands on: `0` and `n`, the immediate out-of-range neighbours of the valid interval -// `[1, n-1]` -- the known answers below. +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp256r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/src/rfc6979_bp384r1.rs b/crypto/ecdsa/src/rfc6979_bp384r1.rs index 289240a41..6def04762 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -8,12 +8,10 @@ //! 2^384` and any 384-bit input is already `< 2n`, despite `n` not being close to `2^384` itself. //! //! `n` not being close to `2^384` has a second consequence: the step h.3 rejection loop is a normal -//! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- -//! and the most-taken in the crate: a uniform 384-bit candidate is `>= n` with probability `1 - -//! n/2^384 = 0.4503`, so nearly half of all messages take at least one extra HMAC round and a fifth -//! take two or more. (RFC 6979 §3.4 calls rejection "utterly improbable"; that assumes `n` close to -//! `2^qlen`, which `qlen == hlen` alone does not give.) [`generate_k`]'s docs give the -//! constant-time argument that holds at this rejection rate. +//! control path on this curve, the most-taken in the crate. A uniform 384-bit candidate is `>= n` +//! with probability `1 - n/2^384 = 0.4503`, so nearly half of all messages take at least one extra +//! HMAC round and a fifth take two or more; [`crate::rfc6979_bp256r1`]'s docs cover what that +//! means on the brainpool curves. //! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; //! `rfc6979_bp384r1_vectors_tests.rs` cross-checks this implementation against an independent @@ -80,14 +78,10 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// # The rejection loop is a normal path on this curve /// /// A candidate is rejected with probability `1 - n/2^384 = 0.4503`, so 45 % of signatures run step -/// h.3's "otherwise" branch at least once and a fifth at least twice (mean 0.82 extra iterations). -/// Each extra iteration is three more HMAC-SHA-384 calls (two for the re-key, one for the next -/// candidate) -- a timing difference a remote observer can see. [`crate::rfc6979::generate_k`]'s -/// docs give the argument for why the count reveals nothing about `d` or the accepted `k`; it does -/// not depend on the rate. What the rate changes is the practical consequence: the count is -/// deterministic per `(d, msg)`, so the timing pattern over chosen messages is a fingerprint worth -/// one to two bits per message -- useless for recovering `d`, usable for confirming a candidate `d` -/// an attacker already holds. +/// h.3's "otherwise" branch at least once and a fifth at least twice (mean 0.82 extra iterations), +/// each three more HMAC-SHA-384 calls. What that count is worth to an observer, and why it reveals +/// nothing about `d`: [`crate::rfc6979_bp256r1::generate_k`]'s docs, which hold for every brainpool +/// curve. pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -122,12 +116,9 @@ pub fn generate_k(d: &Bp384r1Scalar, h1: &[u8; HLEN]) -> Bp384r1Scalar { } } -// `candidate_in_range`'s reject path is ordinary control flow on this curve -- nearly half of -// real messages take it (see the module docs) -- and the integration tests exercise it -// deliberately: in `rfc6979_bp384r1_vectors_tests.rs`, CASE_0 rejects two candidates before its -// `k`, CASE_4 one, and CASE_5 six in a row. What integration tests cannot pin are the exact -// boundaries, which no realistic input lands on: `0` and `n`, the immediate out-of-range -// neighbours of the valid interval `[1, n-1]` -- the known answers below. +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp384r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. #[cfg(test)] mod tests { use super::*; diff --git a/crypto/ecdsa/src/rfc6979_bp512r1.rs b/crypto/ecdsa/src/rfc6979_bp512r1.rs index 5c8275f11..a8d78d1b2 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -9,11 +9,9 @@ //! `2n > 2^512` and any 512-bit input is already `< 2n`, despite `n` not being close to `2^512`. //! //! `n` not being close to `2^512` has a second consequence: the step h.3 rejection loop is a normal -//! control path on this curve, not the near-dead branch it is on the NIST curves and secp256k1 -- a -//! uniform 512-bit candidate is `>= n` with probability `1 - n/2^512 = 0.3326`, so about a third of -//! all messages take at least one extra HMAC round. (RFC 6979 §3.4 calls rejection "utterly -//! improbable"; that assumes `n` close to `2^qlen`, which `qlen == hlen` alone does not give.) -//! [`generate_k`]'s docs give the constant-time argument that holds at this rejection rate. +//! control path on this curve. A uniform 512-bit candidate is `>= n` with probability +//! `1 - n/2^512 = 0.3326`, so about a third of all messages take at least one extra HMAC round; +//! [`crate::rfc6979_bp256r1`]'s docs cover what that means on the brainpool curves. //! //! RFC 6979 has no official Appendix A.2.x-style test vectors for brainpool curves; //! `rfc6979_bp512r1_vectors_tests.rs` cross-checks this implementation against an independent @@ -80,13 +78,9 @@ fn candidate_in_range(candidate: &[u8; HLEN]) -> bool { /// # The rejection loop is a normal path on this curve /// /// A candidate is rejected with probability `1 - n/2^512 = 0.3326`, so 33 % of signatures run step -/// h.3's "otherwise" branch at least once (mean 0.50 extra iterations). Each extra iteration is -/// three more HMAC-SHA-512 calls (two for the re-key, one for the next candidate) -- a timing -/// difference a remote observer can see. [`crate::rfc6979::generate_k`]'s docs give the argument -/// for why the count reveals nothing about `d` or the accepted `k`; it does not depend on the rate. -/// What the rate changes is the practical consequence: the count is deterministic per `(d, msg)`, -/// so the timing pattern over chosen messages is a fingerprint worth one to two bits per message -- -/// useless for recovering `d`, usable for confirming a candidate `d` an attacker already holds. +/// h.3's "otherwise" branch at least once (mean 0.50 extra iterations), each three more +/// HMAC-SHA-512 calls. What that count is worth to an observer, and why it reveals nothing about +/// `d`: [`crate::rfc6979_bp256r1::generate_k`]'s docs, which hold for every brainpool curve. pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { let mut int2octets_d = Secret::<[u8; HLEN]>::new(); *int2octets_d = d.to_be_bytes(); // §2.3.3, d already in [1, n-1] @@ -121,12 +115,9 @@ pub fn generate_k(d: &Bp512r1Scalar, h1: &[u8; HLEN]) -> Bp512r1Scalar { } } -// `candidate_in_range`'s reject path is ordinary control flow on this curve -- a third of real -// messages take it (see the module docs) -- and the integration tests exercise it deliberately: -// in `rfc6979_bp512r1_vectors_tests.rs`, CASE_0 and CASE_1 each reject one candidate before their -// `k` and CASE_5 five in a row. What integration tests cannot pin are the exact boundaries, which -// no realistic input lands on: `0` and `n`, the immediate out-of-range neighbours of the valid -// interval `[1, n-1]` -- the known answers below. +// `candidate_in_range`'s reject path is covered from outside the crate, by the rejecting cases in +// `rfc6979_bp512r1_vectors_tests.rs`; what those cannot pin are the exact boundaries, which no +// realistic input lands on: `0` and `n`, the known answers below. #[cfg(test)] mod tests { use super::*; From 510c99de6269be74fe0ff3db7e0d7f2f6e54989f Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 1 Nov 2026 21:48:56 +0000 Subject: [PATCH 15/16] ec, sm2: correct two SM2 doc strings: the dA range in an expect, and what the REDC KAT bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - sm2 keys.rs: the comment says dA is in [1, n-2]; the expect string two lines below said [1, n-1] - ec sm2_scalar_tests.rs: the REDC final-subtraction KAT's doc said "canonical result below 2^224" - the value that must be below 2^256 - n is the product's Montgomery form abR mod n, not ab mod n - the three pinned products' ab mod n are 254 to 256 bits; their abR mod n are 220 to 224 bits - an expect string and a doc comment only; no code change ====================================================================== [For agents] Commit 868e28b3 moved SM2's private key dA to [1, n-2] per GB/T 32918.1-2016 §6.1 a) and updated the comment above SM2PrivateKey::from_validated_scalar, but not the expect string two lines below it, which still named [1, n-1]. The message is only ever shown if [dA]G were the identity, which the comment explains cannot happen, so this is a string fix with no behaviour change. known_answer_mul_reaching_the_final_subtraction in sm2_scalar_tests.rs pins products whose REDC output before the final conditional subtraction lies in [n, 2^256). Its doc said that needs "the canonical result below 2^224". The test's constants are plain integers a, b and expected = ab mod n, and from_limbs puts a and b into Montgomery form, so mul computes REDC((aR)(bR)) = abR mod n and to_limbs converts back. The pre-subtraction value lies in [n, 2^256) exactly when the canonical Montgomery representative abR mod n is below 2^256 - n (a 225-bit number, about 2^224) and REDC lands on that representative plus n. Recomputed from the test's own constants: the three ab mod n are 255, 256 and 254 bits, so the old sentence named a quantity that is not small at all; the three abR mod n are 224, 220 and 224 bits, all below 2^256 - n, and in all three cases the pre-subtraction value equals abR mod n plus n. The doc now names abR mod n and says what REDC returns versus what the test compares. The rest of the paragraph, the operand-construction recipe and why a plain mul == from_limbs(expected) check would not detect a missing subtraction, is unchanged. Verification: cargo test -p bouncycastle-sm2 and the ec crate's sm2_scalar_tests binary pass; cargo +nightly fmt --all --check clean. An expect string and a doc comment only; no code change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/tests/sm2_scalar_tests.rs | 14 ++++++++------ crypto/sm2/src/keys.rs | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crypto/ec/tests/sm2_scalar_tests.rs b/crypto/ec/tests/sm2_scalar_tests.rs index 9ed00b013..fe18091e1 100644 --- a/crypto/ec/tests/sm2_scalar_tests.rs +++ b/crypto/ec/tests/sm2_scalar_tests.rs @@ -415,12 +415,14 @@ fn known_answer_at_n_minus_1() { /// Products whose REDC pre-reduction value lands in `[n, 2^256)` -- `acc[8] = 0` and `high >= n` /// -- so the final conditional subtraction is the step that produces the canonical result. That -/// needs the canonical result below `2^224` and the wrong representative drawn, `2^-32` per -/// reduction, so no pseudorandom or round-trip test reaches it; these were constructed, not -/// searched (pick the Montgomery operand `a` and the target `u`, solve `m = uR * n^-1 mod a`, -/// `b = (uR - mn) / a`). A plain `mul(..) == from_limbs(expected)` would NOT detect the -/// subtraction's absence: `from_limbs` of the expected value takes the same step and would land -/// on the same non-canonical representative. The two extra assertions are what pin it. +/// needs the product's canonical Montgomery representative, `abR mod n` (what REDC returns; the +/// `ab mod n` the test compares is 254 to 256 bits here), below `2^256 - n`, about `2^224`, and +/// the wrong representative drawn, `2^-32` per reduction, so no pseudorandom or round-trip test +/// reaches it; these were constructed, not searched (pick the Montgomery operand `a` and the +/// target `u`, solve `m = uR * n^-1 mod a`, `b = (uR - mn) / a`). A plain +/// `mul(..) == from_limbs(expected)` would NOT detect the subtraction's absence: `from_limbs` of +/// the expected value takes the same step and would land on the same non-canonical +/// representative. The two extra assertions are what pin it. #[test] fn known_answer_mul_reaching_the_final_subtraction() { let cases: [([u64; 4], [u64; 4], [u64; 4]); 3] = [ diff --git a/crypto/sm2/src/keys.rs b/crypto/sm2/src/keys.rs index aa56b6ce4..f64ec73c8 100644 --- a/crypto/sm2/src/keys.rs +++ b/crypto/sm2/src/keys.rs @@ -82,7 +82,7 @@ impl SM2PrivateKey { let q = comb_multiply_base_point(&d); // dA is in [1, n-2] by construction (every caller validates first) and G has prime order n // (h = 1), so [dA]G is never the identity. - let (x, y) = q.to_affine().expect("[dA]G is never infinity for dA in [1, n-1]"); + let (x, y) = q.to_affine().expect("[dA]G is never infinity for dA in [1, n-2]"); // (1 + dA)^-1, once per key rather than once per signature (see the type's docs). The // three scalar-field intermediates reveal dA and are scrubbed, the same way sign_with_k From a770496f5bb928a4395a6a3d36a8af3b0406a022 Mon Sep 17 00:00:00 2001 From: laruizlo Date: Sun, 1 Nov 2026 21:48:56 +0000 Subject: [PATCH 16/16] ec, ecdsa: Security Considerations headings per QUALITY_AND_STYLE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [For humans] - QUALITY_AND_STYLE.md: the heading is always exactly `# 🚨 Security Considerations 🚨` - the ec and ecdsa crate docs had the plain `# Security Considerations`; sm2's was already right - two heading lines; no other change ====================================================================== [For agents] QUALITY_AND_STYLE.md's Docs section fixes the heading so the sections are easy to spot when reading and to find with a search. Every other crate in the workspace that has the section uses that heading; ec and ecdsa were the two exceptions, both at the crate level (neither crate has the section on a module or item). Prose references to the section, "see the crate docs' Security Considerations" in rfc6979.rs and sm2.rs, are not headings and are unchanged. Docs only; no code change. Assisted-by: Claude:claude-fable-5-1 --- crypto/ec/src/lib.rs | 2 +- crypto/ecdsa/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto/ec/src/lib.rs b/crypto/ec/src/lib.rs index 6bab6159f..9789abb18 100644 --- a/crypto/ec/src/lib.rs +++ b/crypto/ec/src/lib.rs @@ -142,7 +142,7 @@ //! of `G` its width-7 window over `G` indexes (2 KiB for a 256-bit curve, 4.5 KiB for P-521), //! read by public digit and so with no scan. //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! Every arithmetic primitive here is written to be branch-free and to avoid indexing by a //! secret value, per this workspace's constant-time rules. See [`p256`]'s module docs for the diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index a7e4a9dc7..47315fa27 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -142,7 +142,7 @@ //! also the stack-usage measurement harness (see its own doc comment for the `valgrind`/`massif` //! invocation). //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! - **Everything done with the private key `d` and the accepted per-message secret `k` is //! branch-free and does not index memory by their value**: inversion, multiplication, the