Add project files.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using System.Security.Cryptography;
|
||||
using Org.BouncyCastle.Crypto.Agreement;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>A Curve25519 key pair. Private/public keys are raw 32-byte values.</summary>
|
||||
public sealed class ECKeyPair
|
||||
{
|
||||
/// <summary>32-byte clamped X25519 private scalar (little-endian).</summary>
|
||||
public byte[] PrivateKey { get; }
|
||||
|
||||
/// <summary>32-byte Montgomery u-coordinate public key.</summary>
|
||||
public byte[] PublicKey { get; }
|
||||
|
||||
public ECKeyPair(byte[] privateKey, byte[] publicKey)
|
||||
{
|
||||
PrivateKey = privateKey;
|
||||
PublicKey = publicKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// X25519 ECDH plus Signal's DjbECPublicKey (0x05-prefixed, 33-byte) serialization.
|
||||
/// Private keys are clamped at generation so the same scalar is used consistently for both ECDH
|
||||
/// and XEdDSA signing (clamping is idempotent, so BouncyCastle re-clamping during agreement is a no-op).
|
||||
/// </summary>
|
||||
public static class Curve25519
|
||||
{
|
||||
/// <summary>Signal's type byte for Curve25519 (DJB) public keys.</summary>
|
||||
public const byte DjbType = 0x05;
|
||||
|
||||
public static ECKeyPair GenerateKeyPair()
|
||||
{
|
||||
byte[] priv = RandomNumberGenerator.GetBytes(32);
|
||||
Clamp(priv);
|
||||
byte[] pub = DerivePublicKey(priv);
|
||||
return new ECKeyPair(priv, pub);
|
||||
}
|
||||
|
||||
/// <summary>Derives the 32-byte Montgomery public key from a 32-byte private scalar.</summary>
|
||||
public static byte[] DerivePublicKey(byte[] privateKey)
|
||||
{
|
||||
var sk = new X25519PrivateKeyParameters(privateKey, 0);
|
||||
return sk.GeneratePublicKey().GetEncoded();
|
||||
}
|
||||
|
||||
/// <summary>X25519 ECDH. Returns the 32-byte shared secret.</summary>
|
||||
public static byte[] CalculateAgreement(byte[] theirPublicKey, byte[] ourPrivateKey)
|
||||
{
|
||||
var agreement = new X25519Agreement();
|
||||
agreement.Init(new X25519PrivateKeyParameters(ourPrivateKey, 0));
|
||||
var secret = new byte[agreement.AgreementSize];
|
||||
agreement.CalculateAgreement(new X25519PublicKeyParameters(theirPublicKey, 0), secret, 0);
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>Serializes a raw 32-byte public key to a 33-byte DjbECPublicKey (0x05 || u).</summary>
|
||||
public static byte[] EncodePoint(byte[] publicKey)
|
||||
{
|
||||
if (publicKey.Length != 32) throw new ArgumentException("public key must be 32 bytes", nameof(publicKey));
|
||||
var encoded = new byte[33];
|
||||
encoded[0] = DjbType;
|
||||
Array.Copy(publicKey, 0, encoded, 1, 32);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/// <summary>Parses a serialized public key (33-byte 0x05-prefixed, or raw 32-byte) to raw 32 bytes.</summary>
|
||||
public static byte[] DecodePoint(ReadOnlySpan<byte> serialized)
|
||||
{
|
||||
if (serialized.Length == 33)
|
||||
{
|
||||
if (serialized[0] != DjbType) throw new ArgumentException($"unsupported key type {serialized[0]}");
|
||||
return serialized.Slice(1, 32).ToArray();
|
||||
}
|
||||
if (serialized.Length == 32) return serialized.ToArray();
|
||||
throw new ArgumentException($"bad public key length {serialized.Length}");
|
||||
}
|
||||
|
||||
private static void Clamp(byte[] scalar)
|
||||
{
|
||||
scalar[0] &= 248;
|
||||
scalar[31] &= 127;
|
||||
scalar[31] |= 64;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
using Org.BouncyCastle.Math.EC.Rfc7748;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>
|
||||
/// Constant-time Ed25519 primitives for the SIGNING path (the only place a long-term secret is used),
|
||||
/// built on BouncyCastle's vetted constant-time field <see cref="X25519Field"/>. Provides:
|
||||
/// a constant-time fixed-base scalar multiply (no data-dependent branches/table indexing) and the
|
||||
/// ref10 constant-time scalar arithmetic mod L (<see cref="ScReduce"/>, <see cref="ScMulAdd"/>).
|
||||
///
|
||||
/// Correctness is gated by a cross-check test: signatures produced via these must be byte-identical to
|
||||
/// the existing KAT-validated reference (<c>Ed25519Math</c>) for the same inputs, so a bug here can't
|
||||
/// silently change/break signatures.
|
||||
/// </summary>
|
||||
internal static class Ed25519Ct
|
||||
{
|
||||
// Base point B (x,y), little-endian 32-byte encodings (standard Ed25519 generator).
|
||||
private static readonly byte[] BxBytes = Convert.FromHexString("1ad5258f602d56c9b2a7259560c72c695cdcd6fd31e2a4c0fe536ecdd3366921");
|
||||
private static readonly byte[] ByBytes = Convert.FromHexString("5866666666666666666666666666666666666666666666666666666666666666");
|
||||
|
||||
private static readonly int[] D2 = BuildD2();
|
||||
private static readonly Pt Base = BuildBase();
|
||||
|
||||
private sealed class Pt
|
||||
{
|
||||
public readonly int[] X = X25519Field.Create();
|
||||
public readonly int[] Y = X25519Field.Create();
|
||||
public readonly int[] Z = X25519Field.Create();
|
||||
public readonly int[] T = X25519Field.Create();
|
||||
}
|
||||
|
||||
// d = -121665/121666 (mod p); compute it from the definition to avoid transcription error.
|
||||
private static int[] BuildD2()
|
||||
{
|
||||
byte[] numBytes = new byte[32]; numBytes[0] = 0x41; numBytes[1] = 0xDB; numBytes[2] = 0x01; // 121665 = 0x1DB41
|
||||
byte[] denBytes = new byte[32]; denBytes[0] = 0x42; denBytes[1] = 0xDB; denBytes[2] = 0x01; // 121666 = 0x1DB42
|
||||
|
||||
int[] num = X25519Field.Create(); X25519Field.Decode(numBytes, 0, num);
|
||||
int[] den = X25519Field.Create(); X25519Field.Decode(denBytes, 0, den);
|
||||
int[] dinv = X25519Field.Create(); X25519Field.Inv(den, dinv);
|
||||
int[] d = X25519Field.Create(); X25519Field.Mul(num, dinv, d);
|
||||
X25519Field.CNegate(1, d); X25519Field.Carry(d); // d = -num/den
|
||||
int[] d2 = X25519Field.Create(); X25519Field.Add(d, d, d2); X25519Field.Carry(d2);
|
||||
return d2;
|
||||
}
|
||||
|
||||
private static Pt BuildBase()
|
||||
{
|
||||
var b = new Pt();
|
||||
X25519Field.Decode(BxBytes, 0, b.X);
|
||||
X25519Field.Decode(ByBytes, 0, b.Y);
|
||||
X25519Field.One(b.Z);
|
||||
X25519Field.Mul(b.X, b.Y, b.T);
|
||||
return b;
|
||||
}
|
||||
|
||||
// ── constant-time fixed-base scalar multiply ──
|
||||
|
||||
/// <summary>Returns the 32-byte encoding of <c>scalar·B</c>, constant-time in the scalar bits.</summary>
|
||||
public static byte[] ScalarMultBaseEncode(byte[] scalar)
|
||||
{
|
||||
var r = new Pt(); // identity (0, 1, 1, 0)
|
||||
X25519Field.Zero(r.X); X25519Field.One(r.Y); X25519Field.One(r.Z); X25519Field.Zero(r.T);
|
||||
var added = new Pt();
|
||||
|
||||
for (int i = 255; i >= 0; i--)
|
||||
{
|
||||
Double(r, r);
|
||||
Add(r, Base, added);
|
||||
int bit = (scalar[i >> 3] >> (i & 7)) & 1;
|
||||
CMov(bit, added, r);
|
||||
}
|
||||
return Encode(r);
|
||||
}
|
||||
|
||||
private static void Add(Pt p, Pt q, Pt outp)
|
||||
{
|
||||
// No Mul/Sqr writes into one of its own inputs (BC's field Mul is not alias-safe).
|
||||
int[] A = X25519Field.Create(), B = X25519Field.Create(), C = X25519Field.Create();
|
||||
int[] D = X25519Field.Create(), E = X25519Field.Create(), F = X25519Field.Create();
|
||||
int[] G = X25519Field.Create(), H = X25519Field.Create();
|
||||
int[] t1 = X25519Field.Create(), t2 = X25519Field.Create();
|
||||
|
||||
X25519Field.Sub(p.Y, p.X, t1); X25519Field.Sub(q.Y, q.X, t2); X25519Field.Mul(t1, t2, A); // A=(Y1-X1)(Y2-X2)
|
||||
X25519Field.Add(p.Y, p.X, t1); X25519Field.Add(q.Y, q.X, t2); X25519Field.Mul(t1, t2, B); // B=(Y1+X1)(Y2+X2)
|
||||
X25519Field.Mul(p.T, q.T, t1); X25519Field.Mul(t1, D2, C); // C=2d*T1*T2
|
||||
X25519Field.Mul(p.Z, q.Z, t2); X25519Field.Add(t2, t2, D); // D=2*Z1*Z2
|
||||
X25519Field.Sub(B, A, E); X25519Field.Carry(E);
|
||||
X25519Field.Sub(D, C, F); X25519Field.Carry(F);
|
||||
X25519Field.Add(D, C, G); X25519Field.Carry(G);
|
||||
X25519Field.Add(B, A, H); X25519Field.Carry(H);
|
||||
X25519Field.Mul(E, F, outp.X);
|
||||
X25519Field.Mul(G, H, outp.Y);
|
||||
X25519Field.Mul(E, H, outp.T);
|
||||
X25519Field.Mul(F, G, outp.Z);
|
||||
}
|
||||
|
||||
// Dedicated doubling for twisted Edwards with a = -1 (dbl-2008-hwcd, specialized):
|
||||
// A=X², B=Y², C=2Z², E=(X+Y)²-A-B, G=B-A, F=G-C, H=-(A+B).
|
||||
private static void Double(Pt p, Pt outp)
|
||||
{
|
||||
int[] A = X25519Field.Create(), B = X25519Field.Create(), C = X25519Field.Create();
|
||||
int[] E = X25519Field.Create(), F = X25519Field.Create(), G = X25519Field.Create();
|
||||
int[] H = X25519Field.Create(), t1 = X25519Field.Create(), t2 = X25519Field.Create();
|
||||
|
||||
X25519Field.Sqr(p.X, A);
|
||||
X25519Field.Sqr(p.Y, B);
|
||||
X25519Field.Sqr(p.Z, t1); X25519Field.Add(t1, t1, C); // C = 2Z²
|
||||
X25519Field.Add(p.X, p.Y, t1); X25519Field.Sqr(t1, t2); // t2 = (X+Y)²
|
||||
X25519Field.Sub(t2, A, t1); X25519Field.Sub(t1, B, E); X25519Field.Carry(E); // E = (X+Y)² - A - B
|
||||
X25519Field.Sub(B, A, G); X25519Field.Carry(G); // G = B - A
|
||||
X25519Field.Sub(G, C, F); X25519Field.Carry(F); // F = G - C
|
||||
X25519Field.Add(A, B, H); X25519Field.CNegate(1, H); X25519Field.Carry(H); // H = -(A + B)
|
||||
X25519Field.Mul(E, F, outp.X);
|
||||
X25519Field.Mul(G, H, outp.Y);
|
||||
X25519Field.Mul(E, H, outp.T);
|
||||
X25519Field.Mul(F, G, outp.Z);
|
||||
}
|
||||
|
||||
private static void CMov(int cond, Pt src, Pt dst)
|
||||
{
|
||||
int mask = -(cond & 1); // BC's CMov wants a full word mask (0 or 0xFFFFFFFF), not 0/1
|
||||
X25519Field.CMov(mask, src.X, 0, dst.X, 0);
|
||||
X25519Field.CMov(mask, src.Y, 0, dst.Y, 0);
|
||||
X25519Field.CMov(mask, src.Z, 0, dst.Z, 0);
|
||||
X25519Field.CMov(mask, src.T, 0, dst.T, 0);
|
||||
}
|
||||
|
||||
private static byte[] Encode(Pt p)
|
||||
{
|
||||
int[] zInv = X25519Field.Create(), x = X25519Field.Create(), y = X25519Field.Create();
|
||||
X25519Field.Inv(p.Z, zInv);
|
||||
X25519Field.Mul(p.X, zInv, x); X25519Field.Normalize(x);
|
||||
X25519Field.Mul(p.Y, zInv, y); X25519Field.Normalize(y);
|
||||
|
||||
var yBytes = new byte[32];
|
||||
X25519Field.Encode(y, yBytes, 0);
|
||||
var xBytes = new byte[32];
|
||||
X25519Field.Encode(x, xBytes, 0);
|
||||
yBytes[31] |= (byte)((xBytes[0] & 1) << 7);
|
||||
return yBytes;
|
||||
}
|
||||
|
||||
// ── ref10 constant-time scalar arithmetic mod L (faithful portable port of sc.c) ──
|
||||
|
||||
private static long Load3(byte[] x, int o) =>
|
||||
(x[o] & 0xFFL) | ((x[o + 1] & 0xFFL) << 8) | ((x[o + 2] & 0xFFL) << 16);
|
||||
|
||||
private static long Load4(byte[] x, int o) =>
|
||||
(x[o] & 0xFFL) | ((x[o + 1] & 0xFFL) << 8) | ((x[o + 2] & 0xFFL) << 16) | ((x[o + 3] & 0xFFL) << 24);
|
||||
|
||||
/// <summary>Reduces a 64-byte little-endian value mod L → 32 bytes.</summary>
|
||||
public static byte[] ScReduce(byte[] s)
|
||||
{
|
||||
long s0 = 0x1FFFFF & Load3(s, 0);
|
||||
long s1 = 0x1FFFFF & (Load4(s, 2) >> 5);
|
||||
long s2 = 0x1FFFFF & (Load3(s, 5) >> 2);
|
||||
long s3 = 0x1FFFFF & (Load4(s, 7) >> 7);
|
||||
long s4 = 0x1FFFFF & (Load4(s, 10) >> 4);
|
||||
long s5 = 0x1FFFFF & (Load3(s, 13) >> 1);
|
||||
long s6 = 0x1FFFFF & (Load4(s, 15) >> 6);
|
||||
long s7 = 0x1FFFFF & (Load3(s, 18) >> 3);
|
||||
long s8 = 0x1FFFFF & Load3(s, 21);
|
||||
long s9 = 0x1FFFFF & (Load4(s, 23) >> 5);
|
||||
long s10 = 0x1FFFFF & (Load3(s, 26) >> 2);
|
||||
long s11 = 0x1FFFFF & (Load4(s, 28) >> 7);
|
||||
long s12 = 0x1FFFFF & (Load4(s, 31) >> 4);
|
||||
long s13 = 0x1FFFFF & (Load3(s, 34) >> 1);
|
||||
long s14 = 0x1FFFFF & (Load4(s, 36) >> 6);
|
||||
long s15 = 0x1FFFFF & (Load3(s, 39) >> 3);
|
||||
long s16 = 0x1FFFFF & Load3(s, 42);
|
||||
long s17 = 0x1FFFFF & (Load4(s, 44) >> 5);
|
||||
long s18 = 0x1FFFFF & (Load3(s, 47) >> 2);
|
||||
long s19 = 0x1FFFFF & (Load4(s, 49) >> 7);
|
||||
long s20 = 0x1FFFFF & (Load4(s, 52) >> 4);
|
||||
long s21 = 0x1FFFFF & (Load3(s, 55) >> 1);
|
||||
long s22 = 0x1FFFFF & (Load4(s, 57) >> 6);
|
||||
long s23 = Load4(s, 60) >> 3;
|
||||
long carry;
|
||||
|
||||
s11 += s23 * 666643; s12 += s23 * 470296; s13 += s23 * 654183; s14 -= s23 * 997805; s15 += s23 * 136657; s16 -= s23 * 683901;
|
||||
s10 += s22 * 666643; s11 += s22 * 470296; s12 += s22 * 654183; s13 -= s22 * 997805; s14 += s22 * 136657; s15 -= s22 * 683901;
|
||||
s9 += s21 * 666643; s10 += s21 * 470296; s11 += s21 * 654183; s12 -= s21 * 997805; s13 += s21 * 136657; s14 -= s21 * 683901;
|
||||
s8 += s20 * 666643; s9 += s20 * 470296; s10 += s20 * 654183; s11 -= s20 * 997805; s12 += s20 * 136657; s13 -= s20 * 683901;
|
||||
s7 += s19 * 666643; s8 += s19 * 470296; s9 += s19 * 654183; s10 -= s19 * 997805; s11 += s19 * 136657; s12 -= s19 * 683901;
|
||||
s6 += s18 * 666643; s7 += s18 * 470296; s8 += s18 * 654183; s9 -= s18 * 997805; s10 += s18 * 136657; s11 -= s18 * 683901;
|
||||
|
||||
carry = (s6 + (1 << 20)) >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = (s8 + (1 << 20)) >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = (s10 + (1 << 20)) >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = (s12 + (1 << 20)) >> 21; s13 += carry; s12 -= carry << 21;
|
||||
carry = (s14 + (1 << 20)) >> 21; s15 += carry; s14 -= carry << 21;
|
||||
carry = (s16 + (1 << 20)) >> 21; s17 += carry; s16 -= carry << 21;
|
||||
carry = (s7 + (1 << 20)) >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = (s9 + (1 << 20)) >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = (s11 + (1 << 20)) >> 21; s12 += carry; s11 -= carry << 21;
|
||||
carry = (s13 + (1 << 20)) >> 21; s14 += carry; s13 -= carry << 21;
|
||||
carry = (s15 + (1 << 20)) >> 21; s16 += carry; s15 -= carry << 21;
|
||||
|
||||
s5 += s17 * 666643; s6 += s17 * 470296; s7 += s17 * 654183; s8 -= s17 * 997805; s9 += s17 * 136657; s10 -= s17 * 683901;
|
||||
s4 += s16 * 666643; s5 += s16 * 470296; s6 += s16 * 654183; s7 -= s16 * 997805; s8 += s16 * 136657; s9 -= s16 * 683901;
|
||||
s3 += s15 * 666643; s4 += s15 * 470296; s5 += s15 * 654183; s6 -= s15 * 997805; s7 += s15 * 136657; s8 -= s15 * 683901;
|
||||
s2 += s14 * 666643; s3 += s14 * 470296; s4 += s14 * 654183; s5 -= s14 * 997805; s6 += s14 * 136657; s7 -= s14 * 683901;
|
||||
s1 += s13 * 666643; s2 += s13 * 470296; s3 += s13 * 654183; s4 -= s13 * 997805; s5 += s13 * 136657; s6 -= s13 * 683901;
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
s12 = 0;
|
||||
|
||||
carry = (s0 + (1 << 20)) >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = (s2 + (1 << 20)) >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = (s4 + (1 << 20)) >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = (s6 + (1 << 20)) >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = (s8 + (1 << 20)) >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = (s10 + (1 << 20)) >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = (s1 + (1 << 20)) >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = (s3 + (1 << 20)) >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = (s5 + (1 << 20)) >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = (s7 + (1 << 20)) >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = (s9 + (1 << 20)) >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = (s11 + (1 << 20)) >> 21; s12 += carry; s11 -= carry << 21;
|
||||
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
s12 = 0;
|
||||
|
||||
carry = s0 >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = s1 >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = s2 >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = s3 >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = s4 >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = s5 >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = s6 >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = s7 >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = s8 >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = s9 >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = s10 >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = s11 >> 21; s12 += carry; s11 -= carry << 21;
|
||||
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
|
||||
carry = s0 >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = s1 >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = s2 >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = s3 >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = s4 >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = s5 >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = s6 >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = s7 >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = s8 >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = s9 >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = s10 >> 21; s11 += carry; s10 -= carry << 21;
|
||||
|
||||
return Pack(s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11);
|
||||
}
|
||||
|
||||
/// <summary>Returns (a*b + c) mod L, all 32-byte little-endian scalars.</summary>
|
||||
public static byte[] ScMulAdd(byte[] a, byte[] b, byte[] c)
|
||||
{
|
||||
long a0 = 0x1FFFFF & Load3(a, 0);
|
||||
long a1 = 0x1FFFFF & (Load4(a, 2) >> 5);
|
||||
long a2 = 0x1FFFFF & (Load3(a, 5) >> 2);
|
||||
long a3 = 0x1FFFFF & (Load4(a, 7) >> 7);
|
||||
long a4 = 0x1FFFFF & (Load4(a, 10) >> 4);
|
||||
long a5 = 0x1FFFFF & (Load3(a, 13) >> 1);
|
||||
long a6 = 0x1FFFFF & (Load4(a, 15) >> 6);
|
||||
long a7 = 0x1FFFFF & (Load3(a, 18) >> 3);
|
||||
long a8 = 0x1FFFFF & Load3(a, 21);
|
||||
long a9 = 0x1FFFFF & (Load4(a, 23) >> 5);
|
||||
long a10 = 0x1FFFFF & (Load3(a, 26) >> 2);
|
||||
long a11 = Load4(a, 28) >> 7;
|
||||
long b0 = 0x1FFFFF & Load3(b, 0);
|
||||
long b1 = 0x1FFFFF & (Load4(b, 2) >> 5);
|
||||
long b2 = 0x1FFFFF & (Load3(b, 5) >> 2);
|
||||
long b3 = 0x1FFFFF & (Load4(b, 7) >> 7);
|
||||
long b4 = 0x1FFFFF & (Load4(b, 10) >> 4);
|
||||
long b5 = 0x1FFFFF & (Load3(b, 13) >> 1);
|
||||
long b6 = 0x1FFFFF & (Load4(b, 15) >> 6);
|
||||
long b7 = 0x1FFFFF & (Load3(b, 18) >> 3);
|
||||
long b8 = 0x1FFFFF & Load3(b, 21);
|
||||
long b9 = 0x1FFFFF & (Load4(b, 23) >> 5);
|
||||
long b10 = 0x1FFFFF & (Load3(b, 26) >> 2);
|
||||
long b11 = Load4(b, 28) >> 7;
|
||||
long c0 = 0x1FFFFF & Load3(c, 0);
|
||||
long c1 = 0x1FFFFF & (Load4(c, 2) >> 5);
|
||||
long c2 = 0x1FFFFF & (Load3(c, 5) >> 2);
|
||||
long c3 = 0x1FFFFF & (Load4(c, 7) >> 7);
|
||||
long c4 = 0x1FFFFF & (Load4(c, 10) >> 4);
|
||||
long c5 = 0x1FFFFF & (Load3(c, 13) >> 1);
|
||||
long c6 = 0x1FFFFF & (Load4(c, 15) >> 6);
|
||||
long c7 = 0x1FFFFF & (Load3(c, 18) >> 3);
|
||||
long c8 = 0x1FFFFF & Load3(c, 21);
|
||||
long c9 = 0x1FFFFF & (Load4(c, 23) >> 5);
|
||||
long c10 = 0x1FFFFF & (Load3(c, 26) >> 2);
|
||||
long c11 = Load4(c, 28) >> 7;
|
||||
long carry;
|
||||
|
||||
long s0 = c0 + a0 * b0;
|
||||
long s1 = c1 + a0 * b1 + a1 * b0;
|
||||
long s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
|
||||
long s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
|
||||
long s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
|
||||
long s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
|
||||
long s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
|
||||
long s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
|
||||
long s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0;
|
||||
long s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0;
|
||||
long s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0;
|
||||
long s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
|
||||
long s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1;
|
||||
long s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2;
|
||||
long s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3;
|
||||
long s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
|
||||
long s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
|
||||
long s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
|
||||
long s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
|
||||
long s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
|
||||
long s20 = a9 * b11 + a10 * b10 + a11 * b9;
|
||||
long s21 = a10 * b11 + a11 * b10;
|
||||
long s22 = a11 * b11;
|
||||
long s23 = 0;
|
||||
|
||||
carry = (s0 + (1 << 20)) >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = (s2 + (1 << 20)) >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = (s4 + (1 << 20)) >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = (s6 + (1 << 20)) >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = (s8 + (1 << 20)) >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = (s10 + (1 << 20)) >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = (s12 + (1 << 20)) >> 21; s13 += carry; s12 -= carry << 21;
|
||||
carry = (s14 + (1 << 20)) >> 21; s15 += carry; s14 -= carry << 21;
|
||||
carry = (s16 + (1 << 20)) >> 21; s17 += carry; s16 -= carry << 21;
|
||||
carry = (s18 + (1 << 20)) >> 21; s19 += carry; s18 -= carry << 21;
|
||||
carry = (s20 + (1 << 20)) >> 21; s21 += carry; s20 -= carry << 21;
|
||||
carry = (s22 + (1 << 20)) >> 21; s23 += carry; s22 -= carry << 21;
|
||||
carry = (s1 + (1 << 20)) >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = (s3 + (1 << 20)) >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = (s5 + (1 << 20)) >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = (s7 + (1 << 20)) >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = (s9 + (1 << 20)) >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = (s11 + (1 << 20)) >> 21; s12 += carry; s11 -= carry << 21;
|
||||
carry = (s13 + (1 << 20)) >> 21; s14 += carry; s13 -= carry << 21;
|
||||
carry = (s15 + (1 << 20)) >> 21; s16 += carry; s15 -= carry << 21;
|
||||
carry = (s17 + (1 << 20)) >> 21; s18 += carry; s17 -= carry << 21;
|
||||
carry = (s19 + (1 << 20)) >> 21; s20 += carry; s19 -= carry << 21;
|
||||
carry = (s21 + (1 << 20)) >> 21; s22 += carry; s21 -= carry << 21;
|
||||
|
||||
s11 += s23 * 666643; s12 += s23 * 470296; s13 += s23 * 654183; s14 -= s23 * 997805; s15 += s23 * 136657; s16 -= s23 * 683901;
|
||||
s10 += s22 * 666643; s11 += s22 * 470296; s12 += s22 * 654183; s13 -= s22 * 997805; s14 += s22 * 136657; s15 -= s22 * 683901;
|
||||
s9 += s21 * 666643; s10 += s21 * 470296; s11 += s21 * 654183; s12 -= s21 * 997805; s13 += s21 * 136657; s14 -= s21 * 683901;
|
||||
s8 += s20 * 666643; s9 += s20 * 470296; s10 += s20 * 654183; s11 -= s20 * 997805; s12 += s20 * 136657; s13 -= s20 * 683901;
|
||||
s7 += s19 * 666643; s8 += s19 * 470296; s9 += s19 * 654183; s10 -= s19 * 997805; s11 += s19 * 136657; s12 -= s19 * 683901;
|
||||
s6 += s18 * 666643; s7 += s18 * 470296; s8 += s18 * 654183; s9 -= s18 * 997805; s10 += s18 * 136657; s11 -= s18 * 683901;
|
||||
|
||||
carry = (s6 + (1 << 20)) >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = (s8 + (1 << 20)) >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = (s10 + (1 << 20)) >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = (s12 + (1 << 20)) >> 21; s13 += carry; s12 -= carry << 21;
|
||||
carry = (s14 + (1 << 20)) >> 21; s15 += carry; s14 -= carry << 21;
|
||||
carry = (s16 + (1 << 20)) >> 21; s17 += carry; s16 -= carry << 21;
|
||||
carry = (s7 + (1 << 20)) >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = (s9 + (1 << 20)) >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = (s11 + (1 << 20)) >> 21; s12 += carry; s11 -= carry << 21;
|
||||
carry = (s13 + (1 << 20)) >> 21; s14 += carry; s13 -= carry << 21;
|
||||
carry = (s15 + (1 << 20)) >> 21; s16 += carry; s15 -= carry << 21;
|
||||
|
||||
s5 += s17 * 666643; s6 += s17 * 470296; s7 += s17 * 654183; s8 -= s17 * 997805; s9 += s17 * 136657; s10 -= s17 * 683901;
|
||||
s4 += s16 * 666643; s5 += s16 * 470296; s6 += s16 * 654183; s7 -= s16 * 997805; s8 += s16 * 136657; s9 -= s16 * 683901;
|
||||
s3 += s15 * 666643; s4 += s15 * 470296; s5 += s15 * 654183; s6 -= s15 * 997805; s7 += s15 * 136657; s8 -= s15 * 683901;
|
||||
s2 += s14 * 666643; s3 += s14 * 470296; s4 += s14 * 654183; s5 -= s14 * 997805; s6 += s14 * 136657; s7 -= s14 * 683901;
|
||||
s1 += s13 * 666643; s2 += s13 * 470296; s3 += s13 * 654183; s4 -= s13 * 997805; s5 += s13 * 136657; s6 -= s13 * 683901;
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
s12 = 0;
|
||||
|
||||
carry = (s0 + (1 << 20)) >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = (s2 + (1 << 20)) >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = (s4 + (1 << 20)) >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = (s6 + (1 << 20)) >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = (s8 + (1 << 20)) >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = (s10 + (1 << 20)) >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = (s1 + (1 << 20)) >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = (s3 + (1 << 20)) >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = (s5 + (1 << 20)) >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = (s7 + (1 << 20)) >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = (s9 + (1 << 20)) >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = (s11 + (1 << 20)) >> 21; s12 += carry; s11 -= carry << 21;
|
||||
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
s12 = 0;
|
||||
|
||||
carry = s0 >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = s1 >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = s2 >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = s3 >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = s4 >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = s5 >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = s6 >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = s7 >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = s8 >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = s9 >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = s10 >> 21; s11 += carry; s10 -= carry << 21;
|
||||
carry = s11 >> 21; s12 += carry; s11 -= carry << 21;
|
||||
|
||||
s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901;
|
||||
|
||||
carry = s0 >> 21; s1 += carry; s0 -= carry << 21;
|
||||
carry = s1 >> 21; s2 += carry; s1 -= carry << 21;
|
||||
carry = s2 >> 21; s3 += carry; s2 -= carry << 21;
|
||||
carry = s3 >> 21; s4 += carry; s3 -= carry << 21;
|
||||
carry = s4 >> 21; s5 += carry; s4 -= carry << 21;
|
||||
carry = s5 >> 21; s6 += carry; s5 -= carry << 21;
|
||||
carry = s6 >> 21; s7 += carry; s6 -= carry << 21;
|
||||
carry = s7 >> 21; s8 += carry; s7 -= carry << 21;
|
||||
carry = s8 >> 21; s9 += carry; s8 -= carry << 21;
|
||||
carry = s9 >> 21; s10 += carry; s9 -= carry << 21;
|
||||
carry = s10 >> 21; s11 += carry; s10 -= carry << 21;
|
||||
|
||||
return Pack(s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11);
|
||||
}
|
||||
|
||||
private static byte[] Pack(long s0, long s1, long s2, long s3, long s4, long s5,
|
||||
long s6, long s7, long s8, long s9, long s10, long s11)
|
||||
{
|
||||
var r = new byte[32];
|
||||
r[0] = (byte)s0; r[1] = (byte)(s0 >> 8); r[2] = (byte)((s0 >> 16) | (s1 << 5));
|
||||
r[3] = (byte)(s1 >> 3); r[4] = (byte)(s1 >> 11); r[5] = (byte)((s1 >> 19) | (s2 << 2));
|
||||
r[6] = (byte)(s2 >> 6); r[7] = (byte)((s2 >> 14) | (s3 << 7)); r[8] = (byte)(s3 >> 1);
|
||||
r[9] = (byte)(s3 >> 9); r[10] = (byte)((s3 >> 17) | (s4 << 4)); r[11] = (byte)(s4 >> 4);
|
||||
r[12] = (byte)(s4 >> 12); r[13] = (byte)((s4 >> 20) | (s5 << 1)); r[14] = (byte)(s5 >> 7);
|
||||
r[15] = (byte)((s5 >> 15) | (s6 << 6)); r[16] = (byte)(s6 >> 2); r[17] = (byte)(s6 >> 10);
|
||||
r[18] = (byte)((s6 >> 18) | (s7 << 3)); r[19] = (byte)(s7 >> 5); r[20] = (byte)(s7 >> 13);
|
||||
r[21] = (byte)s8; r[22] = (byte)(s8 >> 8); r[23] = (byte)((s8 >> 16) | (s9 << 5));
|
||||
r[24] = (byte)(s9 >> 3); r[25] = (byte)(s9 >> 11); r[26] = (byte)((s9 >> 19) | (s10 << 2));
|
||||
r[27] = (byte)(s10 >> 6); r[28] = (byte)((s10 >> 14) | (s11 << 7)); r[29] = (byte)(s11 >> 1);
|
||||
r[30] = (byte)(s11 >> 9); r[31] = (byte)(s11 >> 17);
|
||||
return r;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>
|
||||
/// Compact reference implementation of the Ed25519 group over GF(2^255-19), using
|
||||
/// <see cref="BigInteger"/> affine coordinates. Chosen for auditability over speed: signing a
|
||||
/// handful of prekeys and verifying signatures does not need constant-time field arithmetic.
|
||||
///
|
||||
/// This mirrors the original ed25519 reference (djb / RFC 8032 "slow" reference). It is shared by
|
||||
/// both <see cref="XEd25519"/> and the RFC 8032 known-answer tests, so passing those KATs validates
|
||||
/// the field/group/scalar/encode/decode routines used in production.
|
||||
/// </summary>
|
||||
internal static class Ed25519Math
|
||||
{
|
||||
/// <summary>Field prime 2^255 - 19.</summary>
|
||||
internal static readonly BigInteger P = BigInteger.Pow(2, 255) - 19;
|
||||
|
||||
/// <summary>Group order L = 2^252 + 27742317777372353535851937790883648493.</summary>
|
||||
internal static readonly BigInteger L =
|
||||
BigInteger.Pow(2, 252) + BigInteger.Parse("27742317777372353535851937790883648493");
|
||||
|
||||
/// <summary>Curve constant d = -121665/121666 mod p.</summary>
|
||||
private static readonly BigInteger D = Mod(-121665 * Inverse(121666), P);
|
||||
|
||||
/// <summary>sqrt(-1) mod p = 2^((p-1)/4).</summary>
|
||||
private static readonly BigInteger SqrtM1 = BigInteger.ModPow(2, (P - 1) / 4, P);
|
||||
|
||||
/// <summary>Base point B = (Bx, 4/5).</summary>
|
||||
private static readonly Point B = MakeBasePoint();
|
||||
|
||||
internal readonly struct Point
|
||||
{
|
||||
internal readonly BigInteger X;
|
||||
internal readonly BigInteger Y;
|
||||
internal Point(BigInteger x, BigInteger y) { X = x; Y = y; }
|
||||
internal Point Negate() => new Point(Mod(-X, P), Y);
|
||||
}
|
||||
|
||||
private static readonly Point Identity = new Point(BigInteger.Zero, BigInteger.One);
|
||||
|
||||
private static Point MakeBasePoint()
|
||||
{
|
||||
BigInteger by = Mod(4 * Inverse(5), P);
|
||||
BigInteger bx = RecoverX(by, 0);
|
||||
return new Point(bx, by);
|
||||
}
|
||||
|
||||
internal static BigInteger Mod(BigInteger a, BigInteger m)
|
||||
{
|
||||
BigInteger r = a % m;
|
||||
return r.Sign < 0 ? r + m : r;
|
||||
}
|
||||
|
||||
internal static BigInteger Inverse(BigInteger z) => BigInteger.ModPow(Mod(z, P), P - 2, P);
|
||||
|
||||
/// <summary>Edwards addition (unified; also doubles) on -x^2 + y^2 = 1 + d x^2 y^2.</summary>
|
||||
internal static Point Add(Point p1, Point p2)
|
||||
{
|
||||
BigInteger x1 = p1.X, y1 = p1.Y, x2 = p2.X, y2 = p2.Y;
|
||||
BigInteger dxy = Mod(D * x1 * x2 % P * y1 % P * y2, P);
|
||||
BigInteger x3 = Mod((x1 * y2 + x2 * y1) * Inverse(Mod(1 + dxy, P)), P);
|
||||
BigInteger y3 = Mod((y1 * y2 + x1 * x2) * Inverse(Mod(1 - dxy, P)), P);
|
||||
return new Point(x3, y3);
|
||||
}
|
||||
|
||||
internal static Point ScalarMult(Point p, BigInteger e)
|
||||
{
|
||||
Point result = Identity;
|
||||
Point addend = p;
|
||||
while (e.Sign > 0)
|
||||
{
|
||||
if (!e.IsEven) result = Add(result, addend);
|
||||
addend = Add(addend, addend);
|
||||
e >>= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static Point ScalarMultBase(BigInteger e) => ScalarMult(B, e);
|
||||
|
||||
/// <summary>Encode a point to 32 bytes (little-endian y with the low bit of x in bit 255).</summary>
|
||||
internal static byte[] Encode(Point p)
|
||||
{
|
||||
byte[] bytes = ToLe32(Mod(p.Y, P));
|
||||
if (!Mod(p.X, P).IsEven) bytes[31] |= 0x80;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static BigInteger RecoverX(BigInteger y, int sign)
|
||||
{
|
||||
BigInteger y2 = Mod(y * y, P);
|
||||
BigInteger num = Mod(y2 - 1, P);
|
||||
BigInteger den = Mod(D * y2 + 1, P);
|
||||
BigInteger xx = Mod(num * Inverse(den), P);
|
||||
BigInteger x = BigInteger.ModPow(xx, (P + 3) / 8, P);
|
||||
if (!Mod(x * x - xx, P).IsZero) x = Mod(x * SqrtM1, P);
|
||||
if (!Mod(x * x - xx, P).IsZero) return BigInteger.MinusOne; // not on curve
|
||||
if (((int)(x & 1)) != sign) x = Mod(-x, P);
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <summary>Decode a point from its y-coordinate and sign bit. Returns false if not on curve.</summary>
|
||||
internal static bool TryDecode(BigInteger y, int sign, out Point point)
|
||||
{
|
||||
BigInteger x = RecoverX(Mod(y, P), sign);
|
||||
if (x.Sign < 0) { point = default; return false; }
|
||||
point = new Point(x, y);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Reduce a 64-byte little-endian hash to a scalar mod L.</summary>
|
||||
internal static BigInteger ScReduce(ReadOnlySpan<byte> hash64) => Mod(FromLe(hash64), L);
|
||||
|
||||
internal static BigInteger FromLe(ReadOnlySpan<byte> bytes) =>
|
||||
new BigInteger(bytes, isUnsigned: true, isBigEndian: false);
|
||||
|
||||
internal static byte[] ToLe32(BigInteger value)
|
||||
{
|
||||
byte[] raw = value.ToByteArray(isUnsigned: true, isBigEndian: false);
|
||||
var result = new byte[32];
|
||||
Array.Copy(raw, result, Math.Min(raw.Length, 32));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>
|
||||
/// libsignal serializes KEM public keys and ciphertexts with a one-byte key-type prefix (analogous
|
||||
/// to the 0x05 DjbECPublicKey prefix). Kyber-1024 is type 0x08. The signed-prekey signature is
|
||||
/// computed over this prefixed form, and prekey bundles carry the prefixed public key.
|
||||
/// </summary>
|
||||
public static class KemKeySerialization
|
||||
{
|
||||
/// <summary>libsignal KEM key type for Kyber-1024.</summary>
|
||||
public const byte Kyber1024Type = 0x08;
|
||||
|
||||
/// <summary>Prepends the Kyber-1024 type byte to a raw public key or ciphertext.</summary>
|
||||
public static byte[] Serialize(byte[] raw)
|
||||
{
|
||||
var serialized = new byte[raw.Length + 1];
|
||||
serialized[0] = Kyber1024Type;
|
||||
Array.Copy(raw, 0, serialized, 1, raw.Length);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/// <summary>Strips the type byte from a serialized Kyber-1024 public key or ciphertext.</summary>
|
||||
public static byte[] Deserialize(ReadOnlySpan<byte> serialized)
|
||||
{
|
||||
if (serialized.Length < 1 || serialized[0] != Kyber1024Type)
|
||||
throw new ArgumentException($"unsupported KEM key type {(serialized.Length > 0 ? serialized[0] : -1)}");
|
||||
return serialized[1..].ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>An ML-KEM/Kyber key pair (encoded public/private key bytes).</summary>
|
||||
public sealed class KyberKeyPair
|
||||
{
|
||||
public byte[] PublicKey { get; }
|
||||
public byte[] PrivateKey { get; }
|
||||
public KyberKeyPair(byte[] publicKey, byte[] privateKey)
|
||||
{
|
||||
PublicKey = publicKey;
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Result of an encapsulation: the ciphertext to send and the shared secret.</summary>
|
||||
public sealed class KyberEncapsulation
|
||||
{
|
||||
public byte[] CipherText { get; }
|
||||
public byte[] SharedSecret { get; }
|
||||
public KyberEncapsulation(byte[] cipherText, byte[] sharedSecret)
|
||||
{
|
||||
CipherText = cipherText;
|
||||
SharedSecret = sharedSecret;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round-3 Kyber-1024 KEM, used for PQXDH pqkem prekeys. This matches libsignal's <c>KYBER_1024</c>
|
||||
/// (type byte 0x08), so prekeys and ciphertexts interoperate with the Signal ecosystem. The raw
|
||||
/// public/private/ciphertext encodings here carry no type-byte prefix — see <see cref="KemKeySerialization"/>.
|
||||
/// </summary>
|
||||
public static class Kyber
|
||||
{
|
||||
public static KyberKeyPair GenerateKeyPair()
|
||||
{
|
||||
byte[] d = RandomNumberGenerator.GetBytes(Kyber1024.SymBytes);
|
||||
byte[] z = RandomNumberGenerator.GetBytes(Kyber1024.SymBytes);
|
||||
Kyber1024.KeyPair(d, z, out byte[] pk, out byte[] sk);
|
||||
return new KyberKeyPair(pk, sk);
|
||||
}
|
||||
|
||||
/// <summary>Encapsulate to a peer's public key. Returns ciphertext + shared secret.</summary>
|
||||
public static KyberEncapsulation Encapsulate(byte[] publicKey)
|
||||
{
|
||||
byte[] m = RandomNumberGenerator.GetBytes(Kyber1024.SymBytes);
|
||||
Kyber1024.Encapsulate(publicKey, m, out byte[] ct, out byte[] ss);
|
||||
return new KyberEncapsulation(ct, ss);
|
||||
}
|
||||
|
||||
/// <summary>Decapsulate a received ciphertext with our private key. Returns the shared secret.</summary>
|
||||
public static byte[] Decapsulate(byte[] privateKey, byte[] cipherText) =>
|
||||
Kyber1024.Decapsulate(cipherText, privateKey);
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
using Org.BouncyCastle.Crypto.Digests;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>
|
||||
/// Pure-C# implementation of round-3 Kyber-1024 (the CRYSTALS-Kyber NIST round-3 submission, as used
|
||||
/// by libsignal's <c>KYBER_1024</c> KEM for PQXDH). Faithfully ported from the pq-crystals reference
|
||||
/// (tag v3.0, ref/), using BouncyCastle only for SHA3/SHAKE. Validated against the reference's
|
||||
/// published test-vector SHA-256 (see KyberKatTests).
|
||||
///
|
||||
/// All polynomial coefficients are 16-bit; arithmetic is unchecked to mirror C int16_t wraparound.
|
||||
/// </summary>
|
||||
internal static class Kyber1024
|
||||
{
|
||||
public const int N = 256;
|
||||
public const int Q = 3329;
|
||||
public const int K = 4;
|
||||
public const int Eta1 = 2;
|
||||
public const int Eta2 = 2;
|
||||
public const int SymBytes = 32;
|
||||
|
||||
public const int PolyBytes = 384;
|
||||
public const int PolyVecBytes = K * PolyBytes; // 1536
|
||||
public const int PolyCompressedBytes = 160; // dv = 5
|
||||
public const int PolyVecCompressedBytes = K * 352; // 1408, du = 11
|
||||
public const int IndcpaPublicKeyBytes = PolyVecBytes + SymBytes; // 1568
|
||||
public const int IndcpaSecretKeyBytes = PolyVecBytes; // 1536
|
||||
public const int IndcpaBytes = PolyVecCompressedBytes + PolyCompressedBytes; // 1568
|
||||
|
||||
public const int PublicKeyBytes = IndcpaPublicKeyBytes; // 1568
|
||||
public const int SecretKeyBytes = IndcpaSecretKeyBytes + IndcpaPublicKeyBytes + 2 * SymBytes; // 3168
|
||||
public const int CiphertextBytes = IndcpaBytes; // 1568
|
||||
public const int SsBytes = 32;
|
||||
|
||||
private const short MONT = -1044; // 2^16 mod q
|
||||
private const short QINV = -3327; // q^-1 mod 2^16
|
||||
|
||||
private static readonly short[] Zetas =
|
||||
{
|
||||
-1044, -758, -359, -1517, 1493, 1422, 287, 202,
|
||||
-171, 622, 1577, 182, 962, -1202, -1474, 1468,
|
||||
573, -1325, 264, 383, -829, 1458, -1602, -130,
|
||||
-681, 1017, 732, 608, -1542, 411, -205, -1571,
|
||||
1223, 652, -552, 1015, -1293, 1491, -282, -1544,
|
||||
516, -8, -320, -666, -1618, -1162, 126, 1469,
|
||||
-853, -90, -271, 830, 107, -1421, -247, -951,
|
||||
-398, 961, -1508, -725, 448, -1065, 677, -1275,
|
||||
-1103, 430, 555, 843, -1251, 871, 1550, 105,
|
||||
422, 587, 177, -235, -291, -460, 1574, 1653,
|
||||
-246, 778, 1159, -147, -777, 1483, -602, 1119,
|
||||
-1590, 644, -872, 349, 418, 329, -156, -75,
|
||||
817, 1097, 603, 610, 1322, -1285, -1465, 384,
|
||||
-1215, -136, 1218, -1335, -874, 220, -1187, -1659,
|
||||
-1185, -1530, -1278, 794, -1510, -854, -870, 478,
|
||||
-108, -308, 996, 991, 958, -1460, 1522, 1628,
|
||||
};
|
||||
|
||||
// ---- reductions ----
|
||||
|
||||
private static short MontgomeryReduce(int a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
short t = (short)((short)a * QINV);
|
||||
return (short)((a - (int)t * Q) >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
private static short BarrettReduce(short a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const int v = ((1 << 26) + Q / 2) / Q;
|
||||
short t = (short)((v * a + (1 << 25)) >> 26);
|
||||
return (short)(a - (short)(t * Q));
|
||||
}
|
||||
}
|
||||
|
||||
private static short FqMul(short a, short b) => MontgomeryReduce(a * b);
|
||||
|
||||
// ---- NTT ----
|
||||
|
||||
private static void Ntt(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int k = 1;
|
||||
for (int len = 128; len >= 2; len >>= 1)
|
||||
{
|
||||
for (int start = 0; start < 256; start += 2 * len)
|
||||
{
|
||||
short zeta = Zetas[k++];
|
||||
for (int j = start; j < start + len; j++)
|
||||
{
|
||||
short t = FqMul(zeta, r[j + len]);
|
||||
r[j + len] = (short)(r[j] - t);
|
||||
r[j] = (short)(r[j] + t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvNtt(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const short f = 1441; // mont^2/128
|
||||
int k = 127;
|
||||
for (int len = 2; len <= 128; len <<= 1)
|
||||
{
|
||||
for (int start = 0; start < 256; start += 2 * len)
|
||||
{
|
||||
short zeta = Zetas[k--];
|
||||
for (int j = start; j < start + len; j++)
|
||||
{
|
||||
short t = r[j];
|
||||
r[j] = BarrettReduce((short)(t + r[j + len]));
|
||||
r[j + len] = (short)(r[j + len] - t);
|
||||
r[j + len] = FqMul(zeta, r[j + len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int j = 0; j < 256; j++)
|
||||
r[j] = FqMul(r[j], f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void BaseMul(short[] r, int rOff, short[] a, int aOff, short[] b, int bOff, short zeta)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
r[rOff] = FqMul(a[aOff + 1], b[bOff + 1]);
|
||||
r[rOff] = FqMul(r[rOff], zeta);
|
||||
r[rOff] = (short)(r[rOff] + FqMul(a[aOff], b[bOff]));
|
||||
r[rOff + 1] = FqMul(a[aOff], b[bOff + 1]);
|
||||
r[rOff + 1] = (short)(r[rOff + 1] + FqMul(a[aOff + 1], b[bOff]));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- hashing (BouncyCastle SHA3/SHAKE) ----
|
||||
|
||||
private static byte[] Sha3_256(byte[] data, int off, int len)
|
||||
{
|
||||
var d = new Sha3Digest(256);
|
||||
d.BlockUpdate(data, off, len);
|
||||
var o = new byte[32];
|
||||
d.DoFinal(o, 0);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static byte[] Sha3_512(byte[] data, int off, int len)
|
||||
{
|
||||
var d = new Sha3Digest(512);
|
||||
d.BlockUpdate(data, off, len);
|
||||
var o = new byte[64];
|
||||
d.DoFinal(o, 0);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static byte[] Shake256(byte[] data, int len)
|
||||
{
|
||||
var d = new ShakeDigest(256);
|
||||
d.BlockUpdate(data, 0, data.Length);
|
||||
var o = new byte[len];
|
||||
d.Output(o, 0, len);
|
||||
return o;
|
||||
}
|
||||
|
||||
// ---- centered binomial distribution (eta = 2) ----
|
||||
|
||||
private static uint Load32Le(byte[] x, int off) =>
|
||||
(uint)(x[off] | (x[off + 1] << 8) | (x[off + 2] << 16) | (x[off + 3] << 24));
|
||||
|
||||
private static void Cbd2(short[] r, byte[] buf)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
uint t = Load32Le(buf, 4 * i);
|
||||
uint d = t & 0x55555555u;
|
||||
d += (t >> 1) & 0x55555555u;
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
short a = (short)((d >> (4 * j + 0)) & 0x3);
|
||||
short b = (short)((d >> (4 * j + 2)) & 0x3);
|
||||
r[8 * i + j] = (short)(a - b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- poly serialization ----
|
||||
|
||||
private static void PolyToBytes(byte[] r, int rOff, short[] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 2; i++)
|
||||
{
|
||||
ushort t0 = (ushort)(a[2 * i] + ((a[2 * i] >> 15) & Q));
|
||||
ushort t1 = (ushort)(a[2 * i + 1] + ((a[2 * i + 1] >> 15) & Q));
|
||||
r[rOff + 3 * i + 0] = (byte)t0;
|
||||
r[rOff + 3 * i + 1] = (byte)((t0 >> 8) | (t1 << 4));
|
||||
r[rOff + 3 * i + 2] = (byte)(t1 >> 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyFromBytes(short[] r, byte[] a, int aOff)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 2; i++)
|
||||
{
|
||||
r[2 * i] = (short)(((a[aOff + 3 * i + 0] >> 0) | (a[aOff + 3 * i + 1] << 8)) & 0xFFF);
|
||||
r[2 * i + 1] = (short)(((a[aOff + 3 * i + 1] >> 4) | (a[aOff + 3 * i + 2] << 4)) & 0xFFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyCompress(byte[] r, int rOff, short[] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var t = new byte[8];
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
int u = a[8 * i + j];
|
||||
u += (u >> 15) & Q;
|
||||
t[j] = (byte)(((((uint)u << 5) + Q / 2) / Q) & 31);
|
||||
}
|
||||
r[rOff + 0] = (byte)((t[0] >> 0) | (t[1] << 5));
|
||||
r[rOff + 1] = (byte)((t[1] >> 3) | (t[2] << 2) | (t[3] << 7));
|
||||
r[rOff + 2] = (byte)((t[3] >> 1) | (t[4] << 4));
|
||||
r[rOff + 3] = (byte)((t[4] >> 4) | (t[5] << 1) | (t[6] << 6));
|
||||
r[rOff + 4] = (byte)((t[6] >> 2) | (t[7] << 3));
|
||||
rOff += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyDecompress(short[] r, byte[] a, int aOff)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var t = new byte[8];
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
t[0] = (byte)(a[aOff + 0] >> 0);
|
||||
t[1] = (byte)((a[aOff + 0] >> 5) | (a[aOff + 1] << 3));
|
||||
t[2] = (byte)(a[aOff + 1] >> 2);
|
||||
t[3] = (byte)((a[aOff + 1] >> 7) | (a[aOff + 2] << 1));
|
||||
t[4] = (byte)((a[aOff + 2] >> 4) | (a[aOff + 3] << 4));
|
||||
t[5] = (byte)(a[aOff + 3] >> 1);
|
||||
t[6] = (byte)((a[aOff + 3] >> 6) | (a[aOff + 4] << 2));
|
||||
t[7] = (byte)(a[aOff + 4] >> 3);
|
||||
aOff += 5;
|
||||
for (int j = 0; j < 8; j++)
|
||||
r[8 * i + j] = (short)(((uint)(t[j] & 31) * Q + 16) >> 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyFromMsg(short[] r, byte[] msg)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
short mask = (short)(-(short)((msg[i] >> j) & 1));
|
||||
r[8 * i + j] = (short)(mask & ((Q + 1) / 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] PolyToMsg(short[] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var msg = new byte[SymBytes];
|
||||
for (int i = 0; i < N / 8; i++)
|
||||
{
|
||||
msg[i] = 0;
|
||||
for (int j = 0; j < 8; j++)
|
||||
{
|
||||
int t = a[8 * i + j];
|
||||
t += (t >> 15) & Q;
|
||||
t = (((t << 1) + Q / 2) / Q) & 1;
|
||||
msg[i] |= (byte)(t << j);
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
private static short[] PolyGetNoiseEta1(byte[] seed, byte nonce) => GetNoise(seed, nonce, Eta1);
|
||||
private static short[] PolyGetNoiseEta2(byte[] seed, byte nonce) => GetNoise(seed, nonce, Eta2);
|
||||
|
||||
private static short[] GetNoise(byte[] seed, byte nonce, int eta)
|
||||
{
|
||||
var extkey = new byte[SymBytes + 1];
|
||||
Array.Copy(seed, extkey, SymBytes);
|
||||
extkey[SymBytes] = nonce;
|
||||
byte[] buf = Shake256(extkey, eta * N / 4);
|
||||
var r = new short[N];
|
||||
Cbd2(r, buf); // eta1 == eta2 == 2 for Kyber-1024
|
||||
return r;
|
||||
}
|
||||
|
||||
private static void PolyNtt(short[] r) { Ntt(r); PolyReduce(r); }
|
||||
private static void PolyInvNttToMont(short[] r) => InvNtt(r);
|
||||
|
||||
private static void PolyBaseMulMont(short[] r, short[] a, short[] b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N / 4; i++)
|
||||
{
|
||||
BaseMul(r, 4 * i, a, 4 * i, b, 4 * i, Zetas[64 + i]);
|
||||
BaseMul(r, 4 * i + 2, a, 4 * i + 2, b, 4 * i + 2, (short)(-Zetas[64 + i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyToMont(short[] r)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
const short f = (short)(((1L << 32) % Q));
|
||||
for (int i = 0; i < N; i++)
|
||||
r[i] = MontgomeryReduce(r[i] * f);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyReduce(short[] r)
|
||||
{
|
||||
for (int i = 0; i < N; i++)
|
||||
r[i] = BarrettReduce(r[i]);
|
||||
}
|
||||
|
||||
private static void PolyAdd(short[] r, short[] a, short[] b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N; i++) r[i] = (short)(a[i] + b[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolySub(short[] r, short[] a, short[] b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
for (int i = 0; i < N; i++) r[i] = (short)(a[i] - b[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- polyvec (short[K][N]) ----
|
||||
|
||||
private static short[][] NewPolyVec()
|
||||
{
|
||||
var v = new short[K][];
|
||||
for (int i = 0; i < K; i++) v[i] = new short[N];
|
||||
return v;
|
||||
}
|
||||
|
||||
private static void PolyVecToBytes(byte[] r, int rOff, short[][] a)
|
||||
{
|
||||
for (int i = 0; i < K; i++) PolyToBytes(r, rOff + i * PolyBytes, a[i]);
|
||||
}
|
||||
|
||||
private static short[][] PolyVecFromBytes(byte[] a, int aOff)
|
||||
{
|
||||
var r = NewPolyVec();
|
||||
for (int i = 0; i < K; i++) PolyFromBytes(r[i], a, aOff + i * PolyBytes);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static void PolyVecCompress(byte[] r, int rOff, short[][] a)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var t = new ushort[8];
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
for (int j = 0; j < N / 8; j++)
|
||||
{
|
||||
for (int k = 0; k < 8; k++)
|
||||
{
|
||||
int c = a[i][8 * j + k];
|
||||
c += (c >> 15) & Q;
|
||||
t[k] = (ushort)(((((uint)c << 11) + Q / 2) / Q) & 0x7ff);
|
||||
}
|
||||
r[rOff + 0] = (byte)(t[0] >> 0);
|
||||
r[rOff + 1] = (byte)((t[0] >> 8) | (t[1] << 3));
|
||||
r[rOff + 2] = (byte)((t[1] >> 5) | (t[2] << 6));
|
||||
r[rOff + 3] = (byte)(t[2] >> 2);
|
||||
r[rOff + 4] = (byte)((t[2] >> 10) | (t[3] << 1));
|
||||
r[rOff + 5] = (byte)((t[3] >> 7) | (t[4] << 4));
|
||||
r[rOff + 6] = (byte)((t[4] >> 4) | (t[5] << 7));
|
||||
r[rOff + 7] = (byte)(t[5] >> 1);
|
||||
r[rOff + 8] = (byte)((t[5] >> 9) | (t[6] << 2));
|
||||
r[rOff + 9] = (byte)((t[6] >> 6) | (t[7] << 5));
|
||||
r[rOff + 10] = (byte)(t[7] >> 3);
|
||||
rOff += 11;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static short[][] PolyVecDecompress(byte[] a, int aOff)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var r = NewPolyVec();
|
||||
var t = new ushort[8];
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
for (int j = 0; j < N / 8; j++)
|
||||
{
|
||||
t[0] = (ushort)((a[aOff + 0] >> 0) | (a[aOff + 1] << 8));
|
||||
t[1] = (ushort)((a[aOff + 1] >> 3) | (a[aOff + 2] << 5));
|
||||
t[2] = (ushort)((a[aOff + 2] >> 6) | (a[aOff + 3] << 2) | (a[aOff + 4] << 10));
|
||||
t[3] = (ushort)((a[aOff + 4] >> 1) | (a[aOff + 5] << 7));
|
||||
t[4] = (ushort)((a[aOff + 5] >> 4) | (a[aOff + 6] << 4));
|
||||
t[5] = (ushort)((a[aOff + 6] >> 7) | (a[aOff + 7] << 1) | (a[aOff + 8] << 9));
|
||||
t[6] = (ushort)((a[aOff + 8] >> 2) | (a[aOff + 9] << 6));
|
||||
t[7] = (ushort)((a[aOff + 9] >> 5) | (a[aOff + 10] << 3));
|
||||
aOff += 11;
|
||||
for (int k = 0; k < 8; k++)
|
||||
r[i][8 * j + k] = (short)(((uint)(t[k] & 0x7FF) * Q + 1024) >> 11);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PolyVecNtt(short[][] r) { for (int i = 0; i < K; i++) PolyNtt(r[i]); }
|
||||
private static void PolyVecInvNttToMont(short[][] r) { for (int i = 0; i < K; i++) PolyInvNttToMont(r[i]); }
|
||||
|
||||
private static void PolyVecBaseMulAccMont(short[] r, short[][] a, short[][] b)
|
||||
{
|
||||
var t = new short[N];
|
||||
PolyBaseMulMont(r, a[0], b[0]);
|
||||
for (int i = 1; i < K; i++)
|
||||
{
|
||||
PolyBaseMulMont(t, a[i], b[i]);
|
||||
PolyAdd(r, r, t);
|
||||
}
|
||||
PolyReduce(r);
|
||||
}
|
||||
|
||||
private static void PolyVecReduce(short[][] r) { for (int i = 0; i < K; i++) PolyReduce(r[i]); }
|
||||
private static void PolyVecAdd(short[][] r, short[][] a, short[][] b) { for (int i = 0; i < K; i++) PolyAdd(r[i], a[i], b[i]); }
|
||||
|
||||
// ---- matrix generation (rejection sampling on SHAKE128) ----
|
||||
|
||||
private const int XofBlockBytes = 168; // SHAKE128 rate
|
||||
private const int GenMatrixNBlocks = (12 * N / 8 * (1 << 12) / Q + XofBlockBytes) / XofBlockBytes; // 3
|
||||
|
||||
private static int RejUniform(short[] r, int rOff, int len, byte[] buf, int buflen)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int ctr = 0, pos = 0;
|
||||
while (ctr < len && pos + 3 <= buflen)
|
||||
{
|
||||
ushort val0 = (ushort)(((buf[pos + 0] >> 0) | (buf[pos + 1] << 8)) & 0xFFF);
|
||||
ushort val1 = (ushort)(((buf[pos + 1] >> 4) | (buf[pos + 2] << 4)) & 0xFFF);
|
||||
pos += 3;
|
||||
if (val0 < Q) r[rOff + ctr++] = (short)val0;
|
||||
if (ctr < len && val1 < Q) r[rOff + ctr++] = (short)val1;
|
||||
}
|
||||
return ctr;
|
||||
}
|
||||
}
|
||||
|
||||
private static short[][][] GenMatrix(byte[] seed, bool transposed)
|
||||
{
|
||||
var a = new short[K][][];
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
a[i] = NewPolyVec();
|
||||
for (int j = 0; j < K; j++)
|
||||
{
|
||||
var extseed = new byte[SymBytes + 2];
|
||||
Array.Copy(seed, extseed, SymBytes);
|
||||
extseed[SymBytes] = (byte)(transposed ? i : j);
|
||||
extseed[SymBytes + 1] = (byte)(transposed ? j : i);
|
||||
|
||||
var xof = new ShakeDigest(128);
|
||||
xof.BlockUpdate(extseed, 0, extseed.Length);
|
||||
|
||||
var buf = new byte[GenMatrixNBlocks * XofBlockBytes + 2];
|
||||
xof.Output(buf, 0, GenMatrixNBlocks * XofBlockBytes);
|
||||
int buflen = GenMatrixNBlocks * XofBlockBytes;
|
||||
int ctr = RejUniform(a[i][j], 0, N, buf, buflen);
|
||||
|
||||
while (ctr < N)
|
||||
{
|
||||
int off = buflen % 3;
|
||||
for (int k = 0; k < off; k++) buf[k] = buf[buflen - off + k];
|
||||
xof.Output(buf, off, XofBlockBytes);
|
||||
buflen = off + XofBlockBytes;
|
||||
ctr += RejUniform(a[i][j], ctr, N - ctr, buf, buflen);
|
||||
}
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// ---- IND-CPA ----
|
||||
|
||||
private static void IndcpaKeypair(byte[] d, out byte[] pk, out byte[] sk)
|
||||
{
|
||||
byte[] buf = Sha3_512(d, 0, SymBytes); // publicseed || noiseseed
|
||||
var publicseed = new byte[SymBytes];
|
||||
var noiseseed = new byte[SymBytes];
|
||||
Array.Copy(buf, 0, publicseed, 0, SymBytes);
|
||||
Array.Copy(buf, SymBytes, noiseseed, 0, SymBytes);
|
||||
|
||||
short[][][] a = GenMatrix(publicseed, transposed: false);
|
||||
|
||||
var skpv = NewPolyVec();
|
||||
var e = NewPolyVec();
|
||||
byte nonce = 0;
|
||||
for (int i = 0; i < K; i++) skpv[i] = PolyGetNoiseEta1(noiseseed, nonce++);
|
||||
for (int i = 0; i < K; i++) e[i] = PolyGetNoiseEta1(noiseseed, nonce++);
|
||||
|
||||
PolyVecNtt(skpv);
|
||||
PolyVecNtt(e);
|
||||
|
||||
var pkpv = NewPolyVec();
|
||||
for (int i = 0; i < K; i++)
|
||||
{
|
||||
PolyVecBaseMulAccMont(pkpv[i], a[i], skpv);
|
||||
PolyToMont(pkpv[i]);
|
||||
}
|
||||
PolyVecAdd(pkpv, pkpv, e);
|
||||
PolyVecReduce(pkpv);
|
||||
|
||||
sk = new byte[IndcpaSecretKeyBytes];
|
||||
PolyVecToBytes(sk, 0, skpv);
|
||||
|
||||
pk = new byte[IndcpaPublicKeyBytes];
|
||||
PolyVecToBytes(pk, 0, pkpv);
|
||||
Array.Copy(publicseed, 0, pk, PolyVecBytes, SymBytes);
|
||||
}
|
||||
|
||||
private static byte[] IndcpaEnc(byte[] m, byte[] pk, byte[] coins)
|
||||
{
|
||||
short[][] pkpv = PolyVecFromBytes(pk, 0);
|
||||
var seed = new byte[SymBytes];
|
||||
Array.Copy(pk, PolyVecBytes, seed, 0, SymBytes);
|
||||
|
||||
short[] k = new short[N];
|
||||
PolyFromMsg(k, m);
|
||||
short[][][] at = GenMatrix(seed, transposed: true);
|
||||
|
||||
var sp = NewPolyVec();
|
||||
var ep = NewPolyVec();
|
||||
byte nonce = 0;
|
||||
for (int i = 0; i < K; i++) sp[i] = PolyGetNoiseEta1(coins, nonce++);
|
||||
for (int i = 0; i < K; i++) ep[i] = PolyGetNoiseEta2(coins, nonce++);
|
||||
short[] epp = PolyGetNoiseEta2(coins, nonce);
|
||||
|
||||
PolyVecNtt(sp);
|
||||
|
||||
var b = NewPolyVec();
|
||||
for (int i = 0; i < K; i++) PolyVecBaseMulAccMont(b[i], at[i], sp);
|
||||
var v = new short[N];
|
||||
PolyVecBaseMulAccMont(v, pkpv, sp);
|
||||
|
||||
PolyVecInvNttToMont(b);
|
||||
PolyInvNttToMont(v);
|
||||
|
||||
PolyVecAdd(b, b, ep);
|
||||
PolyAdd(v, v, epp);
|
||||
PolyAdd(v, v, k);
|
||||
PolyVecReduce(b);
|
||||
PolyReduce(v);
|
||||
|
||||
var c = new byte[IndcpaBytes];
|
||||
PolyVecCompress(c, 0, b);
|
||||
PolyCompress(c, PolyVecCompressedBytes, v);
|
||||
return c;
|
||||
}
|
||||
|
||||
private static byte[] IndcpaDec(byte[] c, byte[] sk)
|
||||
{
|
||||
short[][] b = PolyVecDecompress(c, 0);
|
||||
short[] v = new short[N];
|
||||
PolyDecompress(v, c, PolyVecCompressedBytes);
|
||||
|
||||
short[][] skpv = PolyVecFromBytes(sk, 0);
|
||||
|
||||
PolyVecNtt(b);
|
||||
var mp = new short[N];
|
||||
PolyVecBaseMulAccMont(mp, skpv, b);
|
||||
PolyInvNttToMont(mp);
|
||||
|
||||
PolySub(mp, v, mp);
|
||||
PolyReduce(mp);
|
||||
return PolyToMsg(mp);
|
||||
}
|
||||
|
||||
// ---- CCA-KEM ----
|
||||
|
||||
/// <summary>Generates a key pair from the two 32-byte coins consumed by the reference
|
||||
/// (<paramref name="d"/> drives IND-CPA keygen, <paramref name="z"/> is the implicit-rejection value).</summary>
|
||||
public static void KeyPair(byte[] d, byte[] z, out byte[] pk, out byte[] sk)
|
||||
{
|
||||
IndcpaKeypair(d, out pk, out byte[] indcpaSk);
|
||||
sk = new byte[SecretKeyBytes];
|
||||
Array.Copy(indcpaSk, 0, sk, 0, IndcpaSecretKeyBytes);
|
||||
Array.Copy(pk, 0, sk, IndcpaSecretKeyBytes, IndcpaPublicKeyBytes);
|
||||
byte[] hpk = Sha3_256(pk, 0, PublicKeyBytes);
|
||||
Array.Copy(hpk, 0, sk, SecretKeyBytes - 2 * SymBytes, SymBytes);
|
||||
Array.Copy(z, 0, sk, SecretKeyBytes - SymBytes, SymBytes);
|
||||
}
|
||||
|
||||
/// <summary>Encapsulates to <paramref name="pk"/> using the 32-byte message coin <paramref name="m"/>.</summary>
|
||||
public static void Encapsulate(byte[] pk, byte[] m, out byte[] ct, out byte[] ss)
|
||||
{
|
||||
var buf = new byte[2 * SymBytes];
|
||||
byte[] mh = Sha3_256(m, 0, SymBytes); // don't release system RNG output
|
||||
Array.Copy(mh, 0, buf, 0, SymBytes);
|
||||
byte[] hpk = Sha3_256(pk, 0, PublicKeyBytes);
|
||||
Array.Copy(hpk, 0, buf, SymBytes, SymBytes);
|
||||
|
||||
byte[] kr = Sha3_512(buf, 0, 2 * SymBytes);
|
||||
var coins = new byte[SymBytes];
|
||||
Array.Copy(kr, SymBytes, coins, 0, SymBytes);
|
||||
|
||||
var msg = new byte[SymBytes];
|
||||
Array.Copy(buf, 0, msg, 0, SymBytes);
|
||||
ct = IndcpaEnc(msg, pk, coins);
|
||||
|
||||
byte[] hc = Sha3_256(ct, 0, CiphertextBytes);
|
||||
var krFinal = new byte[2 * SymBytes];
|
||||
Array.Copy(kr, 0, krFinal, 0, SymBytes);
|
||||
Array.Copy(hc, 0, krFinal, SymBytes, SymBytes);
|
||||
ss = Shake256(krFinal, SsBytes);
|
||||
}
|
||||
|
||||
/// <summary>Decapsulates <paramref name="ct"/> with <paramref name="sk"/>, returning the 32-byte shared secret
|
||||
/// (a pseudo-random value on implicit-rejection failure).</summary>
|
||||
public static byte[] Decapsulate(byte[] ct, byte[] sk)
|
||||
{
|
||||
var skCpa = new byte[IndcpaSecretKeyBytes];
|
||||
Array.Copy(sk, 0, skCpa, 0, IndcpaSecretKeyBytes);
|
||||
var pk = new byte[IndcpaPublicKeyBytes];
|
||||
Array.Copy(sk, IndcpaSecretKeyBytes, pk, 0, IndcpaPublicKeyBytes);
|
||||
|
||||
byte[] m = IndcpaDec(ct, skCpa);
|
||||
|
||||
var buf = new byte[2 * SymBytes];
|
||||
Array.Copy(m, 0, buf, 0, SymBytes);
|
||||
Array.Copy(sk, SecretKeyBytes - 2 * SymBytes, buf, SymBytes, SymBytes); // stored H(pk)
|
||||
|
||||
byte[] kr = Sha3_512(buf, 0, 2 * SymBytes);
|
||||
var coins = new byte[SymBytes];
|
||||
Array.Copy(kr, SymBytes, coins, 0, SymBytes);
|
||||
|
||||
byte[] cmp = IndcpaEnc(buf[..SymBytes], pk, coins);
|
||||
int fail = Verify(ct, cmp, CiphertextBytes);
|
||||
|
||||
byte[] hc = Sha3_256(ct, 0, CiphertextBytes);
|
||||
var krFinal = new byte[2 * SymBytes];
|
||||
Array.Copy(kr, 0, krFinal, 0, SymBytes);
|
||||
Array.Copy(hc, 0, krFinal, SymBytes, SymBytes);
|
||||
|
||||
// cmov: replace pre-k with z on failure (constant time)
|
||||
CMov(krFinal, 0, sk, SecretKeyBytes - SymBytes, SymBytes, (byte)fail);
|
||||
return Shake256(krFinal, SsBytes);
|
||||
}
|
||||
|
||||
private static int Verify(byte[] a, byte[] b, int len)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
byte r = 0;
|
||||
for (int i = 0; i < len; i++) r |= (byte)(a[i] ^ b[i]);
|
||||
return (int)((ulong)(0 - (ulong)r) >> 63);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CMov(byte[] r, int rOff, byte[] x, int xOff, int len, byte b)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
b = (byte)(-(sbyte)b);
|
||||
for (int i = 0; i < len; i++)
|
||||
r[rOff + i] ^= (byte)(b & (r[rOff + i] ^ x[xOff + i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Wingnal.Protocol.Curve;
|
||||
|
||||
/// <summary>
|
||||
/// XEdDSA over Curve25519 / Ed25519 (Trevor Perrin's spec, as used by Signal).
|
||||
/// Signs/verifies Ed25519-style signatures using a Montgomery (X25519) key pair, so the same
|
||||
/// identity key can be used for both ECDH (X25519) and signatures.
|
||||
///
|
||||
/// Implemented on top of a compact, auditable BigInteger reference of the Ed25519 group
|
||||
/// (<see cref="Ed25519Math"/>). Correctness of the underlying group/field/scalar arithmetic is
|
||||
/// validated against RFC 8032 known-answer vectors; XEdDSA verify is validated against libsignal's
|
||||
/// own curve25519 known-answer vector (XEd25519VectorTests).
|
||||
///
|
||||
/// Signal-specific detail: the Edwards public key's sign bit is NOT forced to 0. The signer stashes
|
||||
/// A's natural sign bit in the high bit of s (s < L leaves it free), and the verifier reads it from
|
||||
/// signature[63] to reconstruct A with the correct sign before clearing the bit to parse s. (Our
|
||||
/// signer happens to always produce sign-bit-0 keys, which is the special case libsignal accepts.)
|
||||
/// </summary>
|
||||
public static class XEd25519
|
||||
{
|
||||
// hash_1 prefix per XEdDSA spec: little-endian encoding of (2^256 - 1 - 1) = 2^256 - 2.
|
||||
private static readonly byte[] Hash1Prefix = BuildHash1Prefix();
|
||||
|
||||
// (L-1) as a 32-byte little-endian scalar, used to negate a scalar mod L (constant-time).
|
||||
private static readonly byte[] ScalarMinusOne = Ed25519Math.ToLe32(Ed25519Math.Mod(BigInteger.MinusOne, Ed25519Math.L));
|
||||
private static readonly byte[] Zero32 = new byte[32];
|
||||
|
||||
private static byte[] BuildHash1Prefix()
|
||||
{
|
||||
var p = new byte[32];
|
||||
p[0] = 0xFE;
|
||||
for (int i = 1; i < 32; i++) p[i] = 0xFF;
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// XEdDSA sign. <paramref name="privateKey"/> is the 32-byte (clamped) Montgomery/X25519
|
||||
/// private scalar, little-endian. <paramref name="random"/> must be 64 fresh random bytes.
|
||||
/// Returns a 64-byte signature (R || s).
|
||||
/// </summary>
|
||||
public static byte[] CalculateSignature(ReadOnlySpan<byte> privateKey, ReadOnlySpan<byte> message, ReadOnlySpan<byte> random)
|
||||
{
|
||||
if (privateKey.Length != 32) throw new ArgumentException("private key must be 32 bytes", nameof(privateKey));
|
||||
if (random.Length != 64) throw new ArgumentException("random must be 64 bytes", nameof(random));
|
||||
|
||||
// Constant-time signing: the operations that touch the private key (the two fixed-base scalar
|
||||
// multiplies on the secret k and nonce r, and the scalar arithmetic mod L) run through Ed25519Ct
|
||||
// (BouncyCastle's constant-time field). The hash-to-scalar h is over public data (R, A, M) only,
|
||||
// so it stays on the BigInteger reference. Validated byte-identical to the reference (Ed25519CtTests).
|
||||
byte[] sk = privateKey.ToArray();
|
||||
|
||||
// calculate_key_pair(k): A has sign bit 0; a is adjusted so that a·B == A.
|
||||
byte[] enc = Ed25519Ct.ScalarMultBaseEncode(sk); // k·B
|
||||
int xOdd = (enc[31] >> 7) & 1;
|
||||
byte[] aEnc = (byte[])enc.Clone();
|
||||
aEnc[31] &= 0x7F; // A's x is forced even (sign bit 0)
|
||||
|
||||
var k64 = new byte[64];
|
||||
Array.Copy(sk, k64, 32);
|
||||
byte[] kModL = Ed25519Ct.ScReduce(k64); // k mod L
|
||||
byte[] aBytes = xOdd == 1 ? Ed25519Ct.ScMulAdd(ScalarMinusOne, kModL, Zero32) : kModL; // a = ±k mod L
|
||||
|
||||
// r = hash_1(a || M || Z) mod L
|
||||
byte[] r;
|
||||
using (var sha = SHA512.Create())
|
||||
{
|
||||
sha.TransformBlock(Hash1Prefix, 0, Hash1Prefix.Length, null, 0);
|
||||
sha.TransformBlock(aBytes, 0, aBytes.Length, null, 0);
|
||||
TransformSpan(sha, message);
|
||||
TransformSpan(sha, random, final: true);
|
||||
r = Ed25519Ct.ScReduce(sha.Hash!);
|
||||
}
|
||||
|
||||
byte[] rEnc = Ed25519Ct.ScalarMultBaseEncode(r); // R = r·B
|
||||
|
||||
// h = hash(R || A || M) mod L (public inputs only)
|
||||
byte[] hBytes = Ed25519Math.ToLe32(HashToScalar(rEnc, aEnc, message));
|
||||
|
||||
byte[] s = Ed25519Ct.ScMulAdd(hBytes, aBytes, r); // s = h·a + r (mod L)
|
||||
|
||||
var sig = new byte[64];
|
||||
Array.Copy(rEnc, 0, sig, 0, 32);
|
||||
Array.Copy(s, 0, sig, 32, 32);
|
||||
return sig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// XEdDSA verify. <paramref name="montgomeryPublicKey"/> is the 32-byte X25519 public key
|
||||
/// (Montgomery u-coordinate, little-endian). <paramref name="signature"/> is 64 bytes (R || s).
|
||||
/// </summary>
|
||||
public static bool VerifySignature(ReadOnlySpan<byte> montgomeryPublicKey, ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature)
|
||||
{
|
||||
if (montgomeryPublicKey.Length != 32 || signature.Length != 64) return false;
|
||||
|
||||
// Mask the high bit per RFC 7748, then reject u >= p.
|
||||
Span<byte> u32 = stackalloc byte[32];
|
||||
montgomeryPublicKey.CopyTo(u32);
|
||||
u32[31] &= 0x7F;
|
||||
BigInteger u = Ed25519Math.FromLe(u32);
|
||||
if (u >= Ed25519Math.P) return false;
|
||||
|
||||
// Montgomery u -> Edwards y = (u - 1) / (u + 1)
|
||||
BigInteger denom = Ed25519Math.Mod(u + 1, Ed25519Math.P);
|
||||
if (denom.IsZero) return false;
|
||||
BigInteger y = Ed25519Math.Mod((u - 1) * Ed25519Math.Inverse(denom), Ed25519Math.P);
|
||||
|
||||
// Signal's curve25519 XEdDSA stashes the Edwards public key's sign bit in the high bit of s
|
||||
// (signature[63]); the verifier reads it back to reconstruct A with the correct sign, then
|
||||
// clears it before parsing s. Matches libsignal rust/core curve25519 verify_signature.
|
||||
int sign = (signature[63] & 0x80) >> 7;
|
||||
|
||||
Span<byte> s32 = stackalloc byte[32];
|
||||
signature.Slice(32, 32).CopyTo(s32);
|
||||
s32[31] &= 0x7F;
|
||||
if ((s32[31] & 0xE0) != 0) return false; // scalar out of range
|
||||
BigInteger s = Ed25519Math.FromLe(s32);
|
||||
|
||||
// A = decode(y, sign-from-signature); its encoding carries that sign bit and is what's hashed.
|
||||
if (!Ed25519Math.TryDecode(y, sign, out Ed25519Math.Point a)) return false;
|
||||
byte[] aEnc = Ed25519Math.Encode(a);
|
||||
|
||||
byte[] rEnc = signature.Slice(0, 32).ToArray();
|
||||
BigInteger h = HashToScalar(rEnc, aEnc, message);
|
||||
|
||||
// R_check = s*B - h*A
|
||||
Ed25519Math.Point sB = Ed25519Math.ScalarMultBase(s);
|
||||
Ed25519Math.Point hA = Ed25519Math.ScalarMult(a, h);
|
||||
Ed25519Math.Point rCheck = Ed25519Math.Add(sB, hA.Negate());
|
||||
|
||||
return CryptographicOperations.FixedTimeEquals(Ed25519Math.Encode(rCheck), rEnc);
|
||||
}
|
||||
|
||||
private static BigInteger HashToScalar(byte[] rEnc, byte[] aEnc, ReadOnlySpan<byte> message)
|
||||
{
|
||||
using var sha = SHA512.Create();
|
||||
sha.TransformBlock(rEnc, 0, rEnc.Length, null, 0);
|
||||
sha.TransformBlock(aEnc, 0, aEnc.Length, null, 0);
|
||||
TransformSpan(sha, message, final: true);
|
||||
return Ed25519Math.ScReduce(sha.Hash!);
|
||||
}
|
||||
|
||||
private static void TransformSpan(SHA512 sha, ReadOnlySpan<byte> data, bool final = false)
|
||||
{
|
||||
byte[] buf = data.ToArray();
|
||||
if (final)
|
||||
sha.TransformFinalBlock(buf, 0, buf.Length);
|
||||
else
|
||||
sha.TransformBlock(buf, 0, buf.Length, null, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user