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 7 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
133 changes: 106 additions & 27 deletions src/Neo/Cryptography/Crypto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
// modifications are permitted.

using Neo.IO.Caching;
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 +25,18 @@ namespace Neo.Cryptography
/// </summary>
public static class Crypto
{
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.
/// 64 bytes ECDSA signature + 1 byte recovery id
/// </summary>
private static readonly ECDomainParameters secp256r1DomainParams = new ECDomainParameters(bouncySecp256r1.Curve, bouncySecp256r1.G, bouncySecp256r1.N, bouncySecp256r1.H);

private const int RecoverableSignatureLength = 64 + 1;
/// <summary>
/// Holds domain parameters for Secp256k1 elliptic curve.
/// 64 bytes ECDSA signature
/// </summary>
private static readonly ECDomainParameters secp256k1DomainParams = new ECDomainParameters(bouncySecp256k1.Curve, bouncySecp256k1.G, bouncySecp256k1.N, bouncySecp256k1.H);
private const int SignatureLength = 64;
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");

/// <summary>
/// Calculates the 160-bit hash value of the specified message.
Expand Down Expand Up @@ -72,13 +70,9 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n
{
if (hasher == Hasher.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 =
hasher == Hasher.SHA256 ? message.Sha256() :
Expand Down Expand Up @@ -112,6 +106,99 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n
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="hash">32-byte message hash</param>
/// <returns>The recovered public key</returns>
/// <exception cref="ArgumentException">Thrown when signature or hash parameters are invalid</exception>
public static ECC.ECPoint ECRecover(byte[] signature, byte[] hash)
{
if (signature is not { Length: RecoverableSignatureLength })
throw new ArgumentException("Signature must be 65 bytes with recovery value", 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));

// Convert x to field element
var xField = curve.Curve.FromBigInteger(x);

// 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);

// Compute y coordinate
var y = rhs.Sqrt();
if (y == null)
throw new ArgumentException("Invalid x coordinate - no square root exists", nameof(signature));

// Ensure y has correct parity
if (y.ToBigInteger().TestBit(0) != ((recId & 1) == 1))
y = y.Negate();

// Create R point
var R = curve.Curve.CreatePoint(x, y.ToBigInteger());

// Check R * n = infinity
if (!R.Multiply(n).IsInfinity)
throw new ArgumentException("Invalid R point order", nameof(signature));

// Calculate e = -hash mod n
var e = new BigInteger(1, hash).Negate().Mod(n);

// Calculate r^-1
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);
}
catch (ArgumentException)
{
throw;
}
catch (Exception ex)
{
throw new ArgumentException("Invalid signature parameters", nameof(signature), ex);
}
}

/// <summary>
/// Verifies that a digital signature is appropriate for the provided key, message and hash algorithm.
/// </summary>
Expand All @@ -126,18 +213,10 @@ public static bool VerifySignature(ReadOnlySpan<byte> message, ReadOnlySpan<byte

if (hasher == Hasher.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);

Expand Down
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"
);
}
}
26 changes: 26 additions & 0 deletions src/Neo/Cryptography/SignatureFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (C) 2015-2024 The Neo Project.
//
// SignatureFormat.cs file belongs to the neo project and is free
// software distributed under the MIT software license, see the
// accompanying file LICENSE in the main directory of the
// repository or http://www.opensource.org/licenses/mit-license.php
// for more details.
//
// Redistribution and use in source and binary forms with or without
// modifications are permitted.

namespace Neo.Cryptography
{
public enum SignatureFormat : byte
{
/// <summary>
/// Der
/// </summary>
Der = 0,

/// <summary>
/// Fixed 32 bytes per BigInteger
/// </summary>
Fixed32 = 1
}
}
71 changes: 71 additions & 0 deletions src/Neo/SmartContract/Native/CryptoLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Neo.Cryptography.ECC;
using System;
using System.Collections.Generic;
using System.Numerics;

namespace Neo.SmartContract.Native
{
Expand Down Expand Up @@ -77,6 +78,31 @@ public static byte[] Keccak256(byte[] data)
return data.Keccak256();
}

private static byte[] GetMessageHash(byte[] message, Hasher hasher)
{
return hasher switch
{
Hasher.SHA256 => message.Sha256(),
Hasher.Keccak256 => message.Keccak256(),
_ => null
};
}

private static ECPoint ECrecover(byte[] message, byte[] signature, Hasher hasher)
{
var messageHash = GetMessageHash(message, hasher);
if (messageHash == null) return null;

try
{
return Crypto.ECRecover(signature, messageHash);
}
catch
{
return null;
}
}

/// <summary>
/// Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm.
/// </summary>
Expand Down Expand Up @@ -115,5 +141,50 @@ public static bool VerifyWithECDsaV0(byte[] message, byte[] pubkey, byte[] signa
return false;
}
}

/// <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="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>
/// <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 is not { Length: 65 })
return null;

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

var point = ECrecover(message, signature, hasher);

return point?.EncodePoint(true);
}

/// <summary>
/// Recovers the public key from a secp256k1 signature with separate r, s, and v components.
/// </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="r">The r component of the signature (32 bytes).</param>
/// <param name="s">The s component of the signature (32 bytes).</param>
/// <param name="v">The recovery identifier (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")]
public static byte[] Secp256K1Recover(byte[] message, Hasher hasher, byte[] r, byte[] s, BigInteger v)
{
if (r == null || s == null || r.Length != 32 || s.Length != 32)// || (v != 27 && v != 28)) Should we assume v as either 27/28
return null;

var signature = new byte[65];
r.CopyTo(signature, 0);
s.CopyTo(signature, 32);
signature[64] = (byte)v;

return Secp256K1Recover(message, hasher, signature);
}
}
}
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 @@ -80,13 +80,15 @@ public void TestSqrt()
ECFieldElement element = new(new BigInteger(100), ECCurve.Secp256k1);
element.Sqrt().Should().Be(new ECFieldElement(BigInteger.Parse("115792089237316195423570985008687907853269984665640564039457584007908834671653"), ECCurve.Secp256k1));

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[] {
ConstructorInfo constructor = typeof(ECCurve).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(BigInteger), typeof(byte[]), typeof(string) }, null);
ECCurve 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);
element.Sqrt().Should().Be(null);
}
Expand Down
Loading
Loading