crypto: init p3 double scalarmult result when both scalars are zero
What changed, and why it matters
This commit fixes a bug in Monero's cryptographic code where a specific function could return an uninitialized result when both input numbers (scalars) are zero. The fix explicitly sets the result to the point-at-infinity (the elliptic-curve equivalent of zero) before doing any work. The included test verifies this behavior. In cryptographic software, using an uninitialized or incorrect point could lead to wrong signatures, verification failures, or in some designs, security weaknesses, though the commit itself does not describe an active exploit.
Treat as a low-to-moderate correctness fix in cryptographic code. Review all callers of the two functions to confirm they do not rely on or previously misused the uninitialized `r3` output. Include the regression test in CI. Consider whether any signature/verification paths could be coerced into passing all-zero scalars and whether that could affect consensus or privacy guarantees. No immediate emergency response is indicated by the diff alone, but cryptographic fixes should be deployed in the next release.
Security signals we found
Uninitialized/undefined output in elliptic-curve scalar multiplication routine
Fix explicitly initializes result to identity point before computation
Added regression test for zero-scalar edge case
Functions are in low-level Ed25519-style crypto-ops layer used by signatures/key derivation
No explicit security advisory, CVE, or exploit description in commit or supplied references
Evidence from the diff
The patch adds ge_p3_0(r3) calls at the start of ge_double_scalarmult_base_vartime_p3() and ge_double_scalarmult_precomp_vartime2_p3() in src/crypto/crypto-ops.c. These functions compute a combined scalar multiplication a*G + b*A and return the result in both ge_p2 (r) and ge_p3 (r3) forms. Previously, r was initialized to zero with ge_p2_0(&r), but r3 was not initialized. When both scalars are zero, the loop that builds r3 from r is skipped entirely, leaving r3 containing whatever garbage the caller passed in. The fix initializes r3 to the identity point so the output is deterministic and correct even when both scalars are zero. A unit test confirms both functions return the point at infinity for all-zero scalars.
Changed components
src/crypto/crypto-ops.cge_double_scalarmult_base_vartime_p3ge_double_scalarmult_precomp_vartime2_p3tests/unit_tests/crypto.cppInspect captured patch +20 / −0
diff --git a/src/crypto/crypto-ops.c b/src/crypto/crypto-ops.c
index 5a87d79..7a5217e 100644
--- a/src/crypto/crypto-ops.c
+++ b/src/crypto/crypto-ops.c
@@ -42,6 +42,7 @@ DISABLE_VS_WARNINGS(4146 4244)
static void ge_madd(ge_p1p1 *, const ge_p3 *, const ge_precomp *);
static void ge_msub(ge_p1p1 *, const ge_p3 *, const ge_precomp *);
static void ge_p2_0(ge_p2 *);
+static void ge_p3_0(ge_p3 *);
static void fe_divpowm1(fe, const fe, const fe);
/* Common functions */
@@ -1340,6 +1341,7 @@ void ge_double_scalarmult_base_vartime_p3(ge_p3 *r3, const unsigned char *a, con
ge_dsm_precomp(Ai, A);
ge_p2_0(&r);
+ ge_p3_0(r3);
for (i = 255; i >= 0; --i) {
if (aslide[i] || bslide[i]) break;
@@ -2308,6 +2310,7 @@ void ge_double_scalarmult_precomp_vartime2_p3(ge_p3 *r3, const unsigned char *a,
slide(bslide, b);
ge_p2_0(&r);
+ ge_p3_0(r3);
for (i = 255; i >= 0; --i) {
if (aslide[i] || bslide[i]) break;
diff --git a/tests/unit_tests/crypto.cpp b/tests/unit_tests/crypto.cpp
index e0e4713..9a83cfb 100644
--- a/tests/unit_tests/crypto.cpp
+++ b/tests/unit_tests/crypto.cpp
@@ -574,3 +574,20 @@ TEST(Crypto, fe_constants)
ASSERT_TRUE(memcmp(a_inv_3_bytes, A_INV_3_PAPER, 32) == 0);
ASSERT_TRUE(memcmp(fe_c_bytes, FE_C_PAPER, 32) == 0);
}
+
+TEST(Crypto, double_scalarmult_p3_zero_scalars)
+{
+ static const unsigned char zero[32] = {0};
+ const ge_p3 &G = crypto::get_G_p3();
+ ge_dsmp Gi;
+ ge_dsm_precomp(Gi, &G);
+
+ ge_p3 r3;
+ memset(&r3, 0x01, sizeof(r3));
+ ge_double_scalarmult_base_vartime_p3(&r3, zero, &G, zero);
+ EXPECT_TRUE(ge_p3_is_point_at_infinity_vartime(&r3));
+
+ memset(&r3, 0x01, sizeof(r3));
+ ge_double_scalarmult_precomp_vartime2_p3(&r3, zero, Gi, zero, Gi);
+ EXPECT_TRUE(ge_p3_is_point_at_infinity_vartime(&r3));
+}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.