From c25e137c2f3d4ae38a4eb93c5707d884de154725 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 12 May 2025 15:01:15 -0400 Subject: [PATCH 1/5] Add initial implementation of RIP7212 --- packages/common/src/eips.ts | 8 +++ packages/evm/src/evm.ts | 2 +- packages/evm/src/params.ts | 7 +++ .../evm/src/precompiles/12-p256-verify.ts | 51 +++++++++++++++++++ packages/evm/src/precompiles/index.ts | 11 ++++ .../test/precompiles/12-p256verify.spec.ts | 48 +++++++++++++++++ 6 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 packages/evm/src/precompiles/12-p256-verify.ts create mode 100644 packages/evm/test/precompiles/12-p256verify.spec.ts diff --git a/packages/common/src/eips.ts b/packages/common/src/eips.ts index 58bd81db913..537aec0e61b 100644 --- a/packages/common/src/eips.ts +++ b/packages/common/src/eips.ts @@ -366,6 +366,14 @@ export const eipsDict: EIPsDict = { */ requiredEIPs: [2929], }, + /*** + * Description: Precompile for secp256r1 Curve Support + * URL: https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md + * Status: Final + */ + 7212: { + minimumHardfork: Hardfork.Prague, + }, /** * Description : Increase the MAX_EFFECTIVE_BALANCE -> Execution layer triggered consolidations (experimental) * URL : https://eips.ethereum.org/EIPS/eip-7251 diff --git a/packages/evm/src/evm.ts b/packages/evm/src/evm.ts index 5e157b96171..e9c47205d01 100644 --- a/packages/evm/src/evm.ts +++ b/packages/evm/src/evm.ts @@ -251,7 +251,7 @@ export class EVM implements EVMInterface { const supportedEIPs = [ 663, 1153, 1559, 2537, 2565, 2718, 2929, 2930, 2935, 3198, 3529, 3540, 3541, 3607, 3651, 3670, 3855, 3860, 4200, 4399, 4750, 4788, 4844, 4895, 5133, 5450, 5656, 6110, 6206, 6780, 6800, - 7002, 7069, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, + 7002, 7069, 7212, 7251, 7480, 7516, 7620, 7685, 7691, 7692, 7698, 7702, 7709, ] for (const eip of this.common.eips()) { diff --git a/packages/evm/src/params.ts b/packages/evm/src/params.ts index 4b28e728ffb..6bcf5d0b5fa 100644 --- a/packages/evm/src/params.ts +++ b/packages/evm/src/params.ts @@ -392,6 +392,13 @@ export const paramsEVM: ParamsDict = { datacopyGas: 3, // Base fee of the DATACOPY opcode }, /** + * Precompile for secp256r1 Curve Support + */ + 7212: { + // gasPrices + p256verifyGas: 3450, // Base fee of the P256VERIFY precompile + }, + /** . * BLOBBASEFEE opcode . */ 7516: { diff --git a/packages/evm/src/precompiles/12-p256-verify.ts b/packages/evm/src/precompiles/12-p256-verify.ts new file mode 100644 index 00000000000..d4f57aa84fb --- /dev/null +++ b/packages/evm/src/precompiles/12-p256-verify.ts @@ -0,0 +1,51 @@ +import { BIGINT_0, BIGINT_1, bytesToBigInt, bytesToHex } from '@ethereumjs/util' + +import { p256 } from '@noble/curves/nist' +import { EVMError } from '../errors.ts' +import { EVMErrorResult, OOGResult } from '../evm.ts' +import { getPrecompileName } from './index.ts' +import { gasLimitCheck } from './util.ts' + +import type { ExecResult } from '../types.ts' +import type { PrecompileInput } from './types.ts' + +export const p256_MODULUS = BigInt( + '0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff', +) +export async function precompile12(opts: PrecompileInput): Promise { + const pName = getPrecompileName('12') + + const gasUsed = opts.common.param('p256verifyGas') + if (!gasLimitCheck(opts, gasUsed, pName)) { + return OOGResult(opts.gasLimit) + } + + if (opts.data.length !== 160) { + return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUT_LENGTH), opts.gasLimit) + } + + const hash = bytesToHex(opts.data.subarray(0, 32)) // hash of signed data + const r = bytesToBigInt(opts.data.subarray(32, 64)) // r component of signature + const s = bytesToBigInt(opts.data.subarray(64, 96)) // s component of signature + const x = bytesToBigInt(opts.data.subarray(96, 128)) // x coordinate of public key + const y = bytesToBigInt(opts.data.subarray(128, 160)) // y coordinate of public key + + if (r >= p256.CURVE.n || s >= p256.CURVE.n || r < BIGINT_1 || s < BIGINT_1) { + opts._debug?.(`${pName} failed: Invalid signature`) + return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUTS), opts.gasLimit) + } + + if ((x <= BIGINT_0 && y <= BIGINT_0) || (x >= p256_MODULUS && y >= p256_MODULUS)) { + opts._debug?.(`${pName} failed: Invalid public key`) + return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUTS), opts.gasLimit) + } + + const isValid = p256.verify({ r, s }, hash, new p256.ProjectivePoint(x, y, BIGINT_0).toHex()) + + const returnValue = new Uint8Array(32) + returnValue[0] = isValid ? 1 : 0 + return { + executionGasUsed: gasUsed, + returnValue, + } +} diff --git a/packages/evm/src/precompiles/index.ts b/packages/evm/src/precompiles/index.ts index 1d2ac3605d6..11eaf3e845a 100644 --- a/packages/evm/src/precompiles/index.ts +++ b/packages/evm/src/precompiles/index.ts @@ -18,6 +18,7 @@ import { precompile08 } from './08-bn254-pairing.ts' import { precompile09 } from './09-blake2f.ts' import { precompile10 } from './10-bls12-map-fp-to-g1.ts' import { precompile11 } from './11-bls12-map-fp2-to-g2.ts' +import { precompile12 } from './12-p256-verify.ts' import { MCLBLS, NobleBLS } from './bls12_381/index.ts' import { NobleBN254, RustBN254 } from './bn254/index.ts' @@ -213,6 +214,15 @@ const precompileEntries: PrecompileEntry[] = [ precompile: precompile11, name: 'BLS12_MAP_FP_TO_G2 (0x11)', }, + { + address: BYTES_19 + '12', + check: { + type: PrecompileAvailabilityCheck.EIP, + param: 7212, + }, + precompile: precompile12, + name: 'P256_VERIFY (0x12)', + }, ] const precompiles: Precompiles = { @@ -233,6 +243,7 @@ const precompiles: Precompiles = { [BYTES_19 + '0f']: precompile0f, [BYTES_19 + '10']: precompile10, [BYTES_19 + '11']: precompile11, + [BYTES_19 + '12']: precompile12, } type DeletePrecompile = { diff --git a/packages/evm/test/precompiles/12-p256verify.spec.ts b/packages/evm/test/precompiles/12-p256verify.spec.ts new file mode 100644 index 00000000000..a7d9de29f3f --- /dev/null +++ b/packages/evm/test/precompiles/12-p256verify.spec.ts @@ -0,0 +1,48 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { BIGINT_0, bytesToBigInt, concatBytes, hexToBytes, unpadBytes } from '@ethereumjs/util' +import { assert, describe, it } from 'vitest' + +import { createEVM, getActivePrecompiles } from '../../src/index.ts' + +import type { PrefixedHexString } from '@ethereumjs/util' +import type { PrecompileInput } from '../../src/index.ts' + +describe('Precompiles: p256 verify', () => { + it('should work', async () => { + const common = new Common({ chain: Mainnet, hardfork: Hardfork.Prague, eips: [7212] }) + const evm = await createEVM({ + common, + }) + const addressStr = '0000000000000000000000000000000000000012' + const p256Verify = getActivePrecompiles(common).get(addressStr)! + + // Random inputs + const testCase = { + hash: '0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' as PrefixedHexString, + r: '0x0000000000000000000000000000000000000000000000000000000000000002' as PrefixedHexString, + s: '0x0000000000000000000000000000000000000000000000000000000000000001' as PrefixedHexString, + x: '0x0000000000000000000000000000000000000000000000000000000000000002' as PrefixedHexString, + y: '0x0000000000000000000000000000000000000000000000000000000000000003' as PrefixedHexString, + } + + const opts: PrecompileInput = { + data: concatBytes( + hexToBytes(testCase.hash), + hexToBytes(testCase.r), + hexToBytes(testCase.s), + hexToBytes(testCase.x), + hexToBytes(testCase.y), + ), + gasLimit: 0xfffffffffn, + _EVM: evm, + common, + } + + const res = await p256Verify(opts) + assert.strictEqual( + bytesToBigInt(unpadBytes(res.returnValue.slice(32))), + BIGINT_0, + 'p256 verify precompile fails to verify nonsense inputs', + ) + }) +}) From 6116c36171280716f5918ece5cdf747b667c7e0a Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 12 May 2025 15:05:49 -0400 Subject: [PATCH 2/5] use correct import --- packages/evm/src/precompiles/12-p256-verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/src/precompiles/12-p256-verify.ts b/packages/evm/src/precompiles/12-p256-verify.ts index d4f57aa84fb..7cb11762f47 100644 --- a/packages/evm/src/precompiles/12-p256-verify.ts +++ b/packages/evm/src/precompiles/12-p256-verify.ts @@ -1,6 +1,6 @@ import { BIGINT_0, BIGINT_1, bytesToBigInt, bytesToHex } from '@ethereumjs/util' -import { p256 } from '@noble/curves/nist' +import { p256 } from '@noble/curves/p256.js' import { EVMError } from '../errors.ts' import { EVMErrorResult, OOGResult } from '../evm.ts' import { getPrecompileName } from './index.ts' From 5eefe912dbbe74b209e73e5cd0f0690edfebc4ab Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Mon, 12 May 2025 21:02:33 -0400 Subject: [PATCH 3/5] use correct public key constructor --- packages/evm/src/precompiles/12-p256-verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/evm/src/precompiles/12-p256-verify.ts b/packages/evm/src/precompiles/12-p256-verify.ts index 7cb11762f47..499f27eeedf 100644 --- a/packages/evm/src/precompiles/12-p256-verify.ts +++ b/packages/evm/src/precompiles/12-p256-verify.ts @@ -40,7 +40,7 @@ export async function precompile12(opts: PrecompileInput): Promise { return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUTS), opts.gasLimit) } - const isValid = p256.verify({ r, s }, hash, new p256.ProjectivePoint(x, y, BIGINT_0).toHex()) + const isValid = p256.verify({ r, s }, hash, p256.ProjectivePoint.fromAffine({ x, y }).toHex()) const returnValue = new Uint8Array(32) returnValue[0] = isValid ? 1 : 0 From f4e1f02120e831fe30c98773daf7b291fa17014d Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 13 May 2025 13:18:25 -0400 Subject: [PATCH 4/5] webauthn test --- package-lock.json | 24 +++++ packages/evm/package.json | 1 + .../evm/src/precompiles/12-p256-verify.ts | 7 +- .../test/precompiles/12-p256verify.spec.ts | 87 ++++++++++++++++++- 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a3e9118eb2..30926a9e85a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5620,6 +5620,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cbor": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.3.tgz", + "integrity": "sha512-72Jnj81xMsqepqdcSdf2+fflz/UDsThOHy5hj2MW5F5xzHL8Oa0KQ6I6V9CwVUPxg5pf+W9xp6W2KilaRXWWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nofilter": "^3.0.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chai": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", @@ -11644,6 +11657,16 @@ "dev": true, "license": "MIT" }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.19" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -16813,6 +16836,7 @@ "@types/minimist": "^1.2.5", "@types/node-dir": "^0.0.37", "benchmark": "^2.1.4", + "cbor": "^10.0.3", "level": "^9.0.0", "mcl-wasm": "^1.8.0", "memory-level": "^3.0.0", diff --git a/packages/evm/package.json b/packages/evm/package.json index fba48d043ed..d1caa8983fc 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -72,6 +72,7 @@ "@types/minimist": "^1.2.5", "@types/node-dir": "^0.0.37", "benchmark": "^2.1.4", + "cbor": "^10.0.3", "level": "^9.0.0", "mcl-wasm": "^1.8.0", "memory-level": "^3.0.0", diff --git a/packages/evm/src/precompiles/12-p256-verify.ts b/packages/evm/src/precompiles/12-p256-verify.ts index 499f27eeedf..bc7ea86c3b7 100644 --- a/packages/evm/src/precompiles/12-p256-verify.ts +++ b/packages/evm/src/precompiles/12-p256-verify.ts @@ -21,6 +21,7 @@ export async function precompile12(opts: PrecompileInput): Promise { } if (opts.data.length !== 160) { + opts._debug?.(`${pName} failed: Invalid input length: expeted 160, got ${opts.data.length}`) return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUT_LENGTH), opts.gasLimit) } @@ -40,7 +41,11 @@ export async function precompile12(opts: PrecompileInput): Promise { return EVMErrorResult(new EVMError(EVMError.errorMessages.INVALID_INPUTS), opts.gasLimit) } - const isValid = p256.verify({ r, s }, hash, p256.ProjectivePoint.fromAffine({ x, y }).toHex()) + const isValid = p256.verify( + { r, s }, + hash.slice(2), + p256.ProjectivePoint.fromAffine({ x, y }).toHex(), + ) const returnValue = new Uint8Array(32) returnValue[0] = isValid ? 1 : 0 diff --git a/packages/evm/test/precompiles/12-p256verify.spec.ts b/packages/evm/test/precompiles/12-p256verify.spec.ts index a7d9de29f3f..e424c0bcd0b 100644 --- a/packages/evm/test/precompiles/12-p256verify.spec.ts +++ b/packages/evm/test/precompiles/12-p256verify.spec.ts @@ -1,12 +1,22 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { BIGINT_0, bytesToBigInt, concatBytes, hexToBytes, unpadBytes } from '@ethereumjs/util' +import { + BIGINT_0, + bigIntToBytes, + bytesToBigInt, + concatBytes, + hexToBytes, + setLengthLeft, + unpadBytes, +} from '@ethereumjs/util' +import * as cbor from 'cbor' import { assert, describe, it } from 'vitest' - import { createEVM, getActivePrecompiles } from '../../src/index.ts' import type { PrefixedHexString } from '@ethereumjs/util' +import { p256 } from '@noble/curves/p256' +import { base64urlnopad } from '@scure/base' +import { sha256 } from 'ethereum-cryptography/sha256' import type { PrecompileInput } from '../../src/index.ts' - describe('Precompiles: p256 verify', () => { it('should work', async () => { const common = new Common({ chain: Mainnet, hardfork: Hardfork.Prague, eips: [7212] }) @@ -36,6 +46,7 @@ describe('Precompiles: p256 verify', () => { gasLimit: 0xfffffffffn, _EVM: evm, common, + _debug: (msg) => console.log(msg), } const res = await p256Verify(opts) @@ -44,5 +55,75 @@ describe('Precompiles: p256 verify', () => { BIGINT_0, 'p256 verify precompile fails to verify nonsense inputs', ) + + // webauthn generated inputs from https://opotonniee.github.io/webauthn-playground/ + + // Registration output with public key + // rpIdHash: t8DGRTBfls-BhOH2QC404lvdhe_t2_NkvM0nQWEEADc + // Flags: 0b01011101 (User Present, User Verified, Backup Eligible, Backed-up, Attested data) + // Counter: 0 + // Attested cred data: + // AAGUID: ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4 + // CredId: 5CfOKpISziPZT87xaQinBQ + // Pub Key: pQECAyYgASFYIMqn0ISx7iJ1Bq_l2Ektnx8EP4vBunTtIwtBVJXx6I5hIlgg5ZeUA4PKKzIGeolx-8tXoP21R-eNURT22ezmUxQxshU + + // Authentication/signature output + // { + // "clientExtensionResults": {}, + // "rawId": "5CfOKpISziPZT87xaQinBQ", + // "response": { + // "authenticatorData": "t8DGRTBfls-BhOH2QC404lvdhe_t2_NkvM0nQWEEADcdAAAAAA", + // "signature": "MEUCIQCAq9bBDIlH3008aD_p1dnQEnAPFFoumuttjFO5B0txGgIgXgkv9h8OorysU8YK972hnNQLvjdob1dUSI7nTH4-yHI", + // "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRUdZdEFNZ2k4QjJFeTFGTlZmVkY5M201TEV6X0Nmd1R5MDBXMnpvUEVONCIsIm9yaWdpbiI6Imh0dHBzOi8vb3BvdG9ubmllZS5naXRodWIuaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2V9" + // }, + // "authenticatorAttachment": "cross-platform", + // "id": "5CfOKpISziPZT87xaQinBQ", + // "type": "public-key" + // } + + const webAuthnInput = { + publicKey: + 'pQECAyYgASFYINYCnLjxaitogSl7vSSVDixB6JojPCLveobOmmTgV8UMIlggznl2Cy2oqSwvWN5M17E_r2f82QM-sazcYaHj43IADcs', + signature: + 'MEYCIQDW2X6Bg0BfZRCpWPsFUD1btTKjn25MZUGQczm8Q4-n9wIhALFxp95N-yuxRKOpBnQmOjneUjT9s-mkmEl_rtfaJSs4', + authenticatorData: 't8DGRTBfls-BhOH2QC404lvdhe_t2_NkvM0nQWEEADcdAAAAAA', + clientDataJson: + 'eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiRUdZdEFNZ2k4QjJFeTFGTlZmVkY5M201TEV6X0Nmd1R5MDBXMnpvUEVONCIsIm9yaWdpbiI6Imh0dHBzOi8vb3BvdG9ubmllZS5naXRodWIuaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2V9', + } + + const decoded = base64urlnopad.decode(webAuthnInput.publicKey) + // Use cbor to decode the COSE key + const coseKey = cbor.decodeFirstSync(decoded) + // COSE keys for x and y are -2 and -3 + const x = Uint8Array.from(coseKey.get(-2)) + const y = Uint8Array.from(coseKey.get(-3)) + + const hash = sha256( + concatBytes( + base64urlnopad.decode(webAuthnInput.authenticatorData), + base64urlnopad.decode(webAuthnInput.clientDataJson), + ), + ) + const sig = p256.Signature.fromDER(base64urlnopad.decode(webAuthnInput.signature)) + + const webAuthnInputOpts: PrecompileInput = { + data: concatBytes( + hash, + setLengthLeft(bigIntToBytes(sig.r, true), 32), + setLengthLeft(bigIntToBytes(sig.s, true), 32), + x, + y, + ), + gasLimit: 0xfffffffffn, + common, + _EVM: evm, + } + + const webAuthnInputRes = await p256Verify(webAuthnInputOpts) + assert.strictEqual( + webAuthnInputRes.returnValue[0], + 1, + 'p256 verify precompile verifies webauthn inputs', + ) }) }) From c3cc64f8053db0c86f364aafc5897f6af9f8d598 Mon Sep 17 00:00:00 2001 From: acolytec3 <17355484+acolytec3@users.noreply.github.com> Date: Tue, 13 May 2025 14:52:42 -0400 Subject: [PATCH 5/5] more failed fixes --- packages/evm/test/precompiles/12-p256verify.spec.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/evm/test/precompiles/12-p256verify.spec.ts b/packages/evm/test/precompiles/12-p256verify.spec.ts index e424c0bcd0b..b7079cab212 100644 --- a/packages/evm/test/precompiles/12-p256verify.spec.ts +++ b/packages/evm/test/precompiles/12-p256verify.spec.ts @@ -46,7 +46,6 @@ describe('Precompiles: p256 verify', () => { gasLimit: 0xfffffffffn, _EVM: evm, common, - _debug: (msg) => console.log(msg), } const res = await p256Verify(opts) @@ -98,12 +97,8 @@ describe('Precompiles: p256 verify', () => { const x = Uint8Array.from(coseKey.get(-2)) const y = Uint8Array.from(coseKey.get(-3)) - const hash = sha256( - concatBytes( - base64urlnopad.decode(webAuthnInput.authenticatorData), - base64urlnopad.decode(webAuthnInput.clientDataJson), - ), - ) + const hash = sha256(base64urlnopad.decode(webAuthnInput.clientDataJson)) + const sig = p256.Signature.fromDER(base64urlnopad.decode(webAuthnInput.signature)) const webAuthnInputOpts: PrecompileInput = { @@ -123,7 +118,7 @@ describe('Precompiles: p256 verify', () => { assert.strictEqual( webAuthnInputRes.returnValue[0], 1, - 'p256 verify precompile verifies webauthn inputs', + 'p256-verify precompile verifies webauthn inputs', ) }) })