diff --git a/package-lock.json b/package-lock.json index 8f787744f57..d055414be8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5581,6 +5581,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", @@ -11593,6 +11606,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", @@ -16750,6 +16773,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/common/src/eips.ts b/packages/common/src/eips.ts index 054c4710183..e8af5639bd5 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/package.json b/packages/evm/package.json index 49131f85efc..ddc9ea8eeb3 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/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..bc7ea86c3b7 --- /dev/null +++ b/packages/evm/src/precompiles/12-p256-verify.ts @@ -0,0 +1,56 @@ +import { BIGINT_0, BIGINT_1, bytesToBigInt, bytesToHex } from '@ethereumjs/util' + +import { p256 } from '@noble/curves/p256.js' +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) { + opts._debug?.(`${pName} failed: Invalid input length: expeted 160, got ${opts.data.length}`) + 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.slice(2), + p256.ProjectivePoint.fromAffine({ x, y }).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..b7079cab212 --- /dev/null +++ b/packages/evm/test/precompiles/12-p256verify.spec.ts @@ -0,0 +1,124 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +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] }) + 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', + ) + + // 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(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', + ) + }) +})