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

EcRecover #3633

wants to merge 21 commits into from

Conversation

shargon
Copy link
Member

@shargon shargon commented Dec 18, 2024

Description

Close #3628

Type of change

  • Optimization (the change is only an optimization)
  • Style (the change is only a code style for better maintenance or standard purpose)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Crypto tests

Test Configuration:

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@shargon shargon requested a review from Jim8y December 18, 2024 12:06
@shargon shargon changed the base branch from master to HF_Echidna December 18, 2024 12:07
@shargon shargon marked this pull request as ready for review December 18, 2024 12:09
@shargon shargon changed the title Ec recover EcRecover Dec 18, 2024
case NamedCurveHash.secp256k1SHA256:
{
return Crypto.ECRecover(ECCurve.Secp256k1, signature, messageHash,
// TODO: only accept 65?

Choose a reason for hiding this comment

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

64bytes signatures MAY store a parity bit in the first bit of the s value which is always 0.
From what I deduce, you assume the parity value to be 0 if the size is 64 so checking the first bit of s will not change the current behavior if this is not a compressed signature (i.e. parity will always be 0) But in case it is a compressed ERC-2098 signature, it will be supported out-of-the-box

Note: this is predicated on the assumption that N3 contracts might need to verify signatures that originate in NEOX where EIP standards might apply (eg: someone migrates a platform written for Ethereum to NeoX)
for reference, this is the compressed signature standard I am referring to: https://eips.ethereum.org/EIPS/eip-2098

default: throw new InvalidOperationException("Invalid signature format");
}

// Validate values

Choose a reason for hiding this comment

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

on the topic of validation, maybe having the s value in the lower half of the domain would be a good guard against signature maleability. At least that's how that was addressed in ethereum where the wallets only generate (r,s) pairs with s lower than n/2 (where n is the order of the curve). Because a pair (r, n-s) is also a valid solution leading to possible issues.

src/Neo/SmartContract/Native/CryptoLib.cs Outdated Show resolved Hide resolved
}
}
}
catch { }
Copy link
Member

Choose a reason for hiding this comment

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

Several lines below we do throw new NotSupportedException(nameof(ch.Hasher)); if hashing algorythm is not supported. This exception should be properly thrown, so we don't need to catch it here.

@Jim8y
Copy link
Contributor

Jim8y commented Dec 19, 2024

Maybe we can actually have two methods:

==Specification==

===Native Contract Interface===

Two methods will be added to CryptoLib in HF_Echidna:

====Method 1: Recovery from Signature====

{
    "name": "secp256k1Recover",
    "safe": true,
    "parameters": [
        {
            "name": "message",
            "type": "ByteArray"
        },
        {
            "name": "hasher",
            "type": "Integer"
        },
        {
            "name": "signature",
            "type": "ByteArray"
        }
    ],
    "returntype": "ByteArray"
}

====Method 2: Recovery from Components====

{
    "name": "secp256k1Recover",
    "safe": true,
    "parameters": [
        {
            "name": "message",
            "type": "ByteArray"
        },
        {
            "name": "hasher",
            "type": "Integer"
        },
        {
            "name": "r",
            "type": "ByteArray"
        },
        {
            "name": "s",
            "type": "ByteArray"
        },
        {
            "name": "v",
            "type": "Integer"
        }
    ],
    "returntype": "ByteArray"
}

===Method Specification===

Both methods MUST follow these rules:

  1. Input Requirements for secp256k1Recover:

    • message: Original message bytes
    • hasher: Hash function ID
      • 1: SHA256
      • 2: RIPEMD160
      • Others: Reserved for future use
    • signature: 65 bytes (32 bytes r + 32 bytes s + 1 byte v)
  2. Input Requirements for secp256k1Recover (components):

    • message: Original message bytes
    • hasher: Hash function ID (same as above)
    • r: 32 bytes
    • s: 32 bytes
    • v: Recovery ID (27 or 28)
  3. Return Value (both methods):

    • Success: 33-byte compressed public key in SEC format
    • Failure: Returns null if:
      • Invalid signature/component length
      • Invalid recovery value (v)
      • Invalid signature format
      • Recovery failure

Comment on lines 159 to 160
if (r.SignValue < 0) throw new ArgumentException("r should be positive");
if (s.SignValue < 0) throw new ArgumentException("s should be positive");
Copy link
Contributor

Choose a reason for hiding this comment

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

https://github.com/Hecate2/ECrecover/blob/c3ec9b4ca4f74e66c426b48452f86cf643a9f6fb/Program.cs#L83-L84

        if (r.CompareTo(BigInteger.One) < 0 || r.CompareTo(curve.N) > 0) throw new ArgumentException($"Invalid r value {r}; expected [1, {curve.N}]");
        if (s.CompareTo(BigInteger.One) < 0 || s.CompareTo(curve.N) > 0) throw new ArgumentException($"Invalid s value {s}; expected [1, {curve.N}]");

Copy link
Contributor

Choose a reason for hiding this comment

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

Optionally we can require s < n / 2 if v is not specified, for Ethereum

@shargon
Copy link
Member Author

shargon commented Dec 19, 2024

Maybe we can actually have two methods:

==Specification==

===Native Contract Interface===

Two methods will be added to CryptoLib in HF_Echidna:

====Method 1: Recovery from Signature====

{
"name": "secp256k1Recover",
"safe": true,
"parameters": [
{
"name": "message",
"type": "ByteArray"
},
{
"name": "hasher",
"type": "Integer"
},
{
"name": "signature",
"type": "ByteArray"
}
],
"returntype": "ByteArray"
}
====Method 2: Recovery from Components====

{
"name": "secp256k1Recover",
"safe": true,
"parameters": [
{
"name": "message",
"type": "ByteArray"
},
{
"name": "hasher",
"type": "Integer"
},
{
"name": "r",
"type": "ByteArray"
},
{
"name": "s",
"type": "ByteArray"
},
{
"name": "v",
"type": "Integer"
}
],
"returntype": "ByteArray"
}
===Method Specification===

Both methods MUST follow these rules:

  1. Input Requirements for secp256k1Recover:

    • message: Original message bytes

    • hasher: Hash function ID

      • 1: SHA256
      • 2: RIPEMD160
      • Others: Reserved for future use
    • signature: 65 bytes (32 bytes r + 32 bytes s + 1 byte v)

  2. Input Requirements for secp256k1Recover (components):

    • message: Original message bytes
    • hasher: Hash function ID (same as above)
    • r: 32 bytes
    • s: 32 bytes
    • v: Recovery ID (27 or 28)
  3. Return Value (both methods):

    • Success: 33-byte compressed public key in SEC format

    • Failure: Returns null if:

      • Invalid signature/component length
      • Invalid recovery value (v)
      • Invalid signature format
      • Recovery failure

@Jim8y could you change it?

@Jim8y Jim8y mentioned this pull request Dec 19, 2024
11 tasks
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

src/Neo/Cryptography/Crypto.cs Outdated Show resolved Hide resolved
src/Neo/Cryptography/Crypto.cs Outdated Show resolved Hide resolved
src/Neo/Cryptography/Crypto.cs Outdated Show resolved Hide resolved
Co-authored-by: Christopher Schuchardt <[email protected]>
Jim8y and others added 2 commits December 29, 2024 22:03
Co-authored-by: Christopher Schuchardt <[email protected]>
Co-authored-by: Christopher Schuchardt <[email protected]>
@Jim8y
Copy link
Contributor

Jim8y commented Dec 30, 2024

@OT-kraftchain hi, in https://eips.ethereum.org/EIPS/eip-2098, there are two test cases, but we are not able to recover the correct pubkey, do you mind to take a look? For other test cases this pr works fine, but just can not pass the test cases provided by EIP-2098

@Hecate2
Copy link
Contributor

Hecate2 commented Dec 30, 2024

@OT-kraftchain hi, in https://eips.ethereum.org/EIPS/eip-2098, there are two test cases, but we are not able to recover the correct pubkey, do you mind to take a look? For other test cases this pr works fine, but just can not pass the test cases provided by EIP-2098

Neither the following codes are working in public void TestERC2098()

            Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToHexString()}");
            var message1 = Encoding.UTF8.GetBytes("Hello World");
            var messageHash1 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message\n{message1.Count()}")).Concat(message1.Keccak256()).ToArray().Keccak256();
            Console.WriteLine($"Message Hash: {Convert.ToHexString(messageHash1)}");

            // Signature values from EIP-2098 test case
            var r = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes();
            var s = "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes();
            var signature1 = new byte[65];
            Array.Copy(r, 0, signature1, 0, 32);
            Array.Copy(s, 0, signature1, 32, 32);
            signature1[64] = 27;

@OT-kraftchain
Copy link

@OT-kraftchain hi, in https://eips.ethereum.org/EIPS/eip-2098, there are two test cases, but we are not able to recover the correct pubkey, do you mind to take a look? For other test cases this pr works fine, but just can not pass the test cases provided by EIP-2098

Neither the following codes are working in public void TestERC2098()

            Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToHexString()}");
            var message1 = Encoding.UTF8.GetBytes("Hello World");
            var messageHash1 = new byte[] { 0x19 }.Concat(Encoding.UTF8.GetBytes($"Ethereum Signed Message\n{message1.Count()}")).Concat(message1.Keccak256()).ToArray().Keccak256();
            Console.WriteLine($"Message Hash: {Convert.ToHexString(messageHash1)}");

            // Signature values from EIP-2098 test case
            var r = "68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90".HexToBytes();
            var s = "7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064".HexToBytes();
            var signature1 = new byte[65];
            Array.Copy(r, 0, signature1, 0, 32);
            Array.Copy(s, 0, signature1, 32, 32);
            signature1[64] = 27;

Yeah, sure, I'll have a look asap

// Test from https://eips.ethereum.org/EIPS/eip-2098
var privateKey = "1234567890123456789012345678901234567890123456789012345678901234".HexToBytes();

var expectedPubKey1 = (Neo.Cryptography.ECC.ECCurve.Secp256k1.G * privateKey).ToArray();

Choose a reason for hiding this comment

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

Expected values
pubKeyX - 0xe90c7d3640a1568839c31b70a893ab6714ef8415b9de90cedfc1c8f353a6983e
pubKeyY - 0x625529392df7fa514bdd65a2003f6619567d79bee89830e63e932dbd42362d34

eth address for reference - 0x2e988A386a799F506693793c6A5AF6B54dfAaBfB


Console.WriteLine($"Expected PubKey: {expectedPubKey1.ToHexString()}");
var message1 = Encoding.UTF8.GetBytes("Hello World");
var messageHash1 = message1.Keccak256();

Choose a reason for hiding this comment

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

This is supposed to be an Ethereum signed message which is not explicitly stated in ERC-2098

function toEthSignedMessageHash(bytes memory message) internal pure returns (bytes32) {
        return
            keccak256(bytes.concat("\x19Ethereum Signed Message:\n", bytes(Strings.toString(message.length)), message));
}

Expected value: 0xa1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2

@Hecate2
Copy link
Contributor

Hecate2 commented Dec 31, 2024

Fixed EIP-2098 tests, but failed in tests creating wallet in neo-cli. Cannot recognize the reason.

@Jim8y
Copy link
Contributor

Jim8y commented Dec 31, 2024

final version:

==Specification==

===Native Contract Interface===

Three methods will be added to CryptoLib in HF_Echidna:

====Method 1: Recovery from Message and Signature====

{
    "name": "secp256k1Recover",
    "safe": true,
    "parameters": [
        {
            "name": "message",
            "type": "ByteArray"
        },
        {
            "name": "hasher",
            "type": "Integer"
        },
        {
            "name": "signature",
            "type": "ByteArray"
        }
    ],
    "returntype": "ByteArray"
}

====Method 2: Recovery from Message Components====

{
    "name": "secp256k1Recover",
    "safe": true,
    "parameters": [
        {
            "name": "message",
            "type": "ByteArray"
        },
        {
            "name": "hasher",
            "type": "Integer"
        },
        {
            "name": "r",
            "type": "ByteArray"
        },
        {
            "name": "s",
            "type": "ByteArray"
        },
        {
            "name": "v",
            "type": "Integer"
        }
    ],
    "returntype": "ByteArray"
}

====Method 3: Recovery from Pre-computed Hash====

{
    "name": "secp256k1Recover",
    "safe": true,
    "parameters": [
        {
            "name": "hash",
            "type": "ByteArray"
        },
        {
            "name": "signature",
            "type": "ByteArray"
        }
    ],
    "returntype": "ByteArray"
}

===Method Specification===

The methods MUST follow these rules:

  1. Input Requirements for Method 1 (secp256k1Recover with message):

    • message: Original message bytes
    • hasher: Hash function ID
      • 1: SHA256
      • 2: Keccak256
      • Others: Reserved for future use
    • signature: 65 bytes (32 bytes r + 32 bytes s + 1 byte v) or 64 bytes for EIP-2098 format
  2. Input Requirements for Method 2 (secp256k1Recover with components):

    • message: Original message bytes
    • hasher: Hash function ID (same as above)
    • r: 32 bytes
    • s: 32 bytes
    • v: Recovery ID (27 or 28)
  3. Input Requirements for Method 3 (secp256k1Recover with hash):

    • hash: 32-byte hash of the original message
    • signature: 65 bytes (32 bytes r + 32 bytes s + 1 byte v) or 64 bytes for EIP-2098 format
  4. Return Value (all methods):

    • Success: 33-byte compressed public key in SEC format
    • Failure: Returns null if:
      • Invalid signature/component length
      • Invalid recovery value (v)
      • Invalid signature format
      • Recovery failure
      • Null input parameters

@Hecate2
Copy link
Contributor

Hecate2 commented Dec 31, 2024

I think the name hasher can be confusing, between a function and a person who executes the hash. hashFunc can be better, but I do not mind leaving it as is.

@Jim8y
Copy link
Contributor

Jim8y commented Dec 31, 2024

I think the name hasher can be confusing, between a function and a person who executes the hash. hashFunc can be better, but I do not minding leaving it as is.

I remember the hasher name is from the builtin Hasher enum.

public enum Hasher : byte

@cschuchardt88
Copy link
Member

cschuchardt88 commented Dec 31, 2024

I think the name hasher can be confusing, between a function and a person who executes the hash. hashFunc can be better, but I do not minding leaving it as is.

I remember the hasher name is from the builtin Hasher enum.

public enum Hasher : byte

Well I told you all so, once again!!! 😃 When it comes to standards and naming conventions no one listens to me. You dont care -- I dont care. Maybe bad english is the problem.

@Jim8y
Copy link
Contributor

Jim8y commented Dec 31, 2024

I think the name hasher can be confusing, between a function and a person who executes the hash. hashFunc can be better, but I do not minding leaving it as is.

I remember the hasher name is from the builtin Hasher enum.

public enum Hasher : byte

Well I told you all so, once again!!! 😃 When it comes to standards and naming conventions no one listens to me. You dont care -- I dont care. Maybe bad english is the problem.

Bro, i would care for sure, but as you said, maybe english is the problem, but once you point it out and fix it in a pr, i promise you i will support you.

@Jim8y Jim8y mentioned this pull request Jan 1, 2025
14 tasks
@shargon
Copy link
Member Author

shargon commented Jan 2, 2025

We can change the name

I think the name hasher can be confusing, between a function and a person who executes the hash. hashFunc can be better, but I do not minding leaving it as is.

I remember the hasher name is from the builtin Hasher enum.

public enum Hasher : byte

Well I told you all so, once again!!! 😃 When it comes to standards and naming conventions no one listens to me. You dont care -- I dont care. Maybe bad english is the problem.

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

@Jim8y
Copy link
Contributor

Jim8y commented Jan 11, 2025

@OT-kraftchain coredev team prefers to keep only one method in the native contract, can you please pick one that works for you the best.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Recover Public Key from Signature for Secp256k1
6 participants