// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// Ed25519 signing (RFC 8032) for MariaDB's client_ed25519 auth. The client proves
// the password by signing the server's 32-byte challenge; MariaDB derives the
// signing key by treating SHA-512(password) as the expanded secret (where stock
// Ed25519 would expand a 32-byte seed), the rest is deterministic RFC 8032 §5.1.6.
// Only the signing half is needed here — the server verifies — so point
// decompression and the verify equation are deliberately absent.
//
// Curve constants (RFC 8032) as big integers on the builtin BigInt: `p = 2^255-19`
// the field prime, `d` the curve coefficient, `l` the group order, `(bx, by)` the
// base point. Values are computed from the definition, not transcribed.
///|
let ed_p : BigInt = BigInt::from_string(
"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED",
radix=16,
)
///|
let ed_d : BigInt = BigInt::from_string(
"52036CEE2B6FFE738CC740797779E89800700A4D4141D8AB75EB4DCA135978A3",
radix=16,
)
///|
let ed_l : BigInt = BigInt::from_string(
"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED",
radix=16,
)
///|
let ed_bx : BigInt = BigInt::from_string(
"216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A",
radix=16,
)
///|
let ed_by : BigInt = BigInt::from_string(
"6666666666666666666666666666666666666666666666666666666666666658",
radix=16,
)
///|
/// A point on Edwards25519 in affine coordinates. The twisted-Edwards addition law
/// (a = -1) is complete, identity `(0, 1)`, so no infinity flag is needed.
priv struct EdPoint {
x : BigInt
y : BigInt
}
///|
fn edmod(a : BigInt) -> BigInt {
let m = a % ed_p
if m < (0 : BigInt) {
m + ed_p
} else {
m
}
}
///|
fn edmod_l(a : BigInt) -> BigInt {
let m = a % ed_l
if m < (0 : BigInt) {
m + ed_l
} else {
m
}
}
///|
fn edinv(a : BigInt) -> BigInt {
edmod(a).pow(ed_p - 2, modulus=ed_p)
}
///|
fn ed_add(pp : EdPoint, qq : EdPoint) -> EdPoint {
let x1 = pp.x
let y1 = pp.y
let x2 = qq.x
let y2 = qq.y
let dxy = edmod(ed_d * x1 * x2 * y1 * y2)
let x3 = edmod((x1 * y2 + x2 * y1) * edinv(edmod(1 + dxy)))
let y3 = edmod((y1 * y2 + x1 * x2) * edinv(edmod(1 - dxy)))
{ x: x3, y: y3 }
}
///|
/// Scalar multiplication `e · pt` by double-and-add.
fn ed_mul(e : BigInt, pt : EdPoint) -> EdPoint {
let mut result : EdPoint = { x: 0, y: 1 }
let mut addend = pt
let mut k = e
while k > (0 : BigInt) {
if k % 2 == (1 : BigInt) {
result = ed_add(result, addend)
}
addend = ed_add(addend, addend)
k = k / 2
}
result
}
///|
/// Interpret `b` as a little-endian unsigned integer (Ed25519's byte order).
fn ed_le_int(b : Bytes) -> BigInt {
let buf = Buffer()
for i = b.length() - 1; i >= 0; i = i - 1 {
buf.write_byte(b[i])
}
BigInt::from_octets(buf.to_bytes()[:])
}
///|
/// Encode a point as its 32-byte little-endian compressed form (RFC 8032 §5.1.2):
/// `y` in the low 255 bits, the parity of `x` in the top bit.
fn ed_encode_point(pt : EdPoint) -> Bytes {
let be = edmod(pt.y).to_octets(length=32)
let buf = Buffer()
for i = 31; i >= 1; i = i - 1 {
buf.write_byte(be[i])
}
let parity = if edmod(pt.x) % 2 == (1 : BigInt) { 0x80 } else { 0 }
buf.write_byte((be[0].to_int() | parity).to_byte())
buf.to_bytes()
}
///|
/// A big integer as `length` little-endian bytes (Ed25519's byte order for `S`).
fn ed_int_to_le(n : BigInt, length : Int) -> Bytes {
let be = n.to_octets(length~)
let buf = Buffer()
for i = length - 1; i >= 0; i = i - 1 {
buf.write_byte(be[i])
}
buf.to_bytes()
}
///|
/// Expand an Ed25519 secret (RFC 8032 §5.1.5): `SHA-512(secret)`, the low 32 bytes
/// clamped (`&= 0xF8`, top two bits forced) as the little-endian scalar, the high
/// 32 bytes as the PRF prefix. MariaDB feeds the password as the secret directly.
fn ed_expand(secret : Bytes) -> (BigInt, Bytes) {
let h = sha512(secret)
let abuf = Buffer()
abuf.write_byte((h[0].to_int() & 0xF8).to_byte())
for i = 1; i < 31; i = i + 1 {
abuf.write_byte(h[i])
}
abuf.write_byte(((h[31].to_int() & 0x7F) | 0x40).to_byte())
let s = ed_le_int(abuf.to_bytes())
let pbuf = Buffer()
for i = 32; i < 64; i = i + 1 {
pbuf.write_byte(h[i])
}
(s, pbuf.to_bytes())
}
///|
/// Ed25519 signing (RFC 8032 §5.1.6), deterministic, keyed by `secret` (expanded
/// via [`ed_expand`]). Returns the 64-byte `R || S`: `r = SHA-512(prefix || M) mod
/// l`, `R = [r]B`, `k = SHA-512(R || A || M) mod l`, `S = (r + k·s) mod l`.
pub fn ed25519_sign(secret : Bytes, msg : Bytes) -> Bytes {
let base : EdPoint = { x: ed_bx, y: ed_by }
let (s, prefix) = ed_expand(secret)
let big_a = ed_encode_point(ed_mul(s, base))
let rbuf = Buffer()
rbuf.write_bytes(prefix[:])
rbuf.write_bytes(msg[:])
let r = edmod_l(ed_le_int(sha512(rbuf.to_bytes())))
let big_r = ed_encode_point(ed_mul(r, base))
let kbuf = Buffer()
kbuf.write_bytes(big_r[:])
kbuf.write_bytes(big_a[:])
kbuf.write_bytes(msg[:])
let k = edmod_l(ed_le_int(sha512(kbuf.to_bytes())))
let s_scalar = edmod_l(r + k * s)
let out = Buffer()
out.write_bytes(big_r[:])
out.write_bytes(ed_int_to_le(s_scalar, 32)[:])
out.to_bytes()
}
///|
/// The MariaDB client_ed25519 response: the 64-byte Ed25519 signature of the
/// server's 32-byte challenge, keyed by the password. Unlike mysql_native_password,
/// the ed25519 plugin signs unconditionally — an empty password expands to
/// `SHA-512("")` and still yields a valid signature.
pub fn mariadb_ed25519_response(password : Bytes, scramble : Bytes) -> Bytes {
ed25519_sign(password, scramble)
}