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/ec/src/p256k1.rs b/crypto/ec/src/p256k1.rs index 7399d53c5..ad2d534fb 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. `p256k1_field_tests.rs` +//! constructs and pins a product that sets it on this curve. use crate::nat; use bouncycastle_utils::ct; @@ -287,13 +294,23 @@ 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 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]; @@ -303,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); // valid when carry1 == 1: folded < 2^33 there - let (diff, borrow) = nat::sub(&folded, &P_LIMBS); // valid when 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/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/ec/tests/p256k1_field_tests.rs b/crypto/ec/tests/p256k1_field_tests.rs index 12aa1dae6..b4b6db426 100644 --- a/crypto/ec/tests/p256k1_field_tests.rs +++ b/crypto/ec/tests/p256k1_field_tests.rs @@ -330,3 +330,40 @@ 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 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 +/// `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"); +} diff --git a/crypto/ec/tests/sm2_scalar_tests.rs b/crypto/ec/tests/sm2_scalar_tests.rs index 18a1a15f4..fe18091e1 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,47 @@ 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 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] = [ + ( + [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 diff --git a/crypto/ecdsa/src/lib.rs b/crypto/ecdsa/src/lib.rs index db79f3a04..47315fa27 100644 --- a/crypto/ecdsa/src/lib.rs +++ b/crypto/ecdsa/src/lib.rs @@ -142,13 +142,24 @@ //! also the stack-usage measurement harness (see its own doc comment for the `valgrind`/`massif` //! invocation). //! -//! # 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. +//! # ๐Ÿšจ 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 +//! 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: +//! - `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, 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.** //! A repeated `k` leaks `d` directly from two signatures' `(r, s)` pairs (elementary algebra on //! the ECDSA equation). `ECDSA*` therefore depends on its RNG being correctly seeded for every diff --git a/crypto/ecdsa/src/rfc6979.rs b/crypto/ecdsa/src/rfc6979.rs index 6d4c6f260..016cfdb3c 100644 --- a/crypto/ecdsa/src/rfc6979.rs +++ b/crypto/ecdsa/src/rfc6979.rs @@ -69,13 +69,17 @@ 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`, 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); - 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() } /// RFC 6979 ยง3.2 steps a-h: the deterministic per-message secret `k` for private key `d` and @@ -94,15 +98,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: +/// `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). 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] @@ -138,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 0607baa8b..6ab96a2a7 100644 --- a/crypto/ecdsa/src/rfc6979_bp256r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp256r1.rs @@ -9,6 +9,14 @@ //! 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 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 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` //! cross-checks this implementation against an independent Python implementation of the same RFC @@ -63,16 +71,26 @@ 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() } /// 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 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); [`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] @@ -107,11 +125,9 @@ 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 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 74091d62d..6def04762 100644 --- a/crypto/ecdsa/src/rfc6979_bp384r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp384r1.rs @@ -7,6 +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, 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 //! Python implementation of the same RFC instead. @@ -60,16 +66,22 @@ 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() } /// 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 +/// +/// 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 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] @@ -104,11 +116,9 @@ 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 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 2c0deac09..a8d78d1b2 100644 --- a/crypto/ecdsa/src/rfc6979_bp512r1.rs +++ b/crypto/ecdsa/src/rfc6979_bp512r1.rs @@ -8,6 +8,11 @@ //! 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. 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 //! Python implementation of the same RFC instead. @@ -61,16 +66,21 @@ 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() } /// 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 +/// +/// 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 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] @@ -105,11 +115,9 @@ 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 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::*; diff --git a/crypto/ecdsa/src/rfc6979_p256k1.rs b/crypto/ecdsa/src/rfc6979_p256k1.rs index 48860e764..2c3aee5f6 100644 --- a/crypto/ecdsa/src/rfc6979_p256k1.rs +++ b/crypto/ecdsa/src/rfc6979_p256k1.rs @@ -58,16 +58,16 @@ 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() } /// 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 941f054a5..912a7d74b 100644 --- a/crypto/ecdsa/src/rfc6979_p384.rs +++ b/crypto/ecdsa/src/rfc6979_p384.rs @@ -50,15 +50,16 @@ 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() } /// 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 1141e0b25..58e073dd9 100644 --- a/crypto/ecdsa/src/rfc6979_p521.rs +++ b/crypto/ecdsa/src/rfc6979_p521.rs @@ -105,16 +105,17 @@ 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() } /// 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(); diff --git a/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs b/crypto/ecdsa/tests/rfc6979_bp256r1_vectors_tests.rs index 152bccba2..6cde92d30 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 dcc84e0ae..4bf81fec3 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 d462e927b..0ef673298 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() 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..f64ec73c8 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; @@ -29,18 +30,19 @@ 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; -/// 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` /// -/// 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 @@ -56,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 @@ -74,19 +76,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]"); + 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 - // 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 +137,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, @@ -202,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). @@ -218,9 +221,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..936a6a93d 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`. //! @@ -69,7 +70,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}; @@ -110,16 +111,20 @@ //! constant-time inversion for a value that never changes. See [`keys::SM2PrivateKey`]'s own //! docs. //! -//! # Security Considerations +//! # ๐Ÿšจ 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. +//! - **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 -//! 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 ebad5d815..af4095bb3 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`]); @@ -31,16 +34,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 @@ -72,14 +75,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); @@ -101,7 +104,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( @@ -119,9 +122,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 { @@ -156,7 +162,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 { @@ -164,7 +170,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. @@ -218,8 +224,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(); @@ -271,8 +277,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)?; @@ -301,7 +307,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) { @@ -310,9 +316,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); } @@ -320,20 +326,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; @@ -342,7 +348,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/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_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 80dab1189..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"; @@ -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] @@ -411,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. @@ -435,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() { @@ -456,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] diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 6c104445d..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,10 +223,15 @@ 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 + /// 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 } } @@ -360,10 +367,15 @@ 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 + /// 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 } } @@ -451,10 +463,15 @@ 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 + /// 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 } } @@ -531,10 +548,15 @@ 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 + /// 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 } }