-
Notifications
You must be signed in to change notification settings - Fork 1k
EcRecover #3633
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
Changes from 26 commits
bcc5fd0
e63f10b
9d16c53
f746f8d
8d7f9e8
3fc8077
620d938
7e31d0c
993e3fe
37bf0cb
7dba130
0457ccd
d7a291e
6e780c0
02d6ab9
8fb30df
f9244eb
650c9e9
331541a
eb96d14
74498e5
577c431
1c82ed9
b81b127
a3a6fd5
9e28008
73d90c9
2ac18ea
c086661
0f1507f
f91b680
705f4bb
498ed91
81bd7f6
08c3be6
59fe8b5
a784c41
e2acc64
216c39e
2f98dae
6658ad6
b2bbaba
e3673e8
fc9aab8
092d132
548bd05
0782042
b594d1b
3365319
fe28919
409c3b5
6c60137
8470de5
6fc94aa
93fc37b
fb28f36
c60ba6f
7b22712
9112507
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,10 +10,13 @@ | |
| // modifications are permitted. | ||
|
|
||
| using Neo.IO.Caching; | ||
| 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.Runtime.InteropServices; | ||
| using System.Security.Cryptography; | ||
|
|
||
|
|
@@ -24,21 +27,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 RecuperableSignatureLength = 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. | ||
|
|
@@ -72,13 +72,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() : | ||
|
|
@@ -112,6 +108,107 @@ public static byte[] Sign(byte[] message, byte[] priKey, ECC.ECCurve ecCurve = n | |
| return ecdsa.SignData(message, hashAlg); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// ECRecover | ||
| /// </summary> | ||
| /// <param name="curve">Curve</param> | ||
| /// <param name="signature">Signature</param> | ||
| /// <param name="hash">Message hash</param> | ||
| /// <param name="format">Signature format</param> | ||
| /// <returns>Allowed Public keys</returns> | ||
| public static ECC.ECPoint[] ECRecover(ECC.ECCurve curve, byte[] signature, byte[] hash, SignatureFormat format = SignatureFormat.Der) | ||
| { | ||
| BigInteger r, s; | ||
| int recId = 0, recIdTo = 4; | ||
|
|
||
| // Decode signature | ||
|
|
||
| switch (format) | ||
| { | ||
| case SignatureFormat.Der: | ||
| { | ||
| var derSequence = (DerSequence)Asn1Object.FromByteArray(signature); | ||
| r = ((DerInteger)derSequence[0]).Value; | ||
| s = ((DerInteger)derSequence[1]).Value; | ||
|
|
||
| if (derSequence.Count == 3) | ||
| { | ||
| recId = ((DerInteger)derSequence[2]).IntValueExact; | ||
| recIdTo = recId + 1; | ||
| } | ||
| break; | ||
| } | ||
| case SignatureFormat.Fixed32: | ||
| { | ||
| r = new(1, signature, 0, 32); | ||
| s = new(1, signature, 32, 32); | ||
|
|
||
| if (signature.Length == RecuperableSignatureLength) | ||
| { | ||
| recId = signature[SignatureLength]; | ||
| recIdTo = recId + 1; | ||
| } | ||
| break; | ||
| } | ||
| default: throw new InvalidOperationException("Invalid signature format"); | ||
| } | ||
|
|
||
| // Validate values | ||
|
|
||
| if (recId < 0 || recId >= 4) throw new ArgumentException("v should be positive less than 4"); | ||
| if (r.SignValue < 0) throw new ArgumentException("r should be positive"); | ||
| if (s.SignValue < 0) throw new ArgumentException("s should be positive"); | ||
|
||
|
|
||
| // Precompute variables | ||
|
|
||
| var n = curve.BouncyCastleCurve.N; | ||
| var e = new BigInteger(1, hash); | ||
| var eInv = BigInteger.Zero.Subtract(e).Mod(n); | ||
| var rInv = r.ModInverse(n); | ||
| var srInv = rInv.Multiply(s).Mod(n); | ||
| var eInvrInv = rInv.Multiply(eInv).Mod(n); | ||
|
|
||
| // Do the work | ||
|
|
||
| var recovered = new List<ECC.ECPoint>(); | ||
|
|
||
| for (; recId < recIdTo; ++recId) | ||
| { | ||
| var i = BigInteger.ValueOf((long)recId / 2); | ||
| var x = r.Add(i.Multiply(n)); | ||
|
|
||
| if (x.CompareTo(s_prime) >= 0) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var decompressedRKey = DecompressKey(curve.BouncyCastleCurve.Curve, x, (recId & 1) == 1); | ||
| if (!decompressedRKey.Multiply(n).IsInfinity) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var q = Org.BouncyCastle.Math.EC.ECAlgorithms.SumOfTwoMultiplies(curve.BouncyCastleCurve.G, eInvrInv, decompressedRKey, srInv); | ||
| recovered.Add(ECC.ECPoint.FromBytes(q.Normalize().GetEncoded(false), curve)); | ||
| } | ||
|
|
||
| return [.. recovered]; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Decompress key | ||
| /// </summary> | ||
| /// <param name="curve">ECC curve</param> | ||
| /// <param name="xBN">xBN</param> | ||
| /// <param name="yBit">yBit</param> | ||
| /// <returns>ECPoint</returns> | ||
| 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> | ||
|
|
@@ -126,18 +223,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); | ||
|
|
||
|
|
||
| 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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ public enum Hardfork : byte | |
| HF_Aspidochelone, | ||
| HF_Basilisk, | ||
| HF_Cockatrice, | ||
| HF_Domovoi | ||
| HF_Domovoi, | ||
| HF_Echidna | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -77,6 +77,49 @@ public static byte[] Keccak256(byte[] data) | |
| return data.Keccak256(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. | ||
| /// </summary> | ||
| /// <param name="message">The signed message.</param> | ||
| /// <param name="signature">The signature to be verified.</param> | ||
| /// <param name="curveHash">A pair of the curve to be used by the ECDSA algorithm and the hasher function to be used to hash message.</param> | ||
| /// <returns><see langword="true"/> if the signature is valid; otherwise, <see langword="false"/>.</returns> | ||
| [ContractMethod(Hardfork.HF_Aspidochelone, CpuFee = 1 << 10)] | ||
| public static ECPoint[] ECrecover(byte[] message, byte[] signature, NamedCurveHash curveHash) | ||
shargon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| try | ||
| { | ||
| var ch = s_curves[curveHash]; | ||
| var messageHash = | ||
| ch.Hasher == Hasher.SHA256 ? message.Sha256() : | ||
| ch.Hasher == Hasher.Keccak256 ? message.Keccak256() : | ||
| throw new NotSupportedException(nameof(ch.Hasher)); | ||
|
|
||
| switch (curveHash) | ||
| { | ||
| case NamedCurveHash.secp256k1Keccak256: | ||
| case NamedCurveHash.secp256k1SHA256: | ||
| { | ||
| return Crypto.ECRecover(ECCurve.Secp256k1, signature, messageHash, | ||
| // TODO: only accept 65? | ||
|
||
| signature.Length == 64 ? SignatureFormat.Fixed32 : SignatureFormat.Der); | ||
| } | ||
|
|
||
| // TODO: Not tested, check if it works | ||
| case NamedCurveHash.secp256r1Keccak256: | ||
| case NamedCurveHash.secp256r1SHA256: | ||
shargon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| return Crypto.ECRecover(ECCurve.Secp256r1, signature, messageHash, | ||
| // TODO: only accept 65? | ||
| signature.Length == 64 ? SignatureFormat.Fixed32 : SignatureFormat.Der); | ||
| } | ||
| } | ||
| } | ||
| catch { } | ||
|
||
|
|
||
| throw new NotSupportedException(nameof(curveHash)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Verifies that a digital signature is appropriate for the provided key and message using the ECDSA algorithm. | ||
| /// </summary> | ||
|
|
||
There was a problem hiding this comment.
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.