From 2e52e7db4e981eb2e706a93f32b479d18c0b22d5 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 13:32:40 +0200 Subject: [PATCH 1/8] bindings --- src/bindings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bindings b/src/bindings index 72f6e5490..ea5e9c6a1 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit 72f6e549021e7be3a0965fdc7422920b6ed052a3 +Subproject commit ea5e9c6a11fcd4d6037a16c5759e269ae529e828 From 58e4355369a326d463ea99aa830faa5efb851ed2 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 13:32:53 +0200 Subject: [PATCH 2/8] add `a` parameter to ec methods --- src/lib/provable/gadgets/elliptic-curve.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/provable/gadgets/elliptic-curve.ts b/src/lib/provable/gadgets/elliptic-curve.ts index 68b01415a..416ec98b7 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); } From 43f9c15a7f7eac8c09072f52148fe0ff15f3c00d Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 13:43:50 +0200 Subject: [PATCH 3/8] support *r1 in ec circuit unit tests --- src/bindings | 2 +- src/lib/provable/gadgets/elliptic-curve.ts | 2 +- src/lib/provable/test/elliptic-curve.unit-test.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/bindings b/src/bindings index ea5e9c6a1..e081466d5 160000 --- a/src/bindings +++ b/src/bindings @@ -1 +1 @@ -Subproject commit ea5e9c6a11fcd4d6037a16c5759e269ae529e828 +Subproject commit e081466d5e435fe3fbd32fa83ef5c0ef3f030dec diff --git a/src/lib/provable/gadgets/elliptic-curve.ts b/src/lib/provable/gadgets/elliptic-curve.ts index 416ec98b7..c0cff7da5 100644 --- a/src/lib/provable/gadgets/elliptic-curve.ts +++ b/src/lib/provable/gadgets/elliptic-curve.ts @@ -128,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/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 From b82396f51d953fca898985f700d5b370d32bff14 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 13:49:23 +0200 Subject: [PATCH 4/8] add to ecdsa unit test --- src/lib/provable/test/ecdsa.unit-test.ts | 36 +++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/lib/provable/test/ecdsa.unit-test.ts b/src/lib/provable/test/ecdsa.unit-test.ts index c2633b05e..232383097 100644 --- a/src/lib/provable/test/ecdsa.unit-test.ts +++ b/src/lib/provable/test/ecdsa.unit-test.ts @@ -27,9 +27,10 @@ import { Random } from '../../testing/random.js'; // 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]; +let curves = [Secp256k1, Secp256r1, Pallas, Vesta]; for (let Curve of curves) { // prepare test inputs @@ -189,3 +190,36 @@ 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', + 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()); From ed1961004d63f7e8c58a4d85a62bd4d5cbca2368 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 15:29:07 +0200 Subject: [PATCH 5/8] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 144f77cac..6c9b9d375 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 + ## [2.0.0](https://github.com/o1-labs/o1js/compare/7e9394...b04520d) ### Breaking Changes From 34e86be50897615a8c6f246763c4af631e8dab46 Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 15:30:18 +0200 Subject: [PATCH 6/8] minor --- src/lib/provable/test/ecdsa.unit-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/provable/test/ecdsa.unit-test.ts b/src/lib/provable/test/ecdsa.unit-test.ts index 232383097..141cca32a 100644 --- a/src/lib/provable/test/ecdsa.unit-test.ts +++ b/src/lib/provable/test/ecdsa.unit-test.ts @@ -194,7 +194,7 @@ proof.publicOutput.assertTrue('signature verifies'); // check constraints w/o endomorphism let programNoEndo = ZkProgram({ - name: 'ecdsa', + name: 'ecdsa-secp256r1', publicOutput: Bool, methods: { ecdsa: { From a0812d7b117c6c797dc36f961324d51cca83081a Mon Sep 17 00:00:00 2001 From: Gregor Date: Thu, 24 Oct 2024 22:25:47 +0200 Subject: [PATCH 7/8] test ecdsa against web crypto --- src/lib/provable/test/ecdsa.unit-test.ts | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/lib/provable/test/ecdsa.unit-test.ts b/src/lib/provable/test/ecdsa.unit-test.ts index 141cca32a..9f5d897c2 100644 --- a/src/lib/provable/test/ecdsa.unit-test.ts +++ b/src/lib/provable/test/ecdsa.unit-test.ts @@ -24,12 +24,56 @@ 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); + +// 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) { @@ -223,3 +267,16 @@ 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)); +} From ec38e5652bb262953b0614286e261960360c78bb Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 25 Oct 2024 07:36:56 +0200 Subject: [PATCH 8/8] node 18 -> 20 in CI --- .github/workflows/build-action.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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: |