crypto: add Ed25519->X25519 conversion functions
What changed, and why it matters
This commit adds two new math helper functions that convert points between two related elliptic-curve formats used in cryptography (Ed25519 and X25519). It is a pure addition of utility code; there is no bug fix, no change to existing behavior, and no indication it addresses a security flaw. The code includes a safety check that rejects the special 'identity' (zero) point, which is a sensible defensive measure.
No immediate security action required. Treat as routine cryptographic library enhancement. If this code is later used in production key-derivation paths, ensure callers validate return codes and that the variable-time decoding is not exposed to attacker-controlled inputs in timing-sensitive contexts.
Security signals we found
New cryptographic conversion routines added with documented precondition checks
Explicit rejection of the Ed25519 identity point to avoid division-by-zero / invalid X25519 output
No change to existing protocol logic, transaction validation, or key derivation in this commit
No vendor disclosure of a vulnerability or security bug fix
Evidence from the diff
The patch introduces ge_p3_to_x25519 and edwards_bytes_to_x25519_vartime in src/crypto/crypto-ops.c/h. These implement the birational map from Ed25519 y-coordinate to X25519 x-coordinate per RFC 7748 section 4.1, used by Monero’s Carrot spec as ConvertPointE(). The helper edwardsYZ_to_x25519 computes (Z+Y)/(Z-Y), fails when Z-Y is zero (i.e., the identity point), and uses constant-time field operations for the actual conversion. The header documents preconditions and failure modes. No callers are added in this commit, and no existing logic is modified.
Changed components
src/crypto/crypto-ops.csrc/crypto/crypto-ops.hInspect captured patch +58 / −1
diff --git a/src/crypto/crypto-ops.c b/src/crypto/crypto-ops.c
index 788dd4d..e702a31 100644
--- a/src/crypto/crypto-ops.c
+++ b/src/crypto/crypto-ops.c
@@ -1,4 +1,4 @@
-// Copyright (c) 2014-2024, The Monero Project
+// Copyright (c) 2014-2026, The Monero Project
//
// All rights reserved.
//
@@ -3905,6 +3905,39 @@ int sc_isnonzero(const unsigned char *s) {
s[27] | s[28] | s[29] | s[30] | s[31]) - 1) >> 8) + 1;
}
+static int edwardsYZ_to_x25519(unsigned char *xbytes, const fe Y, const fe Z) {
+ //! @see Section 4.1 of RFC 7748: https://www.rfc-editor.org/rfc/rfc7748.html#section-4.1
+ //
+ // y = Y/Z
+ // x_mont = (1 + y) / (1 - y)
+ // = (1 + Y/Z) / (1 - Y/Z)
+ // = (Z + Y) / (Z - Y)
+
+ fe tmp0;
+ fe tmp1;
+ int r;
+ fe_add(tmp0, Z, Y); // Z + Y
+ fe_sub(tmp1, Z, Y); // Z - Y
+ r = -!fe_isnonzero(tmp1); // succeed iff 0 != (Z - Y). AKA fail if identity point or some invalid reprs
+ fe_invert(tmp1, tmp1); // 1/(Z - Y)
+ fe_mul(tmp0, tmp0, tmp1); // (Z + Y) / (Z - Y)
+ fe_tobytes(xbytes, tmp0); // tobytes((Z + Y) / (Z - Y))
+ return r; // 0 on success, otherwise -1
+}
+
+int ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h) {
+ return edwardsYZ_to_x25519(xbytes, h->Y, h->Z);
+}
+
+int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s) {
+ ge_p3 h;
+ const int r = ge_frombytes_vartime(&h, s);
+ if (0 != r)
+ return r;
+
+ return edwardsYZ_to_x25519(xbytes, h.Y, h.Z);
+}
+
int ge_p3_is_point_at_infinity_vartime(const ge_p3 *p) {
// https://eprint.iacr.org/2008/522
// X == T == 0 and Y/Z == 1
diff --git a/src/crypto/crypto-ops.h b/src/crypto/crypto-ops.h
index 7b0cbc4..867311c 100644
--- a/src/crypto/crypto-ops.h
+++ b/src/crypto/crypto-ops.h
@@ -162,6 +162,30 @@ void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b,
int sc_check(const unsigned char *);
int sc_isnonzero(const unsigned char *); /* Doesn't normalize */
+/**
+ * @brief Convert Ed25519 y-coord to X25519 x-coord, AKA "ConvertPointE()" in the Carrot spec
+ * @param[out] xbytes X25519 x-coord as a 255-bit little endian integer
+ * @param h Ed25519 point in projective representation
+ * @return 0 on success, otherwise non-0 on failure
+ *
+ * Returns failure on Ed25519's identity point. Assumes `h` has valid field element representations,
+ * that Z != 0, and that Y/Z is a valid y-coordinate for Ed25519. Returns success iff Y != Z (Y == Z
+ * corresponds with the identity point in valid point representations). The runtime is constant.
+ */
+int ge_p3_to_x25519(unsigned char *xbytes, const ge_p3 *h);
+/**
+ * @brief Convert Ed25519 y-coord to X25519 x-coord, AKA "ConvertPointE()" in the Carrot spec
+ * @param[out] xbytes X25519 x-coord as a 255-bit little endian integer
+ * @param s Ed25519 point in compressed Y representation
+ * @return 0 on success, otherwise non-0 on failure
+ *
+ * Returns failure on Ed25519's identity point (repr {1, 0, 0, ...}). Otherwise, returns success iff
+ * `s` is a valid compressed Y representation of an Ed25519 point. The runtime is variable *only* in
+ * whether `s` is a valid representation. In other words, for all valid Ed25519 points, the runtime
+ * is constant.
+ */
+int edwards_bytes_to_x25519_vartime(unsigned char *xbytes, const unsigned char *s);
+
// internal
uint64_t load_3(const unsigned char *in);
uint64_t load_4(const unsigned char *in);
Why this scored 18/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.