Source
stdlib/ed25519/ed25519.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#include "ed25519.hpp"5
#include "hashlib.hpp"7
#include <array>8
#include <cstdint>9
#include <stdexcept>11
#if defined(_WIN32)12
#include <windows.h>13
#include <bcrypt.h>14
#else15
#include <sys/random.h> // getentropy16
#endif18
// Ed25519 (RFC 8032), implemented from scratch — no external crypto component. The19
// field/curve arithmetic follows the public-domain TweetNaCl reference algorithm20
// (Bernstein, van Gastel, Janssen, Lange, Schwabe, Smetsers), reimplemented in C++ and21
// validated against the RFC 8032 known-answer vectors in stdlib/tests/ed25519_test.cpp.22
// SHA-512 comes from cheatah::hashlib (the same self-contained hash the runtime links).23
namespace cheatah::ed25519 {25
namespace {27
using u8 = std::uint8_t;28
using u64 = std::uint64_t;29
using i64 = std::int64_t;30
using gf = std::array<i64, 16>; // a GF(2^255-19) element: 16 limbs, ~16 bits each32
constexpr gf gf0{};33
constexpr gf gf1{1};34
constexpr gf D{0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070,35
0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203};36
constexpr gf D2{0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0,37
0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406};38
constexpr gf X{0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c,39
0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169};40
constexpr gf Y{0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666,41
0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666};42
constexpr gf I{0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43,43
0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83};45
// The group order L = 2^252 + 27742317777372353535851937790883648493, little-endian.46
constexpr i64 LCONST[32] = {0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58,47
0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,48
0, 0, 0, 0, 0, 0, 0, 0,49
0, 0, 0, 0, 0, 0, 0, 0x10};51
void set25519(gf& r, const gf& a) { r = a; }53
void car25519(gf& o) {54
for (int i = 0; i < 16; ++i) {55
o[i] += (1LL << 16);56
const i64 c = o[i] >> 16;57
// i<15: carry into the next limb; i==15: carry wraps as 38*(c-1) (since 2^256≡38).58
o[(i + 1) * (i < 15)] += c - 1 + 37 * (c - 1) * (i == 15);59
o[i] -= c << 16;60
}61
}63
void sel25519(gf& p, gf& q, int b) {64
const i64 c = ~(b - 1);65
for (int i = 0; i < 16; ++i) {66
const i64 t = c & (p[i] ^ q[i]);67
p[i] ^= t;68
q[i] ^= t;69
}70
}72
void pack25519(u8* o, const gf& n) {73
gf m{}, t = n;74
car25519(t);75
car25519(t);76
car25519(t);77
for (int j = 0; j < 2; ++j) {78
m[0] = t[0] - 0xffed;79
for (int i = 1; i < 15; ++i) {80
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);81
m[i - 1] &= 0xffff;82
}83
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);84
const int b = (m[15] >> 16) & 1;85
m[14] &= 0xffff;86
sel25519(t, m, 1 - b);87
}88
for (int i = 0; i < 16; ++i) {89
o[2 * i] = static_cast<u8>(t[i] & 0xff);90
o[2 * i + 1] = static_cast<u8>(t[i] >> 8);91
}92
}94
int neq25519(const gf& a, const gf& b) {95
u8 c[32], d[32];96
pack25519(c, a);97
pack25519(d, b);98
// crypto_verify_32 (constant-time): 0 if equal.99
unsigned diff = 0;100
for (int i = 0; i < 32; ++i) diff |= static_cast<unsigned>(c[i] ^ d[i]);101
return (1 & ((diff - 1) >> 8)) - 1; // 0 if equal, -1 otherwise102
}104
u8 par25519(const gf& a) {105
u8 d[32];106
pack25519(d, a);107
return d[0] & 1;108
}110
void unpack25519(gf& o, const u8* n) {111
for (int i = 0; i < 16; ++i) o[i] = n[2 * i] + (static_cast<i64>(n[2 * i + 1]) << 8);112
o[15] &= 0x7fff;113
}115
void A(gf& o, const gf& a, const gf& b) {116
for (int i = 0; i < 16; ++i) o[i] = a[i] + b[i];117
}118
void Z(gf& o, const gf& a, const gf& b) {119
for (int i = 0; i < 16; ++i) o[i] = a[i] - b[i];120
}121
void M(gf& o, const gf& a, const gf& b) {122
i64 t[31] = {0};123
for (int i = 0; i < 16; ++i)124
for (int j = 0; j < 16; ++j) t[i + j] += a[i] * b[j];125
for (int i = 0; i < 15; ++i) t[i] += 38 * t[i + 16];126
for (int i = 0; i < 16; ++i) o[i] = t[i];127
car25519(o);128
car25519(o);129
}130
void S(gf& o, const gf& a) { M(o, a, a); }132
void inv25519(gf& o, const gf& i_) {133
gf c = i_;134
for (int a = 253; a >= 0; --a) {135
S(c, c);136
if (a != 2 && a != 4) M(c, c, i_);137
}138
o = c;139
}141
void pow2523(gf& o, const gf& i_) {142
gf c = i_;143
for (int a = 250; a >= 0; --a) {144
S(c, c);145
if (a != 1) M(c, c, i_);146
}147
o = c;148
}150
// A curve point in extended coordinates p = [X, Y, Z, T].151
void add(gf p[4], gf q[4]) {152
gf a, b, c, d, t, e, f, g, h;153
Z(a, p[1], p[0]);154
Z(t, q[1], q[0]);155
M(a, a, t);156
A(b, p[0], p[1]);157
A(t, q[0], q[1]);158
M(b, b, t);159
M(c, p[3], q[3]);160
M(c, c, D2);161
M(d, p[2], q[2]);162
A(d, d, d);163
Z(e, b, a);164
Z(f, d, c);165
A(g, d, c);166
A(h, b, a);167
M(p[0], e, f);168
M(p[1], h, g);169
M(p[2], g, f);170
M(p[3], e, h);171
}173
void cswap(gf p[4], gf q[4], u8 b) {174
for (int i = 0; i < 4; ++i) sel25519(p[i], q[i], b);175
}177
void pack(u8* r, gf p[4]) {178
gf tx, ty, zi;179
inv25519(zi, p[2]);180
M(tx, p[0], zi);181
M(ty, p[1], zi);182
pack25519(r, ty);183
r[31] ^= par25519(tx) << 7;184
}186
void scalarmult(gf p[4], gf q[4], const u8* s) {187
set25519(p[0], gf0);188
set25519(p[1], gf1);189
set25519(p[2], gf1);190
set25519(p[3], gf0);191
for (int i = 255; i >= 0; --i) {192
const u8 b = (s[i / 8] >> (i & 7)) & 1;193
cswap(p, q, b);194
add(q, p);195
add(p, p);196
cswap(p, q, b);197
}198
}200
void scalarbase(gf p[4], const u8* s) {201
gf q[4];202
set25519(q[0], X);203
set25519(q[1], Y);204
set25519(q[2], gf1);205
M(q[3], X, Y);206
scalarmult(p, q, s);207
}209
void modL(u8* r, i64 x[64]) {210
for (int i = 63; i >= 32; --i) {211
i64 carry = 0;212
int j = i - 32;213
for (; j < i - 12; ++j) {214
x[j] += carry - 16 * x[i] * LCONST[j - (i - 32)];215
carry = (x[j] + 128) >> 8;216
x[j] -= carry << 8;217
}218
x[j] += carry;219
x[i] = 0;220
}221
i64 carry = 0;222
for (int j = 0; j < 32; ++j) {223
x[j] += carry - (x[31] >> 4) * LCONST[j];224
carry = x[j] >> 8;225
x[j] &= 255;226
}227
for (int j = 0; j < 32; ++j) x[j] -= carry * LCONST[j];228
for (int i = 0; i < 32; ++i) {229
x[i + 1] += x[i] >> 8;230
r[i] = static_cast<u8>(x[i] & 255);231
}232
}234
void reduce(u8* r) {235
i64 x[64];236
for (int i = 0; i < 64; ++i) x[i] = static_cast<u64>(r[i]);237
for (int i = 0; i < 64; ++i) r[i] = 0;238
modL(r, x);239
}241
// Whether the 32-byte little-endian scalar @p s is canonical, i.e. s < L (the group242
// order). RFC 8032 strict verification rejects a non-canonical S, which closes the243
// signature-malleability door (a forger can't add a multiple of L to S).244
bool scalar_is_canonical(const u8* s) {245
for (int i = 31; i >= 0; --i) {246
const u8 li = static_cast<u8>(LCONST[i]);247
if (s[i] < li) return true;248
if (s[i] > li) return false;249
}250
return false; // s == L is NOT canonical251
}253
int unpackneg(gf r[4], const u8 p[32]) {254
gf t, chk, num, den, den2, den4, den6;255
set25519(r[2], gf1);256
unpack25519(r[1], p);257
S(num, r[1]);258
M(den, num, D);259
Z(num, num, r[2]);260
A(den, r[2], den);261
S(den2, den);262
S(den4, den2);263
M(den6, den4, den2);264
M(t, den6, num);265
M(t, t, den);266
pow2523(t, t);267
M(t, t, num);268
M(t, t, den);269
M(t, t, den);270
M(r[0], t, den);271
S(chk, r[0]);272
M(chk, chk, den);273
if (neq25519(chk, num)) M(r[0], r[0], I);274
S(chk, r[0]);275
M(chk, chk, den);276
if (neq25519(chk, num)) return -1;277
if (par25519(r[0]) == (p[31] >> 7)) Z(r[0], gf0, r[0]);278
M(r[3], r[0], r[1]);279
return 0;280
}282
// SHA-512 of n bytes -> 64-byte digest, via cheatah::hashlib.283
void sha512(u8* out, const u8* in, std::size_t n) {284
const std::string d =285
hashlib::sha512_digest(std::string_view(reinterpret_cast<const char*>(in), n));286
for (int i = 0; i < 64; ++i) out[i] = static_cast<u8>(d[i]);287
}289
// ---- byte/hex helpers: the ONE canonical implementation lives in hashlib ----290
using hashlib::from_hex; // hex -> bytes (throws on odd length / non-hex); inverse of to_hex.291
using hashlib::to_hex; // bytes -> lowercase hex — both the (u8*, n) and string_view overloads.293
void secure_random(u8* out, std::size_t n) {294
#if defined(_WIN32)295
if (::BCryptGenRandom(nullptr, out, static_cast<unsigned long>(n),296
BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0)297
throw std::runtime_error("ed25519: BCryptGenRandom failed");298
#else299
std::size_t off = 0;300
while (off < n) {301
const std::size_t chunk = (n - off < 256) ? (n - off) : 256;302
if (::getentropy(out + off, chunk) != 0) throw std::runtime_error("ed25519: getentropy failed");303
off += chunk;304
}305
#endif306
}308
// Derive the 32-byte public key into pub from a 32-byte seed.309
void public_from_seed(u8 pub[32], const u8 seed[32]) {310
u8 d[64];311
sha512(d, seed, 32);312
d[0] &= 248;313
d[31] &= 127;314
d[31] |= 64;315
gf p[4];316
scalarbase(p, d);317
pack(pub, p);318
}320
} // namespace322
std::string public_key(std::string_view secret_hex) {323
const std::string seed = from_hex(secret_hex);324
if (seed.size() != 32) throw std::invalid_argument("ed25519: secret seed must be 32 bytes (64 hex)");325
u8 pub[32];326
public_from_seed(pub, reinterpret_cast<const u8*>(seed.data()));327
return to_hex(pub, 32);328
}330
std::string generate() {331
u8 seed[32];332
secure_random(seed, 32);333
return to_hex(seed, 32);334
}336
std::string sign(std::string_view secret_hex, std::string_view message) {337
const std::string seed = from_hex(secret_hex);338
if (seed.size() != 32) throw std::invalid_argument("ed25519: secret seed must be 32 bytes (64 hex)");339
const u8* sd = reinterpret_cast<const u8*>(seed.data());340
const std::size_t n = message.size();342
u8 d[64];343
sha512(d, sd, 32);344
d[0] &= 248;345
d[31] &= 127;346
d[31] |= 64; // a = d[0..31] (clamped scalar)348
u8 pub[32];349
{350
gf p[4];351
scalarbase(p, d);352
pack(pub, p);353
}355
// r = SHA512(prefix || M), prefix = d[32..63]356
std::string rbuf_in(reinterpret_cast<const char*>(d + 32), 32);357
rbuf_in.append(message.data(), n);358
u8 r[64];359
sha512(r, reinterpret_cast<const u8*>(rbuf_in.data()), rbuf_in.size());360
reduce(r);362
// R = r * B363
u8 R[32];364
{365
gf p[4];366
scalarbase(p, r);367
pack(R, p);368
}370
// k = SHA512(R || A || M)371
std::string kbuf_in(reinterpret_cast<const char*>(R), 32);372
kbuf_in.append(reinterpret_cast<const char*>(pub), 32);373
kbuf_in.append(message.data(), n);374
u8 h[64];375
sha512(h, reinterpret_cast<const u8*>(kbuf_in.data()), kbuf_in.size());376
reduce(h);378
// S = (r + k*a) mod L379
i64 x[64] = {0};380
for (int i = 0; i < 32; ++i) x[i] = static_cast<u64>(r[i]);381
for (int i = 0; i < 32; ++i)382
for (int j = 0; j < 32; ++j) x[i + j] += static_cast<i64>(h[i]) * static_cast<i64>(d[j]);383
u8 Sout[32];384
modL(Sout, x);386
u8 sig[64];387
for (int i = 0; i < 32; ++i) sig[i] = R[i];388
for (int i = 0; i < 32; ++i) sig[32 + i] = Sout[i];389
return to_hex(sig, 64);390
}392
bool verify(std::string_view public_hex, std::string_view message, std::string_view signature_hex) {393
std::string pub, sig;394
try {395
pub = from_hex(public_hex);396
sig = from_hex(signature_hex);397
} catch (const std::exception&) {398
return false; // malformed hex -> reject399
}400
if (pub.size() != 32 || sig.size() != 64) return false;401
const u8* A_ = reinterpret_cast<const u8*>(pub.data());402
const u8* sg = reinterpret_cast<const u8*>(sig.data());404
if (!scalar_is_canonical(sg + 32)) return false; // reject non-canonical S (S >= L)406
gf q[4];407
if (unpackneg(q, A_) != 0) return false; // not a valid public key point409
// h = SHA512(R || A || M)410
std::string hin(reinterpret_cast<const char*>(sg), 32); // R411
hin.append(reinterpret_cast<const char*>(A_), 32); // A412
hin.append(message.data(), message.size()); // M413
u8 h[64];414
sha512(h, reinterpret_cast<const u8*>(hin.data()), hin.size());415
reduce(h);417
// p = S*B - h*A (q already holds -A)418
gf p[4];419
scalarmult(p, q, h); // p = h * (-A)420
gf g[4];421
scalarbase(g, sg + 32); // S * B422
add(p, g);424
u8 t[32];425
pack(t, p);426
// Accept iff the recomputed R equals the signature's R, constant-time.427
unsigned diff = 0;428
for (int i = 0; i < 32; ++i) diff |= static_cast<unsigned>(sg[i] ^ t[i]);429
return diff == 0;430
}432
} // namespace cheatah::ed25519