diff --git a/.github/workflows/build-action.yml b/.github/workflows/build-action.yml index 2837c800c..28a91f6d8 100644 --- a/.github/workflows/build-action.yml +++ b/.github/workflows/build-action.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Cache dependencies and build uses: actions/cache@v4 @@ -89,7 +89,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Restore cache uses: actions/cache@v4 @@ -133,7 +133,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Restore cache uses: actions/cache@v4 @@ -212,7 +212,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Restore npm cache uses: actions/cache@v4 @@ -264,7 +264,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Build o1js run: | @@ -293,7 +293,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - name: Build mina-signer run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 386946e42..b0cd869cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased](https://github.com/o1-labs/o1js/compare/b04520d...HEAD) +### Added + +- Support secp256r1 in elliptic curve and ECDSA gadgets https://github.com/o1-labs/o1js/pull/1885 + ### Fixed - Witness generation error in `Gadgets.arrayGet()` when accessing out-of-bounds indices https://github.com/o1-labs/o1js/pull/1886 diff --git a/src/bindings b/src/bindings index 72f6e5490..e081466d5 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 72f6e549021e7be3a0965fdc7422920b6ed052a3 +Subproject commit e081466d5e435fe3fbd32fa83ef5c0ef3f030dec diff --git a/src/lib/provable/gadgets/elliptic-curve.ts b/src/lib/provable/gadgets/elliptic-curve.ts index 68b01415a..c0cff7da5 100644 --- a/src/lib/provable/gadgets/elliptic-curve.ts +++ b/src/lib/provable/gadgets/elliptic-curve.ts @@ -20,7 +20,6 @@ import { provable } from '../types/provable-derivers.js'; import { assertPositiveInteger } from '../../../bindings/crypto/non-negative.js'; import { arrayGet, assertNotVectorEquals } from './basic.js'; import { sliceField3 } from './bit-slices.js'; -import { Hashed } from '../packed.js'; import { exists } from '../core/exists.js'; import { ProvableType } from '../types/provable-intf.js'; @@ -54,7 +53,7 @@ namespace Ecdsa { export type signature = { r: bigint; s: bigint }; } -function add(p1: Point, p2: Point, Curve: { modulus: bigint }) { +function add(p1: Point, p2: Point, Curve: { modulus: bigint; a: bigint }) { let { x: x1, y: y1 } = p1; let { x: x2, y: y2 } = p2; let f = Curve.modulus; @@ -63,7 +62,7 @@ function add(p1: Point, p2: Point, Curve: { modulus: bigint }) { // constant case if (Point.isConstant(p1) && Point.isConstant(p2)) { - let p3 = affineAdd(Point.toBigint(p1), Point.toBigint(p2), f); + let p3 = affineAdd(Point.toBigint(p1), Point.toBigint(p2), f, Curve.a); return Point.from(p3); } @@ -120,7 +119,7 @@ function double(p1: Point, Curve: { modulus: bigint; a: bigint }) { // constant case if (Point.isConstant(p1)) { - let p3 = affineDouble(Point.toBigint(p1), f); + let p3 = affineDouble(Point.toBigint(p1), f, Curve.a); return Point.from(p3); } @@ -129,7 +128,7 @@ function double(p1: Point, Curve: { modulus: bigint; a: bigint }) { let [x1_, y1_] = Field3.toBigints(x1, y1); let denom = inverse(mod(2n * y1_, f), f) ?? 0n; - let m = mod(3n * mod(x1_ ** 2n, f) * denom, f); + let m = mod((3n * mod(x1_ ** 2n, f) + Curve.a) * denom, f); let x3 = mod(m * m - 2n * x1_, f); let y3 = mod(m * (x1_ - x3) - y1_, f); diff --git a/src/lib/provable/test/ecdsa.unit-test.ts b/src/lib/provable/test/ecdsa.unit-test.ts index c2633b05e..9f5d897c2 100644 --- a/src/lib/provable/test/ecdsa.unit-test.ts +++ b/src/lib/provable/test/ecdsa.unit-test.ts @@ -24,12 +24,57 @@ import { } from '../../testing/equivalent.js'; import { Bool } from '../bool.js'; import { Random } from '../../testing/random.js'; +import { bytesToBigInt } from '../../../bindings/crypto/bigint-helpers.js'; +import { expect } from 'expect'; // quick tests const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); +const Secp256r1 = createCurveAffine(CurveParams.Secp256r1); const Pallas = createCurveAffine(CurveParams.Pallas); const Vesta = createCurveAffine(CurveParams.Vesta); -let curves = [Secp256k1, Pallas, Vesta]; + +// secp256r1 test against web crypto API +{ + // generate a key pair + let { privateKey, publicKey } = await crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign'] + ); + + // extract public and private key to bigints + let pkBuffer = await crypto.subtle.exportKey('raw', publicKey); + let x = bytesToBigIntBE(pkBuffer.slice(1, 33)); + let y = bytesToBigIntBE(pkBuffer.slice(33)); + let pk = { x, y }; + + var skJwk = await crypto.subtle.exportKey('jwk', privateKey); + let sk = bytesToBigIntBE(fromBase64Url(skJwk.d!)); + + // sanity check: we extracted keys correctly + expect(Secp256r1.from(pk)).toEqual(Secp256r1.scale(Secp256r1.one, sk)); + + // sign a message using web crypto + let message = new TextEncoder().encode('hello world'); + + let sigBuffer = await crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, + privateKey, + message + ); + let r = bytesToBigIntBE(sigBuffer.slice(0, 32)); + let s = bytesToBigIntBE(sigBuffer.slice(32)); + let signature = Ecdsa.Signature.from({ r, s }); + + // check that we can verify the signature on the message hash + let msgHash = await crypto.subtle.digest('SHA-256', message); + let m = Field3.from(Secp256r1.Scalar.mod(bytesToBigIntBE(msgHash))); + + let ok = Ecdsa.verify(Secp256r1, signature, m, Point.from(pk)); + assert(ok, 'web crypto signature verifies'); +} + +let curves = [Secp256k1, Secp256r1, Pallas, Vesta]; for (let Curve of curves) { // prepare test inputs @@ -189,3 +234,49 @@ console.timeEnd('ecdsa verify (prove)'); assert(await program.verify(proof), 'proof verifies'); proof.publicOutput.assertTrue('signature verifies'); + +// check constraints w/o endomorphism + +let programNoEndo = ZkProgram({ + name: 'ecdsa-secp256r1', + publicOutput: Bool, + methods: { + ecdsa: { + privateInputs: [], + async method() { + let signature_ = Provable.witness(Ecdsa.Signature, () => signature); + let msgHash_ = Provable.witness(Field3, () => msgHash); + let publicKey_ = Provable.witness(Point, () => publicKey); + + return { + publicOutput: Ecdsa.verify( + Secp256r1, + signature_, + msgHash_, + publicKey_, + config + ), + }; + }, + }, + }, +}); + +console.time('ecdsa verify, no endomorphism (build constraint system)'); +let csNoEndo = (await programNoEndo.analyzeMethods()).ecdsa; +console.timeEnd('ecdsa verify, no endomorphism (build constraint system)'); + +console.log(csNoEndo.summary()); + +// helpers + +function bytesToBigIntBE(bytes: ArrayBuffer | Uint8Array | number[]) { + return bytesToBigInt([...new Uint8Array(bytes)].reverse()); +} + +function fromBase64Url(b64url: string): Uint8Array { + let b64 = + b64url.replace(/-/g, '+').replace(/_/g, '/') + + '===='.slice(0, (4 - (b64url.length % 4)) % 4); + return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)); +} diff --git a/src/lib/provable/test/elliptic-curve.unit-test.ts b/src/lib/provable/test/elliptic-curve.unit-test.ts index 2db23ecd9..e42632fde 100644 --- a/src/lib/provable/test/elliptic-curve.unit-test.ts +++ b/src/lib/provable/test/elliptic-curve.unit-test.ts @@ -19,9 +19,10 @@ import { foreignField, throwError } from './test-utils.js'; // provable equivalence tests const Secp256k1 = createCurveAffine(CurveParams.Secp256k1); +const Secp256r1 = createCurveAffine(CurveParams.Secp256r1); const Pallas = createCurveAffine(CurveParams.Pallas); const Vesta = createCurveAffine(CurveParams.Vesta); -let curves = [Secp256k1, Pallas, Vesta]; +let curves = [Secp256k1, Secp256r1, Pallas, Vesta]; for (let Curve of curves) { // prepare test inputs