diff --git a/Ed25519/Constants.cs b/Ed25519/Constants.cs new file mode 100644 index 0000000..3bca7fa --- /dev/null +++ b/Ed25519/Constants.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +namespace Ed25519 +{ + internal static class Constants + { + // + // Compiler-time constants: + // + internal const int BIT_LENGTH = 256; + + // + // Run-time constants: + // + internal static readonly BigInteger Q = BigInteger.Parse("57896044618658097711785492504343953926634992332820282019728792003956564819949"); + internal static readonly BigInteger QM2 = BigInteger.Parse("57896044618658097711785492504343953926634992332820282019728792003956564819947"); + internal static readonly BigInteger QP3 = BigInteger.Parse("57896044618658097711785492504343953926634992332820282019728792003956564819952"); + internal static readonly BigInteger L = BigInteger.Parse("7237005577332262213973186563042994240857116359379907606001950938285454250989"); + internal static readonly BigInteger D = BigInteger.Parse("-4513249062541557337682894930092624173785641285191125241628941591882900924598840740"); + internal static readonly BigInteger I = BigInteger.Parse("19681161376707505956807079304988542015446066515923890162744021073123829784752"); + internal static readonly BigInteger U_N = BigInteger.Parse("57896044618658097711785492504343953926634992332820282019728792003956564819967"); + internal static readonly BigInteger TWO = new BigInteger(2); + internal static readonly BigInteger EIGHT = new BigInteger(8); + + // + // Pre-calculated values: + // + internal static readonly EdPoint B = new EdPoint + { + X = BigInteger.Parse("15112221349535400772501151409588531511454012693041857206046113283949847762202").Mod(Q), + Y = BigInteger.Parse("46316835694926478169428394003475163141307993866256225615783033603165251855960").Mod(Q), + }; + + internal static readonly BigInteger RECOVER_X_EXP = Constants.QP3 / Constants.EIGHT; + internal static readonly BigInteger TWO_POW_BIT_LENGTH_MINUS_TWO = BigInteger.Pow(2, BIT_LENGTH - 2); + internal static readonly BigInteger[] TWO_POW_CACHE = Enumerable.Range(0, 2 * BIT_LENGTH).Select(i => BigInteger.Pow(2, i)).ToArray(); + } +} diff --git a/Ed25519/EdPoint.cs b/Ed25519/EdPoint.cs new file mode 100644 index 0000000..68600e3 --- /dev/null +++ b/Ed25519/EdPoint.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace Ed25519 +{ + internal struct EdPoint + { + public BigInteger X { get; set; } + + public BigInteger Y { get; set; } + + public static EdPoint DecodePoint(ReadOnlySpan pointBytes) + { + var y = new BigInteger(pointBytes) & Constants.U_N; + var x = y.RecoverX(); + + if ((x.IsEven ? 0 : 1) != pointBytes.GetBit(Constants.BIT_LENGTH - 1)) + { + x = Constants.Q - x; + } + + var point = new EdPoint + { + X = x, + Y = y, + }; + + if (!point.IsOnCurve()) + throw new ArgumentException("Decoding point that is not on curve"); + + return point; + } + + public ReadOnlySpan EncodePoint() + { + var nout = this.Y.EncodeInt(); + nout[^1] |= this.X.IsEven ? (byte)0 : (byte)0x80; + return nout; + } + + public EdPoint Edwards(EdPoint point2) + { + var xx12 = this.X * point2.X; + var yy12 = this.Y * point2.Y; + var dTemp = Constants.D * xx12 * yy12; + + var x3 = (this.X * point2.Y + point2.X * this.Y) * (1 + dTemp).Inv(); + var y3 = (this.Y * point2.Y + xx12) * (1 - dTemp).Inv(); + + return new EdPoint + { + X = x3.Mod(Constants.Q), + Y = y3.Mod(Constants.Q), + }; + } + + public EdPoint ScalarMul(BigInteger e) + { + if (e.Equals(BigInteger.Zero)) + { + return new EdPoint + { + X = BigInteger.Zero, + Y = BigInteger.One, + }; + } + + var q = this.ScalarMul(e / Constants.TWO); + q = q.EdwardsSquare(); + + return e.IsEven ? q : q.Edwards(this); + } + + public EdPoint EdwardsSquare() + { + var xx = this.X * this.X; + var yy = this.Y * this.Y; + var dTemp = Constants.D * xx * yy; + + var x3 = 2 * this.X * this.Y * (1 + dTemp).Inv(); + var y3 = (yy + xx) * (1 - dTemp).Inv(); + + return new EdPoint + { + X = x3.Mod(Constants.Q), + Y = y3.Mod(Constants.Q), + }; + } + + public bool IsOnCurve() + { + var xx = this.X * this.X; + var yy = this.Y * this.Y; + var dxxyy = Constants.D * yy * xx; + + return (yy - xx - dxxyy - 1).Mod(Constants.Q).Equals(BigInteger.Zero); + } + } +} diff --git a/Ed25519/Extensions.cs b/Ed25519/Extensions.cs new file mode 100644 index 0000000..e5fb821 --- /dev/null +++ b/Ed25519/Extensions.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; + +namespace Ed25519 +{ + public static class Extensions + { + internal static ReadOnlySpan ComputeHash(this ReadOnlySpan data) + { + using var sha512 = SHA512.Create(); + return sha512.ComputeHash(data.ToArray()); + } + + internal static ReadOnlySpan ComputeHash(this Stream inputStream) + { + using var sha512 = SHA512.Create(); + return sha512.ComputeHash(inputStream); + } + + internal static BigInteger Mod(this BigInteger number, BigInteger modulo) + { + var result = number % modulo; + return result < 0 ? result + modulo : result; + } + + internal static BigInteger Inv(this BigInteger number) + { + return number.ExpMod(2, Constants.Q); + } + + internal static BigInteger RecoverX(this BigInteger y) + { + var y2 = y * y; + var xx = (y2 - 1) * (Constants.D * y2 + 1).Inv(); + var x = xx.ExpMod(Constants.RECOVER_X_EXP, Constants.Q); + + if (!(x * x - xx).Mod(Constants.Q).Equals(BigInteger.Zero)) + { + x = (x * Constants.I).Mod(Constants.Q); + } + + if (!x.IsEven) + { + x = Constants.Q - x; + } + + return x; + } + + internal static BigInteger ExpMod(this BigInteger number, BigInteger exponent, BigInteger modulo) + { + if (exponent.Equals(BigInteger.Zero)) + { + return BigInteger.One; + } + + var result = BigInteger.Pow(number.ExpMod(exponent / Constants.TWO, modulo), 2).Mod(modulo); + + if (exponent.IsEven) + return result; + + result *= number; + result = result.Mod(modulo); + return result; + } + + internal static Span EncodeInt(this BigInteger number) + { + var nin = number.ToByteArray(); + var nout = new byte[Math.Max(nin.Length, 32)]; + + Array.Copy(nin, nout, nin.Length); + return nout; + } + + internal static BigInteger DecodeInt(this ReadOnlySpan data) + { + return new BigInteger(data) & Constants.U_N; + } + + internal static BigInteger HashInt(this MemoryStream data) + { + var hash = data.ComputeHash(); + var hashSum = BigInteger.Zero; + + for (var i = 0; i < 2 * Constants.BIT_LENGTH; i++) + { + var bit = hash.GetBit(i); + if (bit != 0) + { + hashSum += Constants.TWO_POW_CACHE[i]; + } + } + + return hashSum; + } + + internal static int GetBit(this ReadOnlySpan data, int index) + { + return data[index / 8] >> (index % 8) & 1; + } + } +} diff --git a/Ed25519/Signer.cs b/Ed25519/Signer.cs new file mode 100644 index 0000000..8a058a1 --- /dev/null +++ b/Ed25519/Signer.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using System.Text; + +namespace Ed25519 +{ + public static class Signer + { + public static ReadOnlySpan Sign(ReadOnlySpan message, ReadOnlySpan privateKey, ReadOnlySpan publicKey) + { + var privateKeyHash = privateKey.ComputeHash(); + var privateKeyBits = Constants.TWO_POW_BIT_LENGTH_MINUS_TWO; + for (var i = 3; i < Constants.BIT_LENGTH - 2; i++) + { + var bit = privateKeyHash.GetBit(i); + if (bit != 0) + { + privateKeyBits += Constants.TWO_POW_CACHE[i]; + } + } + + BigInteger r; + using (var rSub = new MemoryStream((Constants.BIT_LENGTH / 8) + message.Length)) + { + rSub.Write(privateKeyHash[(privateKeyHash.Length/2)..]); + rSub.Write(message); + rSub.Flush(); + + r = rSub.HashInt(); + } + + var bigR = Constants.B.ScalarMul(r); + + BigInteger s; + var encodedBigR = bigR.EncodePoint(); + using (var sTemp = new MemoryStream(encodedBigR.Length + publicKey.Length + message.Length)) + { + sTemp.Write(encodedBigR); + sTemp.Write(publicKey); + sTemp.Write(message); + sTemp.Flush(); + + s = (r + sTemp.HashInt() * privateKeyBits).Mod(Constants.L); + } + + using (var nOut = new MemoryStream(64)) + { + nOut.Write(encodedBigR); + nOut.Write(s.EncodeInt()); + return nOut.ToArray(); + } + } + + public static bool Validate(ReadOnlySpan signature, ReadOnlySpan message, ReadOnlySpan publicKey) + { + if (signature.Length != Constants.BIT_LENGTH / 4) + throw new ArgumentException("Signature length is wrong"); + + if (publicKey.Length != Constants.BIT_LENGTH / 8) + throw new ArgumentException("Public key length is wrong"); + + var signatureSliceLeft = signature[..(Constants.BIT_LENGTH / 8)]; + var pointSignatureLeft = EdPoint.DecodePoint(signatureSliceLeft); + var pointPublicKey = EdPoint.DecodePoint(publicKey); + + var signatureSliceRight = signature[(signature.Length/2)..]; + var signatureRight = signatureSliceRight.DecodeInt(); + var encodedSignatureLeftPoint = pointSignatureLeft.EncodePoint(); + + BigInteger h; + using (var sTemp = new MemoryStream(encodedSignatureLeftPoint.Length + publicKey.Length + message.Length)) + { + sTemp.Write(encodedSignatureLeftPoint); + sTemp.Write(publicKey); + sTemp.Write(message); + sTemp.Flush(); + + h = sTemp.HashInt(); + } + + var ra = Constants.B.ScalarMul(signatureRight); + var ah = pointPublicKey.ScalarMul(h); + var rb = pointSignatureLeft.Edwards(ah); + + return ra.X.Equals(rb.X) && ra.Y.Equals(rb.Y); + } + } +}