Add project files.

This commit is contained in:
micro
2026-06-20 09:31:59 -05:00
parent 9bb3f00d86
commit 4baa4ce8c0
346 changed files with 55751 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
using Org.BouncyCastle.Math.EC.Rfc7748;
namespace Wingnal.Protocol.ZkGroup.Curve;
/// <summary>
/// A field element of GF(2²⁵⁵−19), wrapping BouncyCastle's vetted constant-time <see cref="X25519Field"/>
/// representation (10 limbs) with value-semantics helpers (each op returns a fresh element, so BC's
/// not-alias-safe Mul/Sqr are always called with distinct outputs). Every returned element is carried, so
/// it is safe to feed straight into another Mul/Sqr. This is the base field for the Ristretto255 group
/// hand-port (zkgroup) — see <see cref="Ristretto255"/>.
/// </summary>
internal sealed class Fe
{
internal readonly int[] L; // 10-limb X25519Field representation
private Fe(int[] l) => L = l;
public static Fe Zero() { var f = new Fe(X25519Field.Create()); X25519Field.Zero(f.L); return f; }
public static Fe One() { var f = new Fe(X25519Field.Create()); X25519Field.One(f.L); return f; }
/// <summary>Decodes 32 little-endian bytes, masking bit 255 (dalek <c>FieldElement::from_bytes</c>
/// semantics). The value is reduced mod p on the next Normalize/Encode.</summary>
public static Fe Decode(ReadOnlySpan<byte> bytes32)
{
Span<byte> b = stackalloc byte[32];
bytes32[..32].CopyTo(b);
b[31] &= 0x7f;
var f = new Fe(X25519Field.Create());
X25519Field.Decode(b.ToArray(), 0, f.L);
return f;
}
/// <summary>Canonical 32-byte little-endian encoding (reduced mod p; bit 255 = 0).</summary>
public byte[] Encode()
{
int[] t = (int[])L.Clone();
X25519Field.Normalize(t);
var b = new byte[32];
X25519Field.Encode(t, b, 0);
return b;
}
public Fe Clone() => new((int[])L.Clone());
public static Fe Add(Fe a, Fe b) { var r = new Fe(X25519Field.Create()); X25519Field.Add(a.L, b.L, r.L); X25519Field.Carry(r.L); return r; }
public static Fe Sub(Fe a, Fe b) { var r = new Fe(X25519Field.Create()); X25519Field.Sub(a.L, b.L, r.L); X25519Field.Carry(r.L); return r; }
public static Fe Mul(Fe a, Fe b) { var r = new Fe(X25519Field.Create()); X25519Field.Mul(a.L, b.L, r.L); return r; }
public static Fe Sqr(Fe a) { var r = new Fe(X25519Field.Create()); X25519Field.Sqr(a.L, r.L); return r; }
public static Fe Inv(Fe a) { var r = new Fe(X25519Field.Create()); X25519Field.Inv(a.L, r.L); return r; }
public static Fe Neg(Fe a) { var r = a.Clone(); X25519Field.CNegate(1, r.L); X25519Field.Carry(r.L); return r; }
private static Fe SqrN(Fe x, int n) { Fe r = x; for (int i = 0; i < n; i++) r = Sqr(r); return r; }
/// <summary>x^((p5)/8) = x^(2²⁵²−3), the inverse-fourth-root exponent used by sqrt_ratio. ref10's
/// pow22523 addition chain (BC's equivalent <c>X25519Field.PowPm5d8</c> is internal).</summary>
public static Fe PowP58(Fe z)
{
Fe t0 = Sqr(z); // z^2
Fe t1 = Sqr(Sqr(t0)); // z^8
t1 = Mul(z, t1); // z^9
t0 = Mul(t0, t1); // z^11
t0 = Sqr(t0); // z^22
t0 = Mul(t1, t0); // z^(2^5-1)
t1 = SqrN(t0, 5); t0 = Mul(t1, t0); // 2^10-1
t1 = SqrN(t0, 10); t1 = Mul(t1, t0); // 2^20-1
Fe t2 = SqrN(t1, 20); t1 = Mul(t2, t1); // 2^40-1
t1 = SqrN(t1, 10); t0 = Mul(t1, t0); // 2^50-1
t1 = SqrN(t0, 50); t1 = Mul(t1, t0); // 2^100-1
t2 = SqrN(t1, 100); t1 = Mul(t2, t1); // 2^200-1
t1 = SqrN(t1, 50); t0 = Mul(t1, t0); // 2^250-1
t0 = Sqr(Sqr(t0)); // 2^252-4
return Mul(t0, z); // 2^252-3
}
/// <summary>cond ? b : a (constant-time select).</summary>
public static Fe Select(Fe a, Fe b, bool cond)
{
var r = a.Clone();
X25519Field.CMov(cond ? -1 : 0, b.L, 0, r.L, 0);
return r;
}
public bool IsNegative() => (Encode()[0] & 1) == 1;
public bool IsZero() => Equals(Zero());
public bool ConstantTimeEquals(Fe other) => Encode().AsSpan().SequenceEqual(other.Encode());
public bool Equals(Fe other) => ConstantTimeEquals(other);
/// <summary>|x| = (x is negative) ? x : x.</summary>
public Fe Abs() => IsNegative() ? Neg(this) : Clone();
}
+52
View File
@@ -0,0 +1,52 @@
using System.Security.Cryptography;
namespace Wingnal.Protocol.ZkGroup.Curve;
/// <summary>
/// The "Lizard" encoding from the curve25519-dalek-<b>signal</b> fork (NOT RFC 9496, NOT upstream dalek):
/// reversibly maps 16 bytes (a raw UUID) to a Ristretto255 point and back. zkgroup uses it to put a
/// member's ACI/PNI inside a homomorphically-encryptable group element (see <c>UidStruct.M2</c>).
///
/// Encode: <c>fe = SHA-256(data) with bytes[8..24] overwritten by data, low bit and top two bits cleared;
/// point = ElligatorRistrettoFlavor(fe)</c>. Decode inverts Elligator (up to 8 candidate field elements via
/// the Jacobi quartic) and keeps the unique one whose embedded bytes re-hash to itself.
///
/// Validated against the dalek-signal lizard test vectors (encode) + round-trip (decode).
/// NOT constant-time (data-dependent branching in decode) — acceptable for client-side group decryption.
/// </summary>
public static class Lizard
{
/// <summary>Encodes 16 bytes to a Ristretto255 point.</summary>
public static Ristretto255 Encode(ReadOnlySpan<byte> data16)
{
if (data16.Length != 16) throw new ArgumentException("Lizard.Encode expects 16 bytes");
Span<byte> feBytes = stackalloc byte[32];
SHA256.HashData(data16, feBytes);
data16.CopyTo(feBytes[8..24]);
feBytes[0] &= 254; // make positive — Elligator on r and -r is the same
feBytes[31] &= 63; // < 2²⁵⁴
return Ristretto255.FromSingleElligatorBytes(feBytes);
}
/// <summary>Recovers the 16 bytes from a Lizard-encoded point, or null if it isn't a valid encoding.</summary>
public static byte[]? Decode(Ristretto255 p)
{
(byte mask, Fe[] fes) = p.ElligatorInverse();
byte[]? result = null;
int found = 0;
Span<byte> recomputed = stackalloc byte[32];
for (int j = 0; j < 8; j++)
{
if (((mask >> j) & 1) == 0) continue;
byte[] buf = fes[j].Encode(); // 32-byte canonical encoding
SHA256.HashData(buf.AsSpan(8, 16), recomputed);
buf.AsSpan(8, 16).CopyTo(recomputed[8..24]);
recomputed[0] &= 254;
recomputed[31] &= 63;
if (!recomputed.SequenceEqual(buf)) continue;
result = buf[8..24];
found++;
}
return found == 1 ? result : null;
}
}
@@ -0,0 +1,304 @@
namespace Wingnal.Protocol.ZkGroup.Curve;
/// <summary>
/// Hand-port of the Ristretto255 prime-order group (RFC 9496) on top of <see cref="Fe"/> / BouncyCastle's
/// edwards25519 base field — BouncyCastle 2.5.1 has no Ristretto, and zkgroup is built entirely on this
/// group. Provides point add / scalar-mul, the canonical 32-byte encode/decode, and the one-way map
/// <see cref="FromUniformBytes"/> (Elligator) used for hash-to-group. Correctness is gated by the RFC 9496
/// Appendix-A test vectors (multiples of the generator, invalid-encoding rejection, hash-to-group).
///
/// NOTE: scalar multiplication here is NOT constant-time (double-and-add with a data-dependent add). For
/// the client-side zkgroup proofs this is acceptable for a first correct version; harden later. See
/// docs/GROUPS.md / SHORTCUTS.md.
/// </summary>
public sealed class Ristretto255
{
// ── field constants (computed from definitions; SQRT_M1 hardcoded + self-checked in tests) ──
/// <summary>sqrt(-1) mod p, COMPUTED (not transcribed): p ≡ 5 (mod 8) ⇒ 2 is a non-residue, so
/// 2^((p-1)/4) is a square root of -1, and (p-1)/4 = 2·(p-5)/8 + 1 ⇒ SQRT_M1 = 2·(2^((p-5)/8))².
/// Self-checked SQRT_M1² == -1 by the Phase B vector test.</summary>
internal static readonly Fe SqrtM1 = BuildSqrtM1();
private static Fe BuildSqrtM1()
{
Fe two = Fe.Add(Fe.One(), Fe.One());
return Fe.Mul(Fe.Sqr(Fe.PowP58(two)), two);
}
internal static readonly Fe D = BuildD(); // edwards25519 d = -121665/121666
private static readonly Fe D2 = Fe.Add(D, D); // 2d, for the addition formula
private static readonly Fe OneMinusDSq = Fe.Sub(Fe.One(), Fe.Sqr(D)); // 1 - d²
private static readonly Fe DMinusOneSq = Fe.Sqr(Fe.Sub(D, Fe.One())); // (d - 1)²
// a = -1, so a - d = a*d - 1 = -1 - d.
private static readonly Fe AMinusD = Fe.Sub(Fe.Neg(Fe.One()), D); // -1 - d
// 1/sqrt(-1-d): the abs (even) root — matches dalek INVSQRT_A_MINUS_D (even).
internal static readonly Fe InvSqrtAMinusD = SqrtRatioM1(Fe.One(), AMinusD).root;
// sqrt(-1-d): dalek SQRT_AD_MINUS_ONE is the odd root, so negate the abs (even) root.
private static readonly Fe SqrtADMinusOne = Fe.Neg(SqrtRatioM1(AMinusD, Fe.One()).root);
// ── Lizard constants (computed from the definitions in the dalek-signal lizard_constants test) ──
// SQRT_ID = sqrt(i·d) (abs root); DP1_OVER_DM1 = (d+1)/(d-1);
// MDOUBLE_INVSQRT_A_MINUS_D = -2/sqrt(a-d); MIDOUBLE = that·i; MINVSQRT_ONE_PLUS_D = -1/sqrt(1+d).
private static readonly Fe SqrtId = SqrtRatioM1(Fe.Mul(SqrtM1, D), Fe.One()).root;
private static readonly Fe Dp1OverDm1 = Fe.Mul(Fe.Add(D, Fe.One()), Fe.Inv(Fe.Sub(D, Fe.One())));
private static readonly Fe MDoubleInvSqrtAMinusD = Fe.Neg(Fe.Add(InvSqrtAMinusD, InvSqrtAMinusD));
private static readonly Fe MiDoubleInvSqrtAMinusD = Fe.Mul(MDoubleInvSqrtAMinusD, SqrtM1);
private static readonly Fe MInvSqrtOnePlusD = Fe.Neg(SqrtRatioM1(Fe.One(), Fe.Add(D, Fe.One())).root);
private static Fe BuildD()
{
// d = -121665/121666 (computed, not transcribed).
var num = new byte[32]; num[0] = 0x41; num[1] = 0xDB; num[2] = 0x01; // 121665 = 0x1DB41
var den = new byte[32]; den[0] = 0x42; den[1] = 0xDB; den[2] = 0x01; // 121666 = 0x1DB42
return Fe.Neg(Fe.Mul(Fe.Decode(num), Fe.Inv(Fe.Decode(den))));
}
// ── point (extended twisted-Edwards coordinates X:Y:Z:T) ──
private readonly Fe _x, _y, _z, _t;
private Ristretto255(Fe x, Fe y, Fe z, Fe t) { _x = x; _y = y; _z = z; _t = t; }
/// <summary>The identity element.</summary>
public static Ristretto255 Identity => new(Fe.Zero(), Fe.One(), Fe.One(), Fe.Zero());
/// <summary>The Ristretto255 generator (canonical encoding e2f2ae0a…).</summary>
public static Ristretto255 BasePoint => Decode(
Convert.FromHexString("e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76"))!;
/// <summary>Group addition (the complete a=-1 twisted-Edwards formula; also valid for doubling).</summary>
public static Ristretto255 Add(Ristretto255 p, Ristretto255 q)
{
Fe a = Fe.Mul(Fe.Sub(p._y, p._x), Fe.Sub(q._y, q._x)); // (Y1-X1)(Y2-X2)
Fe b = Fe.Mul(Fe.Add(p._y, p._x), Fe.Add(q._y, q._x)); // (Y1+X1)(Y2+X2)
Fe c = Fe.Mul(Fe.Mul(p._t, q._t), D2); // 2d·T1·T2
Fe d = Fe.Add(Fe.Mul(p._z, q._z), Fe.Mul(p._z, q._z)); // 2·Z1·Z2
Fe e = Fe.Sub(b, a), f = Fe.Sub(d, c), g = Fe.Add(d, c), h = Fe.Add(b, a);
return new Ristretto255(Fe.Mul(e, f), Fe.Mul(g, h), Fe.Mul(f, g), Fe.Mul(e, h));
}
/// <summary>Group negation: (X:Y:Z:T) = (X:Y:Z:T).</summary>
public static Ristretto255 Negate(Ristretto255 p) => new(Fe.Neg(p._x), p._y, p._z, Fe.Neg(p._t));
/// <summary>scalar·this.</summary>
public Ristretto255 Multiply(Scalar25519 s) => Multiply(s.ToBytes());
/// <summary>scalar·this (double-and-add, MSB first; scalar is 32-byte little-endian).</summary>
public Ristretto255 Multiply(ReadOnlySpan<byte> scalarLe)
{
Ristretto255 r = Identity;
for (int i = 255; i >= 0; i--)
{
r = Add(r, r);
if (((scalarLe[i >> 3] >> (i & 7)) & 1) == 1) r = Add(r, this);
}
return r;
}
/// <summary>Ristretto equality: two representatives are equal iff X1·Y2 == Y1·X2 and Y1·Y2 == X1·X2
/// (RFC 9496 §4.3.6). Cheaper + sign-robust vs comparing encodings.</summary>
public bool ConstantTimeEquals(Ristretto255 q)
{
bool a = Fe.Mul(_x, q._y).ConstantTimeEquals(Fe.Mul(_y, q._x));
bool b = Fe.Mul(_y, q._y).ConstantTimeEquals(Fe.Mul(_x, q._x));
return a || b;
}
// ── encode / decode (RFC 9496 §4.3.14.3.2) ──
public byte[] Encode()
{
Fe u1 = Fe.Mul(Fe.Add(_z, _y), Fe.Sub(_z, _y)); // (Z+Y)(Z-Y)
Fe u2 = Fe.Mul(_x, _y);
(_, Fe invsqrt) = SqrtRatioM1(Fe.One(), Fe.Mul(u1, Fe.Sqr(u2)));
Fe den1 = Fe.Mul(invsqrt, u1);
Fe den2 = Fe.Mul(invsqrt, u2);
Fe zInv = Fe.Mul(Fe.Mul(den1, den2), _t);
Fe ix = Fe.Mul(_x, SqrtM1);
Fe iy = Fe.Mul(_y, SqrtM1);
Fe enchantedDenominator = Fe.Mul(den1, InvSqrtAMinusD);
bool rotate = Fe.Mul(_t, zInv).IsNegative();
Fe x = Fe.Select(_x, iy, rotate);
Fe y = Fe.Select(_y, ix, rotate);
Fe denInv = Fe.Select(den2, enchantedDenominator, rotate);
y = Fe.Select(y, Fe.Neg(y), Fe.Mul(x, zInv).IsNegative());
Fe s = Fe.Mul(denInv, Fe.Sub(_z, y)).Abs();
return s.Encode();
}
public static Ristretto255? Decode(ReadOnlySpan<byte> bytes32)
{
if (bytes32.Length != 32) return null;
Fe s = Fe.Decode(bytes32);
// s must be the canonical encoding of a non-negative field element.
if (!s.Encode().AsSpan().SequenceEqual(bytes32) || s.IsNegative()) return null;
Fe ss = Fe.Sqr(s);
Fe u1 = Fe.Sub(Fe.One(), ss); // 1 - s²
Fe u2 = Fe.Add(Fe.One(), ss); // 1 + s²
Fe u2Sqr = Fe.Sqr(u2);
Fe v = Fe.Sub(Fe.Neg(Fe.Mul(D, Fe.Sqr(u1))), u2Sqr); // -(d·u1²) - u2²
(bool wasSquare, Fe invsqrt) = SqrtRatioM1(Fe.One(), Fe.Mul(v, u2Sqr));
Fe denX = Fe.Mul(invsqrt, u2);
Fe denY = Fe.Mul(Fe.Mul(invsqrt, denX), v);
Fe x = Fe.Mul(Fe.Add(s, s), denX).Abs(); // |2·s·den_x|
Fe y = Fe.Mul(u1, denY);
Fe t = Fe.Mul(x, y);
if (!wasSquare || t.IsNegative() || y.IsZero()) return null;
return new Ristretto255(x, y, Fe.One(), t);
}
// ── hash-to-group (RFC 9496 §4.3.4) ──
/// <summary>Maps 64 uniformly-random bytes to a group element (two Elligator maps + add).</summary>
public static Ristretto255 FromUniformBytes(ReadOnlySpan<byte> bytes64)
{
Ristretto255 p1 = ElligatorRistrettoFlavor(Fe.Decode(bytes64[..32]));
Ristretto255 p2 = ElligatorRistrettoFlavor(Fe.Decode(bytes64[32..64]));
return Add(p1, p2);
}
/// <summary>Maps a single 32-byte field element to a group element (one Elligator map). This is
/// dalek-signal's <c>from_uniform_bytes_single_elligator</c> / zkgroup's <c>get_point_single_elligator</c>,
/// and the encode half of Lizard.</summary>
public static Ristretto255 FromSingleElligatorBytes(ReadOnlySpan<byte> bytes32) =>
ElligatorRistrettoFlavor(Fe.Decode(bytes32));
/// <summary>The Ristretto-flavored Elligator2 map (RFC 9496 §4.3.4 MAP). Public so Lizard can reuse it.</summary>
internal static Ristretto255 ElligatorRistrettoFlavor(Fe t)
{
Fe r = Fe.Mul(SqrtM1, Fe.Sqr(t));
Fe u = Fe.Mul(Fe.Add(r, Fe.One()), OneMinusDSq);
Fe c = Fe.Neg(Fe.One());
Fe v = Fe.Mul(Fe.Sub(c, Fe.Mul(r, D)), Fe.Add(r, D));
(bool wasSquare, Fe s) = SqrtRatioM1(u, v);
Fe sPrime = Fe.Neg(Fe.Mul(s, t).Abs());
s = Fe.Select(sPrime, s, wasSquare);
c = Fe.Select(r, c, wasSquare);
Fe n = Fe.Sub(Fe.Mul(Fe.Mul(c, Fe.Sub(r, Fe.One())), DMinusOneSq), v);
Fe w0 = Fe.Add(Fe.Mul(s, v), Fe.Mul(s, v)); // 2·s·v
Fe w1 = Fe.Mul(n, SqrtADMinusOne);
Fe w2 = Fe.Sub(Fe.One(), Fe.Sqr(s));
Fe w3 = Fe.Add(Fe.One(), Fe.Sqr(s));
return new Ristretto255(Fe.Mul(w0, w3), Fe.Mul(w2, w1), Fe.Mul(w1, w3), Fe.Mul(w0, w2));
}
// ── sqrt_ratio_i (RFC 9496 §4.3) : returns (wasSquare, |sqrt(u/v)|) ──
internal static (bool wasSquare, Fe root) SqrtRatioM1(Fe u, Fe v)
{
Fe v3 = Fe.Mul(Fe.Sqr(v), v);
Fe v7 = Fe.Mul(Fe.Sqr(v3), v);
Fe r = Fe.Mul(Fe.Mul(u, v3), Fe.PowP58(Fe.Mul(u, v7)));
Fe check = Fe.Mul(v, Fe.Sqr(r));
Fe uNeg = Fe.Neg(u);
bool correct = check.ConstantTimeEquals(u);
bool flipped = check.ConstantTimeEquals(uNeg);
bool flippedI = check.ConstantTimeEquals(Fe.Mul(uNeg, SqrtM1));
Fe rPrime = Fe.Mul(SqrtM1, r);
r = Fe.Select(r, rPrime, flipped || flippedI);
return (correct || flipped, r.Abs());
}
// ── Elligator inverse (for Lizard decode) — port of the dalek-signal lizard fork ──
private readonly struct JacobiPoint
{
public readonly Fe S, T;
public JacobiPoint(Fe s, Fe t) { S = s; T = t; }
public JacobiPoint Dual() => new(Fe.Neg(S), Fe.Neg(T));
/// <summary>Computes the field element that Elligator2 maps to this Jacobi-quartic point, if any.</summary>
public (bool ok, Fe fe) ElligatorInv()
{
Fe outFe = Fe.Zero();
bool sIsZero = S.IsZero();
bool tEqualsOne = T.ConstantTimeEquals(Fe.One());
outFe = Fe.Select(outFe, SqrtId, tEqualsOne);
bool ret = sIsZero;
bool done = sIsZero;
Fe a = Fe.Mul(Fe.Add(T, Fe.One()), Dp1OverDm1);
Fe a2 = Fe.Sqr(a);
Fe s2 = Fe.Sqr(S);
Fe s4 = Fe.Sqr(s2);
Fe invSqY = Fe.Mul(Fe.Sub(s4, a2), SqrtM1);
(bool sq, Fe y) = SqrtRatioM1(Fe.One(), invSqY); // invsqrt
ret = ret || sq;
done = done || !sq;
Fe pms2 = Fe.Select(s2, Fe.Neg(s2), S.IsNegative()); // sign(s)·s²
Fe x = Fe.Mul(Fe.Add(a, pms2), y);
x = Fe.Select(x, Fe.Neg(x), x.IsNegative()); // |x|
outFe = Fe.Select(outFe, x, !done);
return (ret, outFe);
}
}
/// <summary>Computes the (at most 8) positive field elements f with this == ElligatorRistrettoFlavor(f),
/// plus a bitmask of which slots are set. Assumes this is even. Port of dalek-signal's
/// <c>elligator_ristretto_flavor_inverse</c>.</summary>
internal (byte mask, Fe[] fes) ElligatorInverse()
{
JacobiPoint[] jcs = ToJacobiQuarticRistretto();
var fes = new Fe[8];
for (int i = 0; i < 8; i++) fes[i] = Fe.One();
byte mask = 0;
for (int i = 0; i < 4; i++)
{
(bool ok0, Fe fe0) = jcs[i].ElligatorInv();
fes[2 * i] = fe0;
if (ok0) mask |= (byte)(1 << (2 * i));
(bool ok1, Fe fe1) = jcs[i].Dual().ElligatorInv();
fes[2 * i + 1] = fe1;
if (ok1) mask |= (byte)(1 << (2 * i + 1));
}
return (mask, fes);
}
private JacobiPoint[] ToJacobiQuarticRistretto()
{
Fe x2 = Fe.Sqr(_x), y2 = Fe.Sqr(_y), y4 = Fe.Sqr(y2), z2 = Fe.Sqr(_z);
Fe zMinY = Fe.Sub(_z, _y), zPlY = Fe.Add(_z, _y);
Fe z2MinY2 = Fe.Sub(z2, y2);
// gamma = 1/sqrt(Y⁴·X²·(Z²−Y²))
(_, Fe gamma) = SqrtRatioM1(Fe.One(), Fe.Mul(Fe.Mul(y4, x2), z2MinY2));
Fe den = Fe.Mul(gamma, y2);
Fe sOverX = Fe.Mul(den, zMinY);
Fe spOverXp = Fe.Mul(den, zPlY);
Fe s0 = Fe.Mul(sOverX, _x);
Fe s1 = Fe.Mul(Fe.Neg(spOverXp), _x);
Fe tmp = Fe.Mul(MDoubleInvSqrtAMinusD, _z);
Fe t0 = Fe.Mul(tmp, sOverX);
Fe t1 = Fe.Mul(tmp, spOverXp);
// den = -1/sqrt(1+d)·(Y²−Z²)·gamma (substitution (X,Y,Z) -> (Y,X,iZ))
Fe den2 = Fe.Mul(Fe.Mul(Fe.Neg(z2MinY2), MInvSqrtOnePlusD), gamma);
Fe iz = Fe.Mul(SqrtM1, _z);
Fe izMinX = Fe.Sub(iz, _x), izPlX = Fe.Add(iz, _x);
Fe sOverY = Fe.Mul(den2, izMinX);
Fe spOverYp = Fe.Mul(den2, izPlX);
Fe s2 = Fe.Mul(sOverY, _y);
Fe s3 = Fe.Mul(Fe.Neg(spOverYp), _y);
Fe tmp2 = Fe.Mul(MDoubleInvSqrtAMinusD, iz);
Fe t2 = Fe.Mul(tmp2, sOverY);
Fe t3 = Fe.Mul(tmp2, spOverYp);
// Special case X=0 or Y=0 (then sᵢ=tᵢ=0): return fixed coset points.
bool xy0 = _x.IsZero() || _y.IsZero();
t0 = Fe.Select(t0, Fe.One(), xy0);
t1 = Fe.Select(t1, Fe.One(), xy0);
t2 = Fe.Select(t2, MiDoubleInvSqrtAMinusD, xy0);
t3 = Fe.Select(t3, MiDoubleInvSqrtAMinusD, xy0);
s2 = Fe.Select(s2, Fe.One(), xy0);
s3 = Fe.Select(s3, Fe.Neg(Fe.One()), xy0);
return new[]
{
new JacobiPoint(s0, t0), new JacobiPoint(s1, t1),
new JacobiPoint(s2, t2), new JacobiPoint(s3, t3),
};
}
}
@@ -0,0 +1,69 @@
using System.Numerics;
namespace Wingnal.Protocol.ZkGroup.Curve;
/// <summary>
/// An integer modulo = 2²⁵² + 27742317777372353535851937790883648493 (the order of the Ristretto255 /
/// edwards25519 prime-order group). Backs zkgroup's scalar arithmetic. Implemented on
/// <see cref="BigInteger"/> for a clear first-correct version — NOT constant-time; harden with the ref10
/// <c>sc_*</c> routines later (see docs/GROUPS.md / SHORTCUTS.md). Canonical wire form is 32 little-endian
/// bytes.
/// </summary>
public readonly struct Scalar25519 : IEquatable<Scalar25519>
{
/// <summary>The group order .</summary>
public static readonly BigInteger L =
BigInteger.Pow(2, 252) + BigInteger.Parse("27742317777372353535851937790883648493");
private readonly BigInteger _v; // always reduced into [0, L)
private Scalar25519(BigInteger v)
{
BigInteger m = v % L;
_v = m.Sign < 0 ? m + L : m;
}
public static Scalar25519 Zero => new(BigInteger.Zero);
public static Scalar25519 One => new(BigInteger.One);
/// <summary>Reduces a 32-byte little-endian value mod .</summary>
public static Scalar25519 FromBytesModOrder(ReadOnlySpan<byte> le32) =>
new(new BigInteger(le32, isUnsigned: true, isBigEndian: false));
/// <summary>Reduces a 64-byte little-endian value mod (uniform hash → scalar).</summary>
public static Scalar25519 FromBytesModOrderWide(ReadOnlySpan<byte> le64) =>
new(new BigInteger(le64, isUnsigned: true, isBigEndian: false));
public static Scalar25519 FromBigInteger(BigInteger v) => new(v);
/// <summary>Parses a 32-byte little-endian scalar, returning null if it is not canonical (≥ ).</summary>
public static Scalar25519? FromCanonicalBytes(ReadOnlySpan<byte> le32)
{
if (le32.Length != 32) return null;
var v = new BigInteger(le32, isUnsigned: true, isBigEndian: false);
return v >= L ? null : new Scalar25519(v);
}
/// <summary>32-byte little-endian canonical encoding.</summary>
public byte[] ToBytes()
{
byte[] raw = _v.ToByteArray(isUnsigned: true, isBigEndian: false);
var result = new byte[32];
Array.Copy(raw, result, Math.Min(raw.Length, 32));
return result;
}
public BigInteger ToBigInteger() => _v;
public static Scalar25519 Add(Scalar25519 a, Scalar25519 b) => new(a._v + b._v);
public static Scalar25519 Sub(Scalar25519 a, Scalar25519 b) => new(a._v - b._v);
public static Scalar25519 Mul(Scalar25519 a, Scalar25519 b) => new(a._v * b._v);
public static Scalar25519 Negate(Scalar25519 a) => new(-a._v);
/// <summary>Multiplicative inverse mod ( is prime, so via Fermat: a^(-2)).</summary>
public Scalar25519 Invert() => new(BigInteger.ModPow(_v, L - 2, L));
public bool Equals(Scalar25519 other) => _v == other._v;
public override bool Equals(object? obj) => obj is Scalar25519 s && Equals(s);
public override int GetHashCode() => _v.GetHashCode();
}