Commit f297baaa11 for openssl.org
commit f297baaa1142ab42e628fdd7a54b96592d2559dd
Author: Igor Ustinov <igus@openssl.foundation>
Date: Tue Jul 28 22:09:29 2026 +0200
ec: make ossl_ec_scalar_mul_ladder() scalar padding constant time
Fixes CVE-2026-54872
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:53 2026
diff --git a/crypto/bn/bn_intern.c b/crypto/bn/bn_intern.c
index f963d42b86..3e638b9e7a 100644
--- a/crypto/bn/bn_intern.c
+++ b/crypto/bn/bn_intern.c
@@ -9,6 +9,7 @@
#include "internal/cryptlib.h"
#include "bn_local.h"
+#include "internal/constant_time.h"
/*
* Determine the modified width-(w+1) Non-Adjacent Form (wNAF) of 'scalar'.
@@ -152,6 +153,43 @@ void bn_set_all_zero(BIGNUM *a)
a->d[i] = 0;
}
+/*
+ * Zero-extend |a| so that it occupies exactly |words| words, flag it
+ * BN_FLG_FIXED_TOP and leave its numeric value unchanged.
+ *
+ * This is a companion to bn_correct_top(): where the latter minimises the top
+ * of a BIGNUM, this one pins the top to a caller-chosen, value-independent
+ * width. Constant-time code uses it to make the cost of subsequent word-wise
+ * operations (e.g. BN_uadd()/BN_add()) independent of the magnitude of a
+ * secret value. |words| must be greater than or equal to the current top.
+ *
+ * The routine is itself constant time with respect to the current a->top: it
+ * always sweeps a fixed |words| iterations and selects value-or-zero per word
+ * with an arithmetic mask, rather than looping over the (possibly secret)
+ * a->top..words range. Masking the high words with zero also launders any
+ * uninitialised padding, so it is safe for the memory sanitiser.
+ */
+int bn_set_top_fixed(BIGNUM *a, int words)
+{
+ size_t i, n = (size_t)words;
+ BN_ULONG mask;
+
+ if (words < a->top)
+ return 0;
+ if (bn_wexpand(a, words) == NULL) {
+ ERR_raise(ERR_LIB_BN, ERR_R_BN_LIB);
+ return 0;
+ }
+ for (i = 0; i < n; i++) {
+ /* mask = all ones iff i < a->top, else all zeros */
+ mask = value_barrier_bn((BN_ULONG)0 - ((i - a->top) >> (8 * sizeof(i) - 1)));
+ a->d[i] &= mask;
+ }
+ a->top = words;
+ a->flags |= BN_FLG_FIXED_TOP;
+ return 1;
+}
+
int bn_copy_words(BN_ULONG *out, const BIGNUM *in, int size)
{
if (in->top > size)
diff --git a/crypto/ec/ec_mult.c b/crypto/ec/ec_mult.c
index f728a2f958..f78350b8df 100644
--- a/crypto/ec/ec_mult.c
+++ b/crypto/ec/ec_mult.c
@@ -213,6 +213,18 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r,
goto err;
}
+ /*
+ * Constant-timeness of this copy depends on the caller: BN_copy() moves
+ * scalar->top words unless |scalar| is flagged BN_FLG_CONSTTIME (in which
+ * case scalar->dmax words are moved). Secret scalars are therefore
+ * 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.
+ */
if (BN_copy(k, scalar) == NULL) {
ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
goto err;
@@ -231,10 +243,36 @@ int ossl_ec_scalar_mul_ladder(const EC_GROUP *group, EC_POINT *r,
}
}
+ /*
+ * |k| may still carry a top that depends on the value of the secret
+ * scalar: callers pass either a fixed-top BIGNUM (e.g. the ECDSA nonce)
+ * or a minimal-top one (e.g. SM2), and BN_copy() above preserves that
+ * top. Pin |k| to a fixed number of words (matching the group
+ * cardinality) so that the additions below run in constant time,
+ * independently of the bit length of the scalar. Otherwise the work
+ * done by BN_add()/BN_uadd() depends on the operand tops and leaks the
+ * magnitude of the secret scalar.
+ */
+ if (!bn_set_top_fixed(k, group_top)) {
+ ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
+ goto err;
+ }
+
if (!BN_add(lambda, k, cardinality)) {
ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
goto err;
}
+ /*
+ * |lambda| = scalar + cardinality may or may not have produced a carry
+ * into an extra word depending on the secret scalar. Pin its top to one
+ * word above the group top so that the second addition, which consumes
+ * |lambda|, is likewise constant time and so that the BN_is_bit_set()
+ * below always inspects a defined word.
+ */
+ if (!bn_set_top_fixed(lambda, group_top + 1)) {
+ ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
+ goto err;
+ }
BN_set_flags(lambda, BN_FLG_CONSTTIME);
if (!BN_add(k, lambda, cardinality)) {
ERR_raise(ERR_LIB_EC, ERR_R_BN_LIB);
diff --git a/include/crypto/bn.h b/include/crypto/bn.h
index d2caade6a4..1336cb8d16 100644
--- a/include/crypto/bn.h
+++ b/include/crypto/bn.h
@@ -18,6 +18,7 @@ BIGNUM *bn_wexpand(BIGNUM *a, int words);
BIGNUM *bn_expand2(BIGNUM *a, int words);
void bn_correct_top(BIGNUM *a);
+int bn_set_top_fixed(BIGNUM *a, int words);
/*
* Determine the modified width-(w+1) Non-Adjacent Form (wNAF) of 'scalar'.