Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EcRecover #3633

Open
wants to merge 21 commits into
base: HF_Echidna
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 105 additions & 86 deletions src/Neo/Cryptography/Crypto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
// modifications are permitted.

using Neo.IO.Caching;
using Neo.SmartContract.Native;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Utilities.Encoders;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
Expand All @@ -26,15 +28,15 @@ namespace Neo.Cryptography
/// </summary>
public static class Crypto
{
/// <summary>
/// 64 bytes ECDSA signature + 1 byte recovery id
/// </summary>
private const int RecoverableSignatureLength = 64 + 1;

/// <summary>
/// 64 bytes ECDSA signature
/// </summary>
private const int SignatureLength = 64;
private static readonly BigInteger s_prime = new(1, Hex.Decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"));

private static readonly BigInteger s_prime = new(1,
Hex.Decode("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F"));

private static readonly ECDsaCache CacheECDsa = new();
private static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
private static readonly ECCurve secP256k1 = ECCurve.CreateFromFriendlyName("secP256k1");
Expand Down Expand Up @@ -67,15 +69,19 @@ public static byte[] Hash256(ReadOnlySpan<byte> message)
/// <param name="ecCurve">The <see cref="ECC.ECCurve"/> curve of the signature, default is <see cref="ECC.ECCurve.Secp256r1"/>.</param>
/// <param name="hasher">The hash algorithm to hash the message, default is SHA256.</param>
/// <returns>The ECDSA signature for the specified message.</returns>
public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = null, Hasher hasher = Hasher.SHA256)
public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = null,
Hasher hasher = Hasher.SHA256)
{
if (hasher == Hasher.Keccak256 || (IsOSX && ecCurve == ECC.ECCurve.Secp256k1))
{
var signer = new Org.BouncyCastle.Crypto.Signers.ECDsaSigner();
var privateKey = new BigInteger(1, priKey);
var priKeyParameters = new ECPrivateKeyParameters(privateKey, ecCurve.BouncyCastleDomainParams);
signer.Init(true, priKeyParameters);
var messageHash = CryptoLib.GetMessageHash(message, hasher);
var messageHash =
hasher == Hasher.SHA256 ? message.Sha256() :
hasher == Hasher.Keccak256 ? message.Keccak256() :
throw new NotSupportedException(nameof(hasher));
var signature = signer.GenerateSignature(messageHash);

var signatureBytes = new byte[64];
Expand All @@ -93,98 +99,101 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n
ecCurve == ECC.ECCurve.Secp256k1 ? secP256k1 :
throw new NotSupportedException();

using var ecdsa = ECDsa.Create(new ECParameters
{
Curve = curve,
D = priKey,
});
using var ecdsa = ECDsa.Create(new ECParameters { Curve = curve, D = priKey, });
var hashAlg =
hasher == Hasher.SHA256 ? HashAlgorithmName.SHA256 :
throw new NotSupportedException(nameof(hasher));
hasher == Hasher.SHA256 ? HashAlgorithmName.SHA256 : throw new NotSupportedException(nameof(hasher));
return ecdsa.SignData(message, hashAlg);
}


shargon marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Recovers the public key from a signature and message hash.
/// </summary>
/// <param name="signature">65-byte signature (r[32] || s[32] || v[1])</param>
/// <param name="signature">Signature, either 65 bytes (r[32] || s[32] || v[1]) or
/// 64 bytes in “compact” form (r[32] || yParityAndS[32]).</param>
/// <param name="hash">32-byte message hash</param>
/// <returns>The recovered public key</returns>
/// <exception cref="ArgumentException">Thrown when signature or hash parameters are invalid</exception>
/// <exception cref="ArgumentException">Thrown if signature or hash is invalid</exception>
public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash)
{
if (signature.Length != RecoverableSignatureLength)
throw new ArgumentException("Signature must be 65 bytes with recovery value", nameof(signature));
if (hash.Length != 32)
if (signature.Length != 65 && signature.Length != 64)
throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature));
if (hash is not { Length: 32 })
throw new ArgumentException("Message hash must be 32 bytes", nameof(hash));

try
{
// Extract r, s components (32 bytes each)
var r = new BigInteger(1, signature.Take(32).ToArray());
var s = new BigInteger(1, signature.Skip(32).Take(32).ToArray());

// Get recovery id, allowing both 0-3 and 27-30
var v = signature[SignatureLength];
var recId = v >= 27 ? v - 27 : v;
if (recId > 3)
throw new ArgumentException("Recovery value must be 0-3 or 27-30", nameof(signature));

// Get curve parameters
var curve = ECC.ECCurve.Secp256k1.BouncyCastleCurve;
var n = curve.N;

// Validate r, s values
if (r.SignValue <= 0 || r.CompareTo(n) >= 0)
throw new ArgumentException("r must be in range [1, N-1]", nameof(signature));
if (s.SignValue <= 0 || s.CompareTo(n) >= 0)
throw new ArgumentException("s must be in range [1, N-1]", nameof(signature));

// Calculate x coordinate
var i = BigInteger.ValueOf(recId >> 1);
var x = r.Add(i.Multiply(n));

// Get curve field
var field = curve.Curve.Field;
if (x.CompareTo(field.Characteristic) >= 0)
throw new ArgumentException("Invalid x coordinate", nameof(signature));
// Extract (r, s) and compute integer recId
BigInteger r, s;
int recId;

// Convert x to field element
var xField = curve.Curve.FromBigInteger(x);
if (signature.Length == 65)
{
// Format: r[32] || s[32] || v[1]
r = new BigInteger(1, signature.Take(32).ToArray());
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ranges, it's better

Jim8y marked this conversation as resolved.
Show resolved Hide resolved
s = new BigInteger(1, signature.Skip(32).Take(32).ToArray());
Jim8y marked this conversation as resolved.
Show resolved Hide resolved

// v could be 0..3 or 27..30 (Ethereum style).
var v = signature[64];
recId = v >= 27 ? v - 27 : v; // normalize
if (recId < 0 || recId > 3)
throw new ArgumentException("Recovery value must be in [0..3] after normalization.", nameof(signature));
}
else
{
// 64 bytes “compact” format: r[32] || yParityAndS[32]
// yParity is fused into the top bit of s.

// Compute right-hand side of curve equation: y^2 = x^3 + ax + b
var rhs = xField.Square().Multiply(xField).Add(curve.Curve.A.Multiply(xField)).Add(curve.Curve.B);
r = new BigInteger(1, signature.Take(32).ToArray());
Jim8y marked this conversation as resolved.
Show resolved Hide resolved
var yParityAndS = new BigInteger(1, signature.Skip(32).ToArray());

// Compute y coordinate
var y = rhs.Sqrt();
if (y == null)
throw new ArgumentException("Invalid x coordinate - no square root exists", nameof(signature));
// Mask out top bit to get s
var mask = BigInteger.One.ShiftLeft(255).Subtract(BigInteger.One);
s = yParityAndS.And(mask);

// Ensure y has correct parity
if (y.ToBigInteger().TestBit(0) != ((recId & 1) == 1))
y = y.Negate();
// Extract yParity (0 or 1)
bool yParity = yParityAndS.TestBit(255);

// Create R point
var R = curve.Curve.CreatePoint(x, y.ToBigInteger());
// For “compact,” map parity to recId in [0..1].
// For typical usage, recId in {0,1} is enough:
recId = yParity ? 1 : 0;
}

// Check R * n = infinity
if (!R.Multiply(n).IsInfinity)
throw new ArgumentException("Invalid R point order", nameof(signature));
// Decompose recId into i = recId >> 1 and yBit = recId & 1
int iPart = recId >> 1; // usually 0..1
bool yBit = (recId & 1) == 1;

// Calculate e = -hash mod n
var e = new BigInteger(1, hash).Negate().Mod(n);
// BouncyCastle curve constants
var n = ECC.ECCurve.Secp256k1.BouncyCastleCurve.N;
var e = new BigInteger(1, hash);

// Calculate r^-1
// eInv = -e mod n
var eInv = BigInteger.Zero.Subtract(e).Mod(n);
// rInv = (r^-1) mod n
var rInv = r.ModInverse(n);

// Calculate Q = r^-1 (sR - eG)
var Q = R.Multiply(s).Add(curve.G.Multiply(e)).Multiply(rInv);

if (Q.IsInfinity)
throw new ArgumentException("Invalid public key point at infinity", nameof(signature));

// Convert to Neo ECPoint format
return ECC.ECPoint.FromBytes(Q.GetEncoded(false), ECC.ECCurve.Secp256k1);
// srInv = (s * r^-1) mod n
var srInv = rInv.Multiply(s).Mod(n);
// eInvrInv = (eInv * r^-1) mod n
var eInvrInv = rInv.Multiply(eInv).Mod(n);

// x = r + iPart * n
var x = r.Add(BigInteger.ValueOf(iPart).Multiply(n));
// Verify x is within the curve prime
if (x.CompareTo(s_prime) >= 0)
throw new ArgumentException("x is out of range of the secp256k1 prime.", nameof(signature));

// Decompress to get R
var decompressedRKey = DecompressKey(ECC.ECCurve.Secp256k1.BouncyCastleCurve.Curve, x, yBit);
// Check that R is on curve
if (!decompressedRKey.Multiply(n).IsInfinity)
throw new ArgumentException("R point is not valid on this curve.", nameof(signature));

// Q = (eInv * G) + (srInv * R)
var q = Org.BouncyCastle.Math.EC.ECAlgorithms.SumOfTwoMultiplies(
ECC.ECCurve.Secp256k1.BouncyCastleCurve.G, eInvrInv,
decompressedRKey, srInv);

return ECC.ECPoint.FromBytes(q.Normalize().GetEncoded(false), ECC.ECCurve.Secp256k1);
}
catch (ArgumentException)
{
Expand All @@ -196,6 +205,14 @@ public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash)
}
}

private static Org.BouncyCastle.Math.EC.ECPoint DecompressKey(
Org.BouncyCastle.Math.EC.ECCurve curve, BigInteger xBN, bool yBit)
{
var compEnc = X9IntegerConverter.IntegerToBytes(xBN, 1 + X9IntegerConverter.GetByteLength(curve));
compEnc[0] = (byte)(yBit ? 0x03 : 0x02);
return curve.DecodePoint(compEnc);
}

/// <summary>
/// Verifies that a digital signature is appropriate for the provided key, message and hash algorithm.
/// </summary>
Expand All @@ -204,7 +221,8 @@ public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash)
/// <param name="pubkey">The public key to be used.</param>
/// <param name="hasher">The hash algorithm to be used to hash the message, the default is SHA256.</param>
/// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature, ECC.ECPoint pubkey, Hasher hasher = Hasher.SHA256)
public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature, ECC.ECPoint pubkey,
Hasher hasher = Hasher.SHA256)
{
if (signature.Length != 64) return false;

Expand All @@ -220,15 +238,18 @@ public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte
var sig = signature.ToArray();
var r = new BigInteger(1, sig, 0, 32);
var s = new BigInteger(1, sig, 32, 32);
var messageHash = CryptoLib.GetMessageHash(message, hasher);

var messageHash =
hasher == Hasher.SHA256 ? message.Sha256() :
hasher == Hasher.Keccak256 ? message.Keccak256() :
throw new NotSupportedException(nameof(hasher));

return signer.VerifySignature(messageHash, r, s);
}

var ecdsa = CreateECDsa(pubkey);
var hashAlg =
hasher == Hasher.SHA256 ? HashAlgorithmName.SHA256 :
throw new NotSupportedException(nameof(hasher));
hasher == Hasher.SHA256 ? HashAlgorithmName.SHA256 : throw new NotSupportedException(nameof(hasher));
return ecdsa.VerifyData(message, signature, hashAlg);
}

Expand All @@ -244,6 +265,7 @@ public static ECDsa CreateECDsa(ECC.ECPoint pubkey)
{
return cache.Value;
}

var curve =
pubkey.Curve == ECC.ECCurve.Secp256r1 ? ECCurve.NamedCurves.nistP256 :
pubkey.Curve == ECC.ECCurve.Secp256k1 ? secP256k1 :
Expand All @@ -252,11 +274,7 @@ public static ECDsa CreateECDsa(ECC.ECPoint pubkey)
var ecdsa = ECDsa.Create(new ECParameters
{
Curve = curve,
Q = new ECPoint
{
X = buffer[1..33],
Y = buffer[33..]
}
Q = new ECPoint { X = buffer[1..33], Y = buffer[33..] }
});
CacheECDsa.Add(new ECDsaCacheItem(pubkey, ecdsa));
return ecdsa;
Expand All @@ -271,7 +289,8 @@ public static ECDsa CreateECDsa(ECC.ECPoint pubkey)
/// <param name="curve">The curve to be used by the ECDSA algorithm.</param>
/// <param name="hasher">The hash algorithm to be used hash the message, the default is SHA256.</param>
/// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns>
public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature, ReadOnlySpan<byte> pubkey, ECC.ECCurve curve, Hasher hasher = Hasher.SHA256)
public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte> signature,
ReadOnlySpan<byte> pubkey, ECC.ECCurve curve, Hasher hasher = Hasher.SHA256)
{
return VerifySignature(message, signature, ECC.ECPoint.DecodePoint(pubkey, curve), hasher);
}
Expand Down
18 changes: 4 additions & 14 deletions src/Neo/SmartContract/Native/CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,13 @@ public static byte[] Keccak256(byte[] data)
return data.Keccak256();
}

internal static byte[] GetMessageHash(byte[] message, Hasher hasher)
{
return GetMessageHash(message.AsSpan(), hasher);
}

internal static byte[] GetMessageHash(ReadOnlySpan<byte> message, Hasher hasher)
private static byte[] GetMessageHash(byte[] message, Hasher hasher)
{
return hasher switch
{
Hasher.SHA256 => message.Sha256(),
Hasher.Keccak256 => message.Keccak256(),
_ => throw new NotSupportedException(nameof(hasher))
_ => null
};
}

Expand All @@ -99,7 +94,6 @@ private static ECPoint ECrecover(byte[] message, byte[] signature, Hasher hasher
{
var messageHash = GetMessageHash(message, hasher);
if (messageHash == null) return null;

return Crypto.ECRecover(signature, messageHash);
}
catch
Expand Down Expand Up @@ -152,16 +146,12 @@ public static bool VerifyWithECDsaV0(byte[] message, byte[] pubkey, byte[] signa
/// </summary>
/// <param name="message">The original message that was signed.</param>
/// <param name="hasher">The hash algorithm to be used (SHA256 or Keccak256).</param>
/// <param name="signature">The 65-byte signature in format: r[32] + s[32] + v[1], where v must be 27 or 28.</param>
/// <param name="signature">The 65-byte signature in format: r[32] + s[32] + v[1]. 64-bytes for eip-2098, where v must be 27 or 28.</param>
/// <returns>The recovered public key in compressed format, or null if recovery fails.</returns>
[ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 10, Name = "secp256k1Recover")]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should find the right price

public static byte[] Secp256K1Recover(byte[] message, Hasher hasher, byte[] signature)
{
if (signature.Length != 65)
return null;

var v = signature[64];
if (v != 27 && v != 28)
if (signature.Length != 65 && signature.Length != 64)
return null;

var point = ECrecover(message, signature, hasher);
Expand Down
Loading
Loading