crypto: check key image in ring signature verif
What changed, and why it matters
This Monero patch adds validation checks during ring signature verification to reject certain malformed 'key images.' A key image is a cryptographic fingerprint that proves a coin is being spent without revealing which coin. Before this fix, the verifier accepted some mathematically invalid key images, such as the identity point or points whose order divides a small number (torsioned points). An attacker could potentially craft such a key image to bypass the 'already spent' check, enabling double-spending of coins or denial of service against the network.
Treat this as a security-critical consensus patch. Nodes, exchanges, and wallet services should upgrade promptly to a release containing this commit. Review whether any transactions with small-order or identity key images were accepted on-chain before the patch, and consider whether a chain rollback or blacklisting is necessary. Monitor Monero Project communications for any coordinated disclosure or hard-fork guidance.
Security signals we found
Adds explicit small-order/torsion check for key images in ring signature verification
Rejects identity element as a valid key image
Exposes Ed25519 curve order constant sc_l for subgroup validation
Changes consensus-critical cryptographic validation logic
No explicit CVE or vendor security advisory referenced in commit
Evidence from the diff
The commit modifies crypto.cpp’s ring signature verification to reject key images equal to the point at infinity (identity) and to multiply the decoded key image by the curve order l and verify the result is the identity. This ensures the key image lies in the prime-order subgroup of Ed25519. It also exposes the curve order constant sc_l in crypto-ops-data.c. Previously, the code only checked ge_frombytes_vartime succeeded, but did not verify the point’s order. Small-order or torsioned key images could pass verification and be recorded on the chain, potentially allowing the same output to be spent more than once or causing consensus divergence.
Changed components
src/crypto/crypto.cppsrc/crypto/crypto-ops-data.csrc/crypto/crypto-ops.hMonero ring signature verificationKey image validation pathInspect captured patch +11 / −1
diff --git a/src/crypto/crypto-ops-data.c b/src/crypto/crypto-ops-data.c
index c5b0b08..8a38ee9 100644
--- a/src/crypto/crypto-ops-data.c
+++ b/src/crypto/crypto-ops-data.c
@@ -880,3 +880,6 @@ const ge_p3 ge_p3_H = {
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{23443568, -5110398, -8776029, -4345135, 6889568, -14710814, 7474843, 3279062, 14550766, -7453428}
};
+
+/* curve order l = 2^252 + 27742317777372353535851937790883648493 */
+const unsigned char sc_l[32] = {0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10};
diff --git a/src/crypto/crypto-ops.h b/src/crypto/crypto-ops.h
index 45f2566..a0aee3e 100644
--- a/src/crypto/crypto-ops.h
+++ b/src/crypto/crypto-ops.h
@@ -153,6 +153,7 @@ extern const fe fe_a_inv_3;
extern const fe fe_c;
extern const ge_p3 ge_p3_identity;
extern const ge_p3 ge_p3_H;
+extern const unsigned char sc_l[32];
void ge_fromfe_frombytes_vartime(ge_p2 *, const unsigned char *);
void sc_0(unsigned char *);
void sc_1(unsigned char *);
diff --git a/src/crypto/crypto.cpp b/src/crypto/crypto.cpp
index 4997ea8..c6c5826 100644
--- a/src/crypto/crypto.cpp
+++ b/src/crypto/crypto.cpp
@@ -75,6 +75,8 @@ namespace crypto {
const crypto::public_key null_pkey = crypto::public_key{};
const crypto::secret_key null_skey = crypto::secret_key{};
+ static constexpr ec_point infinity = {{1}};
+
static inline unsigned char *operator &(ec_point &point) {
return &reinterpret_cast<unsigned char &>(point);
}
@@ -333,7 +335,6 @@ namespace crypto {
}
ge_double_scalarmult_base_vartime(&tmp2, &sig.c, &tmp3, &sig.r);
ge_tobytes(&buf.comm, &tmp2);
- static const ec_point infinity = {{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}};
if (memcmp(&buf.comm, &infinity, 32) == 0)
return false;
hash_to_scalar(&buf, sizeof(s_comm), c);
@@ -771,10 +772,15 @@ POP_WARNINGS
assert(check_key(*pubs[i]));
}
#endif
+ if (0 == memcmp(image.data, infinity.data, sizeof(image)))
+ return false; // false if key image is identity
if (ge_frombytes_vartime(&image_unp, &image) != 0) {
return false;
}
ge_dsm_precomp(image_pre, &image_unp);
+ ge_scalarmult_p3(&image_unp, sc_l, &image_unp);
+ if (!ge_p3_is_point_at_infinity_vartime(&image_unp))
+ return false; // false if key image is torsioned
sc_0(&sum);
buf->h = prefix_hash;
for (i = 0; i < pubs_count; i++) {
Why this scored 78/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.