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

EC Recover #3696

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
187 changes: 151 additions & 36 deletions src/Neo/Cryptography/Crypto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Utilities.Encoders;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;

Expand All @@ -24,21 +26,12 @@ namespace Neo.Cryptography
/// </summary>
public static class Crypto
{
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");
private static readonly X9ECParameters bouncySecp256k1 = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
private static readonly X9ECParameters bouncySecp256r1 = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256r1");

/// <summary>
/// Holds domain parameters for Secp256r1 elliptic curve.
/// </summary>
private static readonly ECDomainParameters secp256r1DomainParams = new ECDomainParameters(bouncySecp256r1.Curve, bouncySecp256r1.G, bouncySecp256r1.N, bouncySecp256r1.H);

/// <summary>
/// Holds domain parameters for Secp256k1 elliptic curve.
/// </summary>
private static readonly ECDomainParameters secp256k1DomainParams = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H);

/// <summary>
/// Calculates the 160-bit hash value of the specified message.
Expand Down Expand Up @@ -86,18 +79,11 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n
{
if (hashAlgorithm == HashAlgorithm.Keccak256 || (IsOSX && ecCurve == ECC.ECCurve.Secp256k1))
{
var domain =
ecCurve == null || ecCurve == ECC.ECCurve.Secp256r1 ? secp256r1DomainParams :
ecCurve == ECC.ECCurve.Secp256k1 ? secp256k1DomainParams :
throw new NotSupportedException(nameof(ecCurve));
var signer = new Org.BouncyCastle.Crypto.Signers.ECDsaSigner();
var privateKey = new BigInteger(1, priKey);
var priKeyParameters = new ECPrivateKeyParameters(privateKey, domain);
var priKeyParameters = new ECPrivateKeyParameters(privateKey, ecCurve.BouncyCastleDomainParams);
signer.Init(true, priKeyParameters);
var messageHash =
hashAlgorithm == HashAlgorithm.SHA256 ? message.Sha256() :
hashAlgorithm == HashAlgorithm.Keccak256 ? message.Keccak256() :
throw new NotSupportedException(nameof(hashAlgorithm));
var messageHash = GetMessageHash(message);
var signature = signer.GenerateSignature(messageHash);

var signatureBytes = new byte[64];
Expand Down Expand Up @@ -154,29 +140,17 @@ public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte

if (hashAlgorithm == HashAlgorithm.Keccak256 || (IsOSX && pubkey.Curve == ECC.ECCurve.Secp256k1))
{
var domain =
pubkey.Curve == ECC.ECCurve.Secp256r1 ? secp256r1DomainParams :
pubkey.Curve == ECC.ECCurve.Secp256k1 ? secp256k1DomainParams :
throw new NotSupportedException(nameof(pubkey.Curve));
var curve =
pubkey.Curve == ECC.ECCurve.Secp256r1 ? bouncySecp256r1.Curve :
bouncySecp256k1.Curve;

var point = curve.CreatePoint(
var point = pubkey.Curve.BouncyCastleCurve.Curve.CreatePoint(
new BigInteger(pubkey.X.Value.ToString()),
new BigInteger(pubkey.Y.Value.ToString()));
var pubKey = new ECPublicKeyParameters("ECDSA", point, domain);
var pubKey = new ECPublicKeyParameters("ECDSA", point, pubkey.Curve.BouncyCastleDomainParams);
var signer = new Org.BouncyCastle.Crypto.Signers.ECDsaSigner();
signer.Init(false, pubKey);

var sig = signature.ToArray();
var r = new BigInteger(1, sig, 0, 32);
var s = new BigInteger(1, sig, 32, 32);

var messageHash =
hashAlgorithm == HashAlgorithm.SHA256 ? message.Sha256() :
hashAlgorithm == HashAlgorithm.Keccak256 ? message.Keccak256() :
throw new NotSupportedException(nameof(hashAlgorithm));
var messageHash = GetMessageHash(message, hashAlgorithm);

return signer.VerifySignature(messageHash, r, s);
}
Expand Down Expand Up @@ -246,5 +220,146 @@ public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte
{
return VerifySignature(message, signature, ECC.ECPoint.DecodePoint(pubkey, curve), hashAlgorithm);
}

/// <summary>
/// Get hash from message.
/// </summary>
/// <param name="message">Original message</param>
/// <param name="hashAlgorithm">The hash algorithm to be used hash the message, the default is SHA256.</param>
/// <returns>Hashed message</returns>
public static byte[] GetMessageHash(byte[] message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256)
{
return hashAlgorithm switch
{
HashAlgorithm.SHA256 => message.Sha256(),
HashAlgorithm.Keccak256 => message.Keccak256(),
HashAlgorithm.None => message,
_ => throw new NotSupportedException(nameof(hashAlgorithm))
};
}

/// <summary>
/// Get hash from message.
/// </summary>
/// <param name="message">Original message</param>
/// <param name="hashAlgorithm">The hash algorithm to be used hash the message, the default is SHA256.</param>
/// <returns>Hashed message</returns>
public static byte[] GetMessageHash(ReadOnlySpan<byte> message, HashAlgorithm hashAlgorithm = HashAlgorithm.SHA256)
{
return hashAlgorithm switch
{
HashAlgorithm.SHA256 => message.Sha256(),
HashAlgorithm.Keccak256 => message.Keccak256(),
HashAlgorithm.None => message.ToArray(),
_ => throw new NotSupportedException(nameof(hashAlgorithm))
};
}

/// <summary>
/// Recovers the public key from a signature and message hash.
/// </summary>
/// <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 if signature or hash is invalid</exception>
public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash)
{
if (signature.Length != 65 && signature.Length != 64)
throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature));
if (hash.Length != 32)
throw new ArgumentException("Message hash must be 32 bytes", nameof(hash));

try
{
// Extract (r, s) and compute integer recId
BigInteger r, s;
int recId;

if (signature.Length == 65)
{
// Format: r[32] || s[32] || v[1]
r = new BigInteger(1, [.. signature.Take(32)]);
s = new BigInteger(1, [.. signature.Skip(32).Take(32)]);

// 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.

r = new BigInteger(1, [.. signature.Take(32)]);
var yParityAndS = new BigInteger(1, signature.Skip(32).ToArray());

// Mask out top bit to get s
var mask = BigInteger.One.ShiftLeft(255).Subtract(BigInteger.One);
s = yParityAndS.And(mask);

// Extract yParity (0 or 1)
var yParity = yParityAndS.TestBit(255);

// For “compact,” map parity to recId in [0..1].
// For typical usage, recId in {0,1} is enough:
recId = yParity ? 1 : 0;
}

// Decompose recId into i = recId >> 1 and yBit = recId & 1
var iPart = recId >> 1; // usually 0..1
var yBit = (recId & 1) == 1;

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

// eInv = -e mod n
var eInv = BigInteger.Zero.Subtract(e).Mod(n);
// rInv = (r^-1) mod n
var rInv = r.ModInverse(n);
// 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)
{
throw;
}
catch (Exception ex)
{
throw new ArgumentException("Invalid signature parameters", nameof(signature), ex);
}
}

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);
}
}
}
16 changes: 13 additions & 3 deletions src/Neo/Cryptography/ECC/ECCurve.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// modifications are permitted.

using Neo.Extensions;
using Org.BouncyCastle.Crypto.Parameters;
using System.Globalization;
using System.Numerics;

Expand All @@ -33,9 +34,14 @@ public class ECCurve
/// </summary>
public readonly ECPoint G;

public readonly Org.BouncyCastle.Asn1.X9.X9ECParameters BouncyCastleCurve;
/// <summary>
/// Holds domain parameters for Secp256r1 elliptic curve.
/// </summary>
public readonly ECDomainParameters BouncyCastleDomainParams;
internal readonly int ExpectedECPointLength;

private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G)
private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G, string curveName)
{
this.Q = Q;
ExpectedECPointLength = ((int)VM.Utility.GetBitLength(Q) + 7) / 8;
Expand All @@ -44,6 +50,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G
this.N = N;
Infinity = new ECPoint(null, null, this);
this.G = ECPoint.DecodePoint(G, this);
BouncyCastleCurve = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName(curveName);
BouncyCastleDomainParams = new ECDomainParameters(BouncyCastleCurve.Curve, BouncyCastleCurve.G, BouncyCastleCurve.N, BouncyCastleCurve.H);
}

/// <summary>
Expand All @@ -55,7 +63,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G
BigInteger.Zero,
7,
BigInteger.Parse("00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", NumberStyles.AllowHexSpecifier),
("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes()
("04" + "79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798" + "483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8").HexToBytes(),
"secp256k1"
);

/// <summary>
Expand All @@ -67,7 +76,8 @@ private ECCurve(BigInteger Q, BigInteger A, BigInteger B, BigInteger N, byte[] G
BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier),
("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes()
("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(),
"secp256r1"
);
}
}
5 changes: 5 additions & 0 deletions src/Neo/Cryptography/HashAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,10 @@ public enum HashAlgorithm : byte
/// The Keccak256 hash algorithm.
/// </summary>
Keccak256 = 0x01,

/// <summary>
/// None
/// </summary>
None = 0xFF
}
}
28 changes: 28 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ public sealed partial class CryptoLib : NativeContract

internal CryptoLib() : base() { }

/// <summary>
/// Recovers the public key from a secp256k1 signature in a single byte array format.
/// </summary>
/// <param name="message">The original message that was signed.</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>
/// <param name="hashAlgorithm">The hash algorithm to be used hash the message.</param>
/// <returns>The recovered public key in compressed format, or null if recovery fails.</returns>
[ContractMethod(Hardfork.HF_Echidna, CpuFee = 1 << 10, Name = "recoverSecp256K1")]
public static byte[] RecoverSecp256K1(byte[] message, byte[] signature, HashAlgorithm hashAlgorithm)
shargon marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@Jim8y Jim8y Jan 27, 2025

Choose a reason for hiding this comment

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

@shargon Hi shagon, i think what we should handle is passing the hash and signature, since the message sometimes require special format to be hashed which is not possible to be handled by our native contract, such as keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)) . So we should leave the hash thing to the costum contract.

Copy link
Member Author

Choose a reason for hiding this comment

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

HasAlgoritm.None is allowed

Copy link

Choose a reason for hiding this comment

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

@Jim8y Check this recent conversation #3696 (comment).

Copy link

Choose a reason for hiding this comment

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

Maybe Jimmy's input could be taken as a hint that the current function signature might be misleading if one is not aware of HashAlgorithm.None.

I'm not strongly opinionated on this, but it might be better to just remove the HashAlgorithm parameter from the function signature. Then, it might be more clear that all the function does - as its name says - is recover the public key.

{
// It will be checked in Crypto.ECRecover
// if (signature.Length != 65 && signature.Length != 64)
// throw new ArgumentException("Signature must be 65 or 64 bytes", nameof(signature));

try
{
var messageHash = Crypto.GetMessageHash(message, hashAlgorithm);
if (messageHash == null) return null;

var point = Crypto.ECRecover(signature, messageHash);
return point?.EncodePoint(true);
}
catch
{
return null;
}
}

/// <summary>
/// Computes the hash value for the specified byte array using the ripemd160 algorithm.
/// </summary>
Expand Down
8 changes: 5 additions & 3 deletions tests/Neo.UnitTests/Cryptography/ECC/UT_ECFieldElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ public void TestSqrt()
ECFieldElement element = new(new BigInteger(100), ECCurve.Secp256k1);
Assert.AreEqual(new ECFieldElement(BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007908834671653"), ECCurve.Secp256k1), element.Sqrt());

ConstructorInfo constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]) }, null);
ECCurve testCruve = constructor.Invoke(new object[] {
var constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
[typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]), typeof(string)], null);
var testCruve = constructor.Invoke([
BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFF0", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("00FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFF00", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("005AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", NumberStyles.AllowHexSpecifier),
BigInteger.Parse("00FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", NumberStyles.AllowHexSpecifier),
("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes() }) as ECCurve;
("04" + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296" + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5").HexToBytes(),
"secp256k1"]) as ECCurve;
element = new ECFieldElement(new BigInteger(200), testCruve);
Assert.IsNull(element.Sqrt());
}
Expand Down
Loading
Loading