From 350055be9a7d730584fcda74fcdb9b169c0d0270 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 14:30:55 +0100 Subject: [PATCH 01/16] [ecdsa] pull message hashing out of `impl` procs Done so that future public key recovery can simply call `verifyImpl` to verify public key is found (signImpl changed to match). --- constantine/signatures/ecdsa.nim | 57 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 088c8845c..cf0e121fa 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -176,7 +176,7 @@ proc generateNonce[Name: static Algebra]( proc signImpl[Name: static Algebra; Sig]( sig: var Sig, secretKey: Fr[Name], - message: openArray[byte], + msgHash: Fr[Name], H: type CryptoHash, nonceSampler: NonceSampler = nsRandom) = ## Sign a given `message` using the `secretKey`. @@ -185,20 +185,13 @@ proc signImpl[Name: static Algebra; Sig]( ## but passing `nonceSampler = nsRfc6979` uses RFC 6979 to compute ## a deterministic nonce (and thus deterministic signature) given ## the message and private key as base. - # 1. hash the message in big endian order - var dgst {.noinit.}: array[H.digestSize, byte] - H.hash(dgst, message) - var message_hash: Fr[Name] - # if `dgst` uses more bytes than - message_hash.fromDigest(dgst, truncateInput = true) - # Generator of the curve const G = Name.getGenerator($G1) # loop until we found a valid (non zero) signature while true: # Generate random nonce - var k = generateNonce(nonceSampler, message_hash, secretKey, H) + var k = generateNonce(nonceSampler, msgHash, secretKey, H) var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] # Calculate r (x-coordinate of kG) @@ -217,9 +210,9 @@ proc signImpl[Name: static Algebra; Sig]( # we'd need to truncate to N bits for N being bits in modulo `n`) var s {.noinit.}: Fr[Name] s.prod(r, secretKey) # `r * secretKey` - s += message_hash # `message_hash + r * secretKey` - k.inv() # `k := k⁻¹` - s *= k # `k⁻¹ * (message_hash + r * secretKey)` + s += msgHash # `msgHash + r * secretKey` + k.inv() # `k := k⁻¹` + s *= k # `k⁻¹ * (msgHash + r * secretKey)` # get inversion of `s` for 'lower-s normalization' var sneg = s # inversion of `s` sneg.neg() # q - s @@ -254,33 +247,33 @@ proc coreSign*[Sig, SecKey]( ## indifferentiable from a random oracle [MRH04] under a reasonable ## cryptographic assumption. ## - `message` is the message to hash - signature.signImpl(secretKey, message, H, nonceSampler) + # 1. hash the message in big endian order + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash: Fr[SecKey.Name] + # if `dgst` uses more bits than scalar in `Fr`, truncate + msgHash.fromDigest(dgst, truncateInput = true) + # 2. sign + signature.signImpl(secretKey, msgHash, H, nonceSampler) proc verifyImpl[Name: static Algebra; Sig]( publicKey: EC_ShortW_Aff[Fp[Name], G1], - signature: Sig, # tuple[r, s: Fr[Name]], - message: openArray[byte], - H: type CryptoHash, + signature: Sig, + msgHash: Fr[Name] ): bool = ## Verify a given `signature` for a `message` using the given `publicKey`. - # 1. Hash the message (same as in signing) - var dgst {.noinit.}: array[H.digestSize, byte] - H.hash(dgst, message) - var e {.noinit.}: Fr[Name] - e.fromDigest(dgst, truncateInput = true) - - # 2. Compute w = s⁻¹ + # 1. Compute w = s⁻¹ var w = signature.s w.inv() # w = s⁻¹ - # 3. Compute u₁ = ew and u₂ = rw + # 2. Compute u₁ = ew and u₂ = rw var u1 {.noinit.}: Fr[Name] u2 {.noinit.}: Fr[Name] - u1.prod(e, w) + u1.prod(msgHash, w) u2.prod(signature.r, w) - # 4. Compute u₁G + u₂Q + # 3. Compute u₁G + u₂Q var point1 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] point2 {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] @@ -291,11 +284,11 @@ proc verifyImpl[Name: static Algebra; Sig]( var R {.noinit.}: EC_ShortW_Jac[Fp[Name], G1] R.sum(point1, point2) - # 5. Get x coordinate (in `Fp`) and convert to `Fr` (like in signing) + # 4. Get x coordinate (in `Fp`) and convert to `Fr` (like in signing) let x = R.getAffine().x let r_computed = Fr[Name].fromBig(x.toBig()) - # 6. Verify r_computed equals provided r + # 5. Verify r_computed equals provided r result = bool(r_computed == signature.r) func coreVerify*[Pubkey, Sig]( @@ -308,4 +301,10 @@ func coreVerify*[Pubkey, Sig]( ## This assumes that the PublicKey and Signatures ## have been pre-checked for non-infinity and being in the correct subgroup ## (likely on deserialization) - result = pubKey.verifyImpl(signature, message, H) + # 1. Hash the message (same as in signing) + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash {.noinit.}: Fr[pubkey.F.Name] + msgHash.fromDigest(dgst, truncateInput = true) + # 2. verify + result = pubKey.verifyImpl(signature, msgHash) From 7439be6bf6d203e8f2352212e50473212eb4e162 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 14:32:26 +0100 Subject: [PATCH 02/16] [ecdsa] implement public key recovery --- constantine/ecdsa_secp256k1.nim | 9 +++ constantine/signatures/ecdsa.nim | 98 ++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/ecdsa_secp256k1.nim index bc28b188a..6216d058b 100644 --- a/constantine/ecdsa_secp256k1.nim +++ b/constantine/ecdsa_secp256k1.nim @@ -66,3 +66,12 @@ func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefi ## ## The secret_key MUST be validated public_key.raw.derivePubkey(secret_key.raw) + +proc recoverPubkey*( + publicKey: var PublicKey, + message: openArray[byte], + signature: Signature, + evenY: bool +) {.libPrefix: prefix_ffi, genCharAPI.} = + ## Verify `signature` using `publicKey` for `message`. + publicKey.raw.recoverPubkey(signature, message, evenY, sha256) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index cf0e121fa..792bd3257 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -308,3 +308,101 @@ func coreVerify*[Pubkey, Sig]( msgHash.fromDigest(dgst, truncateInput = true) # 2. verify result = pubKey.verifyImpl(signature, msgHash) +proc recoverPubkeyImpl_vartime[Name: static Algebra; Sig]( + recovered: var EC_ShortW_Aff[Fp[Name], G1], + signature: Sig, + msgHash: Fr[Name], + evenY: bool) = + ## Attempts to recover an associated public key to the given `signature` and + ## hash of a message `msgHash`. + ## + ## Note that as the signature is only dependent on the `x` coordinate of the + ## curve point `R`, two public keys verify the signature. The one with even + ## and the one with odd `y` coordinate (one even & one odd due to curve prime + ## order). + ## + ## `evenY` decides whether we recover the public key associated with the even + ## `y` coordinate of `R` or the odd one. Both verify the (message, signature) + ## pair. + ## + ## If the signature is invalid, `recovered` will be set to the neutral element. + type + ECAff = EC_ShortW_Aff[Fp[Name], G1] + ECJac = EC_ShortW_Jac[Fp[Name], G1] + # 1. Set to neutral so if we don't find a valid signature, return neutral + recovered.setNeutral() + const G = Name.getGenerator($G1) + + let rInit = signature.r.toBig() # initial `r` + var x1 = Fp[Name].fromBig(signature.r.toBig()) # as coordinate in Fp + let M = Fp[Name].fromBig(Fr[Name].getModulus()) + + # Due to the conversion of the `x` coordinate in `Fp` of the point `R` in the signing process + # to a scalar in `Fr`, we potentially reduce it modulo the subgroup order (if `x > M` with + # `M` the subgroup order). + # As we don't know if this is the case, we need to loop until we either find a valid signature, + # adding `M` each iteration or until we roll over again, in which case the signature is invalid. + # NOTE: For secp256k1 this is _extremely_ unlikely, because prime of the curve `p` and subgroup + # order `M` are so close! + var validSig = false + while (not validSig) and bool(x1.toBig() <= rInit): + # 1. Get base `R` point + var R {.noinit.}: EC_ShortW_Aff[Fp[Name], G1] + let valid = R.trySetFromCoordX(x1) # from `r = x1` + if not bool(valid): + x1 += M # add modulus of `Fr`. As long as we don't overflow in `Fp` we try again + continue # try next `i` in `x1 = r + i·M` + + let isEven = R.y.toBig().isEven() + # 2. only negate `y ↦ -y` if current and target even-ness disagree + R.y.cneg(isEven xor SecretBool evenY) + + # 3. perform recovery calculation, `Q = -m·r⁻¹ * G + s·r⁻¹ * R` + # Note: Calculate with `r⁻¹` included in each coefficient to avoid 3rd `scalarMul`. + var rInv = signature.r + rInv.inv() # `r⁻¹` + + var u1 {.noinit.}, u2 {.noinit.}: Fr[Name] + u1.prod(msgHash, rInv) # `u₁ = m·r⁻¹` + u1.neg() # `u₁ = -m·r⁻¹` + u2.prod(signature.s, rInv) # `u₂ = s·r⁻¹` + + var Q {.noinit.}: ECJac # the potential public key + var point1 {.noinit.}, point2 {.noinit.}: ECJac + point1.scalarMul(u1, G) # `p₁ = u₁ * G` + point2.scalarMul(u2, R) # `p₂ = u₂ * R` + Q.sum(point1, point2) # `Q = p₁ + p₂` + + # 4. Verify signature with this point + validSig = Q.getAffine().verifyImpl(signature, msgHash) + + # 5. If valid copy to `recovered`, else keep neutral point + recovered.ccopy(Q.getAffine(), SecretBool validSig) # Copy `Q` if valid + # 6. try next `i` in `x1 = r + i·M` + x1 += M +proc recoverPubkey*[Pubkey; Sig]( + recovered: var Pubkey, + signature: Sig, + message: openArray[byte], + evenY: bool, + H: type CryptoHash) = + ## Attempts to recover an associated public key to the given `signature` and + ## hash of a message `msgHash`. + ## + ## Note that as the signature is only dependent on the `x` coordinate of the + ## curve point `R`, two public keys verify the signature. The one with even + ## and the one with odd `y` coordinate (one even & one odd due to curve prime + ## order). + ## + ## `evenY` decides whether we recover the public key associated with the even + ## `y` coordinate of `R` or the odd one. Both verify the (message, signature) + ## pair. + ## + ## If the signature is invalid, `recovered` will be set to the neutral element. + # 1. Hash the message (same as in signing) + var dgst {.noinit.}: array[H.digestSize, byte] + H.hash(dgst, message) + var msgHash {.noinit.}: Fr[recovered.F.Name] + msgHash.fromDigest(dgst, truncateInput = true) + # 2. recover + recovered.recoverPubkeyImpl_vartime(signature, msgHash, evenY) From 350f0935fc77378bd00b3c6cbcb478d35c0ef5dd Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 14:32:56 +0100 Subject: [PATCH 03/16] [tests] add test case to recover public key from sig&msgHash --- tests/ecdsa/t_ecdsa_verify_openssl.nim | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/tests/ecdsa/t_ecdsa_verify_openssl.nim b/tests/ecdsa/t_ecdsa_verify_openssl.nim index dac80a50f..51bfeec7f 100644 --- a/tests/ecdsa/t_ecdsa_verify_openssl.nim +++ b/tests/ecdsa/t_ecdsa_verify_openssl.nim @@ -66,7 +66,7 @@ func getPublicKey(secKey: SecretKey): PublicKey {.noinit.} = template toOA(x: string): untyped = toOpenArrayByte(x, 0, x.len-1) when not defined(windows) and not defined(macosx): # see above - proc signAndVerify(num: int, msg = "", nonceSampler = nsRandom) = + proc signVerifyRecover(num: int, msg = "", nonceSampler = nsRandom) = ## Generates `num` signatures and verify them against OpenSSL. ## ## If `msg` is given, use a fixed message. Otherwise will generate a message with @@ -111,6 +111,22 @@ when not defined(windows) and not defined(macosx): # see above derSig.toDER(rOslFr, sOslFr) check derSig.data == osSig + # Attempt to recover public key from signature and hash. Two possible public keys + # verify the signature. Only one of them is the public key we actually derived from + # our private key. So recover both, check they verify the signature and one of them + # is equal to our initial public key. + var recEven {.noinit.}: PublicKey + var recOdd {.noinit.}: PublicKey + recEven.recoverPubkey(toOA msg, sigCTT, evenY = true) + recOdd.recoverPubkey(toOA msg, sigCTT, evenY = false) + + # Check both verify signature + check recEven.verify(toOA msg, sigCTT) + check recOdd.verify(toOA msg, sigCTT) + + # Check one of them is equal to our initial pubkey + check pubkeys_are_equal(recEven, pubKey) or pubkeys_are_equal(recOdd, pubKey) + proc verifyPemWriter(num: int, msg = "") = ## We verify our PEM writers in a bit of a roundabout way. ## @@ -148,7 +164,6 @@ when not defined(windows) and not defined(macosx): # see above ## using deterministic nonce generation and verifies the signature comes out ## identical each time. var derSig: DerSignature[DerSigSize(Secp256k1)] - let secKey = generatePrivateKey() var sig {.noinit.}: Signature sig.sign(secKey, toOA msg, nonceSampler = nsRfc6979) @@ -171,10 +186,10 @@ when not defined(windows) and not defined(macosx): # see above suite "ECDSA over secp256k1": test "Verify OpenSSL generated signatures from a fixed message (different nonces)": - signAndVerify(100, "Hello, Constantine!") # fixed message + signVerifyRecover(100, "Hello, Constantine!") # fixed message test "Verify OpenSSL generated signatures for different messages": - signAndVerify(100) # randomly generated message + signVerifyRecover(100) # randomly generated message test "Verify deterministic nonce generation via RFC6979 yields deterministic signatures": signRfc6979("Hello, Constantine!") From 9053b21fdc53f30e9318902ab4a645b6764ad7ac Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 20:32:40 +0100 Subject: [PATCH 04/16] [ecdsa] allow customizing the hash function to be used ECDSA over secp256k1 commonly uses both SHA256 (e.g. Bitcoin) and Keccak256 (e.g. Ethereum). Other combinations may also exist. We default to SHA256 for the time being. --- constantine/ecdsa_secp256k1.nim | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/ecdsa_secp256k1.nim index 6216d058b..ca7420787 100644 --- a/constantine/ecdsa_secp256k1.nim +++ b/constantine/ecdsa_secp256k1.nim @@ -9,7 +9,7 @@ import constantine/zoo_exports, constantine/signatures/ecdsa, - constantine/hashes/h_sha256, + constantine/hashes, constantine/named/algebras, constantine/math/elliptic/[ec_shortweierstrass_affine], constantine/math/[arithmetic, ec_shortweierstrass], @@ -47,19 +47,21 @@ func signatures_are_equal*(a, b: Signature): bool {.libPrefix: prefix_ffi.} = proc sign*(sig: var Signature, secretKey: SecretKey, message: openArray[byte], - nonceSampler: NonceSampler = nsRandom) {.libPrefix: prefix_ffi, genCharAPI.} = + nonceSampler: NonceSampler = nsRandom, + H: type CryptoHash = sha256) {.libPrefix: prefix_ffi, genCharAPI.} = ## Sign `message` using `secretKey` and store the signature in `sig`. The nonce ## will either be randomly sampled `nsRandom` or deterministically calculated according ## to RFC6979 (`nsRfc6979`) - sig.coreSign(secretKey.raw, message, sha256, nonceSampler) + sig.coreSign(secretKey.raw, message, H, nonceSampler) proc verify*( publicKey: PublicKey, message: openArray[byte], - signature: Signature + signature: Signature, + H: type CryptoHash = sha256 ): bool {.libPrefix: prefix_ffi, genCharAPI.} = ## Verify `signature` using `publicKey` for `message`. - result = publicKey.raw.coreVerify(message, signature, sha256) + result = publicKey.raw.coreVerify(message, signature, H) func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefix: prefix_ffi.} = ## Derive the public key matching with a secret key @@ -71,7 +73,11 @@ proc recoverPubkey*( publicKey: var PublicKey, message: openArray[byte], signature: Signature, - evenY: bool + evenY: bool, + H: type CryptoHash = sha256 ) {.libPrefix: prefix_ffi, genCharAPI.} = ## Verify `signature` using `publicKey` for `message`. - publicKey.raw.recoverPubkey(signature, message, evenY, sha256) + ## + ## `evenY == true` returns the public key corresponding to the + ## even `y` coordinate of the `R` point. + publicKey.raw.recoverPubkey(signature, message, evenY, H) From 1d439a4ef53978cc25c34877c2741e4d5a02a19a Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 20:33:48 +0100 Subject: [PATCH 05/16] [ecdsa] add `recoverPubkey` which directly takes a hash digest as scalar ECRecover provides the message hash and not the message. We need an API to pass that directly to the internal ECDSA procedure. We export the impl `vartime` routine for that purpose. We could alternatively also import that file using `{.all.}`. --- constantine/ecdsa_secp256k1.nim | 18 ++++++++++++++++++ constantine/signatures/ecdsa.nim | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/ecdsa_secp256k1.nim index ca7420787..f26ee741d 100644 --- a/constantine/ecdsa_secp256k1.nim +++ b/constantine/ecdsa_secp256k1.nim @@ -81,3 +81,21 @@ proc recoverPubkey*( ## `evenY == true` returns the public key corresponding to the ## even `y` coordinate of the `R` point. publicKey.raw.recoverPubkey(signature, message, evenY, H) + +proc recoverPubkey*( + publicKey: var PublicKey, + msgHash: Fr[Secp256k1], + signature: Signature, + evenY: bool +) {.libPrefix: prefix_ffi.} = + ## Verify `signature` using `publicKey` for the given message digest + ## given as a scalar in the field `Fr[Secp256k1]`. + ## + ## `evenY == true` returns the public key corresponding to the + ## even `y` coordinate of the `R` point. + ## + ## As this overload works directly with a message hash as a scalar, + ## it requires no hash function. Internally, it also calls the + ## `verify` implementation, which already takes a scalar and thus + ## requires no hash function there either. + publicKey.raw.recoverPubkeyImpl_vartime(signature, msgHash, evenY) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 792bd3257..7176d8840 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -308,7 +308,8 @@ func coreVerify*[Pubkey, Sig]( msgHash.fromDigest(dgst, truncateInput = true) # 2. verify result = pubKey.verifyImpl(signature, msgHash) -proc recoverPubkeyImpl_vartime[Name: static Algebra; Sig]( + +proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( recovered: var EC_ShortW_Aff[Fp[Name], G1], signature: Sig, msgHash: Fr[Name], From a217b273f1f9c7c6f7bc8b7984c4869a3aa58e24 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 20:36:27 +0100 Subject: [PATCH 06/16] [precompiles] add ECRecover Ethereum precompile We extend the CttEVMStatus enum by two further elements. One for an invalid signature in ECRecover and another for an invalid `v` value. --- constantine/ethereum_evm_precompiles.nim | 81 +++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/constantine/ethereum_evm_precompiles.nim b/constantine/ethereum_evm_precompiles.nim index 852c6c325..f06fc4b61 100644 --- a/constantine/ethereum_evm_precompiles.nim +++ b/constantine/ethereum_evm_precompiles.nim @@ -22,7 +22,9 @@ import ./hash_to_curve/hash_to_curve, # For KZG point precompile ./ethereum_eip4844_kzg, - ./serialization/codecs_status_codes + ./serialization/codecs_status_codes, + # ECDSA for ECRecover + ./ecdsa_secp256k1 # For KZG point precompile export EthereumKZGContext, TrustedSetupFormat, TrustedSetupStatus, trusted_setup_load, trusted_setup_delete @@ -48,6 +50,8 @@ type cttEVM_PointNotOnCurve cttEVM_PointNotInSubgroup cttEVM_VerificationFailure + cttEVM_InvalidSignature + cttEVM_InvalidV # `v` of signature `(r, s, v)` is invalid func eth_evm_sha256*(r: var openArray[byte], inputs: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} = ## SHA256 @@ -1276,3 +1280,78 @@ func eth_evm_kzg_point_evaluation*(ctx: ptr EthereumKZGContext, r.toOpenArray(32, 64-1).marshal(Fr[BLS12_381].getModulus(), bigEndian) result = cttEVM_Success + +import std / importutils # Alternatively make `r`, `s` visible or define setter or constructor +func eth_evm_ecrecover*(r: var openArray[byte], + input: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} = + ## Attempts to recover the public key, which was used to sign the given `data` + ## to obtain the given signature `sig`. + ## + ## If the signature is invalid, the result array `r` will contain the neutral + ## element of the curve. + ## + ## Inputs: + ## - `r`: Array of the recovered public key. An elliptic curve point in affine + ## coordinates (`EC_ShortW_Aff[Fp[Secp256k1], G1]`). + ## - `input`: The input data as an array of 128 bytes. The data is as follows: + ## - 32 byte: `keccak256` digest of the message that was signed + ## - 32 byte: `v`, decides if the even or odd coordinate in `R` was used + ## - 32 byte: `r` of the signature, scalar `Fr[Secp256k1]` + ## - 32 byte: `s` of the signature, scalar `Fr[Secp256k1]` + ## + ## Implementation follows Geth here: + ## https://github.com/ethereum/go-ethereum/blob/341647f1865dab437a690dc1424ba71495de2dd8/core/vm/contracts.go#L243-L272 + ## + ## and to a lesser extent the Ethereum Yellow Paper in appendix F: + ## https://ethereum.github.io/yellowpaper/paper.pdf + ## + ## Internal Geth implementation in: + ## https://github.com/ethereum/go-ethereum/blob/master/signer/core/signed_data.go#L292-L319 + if len(input) != 128: + return cttEVM_InvalidInputSize + + if len(r) != 32: + return cttEVM_InvalidOutputSize + + # 1. construct message hash as scalar in field `Fr[Secp256k1]` + var msgBI {.noinit.}: BigInt[256] + msgBI.unmarshal(input.toOpenArray(0, 32-1), bigEndian) + var msgHash {.noinit.}: Fr[Secp256k1] + msgHash.fromBig(msgBI) + + # 2. verify `v` data is valid + ## XXX: Or construct a `BigInt[256]` instead and compare? (or compare with uint64s?) + for i in 32 ..< 63: # first 31 bytes must be zero for a valid `v` + if input[i] != byte 0: + return cttEVM_InvalidV + let v = input[63] + if v notin [byte 0, 1, 27, 28]: + return cttEVM_InvalidSignature + # 2a. determine if even or odd `y` coordinate + let evenY = v in [byte 0, 27] # 0 / 27 indicates `y` to be even, 1 / 28 odd + + # 3. unmarshal signature data + var signature {.noinit.}: Signature + privateAccess(Signature) + var rSig {.noinit}, sSig {.noinit.}: BigInt[256] + rSig.unmarshal(input.toOpenArray(64, 96-1), bigEndian) + sSig.unmarshal(input.toOpenArray(96, 128-1), bigEndian) + signature.r = Fr[Secp256k1].fromBig(rSig) + signature.s = Fr[Secp256k1].fromBig(sSig) + + # 4. perform pubkey recovery + var pubKey {.noinit.}: PublicKey + pubKey.recoverPubkey(msgHash, signature, evenY) # , keccak256) + + # 4. now calculate the Ethereum address of the public key (keccak256) + privateAccess(PublicKey) + var rawPubkey {.noinit.}: array[64, byte] # `[x, y]` coordinates of public key + rawPubkey.toOpenArray( 0, 32-1).marshal(pubKey.raw.x, bigEndian) + rawPubkey.toOpenArray(32, 64-1).marshal(pubKey.raw.y, bigEndian) + var dgst {.noinit.}: array[32, byte] # keccak256 digest + keccak256.hash(dgst, rawPubkey) + + # 5. and effectively truncate to last 20 bytes of digest + r.rawCopy(12, dgst, 12, 20) + + result = cttEVM_Success From 5c446c245e6a0eda617b60290407f12d432490e8 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Mon, 30 Dec 2024 20:37:25 +0100 Subject: [PATCH 07/16] [tests] add test case for ECRecover --- constantine/signatures/ecdsa.nim | 1 + tests/t_ethereum_evm_precompiles.nim | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 7176d8840..1575d27cc 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -381,6 +381,7 @@ proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( recovered.ccopy(Q.getAffine(), SecretBool validSig) # Copy `Q` if valid # 6. try next `i` in `x1 = r + i·M` x1 += M + proc recoverPubkey*[Pubkey; Sig]( recovered: var Pubkey, signature: Sig, diff --git a/tests/t_ethereum_evm_precompiles.nim b/tests/t_ethereum_evm_precompiles.nim index 69b0c642d..b0d873036 100644 --- a/tests/t_ethereum_evm_precompiles.nim +++ b/tests/t_ethereum_evm_precompiles.nim @@ -82,7 +82,9 @@ template runPrecompileTests(filename: string, funcname: untyped, outsize: int, n else: let status = funcname(r, inputbytes) if status != cttEVM_Success: - doAssert test.ExpectedError.len > 0, "[Test Failure]\n" & + # `ecRecover.json` has failing test vectors where no `ExpectedError` exists, but the + # `Expected` output is simply empty. + doAssert test.ExpectedError.len > 0 or test.Expected.len == 0, "[Test Failure]\n" & " " & test.Name & "\n" & " " & funcname.astToStr & "\n" & " " & "Nim proc returned failure, but test expected to pass.\n" & @@ -163,3 +165,5 @@ runPrecompileTests("eip-2537/map_fp2_to_G2_bls.json", eth_evm_bls12381_map_fp2_t runPrecompileTests("eip-2537/fail-map_fp2_to_G2_bls.json", eth_evm_bls12381_map_fp2_to_g2, 256) runPrecompileTests("eip-4844/pointEvaluation.json", eth_evm_kzg_point_evaluation, 64, true) + +runPrecompileTests("ecRecover.json", eth_evm_ecrecover, 32) From 28769f4a67416edd08a7a93de3e23d96b165da3a Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Jan 2025 18:38:35 +0100 Subject: [PATCH 08/16] Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy --- constantine/signatures/ecdsa.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 1575d27cc..308208790 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -339,7 +339,7 @@ proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( let M = Fp[Name].fromBig(Fr[Name].getModulus()) # Due to the conversion of the `x` coordinate in `Fp` of the point `R` in the signing process - # to a scalar in `Fr`, we potentially reduce it modulo the subgroup order (if `x > M` with + # to a scalar in `Fr`, we potentially reduce it modulo the curve order (if `x >= r` with # `M` the subgroup order). # As we don't know if this is the case, we need to loop until we either find a valid signature, # adding `M` each iteration or until we roll over again, in which case the signature is invalid. From 3ca04a93f3ebe040186d3f15e123a0677230fb91 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Jan 2025 18:40:29 +0100 Subject: [PATCH 09/16] Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy --- constantine/signatures/ecdsa.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index 308208790..c32626475 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -340,7 +340,7 @@ proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( # Due to the conversion of the `x` coordinate in `Fp` of the point `R` in the signing process # to a scalar in `Fr`, we potentially reduce it modulo the curve order (if `x >= r` with - # `M` the subgroup order). + # `r` the curve order). # As we don't know if this is the case, we need to loop until we either find a valid signature, # adding `M` each iteration or until we roll over again, in which case the signature is invalid. # NOTE: For secp256k1 this is _extremely_ unlikely, because prime of the curve `p` and subgroup From d75f96e1d5154e7220119c2b5551318780b7eb64 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 4 Jan 2025 18:40:36 +0100 Subject: [PATCH 10/16] Update constantine/signatures/ecdsa.nim Co-authored-by: Mamy Ratsimbazafy --- constantine/signatures/ecdsa.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constantine/signatures/ecdsa.nim b/constantine/signatures/ecdsa.nim index c32626475..6f3243023 100644 --- a/constantine/signatures/ecdsa.nim +++ b/constantine/signatures/ecdsa.nim @@ -344,7 +344,7 @@ proc recoverPubkeyImpl_vartime*[Name: static Algebra; Sig]( # As we don't know if this is the case, we need to loop until we either find a valid signature, # adding `M` each iteration or until we roll over again, in which case the signature is invalid. # NOTE: For secp256k1 this is _extremely_ unlikely, because prime of the curve `p` and subgroup - # order `M` are so close! + # order `r` are so close! var validSig = false while (not validSig) and bool(x1.toBig() <= rInit): # 1. Get base `R` point From 87ae09a0dc41997f0f01a1cfb248c71cb6559a6d Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sat, 4 Jan 2025 19:02:17 +0100 Subject: [PATCH 11/16] [precompiles] remove invalid V enum field, invalid -> malformed sig --- constantine/ethereum_evm_precompiles.nim | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/constantine/ethereum_evm_precompiles.nim b/constantine/ethereum_evm_precompiles.nim index f06fc4b61..3bd72600e 100644 --- a/constantine/ethereum_evm_precompiles.nim +++ b/constantine/ethereum_evm_precompiles.nim @@ -50,8 +50,7 @@ type cttEVM_PointNotOnCurve cttEVM_PointNotInSubgroup cttEVM_VerificationFailure - cttEVM_InvalidSignature - cttEVM_InvalidV # `v` of signature `(r, s, v)` is invalid + cttEVM_MalformedSignature func eth_evm_sha256*(r: var openArray[byte], inputs: openArray[byte]): CttEVMStatus {.libPrefix: prefix_ffi, meter.} = ## SHA256 @@ -1323,10 +1322,10 @@ func eth_evm_ecrecover*(r: var openArray[byte], ## XXX: Or construct a `BigInt[256]` instead and compare? (or compare with uint64s?) for i in 32 ..< 63: # first 31 bytes must be zero for a valid `v` if input[i] != byte 0: - return cttEVM_InvalidV + return cttEVM_MalformedSignature let v = input[63] if v notin [byte 0, 1, 27, 28]: - return cttEVM_InvalidSignature + return cttEVM_MalformedSignature # 2a. determine if even or odd `y` coordinate let evenY = v in [byte 0, 27] # 0 / 27 indicates `y` to be even, 1 / 28 odd From ab4b35b925f5dbdc140fe9d30d8bea3142935102 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sat, 4 Jan 2025 19:13:54 +0100 Subject: [PATCH 12/16] [ecdsa] rename ECDSA over secp256k1 file to eth specific --- constantine/{ecdsa_secp256k1.nim => eth_ecdsa_signatures.nim} | 2 +- constantine/ethereum_evm_precompiles.nim | 2 +- constantine/serialization/codecs_ecdsa_secp256k1.nim | 2 +- tests/ecdsa/t_ecdsa_verify_openssl.nim | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename constantine/{ecdsa_secp256k1.nim => eth_ecdsa_signatures.nim} (98%) diff --git a/constantine/ecdsa_secp256k1.nim b/constantine/eth_ecdsa_signatures.nim similarity index 98% rename from constantine/ecdsa_secp256k1.nim rename to constantine/eth_ecdsa_signatures.nim index f26ee741d..8e7a18253 100644 --- a/constantine/ecdsa_secp256k1.nim +++ b/constantine/eth_ecdsa_signatures.nim @@ -17,7 +17,7 @@ import export NonceSampler -const prefix_ffi = "ctt_ecdsa_secp256k1_" +const prefix_ffi = "ctt_eth_ecdsa" type SecretKey* {.byref, exportc: prefix_ffi & "seckey".} = object ## A Secp256k1 secret key diff --git a/constantine/ethereum_evm_precompiles.nim b/constantine/ethereum_evm_precompiles.nim index 3bd72600e..0ba55381f 100644 --- a/constantine/ethereum_evm_precompiles.nim +++ b/constantine/ethereum_evm_precompiles.nim @@ -24,7 +24,7 @@ import ./ethereum_eip4844_kzg, ./serialization/codecs_status_codes, # ECDSA for ECRecover - ./ecdsa_secp256k1 + ./eth_ecdsa_signatures # For KZG point precompile export EthereumKZGContext, TrustedSetupFormat, TrustedSetupStatus, trusted_setup_load, trusted_setup_delete diff --git a/constantine/serialization/codecs_ecdsa_secp256k1.nim b/constantine/serialization/codecs_ecdsa_secp256k1.nim index c92f84b5e..40981ee8d 100644 --- a/constantine/serialization/codecs_ecdsa_secp256k1.nim +++ b/constantine/serialization/codecs_ecdsa_secp256k1.nim @@ -38,7 +38,7 @@ import constantine/math/arithmetic/finite_fields, constantine/math/elliptic/ec_shortweierstrass_affine, constantine/math/io/io_bigints, - constantine/ecdsa_secp256k1 + constantine/eth_ecdsa_signatures import std / [strutils, base64, math, importutils] diff --git a/tests/ecdsa/t_ecdsa_verify_openssl.nim b/tests/ecdsa/t_ecdsa_verify_openssl.nim index 51bfeec7f..3e72ae8fd 100644 --- a/tests/ecdsa/t_ecdsa_verify_openssl.nim +++ b/tests/ecdsa/t_ecdsa_verify_openssl.nim @@ -17,7 +17,7 @@ import constantine/serialization/[codecs, codecs_ecdsa, codecs_ecdsa_secp256k1], constantine/math/arithmetic/[bigints, finite_fields], constantine/platforms/abstractions, - constantine/ecdsa_secp256k1 + constantine/eth_ecdsa_signatures when not defined(windows) and not defined(macosx): # Windows (at least in GH actions CI) does not provide, among others `BN_new` From 893228d8c54056abba53a8cd982b4d5b03fcb90a Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 5 Jan 2025 11:16:17 +0100 Subject: [PATCH 13/16] [ecdsa] remove hash from Eth ECDSA file, specific to Eth now --- constantine/eth_ecdsa_signatures.nim | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/constantine/eth_ecdsa_signatures.nim b/constantine/eth_ecdsa_signatures.nim index 8e7a18253..a5efa4360 100644 --- a/constantine/eth_ecdsa_signatures.nim +++ b/constantine/eth_ecdsa_signatures.nim @@ -47,21 +47,19 @@ func signatures_are_equal*(a, b: Signature): bool {.libPrefix: prefix_ffi.} = proc sign*(sig: var Signature, secretKey: SecretKey, message: openArray[byte], - nonceSampler: NonceSampler = nsRandom, - H: type CryptoHash = sha256) {.libPrefix: prefix_ffi, genCharAPI.} = + nonceSampler: NonceSampler = nsRandom) {.libPrefix: prefix_ffi, genCharAPI.} = ## Sign `message` using `secretKey` and store the signature in `sig`. The nonce ## will either be randomly sampled `nsRandom` or deterministically calculated according ## to RFC6979 (`nsRfc6979`) - sig.coreSign(secretKey.raw, message, H, nonceSampler) + sig.coreSign(secretKey.raw, message, keccak256, nonceSampler) proc verify*( publicKey: PublicKey, message: openArray[byte], - signature: Signature, - H: type CryptoHash = sha256 + signature: Signature ): bool {.libPrefix: prefix_ffi, genCharAPI.} = ## Verify `signature` using `publicKey` for `message`. - result = publicKey.raw.coreVerify(message, signature, H) + result = publicKey.raw.coreVerify(message, signature, keccak256) func derive_pubkey*(public_key: var PublicKey, secret_key: SecretKey) {.libPrefix: prefix_ffi.} = ## Derive the public key matching with a secret key @@ -73,14 +71,13 @@ proc recoverPubkey*( publicKey: var PublicKey, message: openArray[byte], signature: Signature, - evenY: bool, - H: type CryptoHash = sha256 + evenY: bool ) {.libPrefix: prefix_ffi, genCharAPI.} = ## Verify `signature` using `publicKey` for `message`. ## ## `evenY == true` returns the public key corresponding to the ## even `y` coordinate of the `R` point. - publicKey.raw.recoverPubkey(signature, message, evenY, H) + publicKey.raw.recoverPubkey(signature, message, evenY, keccak256) proc recoverPubkey*( publicKey: var PublicKey, From 67d6da8dcc0ea0368c899bd637718468fa28aa75 Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 5 Jan 2025 11:16:44 +0100 Subject: [PATCH 14/16] [tests] update the OpenSSL wrapper signing function to use Keccak256 --- tests/openssl_wrapper.nim | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/openssl_wrapper.nim b/tests/openssl_wrapper.nim index a5be82681..dc5f71160 100644 --- a/tests/openssl_wrapper.nim +++ b/tests/openssl_wrapper.nim @@ -92,6 +92,9 @@ proc EVP_DigestSignInit*(ctx: EVP_MD_CTX, e: pointer, pkey: EVP_PKEY): cint + +proc EVP_MD_fetch*(ctx: OSSL_LIB_CTX, algorithm: cstring, properties: cstring): pointer + proc EVP_DigestSign*(ctx: EVP_MD_CTX, sig: ptr byte, siglen: ptr uint, @@ -138,7 +141,12 @@ proc signMessageOpenSSL*(sig: var array[72, byte], msg: openArray[byte], key: EV let ctx = EVP_MD_CTX_new() var pctx: EVP_PKEY_CTX - if EVP_DigestSignInit(ctx, addr pctx, EVP_sha256(), nil, key) <= 0: + let md = EVP_MD_fetch(nil, "KECCAK-256", nil) + if md.isNil: + raise newException(Exception, "Failed to fetch KECCAK-256") + + + if EVP_DigestSignInit(ctx, addr pctx, md, nil, key) <= 0: raise newException(Exception, "Signing init failed") # Get required signature length From f9c377a34f0f0178319c600c9e2772f5fe88ed7d Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 5 Jan 2025 11:36:15 +0100 Subject: [PATCH 15/16] [ecdsa] name `recoverPubkey` -> `recoverPubkeyFromDigest` for variant Given that we generate a C API from the code, we need to differentiate the function names for the types. The default takes a message and this variant takes a digest (as used in Ethereum's precompile for ECRecover). --- constantine/eth_ecdsa_signatures.nim | 2 +- constantine/ethereum_evm_precompiles.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/constantine/eth_ecdsa_signatures.nim b/constantine/eth_ecdsa_signatures.nim index a5efa4360..540baff9a 100644 --- a/constantine/eth_ecdsa_signatures.nim +++ b/constantine/eth_ecdsa_signatures.nim @@ -79,7 +79,7 @@ proc recoverPubkey*( ## even `y` coordinate of the `R` point. publicKey.raw.recoverPubkey(signature, message, evenY, keccak256) -proc recoverPubkey*( +proc recoverPubkeyFromDigest*( publicKey: var PublicKey, msgHash: Fr[Secp256k1], signature: Signature, diff --git a/constantine/ethereum_evm_precompiles.nim b/constantine/ethereum_evm_precompiles.nim index 0ba55381f..c551a1a2e 100644 --- a/constantine/ethereum_evm_precompiles.nim +++ b/constantine/ethereum_evm_precompiles.nim @@ -1340,7 +1340,7 @@ func eth_evm_ecrecover*(r: var openArray[byte], # 4. perform pubkey recovery var pubKey {.noinit.}: PublicKey - pubKey.recoverPubkey(msgHash, signature, evenY) # , keccak256) + pubKey.recoverPubkeyFromDigest(msgHash, signature, evenY) # 4. now calculate the Ethereum address of the public key (keccak256) privateAccess(PublicKey) From ef7ae07db3cbca742a283241561502c3bc91659a Mon Sep 17 00:00:00 2001 From: Vindaar Date: Sun, 12 Jan 2025 15:11:53 +0100 Subject: [PATCH 16/16] take out ECDSA test requiring OpenSSL v3.3 or higher --- constantine.nimble | 3 ++- tests/ecdsa/t_ecdsa_verify_openssl.nim | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/constantine.nimble b/constantine.nimble index 28a47c35d..b2c78db47 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -609,7 +609,8 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[ ("tests/t_ethereum_verkle_ipa_primitives.nim", false), # Signatures - ("tests/ecdsa/t_ecdsa_verify_openssl.nim", false), + # NOTE: Requires OpenSSL version >=v3.3 for to Keccak256 support + # ("tests/ecdsa/t_ecdsa_verify_openssl.nim", false), # Proof systems # ---------------------------------------------------------- diff --git a/tests/ecdsa/t_ecdsa_verify_openssl.nim b/tests/ecdsa/t_ecdsa_verify_openssl.nim index 3e72ae8fd..9d3a3711c 100644 --- a/tests/ecdsa/t_ecdsa_verify_openssl.nim +++ b/tests/ecdsa/t_ecdsa_verify_openssl.nim @@ -8,6 +8,8 @@ We generate test vectors following these cases: Further, generate signatures using Constantine, which we verify with OpenSSL. + +NOTE: This test requires OpenSSL version >= 3.3, for Keccak256 support. ]## import