Commit 2e9244c778 for openssl.org
commit 2e9244c77879db4618ba24c0e9e771ebf07191a2
Author: Igor Ustinov <igus@openssl.foundation>
Date: Fri Jul 31 17:18:41 2026 +0200
sm2: make sm2_sig_gen() constant time
After fixing CVE-2025-9231, sm2_sig_gen() still used variable-time
BN_mod_mul() and BN_sub() on private key and nonce material.
This commit makes it fully constant time.
Fixes CVE-2026-77696
Co-authored-by: Viktor Dukhovni <viktor@openssl.org>
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Alicja Kario <hkario@redhat.com>
Reviewed-by: Viktor Dukhovni <viktor@openssl.org>
Merge-date: Tue Sep 29 11:23:54 2026
diff --git a/crypto/ec/ec_mult.c b/crypto/ec/ec_mult.c
index f78350b8df..19e35c339e 100644
--- a/crypto/ec/ec_mult.c
+++ b/crypto/ec/ec_mult.c
@@ -220,10 +220,9 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r,
* expected to arrive either BN_FLG_CONSTTIME or fixed-top, so that their
* top is a public, value-independent width and the copy length does not
* leak their magnitude. ECDSA satisfies this via
- * ossl_bn_priv_rand_range_fixed_top(); the generic SM2 signing path does
- * not yet (see the sm2_sig_gen() hardening tracked separately). The
- * fixed-top pinning below makes the subsequent arithmetic constant time
- * regardless, but cannot retroactively fix the copy length here.
+ * ossl_bn_priv_rand_range_fixed_top(). The fixed-top pinning below makes
+ * the subsequent arithmetic constant time regardless, but cannot
+ * retroactively fix the copy length here.
*/
if (BN_copy(k, scalar) == NULL) {
ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
diff --git a/crypto/sm2/sm2_sign.c b/crypto/sm2/sm2_sign.c
index 5e29900968..7f1a40452c 100644
--- a/crypto/sm2/sm2_sign.c
+++ b/crypto/sm2/sm2_sign.c
@@ -14,6 +14,7 @@
#include "crypto/sm2.h"
#include "crypto/sm2err.h"
#include "crypto/ec.h" /* ossl_ec_group_do_inverse_ord() */
+#include "crypto/bn.h" /* fixed-top / Montgomery constant-time BN helpers */
#include "internal/numbers.h"
#include <openssl/err.h>
#include <openssl/evp.h>
@@ -235,17 +236,22 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e)
EC_POINT *kG = NULL;
BN_CTX *ctx = NULL;
BIGNUM *k = NULL;
- BIGNUM *rk = NULL;
BIGNUM *r = NULL;
BIGNUM *s = NULL;
BIGNUM *x1 = NULL;
BIGNUM *tmp = NULL;
+ BN_MONT_CTX *mont = EC_GROUP_get_mont_data(group);
OSSL_LIB_CTX *libctx = ossl_ec_key_get_libctx(key);
if (dA == NULL) {
ERR_raise(ERR_LIB_SM2, SM2_R_INVALID_PRIVATE_KEY);
goto done;
}
+
+ if (mont == NULL) {
+ ERR_raise(ERR_LIB_SM2, ERR_R_EC_LIB);
+ goto done;
+ }
kG = EC_POINT_new(group);
if (kG == NULL) {
ERR_raise(ERR_LIB_SM2, ERR_R_EC_LIB);
@@ -259,7 +265,6 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e)
BN_CTX_start(ctx);
k = BN_CTX_get(ctx);
- rk = BN_CTX_get(ctx);
x1 = BN_CTX_get(ctx);
tmp = BN_CTX_get(ctx);
if (tmp == NULL) {
@@ -293,6 +298,18 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e)
ERR_raise(ERR_LIB_SM2, ERR_R_INTERNAL_ERROR);
goto done;
}
+ /*
+ * Pin the nonce to a fixed, value-independent width and flag it
+ * BN_FLG_CONSTTIME, so its magnitude does not leak through operand
+ * lengths in the scalar copy inside the ladder, or in the arithmetic
+ * below. BN_priv_rand_range_ex() is kept so the nonce value itself
+ * is unchanged; only its representation is pinned.
+ */
+ BN_set_flags(k, BN_FLG_CONSTTIME);
+ if (!bn_set_top_fixed(k, bn_get_top(order))) {
+ ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB);
+ goto done;
+ }
if (!EC_POINT_mul(group, kG, k, NULL, NULL, ctx)
|| !EC_POINT_get_affine_coordinates(group, kG, x1, NULL,
@@ -302,23 +319,49 @@ static ECDSA_SIG *sm2_sig_gen(const EC_KEY *key, const BIGNUM *e)
goto done;
}
- /* try again if r == 0 or r+k == n */
+ /* try again if r == 0 or r + k == n */
if (BN_is_zero(r))
continue;
- if (!BN_add(rk, r, k)) {
- ERR_raise(ERR_LIB_SM2, ERR_R_INTERNAL_ERROR);
+ /*
+ * Since 0 < r < n and 0 < k < n, r + k == n is the same as
+ * k == n - r. Both operands of the subtraction are public, so
+ * compute it in the open and then compare against the nonce with a
+ * fixed-width constant-time comparison. A BN_cmp() on r + k would
+ * branch on whether the sum carried into an extra word, which
+ * depends on the value of k.
+ */
+ if (!BN_sub(tmp, order, r)
+ || !bn_set_top_fixed(tmp, bn_get_top(order))) {
+ ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB);
goto done;
}
- if (BN_cmp(rk, order) == 0)
+ if (CRYPTO_memcmp(bn_get_words(k), bn_get_words(tmp),
+ bn_get_top(order) * sizeof(BN_ULONG))
+ == 0)
continue;
+ /*
+ * s = ((1 + dA)^-1 * (k - r * dA)) mod order
+ *
+ * Computed with fixed-top / Montgomery constant-time primitives, so
+ * that the running time does not depend on the secret k or dA (the
+ * generic BN_mod_mul()/BN_sub() used previously reduce via BN_div(),
+ * whose timing is value dependent). This mirrors the ECDSA path.
+ *
+ * s holds (1 + dA)^-1 throughout; the (k - r * dA) term is built in
+ * tmp. bn_mul_mont_fixed_top() with one operand in the Montgomery
+ * domain yields the plain product, and the final
+ * BN_mod_mul_montgomery() returns the user-visible, normalised value.
+ */
if (!BN_add(s, dA, BN_value_one())
|| !ossl_ec_group_do_inverse_ord(group, s, s, ctx)
- || !BN_mod_mul(tmp, dA, r, order, ctx)
- || !BN_sub(tmp, k, tmp)
- || !BN_mod_mul(s, s, tmp, order, ctx)) {
+ || !bn_to_mont_fixed_top(tmp, r, mont, ctx)
+ || !bn_mul_mont_fixed_top(tmp, tmp, dA, mont, ctx)
+ || !bn_mod_sub_fixed_top(tmp, k, tmp, order)
+ || !bn_to_mont_fixed_top(tmp, tmp, mont, ctx)
+ || !BN_mod_mul_montgomery(s, tmp, s, mont, ctx)) {
ERR_raise(ERR_LIB_SM2, ERR_R_BN_LIB);
goto done;
}