From 1ee2ea137cb499b2f6711ab2d9445c2534cf7d32 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 20:50:11 +0100 Subject: [PATCH 01/36] enable some smaller range checks with rangeCheck64 --- src/lib/gadgets/gadgets.ts | 4 ++++ src/lib/gadgets/range-check.ts | 20 ++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/lib/gadgets/gadgets.ts b/src/lib/gadgets/gadgets.ts index 00e11a7a72..1434db4435 100644 --- a/src/lib/gadgets/gadgets.ts +++ b/src/lib/gadgets/gadgets.ts @@ -34,6 +34,10 @@ const Gadgets = { * **Note**: Small "negative" field element inputs are interpreted as large integers close to the field size, * and don't pass the 64-bit check. If you want to prove that a value lies in the int64 range [-2^63, 2^63), * you could use `rangeCheck64(x.add(1n << 63n))`. + * + * _Advanced usage_: This returns the 4 highest limbs of x, in reverse order, i.e. [x52, x40, x28, x16]. + * This is useful if you want to do a range check for 52, 40, 28, or 16 bits instead of 64, + * by constraining some of the returned limbs to be 0. */ rangeCheck64(x: Field) { return rangeCheck64(x); diff --git a/src/lib/gadgets/range-check.ts b/src/lib/gadgets/range-check.ts index 1f4f369157..8ab900f89a 100644 --- a/src/lib/gadgets/range-check.ts +++ b/src/lib/gadgets/range-check.ts @@ -1,18 +1,28 @@ import { Field } from '../field.js'; import * as Gates from '../gates.js'; +import { TupleN } from '../util/types.js'; import { bitSlice, exists } from './common.js'; export { rangeCheck64, multiRangeCheck, compactMultiRangeCheck, L }; /** - * Asserts that x is in the range [0, 2^64) + * Asserts that x is in the range [0, 2^64). + * + * Returns the 4 highest 12-bit limbs of x in reverse order: [x52, x40, x28, x16]. */ -function rangeCheck64(x: Field) { +function rangeCheck64(x: Field): TupleN { if (x.isConstant()) { - if (x.toBigInt() >= 1n << 64n) { + let xx = x.toBigInt(); + if (xx >= 1n << 64n) { throw Error(`rangeCheck64: expected field to fit in 64 bits, got ${x}`); } - return; + // returned for consistency with the provable case + return [ + new Field(bitSlice(xx, 52, 12)), + new Field(bitSlice(xx, 40, 12)), + new Field(bitSlice(xx, 28, 12)), + new Field(bitSlice(xx, 16, 12)), + ]; } // crumbs (2-bit limbs) @@ -47,6 +57,8 @@ function rangeCheck64(x: Field) { [x14, x12, x10, x8, x6, x4, x2, x0], false // not using compact mode ); + + return [x52, x40, x28, x16]; } // default bigint limb size From 9a340f88d8c3b6966036b9c8e4796fe95441e34e Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 21:02:25 +0100 Subject: [PATCH 02/36] expose witnessFields --- src/lib/provable.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lib/provable.ts b/src/lib/provable.ts index 0b1a5e00aa..f13d982403 100644 --- a/src/lib/provable.ts +++ b/src/lib/provable.ts @@ -13,7 +13,7 @@ import { InferProvable, InferredProvable, } from '../bindings/lib/provable-snarky.js'; -import { isField } from './field.js'; +import { FieldConst, isField } from './field.js'; import { inCheckedComputation, inProver, @@ -24,6 +24,8 @@ import { constraintSystem, } from './provable-context.js'; import { isBool } from './bool.js'; +import { TupleN } from './util/types.js'; +import { MlArray } from './ml/base.js'; // external API export { Provable }; @@ -63,6 +65,12 @@ const Provable = { * ``` */ witness, + /** + * Witness a tuple of field elements. This works just like {@link Provable.witness}, + * but optimized for witnessing plain field elements, which is especially common + * in low-level provable code. + */ + witnessFields, /** * Proof-compatible if-statement. * This behaves like a ternary conditional statement in JS. @@ -235,6 +243,17 @@ function witness = FlexibleProvable>( return value; } +function witnessFields TupleN>( + n: N, + compute: C +) { + let varsMl = Snarky.exists(n, () => + MlArray.mapTo(compute(), FieldConst.fromBigint) + ); + let vars = MlArray.mapFrom(varsMl, (v) => new Field(v)); + return TupleN.fromArray(n, vars); +} + type ToFieldable = { toFields(): Field[] }; // general provable methods From c0b56eab9365414ce0d090876cd99a9208e4a6ea Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 22:02:32 +0100 Subject: [PATCH 03/36] start rsa example: multiplication --- src/examples/provable-methods/README.md | 4 + src/examples/provable-methods/rsa.ts | 121 ++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 src/examples/provable-methods/README.md create mode 100644 src/examples/provable-methods/rsa.ts diff --git a/src/examples/provable-methods/README.md b/src/examples/provable-methods/README.md new file mode 100644 index 0000000000..ee6ad235b6 --- /dev/null +++ b/src/examples/provable-methods/README.md @@ -0,0 +1,4 @@ +# Provable methods + +This folder showcases advanced examples of writing provable code. + diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts new file mode 100644 index 0000000000..86028199df --- /dev/null +++ b/src/examples/provable-methods/rsa.ts @@ -0,0 +1,121 @@ +/** + * RSA signature verification with o1js + */ +import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; + +const mask = (1n << 116n) - 1n; + +/** + * We use 116-bit limbs, which means 18 limbs for a 2048-bit numbers as used in RSA. + */ +const Field18 = Provable.Array(Field, 18); + +class Bigint2048 extends Struct({ values: Field18 }) { + toBigInt() { + let result = 0n; + let bitPosition = 0n; + for (let i = 0; i < 18; i++) { + result += this.values[i].toBigInt() << bitPosition; + bitPosition += 116n; + } + return result; + } + + static from(x: bigint) { + let values = []; + for (let i = 0; i < 18; i++) { + values.push(Field(x & mask)); + x >>= 116n; + } + return new Bigint2048({ values }); + } + + static check(x: { values: Field[] }) { + for (let i = 0; i < 18; i++) { + rangeCheck116(x.values[i]); + } + } +} + +/** + * x*y mod p + */ +function multiply(x: Bigint2048, y: Bigint2048, p: bigint) { + // witness q, r so that x*y = q*p + r + // this also adds the range checks in `check()` + let { q, r } = Provable.witness( + provable({ q: Bigint2048, r: Bigint2048 }), + () => { + let xy = x.toBigInt() * y.toBigInt(); + let q = xy / p; + let r = xy - q * p; + return { q: Bigint2048.from(q), r: Bigint2048.from(r) }; + } + ); + + let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); + let [X, Y, Q, R] = [x.values, y.values, q.values, r.values]; + + const P = Bigint2048.from(p).values; + + for (let i = 0; i < 18; i++) { + for (let j = 0; j < 18; j++) { + let xy = X[i].mul(Y[j]); + let qp = Q[i].mul(P[j]); + res[i + j] = res[i + j].add(xy).sub(qp); + } + } + for (let i = 0; i < 18; i++) { + res[i] = res[i].sub(R[i]); + } + + // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 + + let carry = Field(0); + + for (let i = 0; i < 2 * 18 - 2; i++) { + let res_i = res[i].add(carry); + + [carry] = Provable.witnessFields(1, () => [res_i.toBigInt() >> 116n]); + rangeCheck128(carry); + + res_i.assertEquals(carry.mul(1n << 116n)); + } + + // last carry is 0 ==> xy - qp - r is 0 + let res_i = res[2 * 18 - 1].add(carry); + res_i.assertEquals(0n); + + return r; +} + +/** + * Custom range check for a single limb + */ +function rangeCheck116(x: Field) { + let [x0, x1] = Provable.witnessFields(2, () => [ + x.toBigInt() & mask, + x.toBigInt() >> 116n, + ]); + + Gadgets.rangeCheck64(x0); + let [x52] = Gadgets.rangeCheck64(x1); + x52.assertEquals(0n); + + x0.add(x1.mul(1n << 116n)).assertEquals(x); +} + +/** + * Custom range check for carries + */ +function rangeCheck128(x: Field) { + let [x0, x1] = Provable.witnessFields(2, () => [ + x.toBigInt() & mask, + x.toBigInt() >> 116n, + ]); + + Gadgets.rangeCheck64(x0); + Gadgets.rangeCheck64(x1); + + x0.add(x1.mul(1n << 116n)).assertEquals(x); +} From adfea0f0700a38102cae5e56f044658f86d2a599 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 22:17:03 +0100 Subject: [PATCH 04/36] rsa verification --- src/examples/provable-methods/rsa.ts | 60 ++++++++++++++++++---------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 86028199df..906adf3fc4 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -10,29 +10,27 @@ const mask = (1n << 116n) - 1n; */ const Field18 = Provable.Array(Field, 18); -class Bigint2048 extends Struct({ values: Field18 }) { +class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { + modmul(x: Bigint2048, y: Bigint2048) { + return multiply(x, y, this); + } + toBigInt() { - let result = 0n; - let bitPosition = 0n; - for (let i = 0; i < 18; i++) { - result += this.values[i].toBigInt() << bitPosition; - bitPosition += 116n; - } - return result; + return this.value; } static from(x: bigint) { - let values = []; + let fields = []; for (let i = 0; i < 18; i++) { - values.push(Field(x & mask)); + fields.push(Field(x & mask)); x >>= 116n; } - return new Bigint2048({ values }); + return new Bigint2048({ fields, value: x }); } - static check(x: { values: Field[] }) { + static check(x: { fields: Field[] }) { for (let i = 0; i < 18; i++) { - rangeCheck116(x.values[i]); + rangeCheck116(x.fields[i]); } } } @@ -40,23 +38,22 @@ class Bigint2048 extends Struct({ values: Field18 }) { /** * x*y mod p */ -function multiply(x: Bigint2048, y: Bigint2048, p: bigint) { +function multiply(x: Bigint2048, y: Bigint2048, p: Bigint2048) { // witness q, r so that x*y = q*p + r // this also adds the range checks in `check()` let { q, r } = Provable.witness( provable({ q: Bigint2048, r: Bigint2048 }), () => { let xy = x.toBigInt() * y.toBigInt(); - let q = xy / p; - let r = xy - q * p; + let p0 = p.toBigInt(); + let q = xy / p0; + let r = xy - q * p0; return { q: Bigint2048.from(q), r: Bigint2048.from(r) }; } ); let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); - let [X, Y, Q, R] = [x.values, y.values, q.values, r.values]; - - const P = Bigint2048.from(p).values; + let [X, Y, Q, R, P] = [x.fields, y.fields, q.fields, r.fields, p.fields]; for (let i = 0; i < 18; i++) { for (let j = 0; j < 18; j++) { @@ -70,7 +67,6 @@ function multiply(x: Bigint2048, y: Bigint2048, p: bigint) { } // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 - let carry = Field(0); for (let i = 0; i < 2 * 18 - 2; i++) { @@ -89,6 +85,30 @@ function multiply(x: Bigint2048, y: Bigint2048, p: bigint) { return r; } +/** + * RSA signature verification + * + * TODO this is a bit simplistic, according to RSA spec, message must be 256 bits and the remaining + * bits must follow a specific pattern. + */ +function rsaVerify65537( + message: Bigint2048, + signature: Bigint2048, + modulus: Bigint2048 +) { + // compute signature^(2^16 + 1) mod modulus + // square 16 times + let x = signature; + for (let i = 0; i < 16; i++) { + x = modulus.modmul(x, x); + } + // multiply by signature + x = modulus.modmul(x, signature); + + // check that x == message + Provable.assertEqual(Bigint2048, message, x); +} + /** * Custom range check for a single limb */ From 409526c4d7b9028018305cbd3d5edad35297d7b1 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 22:20:22 +0100 Subject: [PATCH 05/36] so far it has 14.5k rows --- src/examples/provable-methods/rsa.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 906adf3fc4..8f5a6d95dd 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -79,7 +79,7 @@ function multiply(x: Bigint2048, y: Bigint2048, p: Bigint2048) { } // last carry is 0 ==> xy - qp - r is 0 - let res_i = res[2 * 18 - 1].add(carry); + let res_i = res[2 * 18 - 2].add(carry); res_i.assertEquals(0n); return r; @@ -139,3 +139,16 @@ function rangeCheck128(x: Field) { x0.add(x1.mul(1n << 116n)).assertEquals(x); } + +let TODO = 0n; + +let { rows, gates } = Provable.constraintSystem(() => { + let message = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); + let signature = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); + let modulus = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); + + rsaVerify65537(message, signature, modulus); +}); + +console.log('gates', gates); +console.log('rows', rows); From 4c49f26a12da2ce7c89074e8dc35debb19cc8d27 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sun, 5 Nov 2023 22:24:59 +0100 Subject: [PATCH 06/36] down to 12k with special squaring --- src/examples/provable-methods/rsa.ts | 62 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 8f5a6d95dd..64779fa49d 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -15,6 +15,10 @@ class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { return multiply(x, y, this); } + modsquare(x: Bigint2048) { + return square(x, this); + } + toBigInt() { return this.value; } @@ -85,6 +89,62 @@ function multiply(x: Bigint2048, y: Bigint2048, p: Bigint2048) { return r; } +/** + * x^2 mod p + */ +function square(x: Bigint2048, p: Bigint2048) { + // witness q, r so that x^2 = q*p + r + // this also adds the range checks in `check()` + let { q, r } = Provable.witness( + provable({ q: Bigint2048, r: Bigint2048 }), + () => { + let xx = x.toBigInt() ** 2n; + let p0 = p.toBigInt(); + let q = xx / p0; + let r = xx - q * p0; + return { q: Bigint2048.from(q), r: Bigint2048.from(r) }; + } + ); + + let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); + let [X, Q, R, P] = [x.fields, q.fields, r.fields, p.fields]; + + for (let i = 0; i < 18; i++) { + for (let j = 0; j < i; j++) { + let xy = X[i].mul(X[j]).mul(2n); + res[i + j] = res[i + j].add(xy); + } + let xy = X[i].mul(X[i]); + res[2 * i] = res[2 * i].add(xy); + + for (let j = 0; j < 18; j++) { + let qp = Q[i].mul(P[j]); + res[i + j] = res[i + j].sub(qp); + } + } + for (let i = 0; i < 18; i++) { + res[i] = res[i].sub(R[i]); + } + + // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 + let carry = Field(0); + + for (let i = 0; i < 2 * 18 - 2; i++) { + let res_i = res[i].add(carry); + + [carry] = Provable.witnessFields(1, () => [res_i.toBigInt() >> 116n]); + rangeCheck128(carry); + + res_i.assertEquals(carry.mul(1n << 116n)); + } + + // last carry is 0 ==> xy - qp - r is 0 + let res_i = res[2 * 18 - 2].add(carry); + res_i.assertEquals(0n); + + return r; +} + /** * RSA signature verification * @@ -100,7 +160,7 @@ function rsaVerify65537( // square 16 times let x = signature; for (let i = 0; i < 16; i++) { - x = modulus.modmul(x, x); + x = modulus.modsquare(x); } // multiply by signature x = modulus.modmul(x, signature); From 9eae3277c8dae5e9319b53a9e48e0f4f9c257137 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 6 Nov 2023 12:21:30 +0100 Subject: [PATCH 07/36] some fixes, collapse mul and square --- src/examples/provable-methods/rsa.ts | 120 ++++++++++----------------- 1 file changed, 45 insertions(+), 75 deletions(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 64779fa49d..6d0768ad49 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -16,7 +16,7 @@ class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { } modsquare(x: Bigint2048) { - return square(x, this); + return multiply(x, x, this, { isSquare: true }); } toBigInt() { @@ -25,11 +25,12 @@ class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { static from(x: bigint) { let fields = []; + let value = x; for (let i = 0; i < 18; i++) { fields.push(Field(x & mask)); x >>= 116n; } - return new Bigint2048({ fields, value: x }); + return new Bigint2048({ fields, value }); } static check(x: { fields: Field[] }) { @@ -42,7 +43,14 @@ class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { /** * x*y mod p */ -function multiply(x: Bigint2048, y: Bigint2048, p: Bigint2048) { +function multiply( + x: Bigint2048, + y: Bigint2048, + p: Bigint2048, + { isSquare = false } = {} +) { + if (isSquare) y = x; + // witness q, r so that x*y = q*p + r // this also adds the range checks in `check()` let { q, r } = Provable.witness( @@ -56,91 +64,51 @@ function multiply(x: Bigint2048, y: Bigint2048, p: Bigint2048) { } ); + // compute res = xy - qp - r + // note that we can use a sum of native field products for each result limb, because + // input limbs are range-checked to 116 bits, and 2*116 + log(2*18-1) = 232 + 6 fits the native field. let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); let [X, Y, Q, R, P] = [x.fields, y.fields, q.fields, r.fields, p.fields]; for (let i = 0; i < 18; i++) { - for (let j = 0; j < 18; j++) { - let xy = X[i].mul(Y[j]); - let qp = Q[i].mul(P[j]); - res[i + j] = res[i + j].add(xy).sub(qp); + // when squaring, we can save constraints by not computing xi * xj twice + if (isSquare) { + for (let j = 0; j < i; j++) { + let xy = X[i].mul(X[j]).mul(2n); + res[i + j] = res[i + j].add(xy); + } + let xy = X[i].mul(X[i]); + res[2 * i] = res[2 * i].add(xy); + } else { + for (let j = 0; j < 18; j++) { + let xy = X[i].mul(Y[j]); + res[i + j] = res[i + j].add(xy); + } } - } - for (let i = 0; i < 18; i++) { - res[i] = res[i].sub(R[i]); - } - - // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 - let carry = Field(0); - - for (let i = 0; i < 2 * 18 - 2; i++) { - let res_i = res[i].add(carry); - - [carry] = Provable.witnessFields(1, () => [res_i.toBigInt() >> 116n]); - rangeCheck128(carry); - - res_i.assertEquals(carry.mul(1n << 116n)); - } - - // last carry is 0 ==> xy - qp - r is 0 - let res_i = res[2 * 18 - 2].add(carry); - res_i.assertEquals(0n); - - return r; -} - -/** - * x^2 mod p - */ -function square(x: Bigint2048, p: Bigint2048) { - // witness q, r so that x^2 = q*p + r - // this also adds the range checks in `check()` - let { q, r } = Provable.witness( - provable({ q: Bigint2048, r: Bigint2048 }), - () => { - let xx = x.toBigInt() ** 2n; - let p0 = p.toBigInt(); - let q = xx / p0; - let r = xx - q * p0; - return { q: Bigint2048.from(q), r: Bigint2048.from(r) }; - } - ); - - let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); - let [X, Q, R, P] = [x.fields, q.fields, r.fields, p.fields]; - - for (let i = 0; i < 18; i++) { - for (let j = 0; j < i; j++) { - let xy = X[i].mul(X[j]).mul(2n); - res[i + j] = res[i + j].add(xy); - } - let xy = X[i].mul(X[i]); - res[2 * i] = res[2 * i].add(xy); for (let j = 0; j < 18; j++) { let qp = Q[i].mul(P[j]); res[i + j] = res[i + j].sub(qp); } - } - for (let i = 0; i < 18; i++) { + res[i] = res[i].sub(R[i]); } - // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 + // perform carrying on res to show that it is zero let carry = Field(0); for (let i = 0; i < 2 * 18 - 2; i++) { let res_i = res[i].add(carry); [carry] = Provable.witnessFields(1, () => [res_i.toBigInt() >> 116n]); - rangeCheck128(carry); + rangeCheck128Signed(carry); + // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 res_i.assertEquals(carry.mul(1n << 116n)); } // last carry is 0 ==> xy - qp - r is 0 - let res_i = res[2 * 18 - 2].add(carry); - res_i.assertEquals(0n); + res[2 * 18 - 2].add(carry).assertEquals(0n); return r; } @@ -148,8 +116,8 @@ function square(x: Bigint2048, p: Bigint2048) { /** * RSA signature verification * - * TODO this is a bit simplistic, according to RSA spec, message must be 256 bits and the remaining - * bits must follow a specific pattern. + * TODO this is a bit simplistic; according to RSA spec, message must be 256 bits + * and the remaining bits must follow a specific pattern. */ function rsaVerify65537( message: Bigint2048, @@ -170,34 +138,36 @@ function rsaVerify65537( } /** - * Custom range check for a single limb + * Custom range check for a single limb, x in [0, 2^116) */ function rangeCheck116(x: Field) { let [x0, x1] = Provable.witnessFields(2, () => [ - x.toBigInt() & mask, - x.toBigInt() >> 116n, + x.toBigInt() & ((1n << 64n) - 1n), + x.toBigInt() >> 64n, ]); Gadgets.rangeCheck64(x0); let [x52] = Gadgets.rangeCheck64(x1); x52.assertEquals(0n); - x0.add(x1.mul(1n << 116n)).assertEquals(x); + x0.add(x1.mul(1n << 64n)).assertEquals(x); } /** - * Custom range check for carries + * Custom range check for carries, x in [-2^127, 2^127) */ -function rangeCheck128(x: Field) { +function rangeCheck128Signed(xSigned: Field) { + let x = xSigned.add(1n << 127n); + let [x0, x1] = Provable.witnessFields(2, () => [ - x.toBigInt() & mask, - x.toBigInt() >> 116n, + x.toBigInt() & ((1n << 64n) - 1n), + x.toBigInt() >> 64n, ]); Gadgets.rangeCheck64(x0); Gadgets.rangeCheck64(x1); - x0.add(x1.mul(1n << 116n)).assertEquals(x); + x0.add(x1.mul(1n << 64n)).assertEquals(x); } let TODO = 0n; From 36b55d8f8f29a6edb38893cafe5d1ae98fcdb22e Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 6 Nov 2023 12:33:37 +0100 Subject: [PATCH 08/36] improve some comments --- src/examples/provable-methods/rsa.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 6d0768ad49..5a0d226051 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -65,7 +65,7 @@ function multiply( ); // compute res = xy - qp - r - // note that we can use a sum of native field products for each result limb, because + // we can use a sum of native field products for each result limb, because // input limbs are range-checked to 116 bits, and 2*116 + log(2*18-1) = 232 + 6 fits the native field. let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); let [X, Y, Q, R, P] = [x.fields, y.fields, q.fields, r.fields, p.fields]; @@ -74,21 +74,17 @@ function multiply( // when squaring, we can save constraints by not computing xi * xj twice if (isSquare) { for (let j = 0; j < i; j++) { - let xy = X[i].mul(X[j]).mul(2n); - res[i + j] = res[i + j].add(xy); + res[i + j] = res[i + j].add(X[i].mul(X[j]).mul(2n)); } - let xy = X[i].mul(X[i]); - res[2 * i] = res[2 * i].add(xy); + res[2 * i] = res[2 * i].add(X[i].mul(X[i])); } else { for (let j = 0; j < 18; j++) { - let xy = X[i].mul(Y[j]); - res[i + j] = res[i + j].add(xy); + res[i + j] = res[i + j].add(X[i].mul(Y[j])); } } for (let j = 0; j < 18; j++) { - let qp = Q[i].mul(P[j]); - res[i + j] = res[i + j].sub(qp); + res[i + j] = res[i + j].sub(Q[i].mul(P[j])); } res[i] = res[i].sub(R[i]); @@ -104,10 +100,11 @@ function multiply( rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 + // proves that bits i*116 to (i+1)*116 of res are zero res_i.assertEquals(carry.mul(1n << 116n)); } - // last carry is 0 ==> xy - qp - r is 0 + // last carry is 0 ==> all of res is 0 ==> x*y = q*p + r as integers res[2 * 18 - 2].add(carry).assertEquals(0n); return r; @@ -148,8 +145,8 @@ function rangeCheck116(x: Field) { Gadgets.rangeCheck64(x0); let [x52] = Gadgets.rangeCheck64(x1); - x52.assertEquals(0n); - + x52.assertEquals(0n); // => x1 is 52 bits + // 64 + 52 = 116 x0.add(x1.mul(1n << 64n)).assertEquals(x); } @@ -180,5 +177,8 @@ let { rows, gates } = Provable.constraintSystem(() => { rsaVerify65537(message, signature, modulus); }); -console.log('gates', gates); +console.log( + 'gates', + gates.map((g) => g.type) +); console.log('rows', rows); From e57c4463805da16c4b546ec403f7ba80a50e0176 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 6 Nov 2023 12:40:07 +0100 Subject: [PATCH 09/36] add a zkprogram for testing --- src/examples/provable-methods/rsa.ts | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 5a0d226051..47cf811fb3 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -1,7 +1,8 @@ /** * RSA signature verification with o1js */ -import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; +import { Field, Gadgets, Provable, Struct, ZkProgram, provable } from 'o1js'; +import { tic, toc } from '../utils/tic-toc.node.js'; const mask = (1n << 116n) - 1n; @@ -167,18 +168,28 @@ function rangeCheck128Signed(xSigned: Field) { x0.add(x1.mul(1n << 64n)).assertEquals(x); } -let TODO = 0n; +let rsa = ZkProgram({ + name: 'rsa-verify', -let { rows, gates } = Provable.constraintSystem(() => { - let message = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); - let signature = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); - let modulus = Provable.witness(Bigint2048, () => Bigint2048.from(TODO)); + methods: { + verify: { + privateInputs: [Bigint2048, Bigint2048, Bigint2048], - rsaVerify65537(message, signature, modulus); + method(message: Bigint2048, signature: Bigint2048, modulus: Bigint2048) { + rsaVerify65537(message, signature, modulus); + }, + }, + }, }); +let [{ rows, gates }] = rsa.analyzeMethods(); + console.log( 'gates', gates.map((g) => g.type) ); console.log('rows', rows); + +tic('compile'); +await rsa.compile(); +toc(); From d4a127281c69035a420ee58af93974917b1adf9e Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 23 Jan 2024 10:41:41 +0300 Subject: [PATCH 10/36] Trigger Build From 121f34d4921dcb385dc1df373040675aaba999ac Mon Sep 17 00:00:00 2001 From: Gregor Date: Wed, 14 Feb 2024 18:27:20 +0100 Subject: [PATCH 11/36] account for underflow --- src/examples/provable-methods/rsa.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/provable-methods/rsa.ts index 47cf811fb3..62cec7dc45 100644 --- a/src/examples/provable-methods/rsa.ts +++ b/src/examples/provable-methods/rsa.ts @@ -97,7 +97,11 @@ function multiply( for (let i = 0; i < 2 * 18 - 2; i++) { let res_i = res[i].add(carry); - [carry] = Provable.witnessFields(1, () => [res_i.toBigInt() >> 116n]); + [carry] = Provable.witnessFields(1, () => { + let res_in = res_i.toBigInt(); + if (res_in > 1n << 128n) res_in -= Field.ORDER; + return [res_in >> 116n]; + }); rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 @@ -182,13 +186,10 @@ let rsa = ZkProgram({ }, }); -let [{ rows, gates }] = rsa.analyzeMethods(); +let { verify } = rsa.analyzeMethods(); -console.log( - 'gates', - gates.map((g) => g.type) -); -console.log('rows', rows); +console.log(verify.summary()); +console.log('rows', verify.rows); tic('compile'); await rsa.compile(); From 8c495d75dde7eb29097fc3d0936628dd96fce60d Mon Sep 17 00:00:00 2001 From: Gregor Date: Fri, 19 Apr 2024 15:33:29 +0200 Subject: [PATCH 12/36] move rsa example --- src/examples/crypto/README.md | 4 ++++ src/examples/{provable-methods => crypto}/rsa.ts | 0 src/examples/provable-methods/README.md | 4 ---- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/examples/{provable-methods => crypto}/rsa.ts (100%) delete mode 100644 src/examples/provable-methods/README.md diff --git a/src/examples/crypto/README.md b/src/examples/crypto/README.md index c2f913defa..4962272875 100644 --- a/src/examples/crypto/README.md +++ b/src/examples/crypto/README.md @@ -4,3 +4,7 @@ These examples show how to use some of the crypto primitives that are supported - Non-native field arithmetic: `foreign-field.ts` - Non-native ECDSA verification: `ecdsa.ts` + +As a more low-level example, you will also fine an RSA implementation using a custom bigint type implemented using efficient o1js range checks: + +- RSA signature verification: `rsa.ts` diff --git a/src/examples/provable-methods/rsa.ts b/src/examples/crypto/rsa.ts similarity index 100% rename from src/examples/provable-methods/rsa.ts rename to src/examples/crypto/rsa.ts diff --git a/src/examples/provable-methods/README.md b/src/examples/provable-methods/README.md deleted file mode 100644 index ee6ad235b6..0000000000 --- a/src/examples/provable-methods/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Provable methods - -This folder showcases advanced examples of writing provable code. - From 8bb1e031c98a81aa9613f1c414349481cea39059 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Fri, 19 Apr 2024 16:01:36 +0100 Subject: [PATCH 13/36] Add new directory for rsa and move the existing rsa.ts file --- src/examples/crypto/{ => rsa}/rsa.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/examples/crypto/{ => rsa}/rsa.ts (100%) diff --git a/src/examples/crypto/rsa.ts b/src/examples/crypto/rsa/rsa.ts similarity index 100% rename from src/examples/crypto/rsa.ts rename to src/examples/crypto/rsa/rsa.ts From 5aa9faf347fefd3ccd66759f3d28e1e4c1d2d3f3 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Fri, 19 Apr 2024 21:53:44 +0100 Subject: [PATCH 14/36] Fix carry computation bug in multiplication for provable Bigint2048 and Refactor rangeCheck128Signed function --- src/examples/crypto/rsa/rsa.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/examples/crypto/rsa/rsa.ts b/src/examples/crypto/rsa/rsa.ts index ddbf2d8b34..faf4990ddf 100644 --- a/src/examples/crypto/rsa/rsa.ts +++ b/src/examples/crypto/rsa/rsa.ts @@ -2,7 +2,12 @@ * RSA signature verification with o1js */ import { Field, Gadgets, Provable, Struct, ZkProgram, provable } from 'o1js'; -import { tic, toc } from '../utils/tic-toc.node.js'; +import { tic, toc } from '../../utils/tic-toc.node.js'; + +export { + Bigint2048, + rsaVerify65537, +} const mask = (1n << 116n) - 1n; @@ -97,11 +102,7 @@ function multiply( for (let i = 0; i < 2 * 18 - 2; i++) { let res_i = res[i].add(carry); - [carry] = Provable.witnessFields(1, () => { - let res_in = res_i.toBigInt(); - if (res_in > 1n << 128n) res_in -= Field.ORDER; - return [res_in >> 116n]; - }); + carry = res_i.div(2n ** 116n); rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 @@ -160,16 +161,9 @@ function rangeCheck116(x: Field) { */ function rangeCheck128Signed(xSigned: Field) { let x = xSigned.add(1n << 127n); - - let [x0, x1] = Provable.witnessFields(2, () => [ - x.toBigInt() & ((1n << 64n) - 1n), - x.toBigInt() >> 64n, - ]); - - Gadgets.rangeCheck64(x0); - Gadgets.rangeCheck64(x1); - - x0.add(x1.mul(1n << 64n)).assertEquals(x); + Gadgets + .isDefinitelyInRangeN(128, x) + .assertTrue("BigInt carry should not exceed 128 bits!"); } let rsa = ZkProgram({ From 3dcb99a12bb6f2e7d51019cee098acd7b5184637 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Fri, 19 Apr 2024 21:54:59 +0100 Subject: [PATCH 15/36] Add big-integer dependency --- package-lock.json | 9 +++++++++ package.json | 1 + 2 files changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index b31f159112..83530c1960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.18.0", "license": "Apache-2.0", "dependencies": { + "big-integer": "^1.6.52", "blakejs": "1.2.1", "cachedir": "^2.4.0", "isomorphic-fetch": "^3.0.0", @@ -2358,6 +2359,14 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", diff --git a/package.json b/package.json index 78b5592fef..824a4c4201 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "typescript": "5.1" }, "dependencies": { + "big-integer": "^1.6.52", "blakejs": "1.2.1", "cachedir": "^2.4.0", "isomorphic-fetch": "^3.0.0", From 4a6950d32403f2cd314d876b32465e87340aa55f Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Fri, 19 Apr 2024 23:55:10 +0100 Subject: [PATCH 16/36] Add test utility functions for generating RSA parameters --- src/examples/crypto/rsa/testUtils.ts | 92 ++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/examples/crypto/rsa/testUtils.ts diff --git a/src/examples/crypto/rsa/testUtils.ts b/src/examples/crypto/rsa/testUtils.ts new file mode 100644 index 0000000000..631dc8dceb --- /dev/null +++ b/src/examples/crypto/rsa/testUtils.ts @@ -0,0 +1,92 @@ +import { createHash } from 'crypto'; +import bigInt from 'big-integer'; + +export { + generateDigestBigint, + toBigInt, + generateRandomPrime, + generateRsaParams, + rsaSign, +} + +/** + * Generates a SHA-256 digest of the input message and returns the hash as a native bigint. + * @param message - The input message to be hashed. + * @returns The SHA-256 hash of the input message as a native bigint. + */ +function generateDigestBigint(message: string) { + const digest = createHash('sha256').update(message, 'utf8').digest('hex'); + return BigInt('0x' + digest); +} + +/** + * Converts a big-integer object to a native bigint. + * @param x - The big-integer object to be converted. + * @returns The big-integer value converted to a native bigint. + */ +function toBigInt(x: bigInt.BigInteger): bigint { + return BigInt('0x' + x.toString(16)); +} + +/** + * Generates a random prime number with the specified bit length. + * @param bitLength - The desired bit length of the prime number. Default is 1024. + * @returns A random prime number with the specified bit length. + */ +function generateRandomPrime(bitLength: number): bigint { + let primeCandidate; + do { + // Generate a random number with the desired bit length + primeCandidate = bigInt.randBetween( + bigInt(2).pow(bitLength - 1), // Lower bound + bigInt(2).pow(bitLength).minus(1) // Upper bound + ); + + // Ensure the number is odd + if (!primeCandidate.isOdd()) { + primeCandidate = primeCandidate.add(1); + } + } while (!primeCandidate.isPrime()); + + return toBigInt(primeCandidate); +} + +/** + * Generates RSA parameters including prime numbers, public exponent, and private exponent. + * @param primeSize - The bit size of the prime numbers used for generating the RSA parameters. + * @returns An object containing the RSA parameters: + * - p (prime), + * - q (prime), + * - n (modulus), + * - phiN (Euler's totient function), + * - e (public exponent), + * - d (private exponent). + */ +function generateRsaParams(primeSize: number) { + // Generate two random prime numbers + const p = generateRandomPrime(primeSize / 2); + const q = generateRandomPrime(primeSize / 2); + + // Public exponent + const e = 65537n; + + // Euler's totient function + const phiN = (p - 1n) * (q - 1n); + + // Private exponent + const d = toBigInt(bigInt(e).modInv(phiN)); + + return { p, q, n: p * q, phiN, e, d }; +} + +/** + * Generates an RSA signature for the given message using the private key and modulus. + * @param message - The message to be signed. + * @param privateKey - The private exponent used for signing. + * @param modulus - The modulus used for signing. + * @returns The RSA signature of the message. + */ +function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { + // Calculate the signature using modular exponentiation + return toBigInt(bigInt(message).modPow(privateKey, modulus)); +} From ab5b4c15e445bd094e14b869c773eec98780c0d1 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Sat, 20 Apr 2024 00:00:12 +0100 Subject: [PATCH 17/36] Add tests for provable RSA65537 signature verification --- src/examples/crypto/rsa/rsa.test.ts | 171 ++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/examples/crypto/rsa/rsa.test.ts diff --git a/src/examples/crypto/rsa/rsa.test.ts b/src/examples/crypto/rsa/rsa.test.ts new file mode 100644 index 0000000000..b8b6394dfa --- /dev/null +++ b/src/examples/crypto/rsa/rsa.test.ts @@ -0,0 +1,171 @@ +import bigInt from 'big-integer'; + +import { + Bigint2048, + rsaVerify65537 +} from '../../../../dist/examples/crypto/rsa/rsa.js'; + +import { + generateDigestBigint, + generateRsaParams, + rsaSign, +} from '../../../../dist/examples/crypto/rsa/testUtils.js'; + +//TODO Refactor tests +describe('RSA65537 verification tests', () => { + it('should accept a simple RSA signature', () => { + const message = Bigint2048.from(4n); + const rsaSig = Bigint2048.from(31n); + const modul = Bigint2048.from(33n); + + // Provable RSA verification + rsaVerify65537(message, rsaSig, modul); + }); + + // Params imported from https://github.com/rzcoder/node-rsa#:~:text=key.importKey(%7B,%2C%20%27components%27)%3B + it('should accept RSA signature with hardcoded valid parameters', () => { + const params = { + n: BigInt('0x0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783'), + e: 65537n, + d: BigInt('0x5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1'), + p: BigInt('0x00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927f'), + q: BigInt('0x00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fd'), + dmp1: BigInt('0x008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85'), + dmq1: BigInt('0x1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21'), + coeff: BigInt('0x00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45') + } + + const message = Bigint2048.from(13n); + const rsaSig = Bigint2048.from(BigInt('0x' + (bigInt(13n).modPow(params.d, params.n)).toString(16))); + const modul = Bigint2048.from(params.n); + + rsaVerify65537(message, rsaSig, modul); + }); + + it('should accept RSA signature with randomly generated parameters: 512-bits', () => { + const input = generateDigestBigint('hello there!'); + const params = generateRsaParams(512); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + rsaVerify65537(message, signature, modulus); + }); + + it.skip('should accept RSA signature with randomly generated parameters: 512-bits - 100 iterations', () => { + for (let i = 0; i < 100; i++) { + const input = generateDigestBigint('hello there!'); + const params = generateRsaParams(512); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + rsaVerify65537(message, signature, modulus); + } + }); + + it('should accept RSA signature with randomly generated parameters: 1024-bits', () => { + const input = generateDigestBigint('how is it going!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + rsaVerify65537(message, signature, modulus); + }); + + it.skip('should accept RSA signature with randomly generated parameters: 1024-bits - 100 iterations', () => { + for (let i = 0; i < 50; i++) { + const input = generateDigestBigint('how is it going!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); // domain public key + + rsaVerify65537(message, signature, modulus); + } + }); + + it('should accept RSA signature with randomly generated parameters: 2048-bits', () => { + const input = generateDigestBigint('how are you!'); + const params = generateRsaParams(2048); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + rsaVerify65537(message, signature, modulus); + }); + + it.skip('should accept RSA signature with randomly generated parameters: 2048-bits - 50 iterations', () => { + for (let i = 0; i < 25; i++) { + const input = generateDigestBigint('how are you!'); + const params = generateRsaParams(2048); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + rsaVerify65537(message, signature, modulus); + } + }); + + it('should reject RSA signature with randomly generated parameters larger than 2048-bits', () => { + const input = generateDigestBigint('how are you!'); + const params = generateRsaParams(2560); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); + }); + + it('should reject RSA signature with non-compliant modulus: 1024-bits', () => { + const input = generateDigestBigint('hello!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.phiN); // Tamper with modulus + + expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); + }); + + it('should reject RSA signature with non-compliant input: 1024-bits', () => { + const input = generateDigestBigint('hello!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(35n); // Tamper with input + const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); + const modulus = Bigint2048.from(params.n); + + expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); + }); + + it('should reject non compliant RSA signature: false private key: 1024-bits', () => { + const input = generateDigestBigint('hello!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.e, params.n)); // Tamper with private key + const modulus = Bigint2048.from(params.n); + + expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); + }); + + it('should reject non-compliant RSA signature: false signature modulus : 1024-bits', () => { + const input = generateDigestBigint('hello!'); + const params = generateRsaParams(1024); + + const message = Bigint2048.from(input); + const signature = Bigint2048.from(rsaSign(input, params.d, 1223n)); // Tamper with signature modulus + const modulus = Bigint2048.from(params.n); + + expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); + }); +}); \ No newline at end of file From 078f51c4994eccbcb9ce916b77e70037885b9649 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Sat, 20 Apr 2024 10:18:17 +0100 Subject: [PATCH 18/36] Move RSA ZkProgram to a new file named run.ts --- src/examples/crypto/rsa/rsa.ts | 32 ++----------------- src/examples/crypto/rsa/run.ts | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 src/examples/crypto/rsa/run.ts diff --git a/src/examples/crypto/rsa/rsa.ts b/src/examples/crypto/rsa/rsa.ts index faf4990ddf..9cf574f2ca 100644 --- a/src/examples/crypto/rsa/rsa.ts +++ b/src/examples/crypto/rsa/rsa.ts @@ -1,8 +1,7 @@ /** * RSA signature verification with o1js */ -import { Field, Gadgets, Provable, Struct, ZkProgram, provable } from 'o1js'; -import { tic, toc } from '../../utils/tic-toc.node.js'; +import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; export { Bigint2048, @@ -164,31 +163,4 @@ function rangeCheck128Signed(xSigned: Field) { Gadgets .isDefinitelyInRangeN(128, x) .assertTrue("BigInt carry should not exceed 128 bits!"); -} - -let rsa = ZkProgram({ - name: 'rsa-verify', - - methods: { - verify: { - privateInputs: [Bigint2048, Bigint2048, Bigint2048], - - async method( - message: Bigint2048, - signature: Bigint2048, - modulus: Bigint2048 - ) { - rsaVerify65537(message, signature, modulus); - }, - }, - }, -}); - -let { verify } = await rsa.analyzeMethods(); - -console.log(verify.summary()); -console.log('rows', verify.rows); - -tic('compile'); -await rsa.compile(); -toc(); +} \ No newline at end of file diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts new file mode 100644 index 0000000000..9f6112d5f6 --- /dev/null +++ b/src/examples/crypto/rsa/run.ts @@ -0,0 +1,58 @@ + + + +import { ZkProgram } from 'o1js'; +import { + Bigint2048, + rsaVerify65537, +} from '../../../../dist/examples/crypto/rsa/rsa.js'; +import { + generateDigestBigint, + generateRsaParams, + rsaSign, +} from '../../../../dist/examples/crypto/rsa/testUtils.js'; + +let rsaZkProgram = ZkProgram({ + name: 'rsa-verify', + + methods: { + verifyRsa65537: { + privateInputs: [Bigint2048, Bigint2048, Bigint2048], + + async method( + message: Bigint2048, + signature: Bigint2048, + modulus: Bigint2048 + ) { + rsaVerify65537(message, signature, modulus); + }, + }, + }, +}); + +let { verifyRsa65537 } = await rsaZkProgram.analyzeMethods(); + +console.log(verifyRsa65537.summary()); +// console.log('rows', verifyRsa65537.rows); + +console.time('compile'); +const forceRecompileEnabled = false; +await rsaZkProgram.compile({ forceRecompile: forceRecompileEnabled }); +console.timeEnd('compile'); + +const input = generateDigestBigint('How are you!'); +const params = generateRsaParams(2048); + +console.time('generate RSA parameters: 2048 bits'); +const message = Bigint2048.from(input); +const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); +const modulus = Bigint2048.from(params.n); +console.timeEnd('generate RSA parameters: 2048 bits'); + +console.time('prove'); +let proof = await rsaZkProgram.verifyRsa65537(message, signature, modulus); +console.timeEnd('prove'); + +console.time('verify'); +await rsaZkProgram.verify(proof); +console.timeEnd('verify'); \ No newline at end of file From 8724c92702edc423bd28d25b323117c71b0a9717 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Sat, 20 Apr 2024 22:52:26 +0100 Subject: [PATCH 19/36] Remove empty lines --- src/examples/crypto/rsa/run.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts index 9f6112d5f6..02924ff0be 100644 --- a/src/examples/crypto/rsa/run.ts +++ b/src/examples/crypto/rsa/run.ts @@ -1,6 +1,3 @@ - - - import { ZkProgram } from 'o1js'; import { Bigint2048, From 0c26bf45de9b6a445925a76ea4c4a74777e6b623 Mon Sep 17 00:00:00 2001 From: Shigoto-dev19 Date: Sat, 20 Apr 2024 22:55:44 +0100 Subject: [PATCH 20/36] Minor fixes for constraints optimization --- src/examples/crypto/rsa/rsa.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/examples/crypto/rsa/rsa.ts b/src/examples/crypto/rsa/rsa.ts index 9cf574f2ca..5c88aa5f80 100644 --- a/src/examples/crypto/rsa/rsa.ts +++ b/src/examples/crypto/rsa/rsa.ts @@ -2,6 +2,7 @@ * RSA signature verification with o1js */ import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; +import { Fp } from '../../../bindings/crypto/finite-field.js'; export { Bigint2048, @@ -101,7 +102,10 @@ function multiply( for (let i = 0; i < 2 * 18 - 2; i++) { let res_i = res[i].add(carry); - carry = res_i.div(2n ** 116n); + carry = Provable.witness(Field, () => { + let res_in = res_i.toBigInt(); + return Field(res_in * (Fp.inverse(2n ** 116n) ?? 0n)); + }); rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 @@ -160,7 +164,15 @@ function rangeCheck116(x: Field) { */ function rangeCheck128Signed(xSigned: Field) { let x = xSigned.add(1n << 127n); - Gadgets - .isDefinitelyInRangeN(128, x) - .assertTrue("BigInt carry should not exceed 128 bits!"); + + let [x0, x1] = Provable.witness(Provable.Array(Field, 2), () => { + const x0 = x.toBigInt() & ((1n << 64n) - 1n); + const x1 = x.toBigInt() >> 64n; + return [x0, x1].map(Field) + }); + + Gadgets.rangeCheck64(x0); + Gadgets.rangeCheck64(x1); + + x0.add(x1.mul(1n << 64n)).assertEquals(x); } \ No newline at end of file From 5633ea938b074e621faa37cb21667ca292045bd1 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 20:24:58 +0200 Subject: [PATCH 21/36] autoformat --- src/examples/crypto/rsa/run.ts | 44 +++++++-------- src/examples/crypto/rsa/testUtils.ts | 82 ++++++++++++++-------------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts index 02924ff0be..59e360f8c1 100644 --- a/src/examples/crypto/rsa/run.ts +++ b/src/examples/crypto/rsa/run.ts @@ -1,30 +1,30 @@ import { ZkProgram } from 'o1js'; -import { - Bigint2048, - rsaVerify65537, +import { + Bigint2048, + rsaVerify65537, } from '../../../../dist/examples/crypto/rsa/rsa.js'; -import { - generateDigestBigint, - generateRsaParams, - rsaSign, +import { + generateDigestBigint, + generateRsaParams, + rsaSign, } from '../../../../dist/examples/crypto/rsa/testUtils.js'; let rsaZkProgram = ZkProgram({ - name: 'rsa-verify', - - methods: { - verifyRsa65537: { - privateInputs: [Bigint2048, Bigint2048, Bigint2048], - - async method( - message: Bigint2048, - signature: Bigint2048, - modulus: Bigint2048 - ) { - rsaVerify65537(message, signature, modulus); - }, - }, + name: 'rsa-verify', + + methods: { + verifyRsa65537: { + privateInputs: [Bigint2048, Bigint2048, Bigint2048], + + async method( + message: Bigint2048, + signature: Bigint2048, + modulus: Bigint2048 + ) { + rsaVerify65537(message, signature, modulus); + }, }, + }, }); let { verifyRsa65537 } = await rsaZkProgram.analyzeMethods(); @@ -52,4 +52,4 @@ console.timeEnd('prove'); console.time('verify'); await rsaZkProgram.verify(proof); -console.timeEnd('verify'); \ No newline at end of file +console.timeEnd('verify'); diff --git a/src/examples/crypto/rsa/testUtils.ts b/src/examples/crypto/rsa/testUtils.ts index 631dc8dceb..296b6a7afb 100644 --- a/src/examples/crypto/rsa/testUtils.ts +++ b/src/examples/crypto/rsa/testUtils.ts @@ -1,13 +1,13 @@ import { createHash } from 'crypto'; import bigInt from 'big-integer'; -export { - generateDigestBigint, - toBigInt, - generateRandomPrime, - generateRsaParams, - rsaSign, -} +export { + generateDigestBigint, + toBigInt, + generateRandomPrime, + generateRsaParams, + rsaSign, +}; /** * Generates a SHA-256 digest of the input message and returns the hash as a native bigint. @@ -15,8 +15,8 @@ export { * @returns The SHA-256 hash of the input message as a native bigint. */ function generateDigestBigint(message: string) { - const digest = createHash('sha256').update(message, 'utf8').digest('hex'); - return BigInt('0x' + digest); + const digest = createHash('sha256').update(message, 'utf8').digest('hex'); + return BigInt('0x' + digest); } /** @@ -25,7 +25,7 @@ function generateDigestBigint(message: string) { * @returns The big-integer value converted to a native bigint. */ function toBigInt(x: bigInt.BigInteger): bigint { - return BigInt('0x' + x.toString(16)); + return BigInt('0x' + x.toString(16)); } /** @@ -34,49 +34,49 @@ function toBigInt(x: bigInt.BigInteger): bigint { * @returns A random prime number with the specified bit length. */ function generateRandomPrime(bitLength: number): bigint { - let primeCandidate; - do { - // Generate a random number with the desired bit length - primeCandidate = bigInt.randBetween( - bigInt(2).pow(bitLength - 1), // Lower bound - bigInt(2).pow(bitLength).minus(1) // Upper bound - ); + let primeCandidate; + do { + // Generate a random number with the desired bit length + primeCandidate = bigInt.randBetween( + bigInt(2).pow(bitLength - 1), // Lower bound + bigInt(2).pow(bitLength).minus(1) // Upper bound + ); - // Ensure the number is odd - if (!primeCandidate.isOdd()) { - primeCandidate = primeCandidate.add(1); - } - } while (!primeCandidate.isPrime()); + // Ensure the number is odd + if (!primeCandidate.isOdd()) { + primeCandidate = primeCandidate.add(1); + } + } while (!primeCandidate.isPrime()); - return toBigInt(primeCandidate); + return toBigInt(primeCandidate); } /** * Generates RSA parameters including prime numbers, public exponent, and private exponent. * @param primeSize - The bit size of the prime numbers used for generating the RSA parameters. - * @returns An object containing the RSA parameters: - * - p (prime), - * - q (prime), - * - n (modulus), + * @returns An object containing the RSA parameters: + * - p (prime), + * - q (prime), + * - n (modulus), * - phiN (Euler's totient function), * - e (public exponent), * - d (private exponent). */ -function generateRsaParams(primeSize: number) { - // Generate two random prime numbers - const p = generateRandomPrime(primeSize / 2); - const q = generateRandomPrime(primeSize / 2); - - // Public exponent - const e = 65537n; +function generateRsaParams(primeSize: number) { + // Generate two random prime numbers + const p = generateRandomPrime(primeSize / 2); + const q = generateRandomPrime(primeSize / 2); + + // Public exponent + const e = 65537n; - // Euler's totient function - const phiN = (p - 1n) * (q - 1n); + // Euler's totient function + const phiN = (p - 1n) * (q - 1n); - // Private exponent - const d = toBigInt(bigInt(e).modInv(phiN)); + // Private exponent + const d = toBigInt(bigInt(e).modInv(phiN)); - return { p, q, n: p * q, phiN, e, d }; + return { p, q, n: p * q, phiN, e, d }; } /** @@ -87,6 +87,6 @@ function generateRsaParams(primeSize: number) { * @returns The RSA signature of the message. */ function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { - // Calculate the signature using modular exponentiation - return toBigInt(bigInt(message).modPow(privateKey, modulus)); + // Calculate the signature using modular exponentiation + return toBigInt(bigInt(message).modPow(privateKey, modulus)); } From 14f6e352b062cdd6fc19a9feb9dc789fe40e4029 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 20:26:34 +0200 Subject: [PATCH 22/36] rename --- src/examples/crypto/rsa/{testUtils.ts => utils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/examples/crypto/rsa/{testUtils.ts => utils.ts} (100%) diff --git a/src/examples/crypto/rsa/testUtils.ts b/src/examples/crypto/rsa/utils.ts similarity index 100% rename from src/examples/crypto/rsa/testUtils.ts rename to src/examples/crypto/rsa/utils.ts From 943008d806a75bcbd8b1f456e73d58e734d66ffe Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 20:28:30 +0200 Subject: [PATCH 23/36] fix run imports --- src/examples/crypto/rsa/run.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts index 59e360f8c1..ab6971614c 100644 --- a/src/examples/crypto/rsa/run.ts +++ b/src/examples/crypto/rsa/run.ts @@ -1,13 +1,6 @@ import { ZkProgram } from 'o1js'; -import { - Bigint2048, - rsaVerify65537, -} from '../../../../dist/examples/crypto/rsa/rsa.js'; -import { - generateDigestBigint, - generateRsaParams, - rsaSign, -} from '../../../../dist/examples/crypto/rsa/testUtils.js'; +import { Bigint2048, rsaVerify65537 } from './rsa.js'; +import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; let rsaZkProgram = ZkProgram({ name: 'rsa-verify', From 05b30145c66831e5cdfa9bad9378cdd12b6014c2 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 20:46:19 +0200 Subject: [PATCH 24/36] clean up mul circuit --- src/examples/crypto/rsa/rsa.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/examples/crypto/rsa/rsa.ts b/src/examples/crypto/rsa/rsa.ts index 5c88aa5f80..92a2bdc800 100644 --- a/src/examples/crypto/rsa/rsa.ts +++ b/src/examples/crypto/rsa/rsa.ts @@ -2,17 +2,13 @@ * RSA signature verification with o1js */ import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; -import { Fp } from '../../../bindings/crypto/finite-field.js'; -export { - Bigint2048, - rsaVerify65537, -} +export { Bigint2048, rsaVerify65537 }; const mask = (1n << 116n) - 1n; /** - * We use 116-bit limbs, which means 18 limbs for a 2048-bit numbers as used in RSA. + * We use 116-bit limbs, which means 18 limbs for 2048-bit numbers as used in RSA. */ const Field18 = Provable.Array(Field, 18); @@ -102,10 +98,7 @@ function multiply( for (let i = 0; i < 2 * 18 - 2; i++) { let res_i = res[i].add(carry); - carry = Provable.witness(Field, () => { - let res_in = res_i.toBigInt(); - return Field(res_in * (Fp.inverse(2n ** 116n) ?? 0n)); - }); + carry = Provable.witness(Field, () => res_i.div(1n << 116n)); rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 @@ -165,14 +158,14 @@ function rangeCheck116(x: Field) { function rangeCheck128Signed(xSigned: Field) { let x = xSigned.add(1n << 127n); - let [x0, x1] = Provable.witness(Provable.Array(Field, 2), () => { + let [x0, x1] = Provable.witnessFields(2, () => { const x0 = x.toBigInt() & ((1n << 64n) - 1n); const x1 = x.toBigInt() >> 64n; - return [x0, x1].map(Field) + return [x0, x1]; }); Gadgets.rangeCheck64(x0); Gadgets.rangeCheck64(x1); x0.add(x1.mul(1n << 64n)).assertEquals(x); -} \ No newline at end of file +} From d5dcffbd7f8cb4607268be4ac682772a3e62d2e8 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 21:06:10 +0200 Subject: [PATCH 25/36] make witnessFields work outside provable code --- src/lib/provable/provable.ts | 5 ++--- src/lib/provable/types/witness.ts | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/lib/provable/provable.ts b/src/lib/provable/provable.ts index 9d6018885f..07e4dbdc7f 100644 --- a/src/lib/provable/provable.ts +++ b/src/lib/provable/provable.ts @@ -21,9 +21,8 @@ import { constraintSystem, generateWitness, } from './core/provable-context.js'; -import { witness, witnessAsync } from './types/witness.js'; +import { witness, witnessAsync, witnessFields } from './types/witness.js'; import { InferValue } from '../../bindings/lib/provable-generic.js'; -import { exists } from './core/exists.js'; // external API export { Provable }; @@ -74,7 +73,7 @@ const Provable = { * but optimized for witnessing plain field elements, which is especially common * in low-level provable code. */ - witnessFields: exists, + witnessFields, /** * Create a new witness from an async callback. * diff --git a/src/lib/provable/types/witness.ts b/src/lib/provable/types/witness.ts index da219279ab..15efb266f4 100644 --- a/src/lib/provable/types/witness.ts +++ b/src/lib/provable/types/witness.ts @@ -7,8 +7,10 @@ import { } from '../core/provable-context.js'; import { exists, existsAsync } from '../core/exists.js'; import { From } from '../../../bindings/lib/provable-generic.js'; +import { TupleN } from '../../util/types.js'; +import { createField } from '../core/field-constructor.js'; -export { witness, witnessAsync }; +export { witness, witnessAsync, witnessFields }; function witness, T extends From = From>( type: A, @@ -81,6 +83,25 @@ async function witnessAsync< return value; } +function witnessFields< + N extends number, + C extends () => TupleN +>(size: N, compute: C): TupleN { + // outside provable code, we just call the callback and return its cloned result + if (!inCheckedComputation() || snarkContext.get().inWitnessBlock) { + let fields = compute().map((x) => createField(x)); + return TupleN.fromArray(size, fields); + } + + // call into `exists` to witness the field elements + return exists(size, () => { + let fields = compute().map((x) => + typeof x === 'bigint' ? x : x.toBigInt() + ); + return TupleN.fromArray(size, fields); + }); +} + function clone>(type: S, value: T): T { let fields = type.toFields(value); let aux = type.toAuxiliary?.(value) ?? []; From 1a816804cc9f626bba066d517eaf804ee3828dc9 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 21:08:39 +0200 Subject: [PATCH 26/36] start cleaning up rsa test --- src/examples/crypto/rsa/rsa.test.ts | 48 +++++++++++++---------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/src/examples/crypto/rsa/rsa.test.ts b/src/examples/crypto/rsa/rsa.test.ts index b8b6394dfa..bff9d72c40 100644 --- a/src/examples/crypto/rsa/rsa.test.ts +++ b/src/examples/crypto/rsa/rsa.test.ts @@ -1,44 +1,40 @@ import bigInt from 'big-integer'; - -import { - Bigint2048, - rsaVerify65537 -} from '../../../../dist/examples/crypto/rsa/rsa.js'; - -import { - generateDigestBigint, - generateRsaParams, - rsaSign, -} from '../../../../dist/examples/crypto/rsa/testUtils.js'; +import { Bigint2048, rsaVerify65537 } from './rsa.js'; +import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; +import { expect } from 'expect'; +import { it, describe } from 'node:test'; //TODO Refactor tests -describe('RSA65537 verification tests', () => { +describe('RSA65537 verification tests', () => { it('should accept a simple RSA signature', () => { const message = Bigint2048.from(4n); const rsaSig = Bigint2048.from(31n); const modul = Bigint2048.from(33n); - - // Provable RSA verification + + // Provable RSA verification rsaVerify65537(message, rsaSig, modul); }); // Params imported from https://github.com/rzcoder/node-rsa#:~:text=key.importKey(%7B,%2C%20%27components%27)%3B it('should accept RSA signature with hardcoded valid parameters', () => { const params = { - n: BigInt('0x0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783'), + n: 0x0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783n, e: 65537n, - d: BigInt('0x5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1'), - p: BigInt('0x00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927f'), - q: BigInt('0x00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fd'), - dmp1: BigInt('0x008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85'), - dmq1: BigInt('0x1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21'), - coeff: BigInt('0x00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45') - } + d: 0x5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1n, + p: 0x00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927fn, + q: 0x00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fdn, + dmp1: 0x008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85n, + dmq1: 0x1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21n, + coeff: + 0x00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45n, + }; const message = Bigint2048.from(13n); - const rsaSig = Bigint2048.from(BigInt('0x' + (bigInt(13n).modPow(params.d, params.n)).toString(16))); + const rsaSig = Bigint2048.from( + BigInt('0x' + bigInt(13n).modPow(params.d, params.n).toString(16)) + ); const modul = Bigint2048.from(params.n); - + rsaVerify65537(message, rsaSig, modul); }); @@ -84,7 +80,7 @@ describe('RSA65537 verification tests', () => { const message = Bigint2048.from(input); const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); - const modulus = Bigint2048.from(params.n); // domain public key + const modulus = Bigint2048.from(params.n); // domain public key rsaVerify65537(message, signature, modulus); } @@ -168,4 +164,4 @@ describe('RSA65537 verification tests', () => { expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); }); -}); \ No newline at end of file +}); From 8b2c30fcabeedb70c9d4a1b578799cdf96344a5f Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 21:18:30 +0200 Subject: [PATCH 27/36] replace modpow with bigint impl --- src/examples/crypto/rsa/utils.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/examples/crypto/rsa/utils.ts b/src/examples/crypto/rsa/utils.ts index 296b6a7afb..a80eaf39a4 100644 --- a/src/examples/crypto/rsa/utils.ts +++ b/src/examples/crypto/rsa/utils.ts @@ -1,4 +1,4 @@ -import { createHash } from 'crypto'; +import { createHash } from 'node:crypto'; import bigInt from 'big-integer'; export { @@ -88,5 +88,22 @@ function generateRsaParams(primeSize: number) { */ function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { // Calculate the signature using modular exponentiation - return toBigInt(bigInt(message).modPow(privateKey, modulus)); + return power(message, privateKey, modulus); +} + +// modular exponentiation, a^n % p +function power(a: bigint, n: bigint, p: bigint) { + a = mod(a, p); + let x = 1n; + for (; n > 0n; n >>= 1n) { + if (n & 1n) x = mod(x * a, p); + a = mod(a * a, p); + } + return x; +} + +function mod(x: bigint, p: bigint) { + x = x % p; + if (x < 0) return x + p; + return x; } From 4b6f0bdbfd8667d4f88d41bfae22184a5c250538 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Sat, 27 Apr 2024 21:29:49 +0200 Subject: [PATCH 28/36] replace modinv --- src/examples/crypto/rsa/utils.ts | 84 ++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/src/examples/crypto/rsa/utils.ts b/src/examples/crypto/rsa/utils.ts index a80eaf39a4..da9d478c6e 100644 --- a/src/examples/crypto/rsa/utils.ts +++ b/src/examples/crypto/rsa/utils.ts @@ -9,6 +9,18 @@ export { rsaSign, }; +/** + * Generates an RSA signature for the given message using the private key and modulus. + * @param message - The message to be signed. + * @param privateKey - The private exponent used for signing. + * @param modulus - The modulus used for signing. + * @returns The RSA signature of the message. + */ +function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { + // Calculate the signature using modular exponentiation + return power(message, privateKey, modulus); +} + /** * Generates a SHA-256 digest of the input message and returns the hash as a native bigint. * @param message - The input message to be hashed. @@ -28,29 +40,6 @@ function toBigInt(x: bigInt.BigInteger): bigint { return BigInt('0x' + x.toString(16)); } -/** - * Generates a random prime number with the specified bit length. - * @param bitLength - The desired bit length of the prime number. Default is 1024. - * @returns A random prime number with the specified bit length. - */ -function generateRandomPrime(bitLength: number): bigint { - let primeCandidate; - do { - // Generate a random number with the desired bit length - primeCandidate = bigInt.randBetween( - bigInt(2).pow(bitLength - 1), // Lower bound - bigInt(2).pow(bitLength).minus(1) // Upper bound - ); - - // Ensure the number is odd - if (!primeCandidate.isOdd()) { - primeCandidate = primeCandidate.add(1); - } - } while (!primeCandidate.isPrime()); - - return toBigInt(primeCandidate); -} - /** * Generates RSA parameters including prime numbers, public exponent, and private exponent. * @param primeSize - The bit size of the prime numbers used for generating the RSA parameters. @@ -74,23 +63,36 @@ function generateRsaParams(primeSize: number) { const phiN = (p - 1n) * (q - 1n); // Private exponent - const d = toBigInt(bigInt(e).modInv(phiN)); + const d = inverse(e, phiN); return { p, q, n: p * q, phiN, e, d }; } /** - * Generates an RSA signature for the given message using the private key and modulus. - * @param message - The message to be signed. - * @param privateKey - The private exponent used for signing. - * @param modulus - The modulus used for signing. - * @returns The RSA signature of the message. + * Generates a random prime number with the specified bit length. + * @param bitLength - The desired bit length of the prime number. Default is 1024. + * @returns A random prime number with the specified bit length. */ -function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { - // Calculate the signature using modular exponentiation - return power(message, privateKey, modulus); +function generateRandomPrime(bitLength: number): bigint { + let primeCandidate; + do { + // Generate a random number with the desired bit length + primeCandidate = bigInt.randBetween( + bigInt(2).pow(bitLength - 1), // Lower bound + bigInt(2).pow(bitLength).minus(1) // Upper bound + ); + + // Ensure the number is odd + if (!primeCandidate.isOdd()) { + primeCandidate = primeCandidate.add(1); + } + } while (!primeCandidate.isPrime()); + + return toBigInt(primeCandidate); } +// finite field helpers (copied here from src/bindings/crypto/finite-field.ts) + // modular exponentiation, a^n % p function power(a: bigint, n: bigint, p: bigint) { a = mod(a, p); @@ -102,6 +104,24 @@ function power(a: bigint, n: bigint, p: bigint) { return x; } +// modular inverse, 1/a in Z_p +function inverse(a_: bigint, p: bigint) { + let a = mod(a_, p); + if (a === 0n) throw Error('modular inverse: division by 0'); + let b = p; + let [x, y, u, v] = [0n, 1n, 1n, 0n]; + while (a !== 0n) { + let q = b / a; + [b, a] = [a, b - a * q]; + [x, u] = [u, x - u * q]; + [y, v] = [v, y - v * q]; + } + if (b !== 1n) throw Error('modular inverse failed (b != 1)'); + // TODO remove + if (mod(x * a_, p) !== 1n) throw Error('modular inverse failed'); + return mod(x, p); +} + function mod(x: bigint, p: bigint) { x = x % p; if (x < 0) return x + p; From 2b329cf4733c12ea107941c26b7fdd87af0fbef7 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 29 Apr 2024 09:07:36 +0200 Subject: [PATCH 29/36] primality test --- src/examples/crypto/rsa/utils.ts | 133 ++++++++++++++++++++++--------- 1 file changed, 95 insertions(+), 38 deletions(-) diff --git a/src/examples/crypto/rsa/utils.ts b/src/examples/crypto/rsa/utils.ts index da9d478c6e..162559e3b8 100644 --- a/src/examples/crypto/rsa/utils.ts +++ b/src/examples/crypto/rsa/utils.ts @@ -1,13 +1,6 @@ import { createHash } from 'node:crypto'; -import bigInt from 'big-integer'; -export { - generateDigestBigint, - toBigInt, - generateRandomPrime, - generateRsaParams, - rsaSign, -}; +export { generateDigestBigint, generateRsaParams, rsaSign }; /** * Generates an RSA signature for the given message using the private key and modulus. @@ -31,15 +24,6 @@ function generateDigestBigint(message: string) { return BigInt('0x' + digest); } -/** - * Converts a big-integer object to a native bigint. - * @param x - The big-integer object to be converted. - * @returns The big-integer value converted to a native bigint. - */ -function toBigInt(x: bigInt.BigInteger): bigint { - return BigInt('0x' + x.toString(16)); -} - /** * Generates RSA parameters including prime numbers, public exponent, and private exponent. * @param primeSize - The bit size of the prime numbers used for generating the RSA parameters. @@ -53,8 +37,8 @@ function toBigInt(x: bigInt.BigInteger): bigint { */ function generateRsaParams(primeSize: number) { // Generate two random prime numbers - const p = generateRandomPrime(primeSize / 2); - const q = generateRandomPrime(primeSize / 2); + const p = randomPrime(primeSize / 2); + const q = randomPrime(primeSize / 2); // Public exponent const e = 65537n; @@ -68,27 +52,86 @@ function generateRsaParams(primeSize: number) { return { p, q, n: p * q, phiN, e, d }; } +// random primes + /** - * Generates a random prime number with the specified bit length. - * @param bitLength - The desired bit length of the prime number. Default is 1024. - * @returns A random prime number with the specified bit length. + * returns a random prime of a given bit length (which is a multiple of 8) */ -function generateRandomPrime(bitLength: number): bigint { - let primeCandidate; - do { - // Generate a random number with the desired bit length - primeCandidate = bigInt.randBetween( - bigInt(2).pow(bitLength - 1), // Lower bound - bigInt(2).pow(bitLength).minus(1) // Upper bound - ); - - // Ensure the number is odd - if (!primeCandidate.isOdd()) { - primeCandidate = primeCandidate.add(1); +function randomPrime(bitLength: number) { + if (bitLength < 1) throw Error('bitLength must be at least 1'); + if (bitLength % 8 !== 0) throw Error('bitLength must be a multiple of 8'); + let byteLength = bitLength / 8; + + while (true) { + let p = randomBigintLength(byteLength); + + // enforce p has the full length + p |= 1n << BigInt(bitLength - 1); + + if (millerRabinTest(p) === 'probably prime') return p; + } +} + +// primality test +// after https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Miller%E2%80%93Rabin_test + +function millerRabinTest(n: bigint): 'composite' | 'probably prime' { + const k = 10; + if (n === 2n || n === 3n) return 'probably prime'; + if (n < 2n) return 'composite'; + // check if divisible by one of first few primes + for (let p of knownPrimes) { + if (n % p === 0n && n > p) return 'composite'; + } + + // write n - 1 = 2^r * d, d odd + let d = n - 1n; + let r = 0n; + for (; d % 2n !== 0n; d /= 2n, r++); + + WitnessLoop: for (let i = 0; i < k; i++) { + let a = randomBigintRange(2n, n - 2n); + // let x = a ** d % n; // too large + let x = power(a, d, n); + if (x === 1n || x === n - 1n) continue; + for (let i = 0; i + 1 < r; i++) { + x = (x * x) % n; + if (x === n - 1n) continue WitnessLoop; } - } while (!primeCandidate.isPrime()); + return 'composite'; + } + return 'probably prime'; +} - return toBigInt(primeCandidate); +// bigint helpers + +// random bigint in [min, max] +function randomBigintRange(min: bigint, max: bigint) { + let length = byteLength(max - min); + while (true) { + let n = randomBigintLength(length); + if (n <= max - min) return min + n; + } +} + +// random bigint in [0, 2^(8*byteLength)) +function randomBigintLength(byteLength: number) { + let bytes = crypto.getRandomValues(new Uint8Array(byteLength)); + return bytesToBigint(bytes); +} + +function bytesToBigint(bytes: Uint8Array | number[]) { + let x = 0n; + let bitPosition = 0n; + for (let byte of bytes) { + x += BigInt(byte) << bitPosition; + bitPosition += 8n; + } + return x; +} + +function byteLength(x: bigint) { + return Math.ceil(x.toString(16).length / 2); } // finite field helpers (copied here from src/bindings/crypto/finite-field.ts) @@ -117,8 +160,6 @@ function inverse(a_: bigint, p: bigint) { [y, v] = [v, y - v * q]; } if (b !== 1n) throw Error('modular inverse failed (b != 1)'); - // TODO remove - if (mod(x * a_, p) !== 1n) throw Error('modular inverse failed'); return mod(x, p); } @@ -127,3 +168,19 @@ function mod(x: bigint, p: bigint) { if (x < 0) return x + p; return x; } + +// primes up to 1000, to speed up miller-rabin +// prettier-ignore +let knownPrimes = [ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, + 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, + 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, + 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, + 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, + 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, + 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, + 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, + 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997 +].map(BigInt); From 79a2ff780eea3fd67c7362469deb4c8ae66d5654 Mon Sep 17 00:00:00 2001 From: Gregor Mitscha-Baude Date: Mon, 29 Apr 2024 09:08:06 +0200 Subject: [PATCH 30/36] clean up tests and remove big-integer lib --- package-lock.json | 9 ------ package.json | 1 - src/examples/crypto/rsa/rsa.test.ts | 47 +++++------------------------ src/examples/crypto/rsa/run.ts | 1 - 4 files changed, 7 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83530c1960..b31f159112 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.18.0", "license": "Apache-2.0", "dependencies": { - "big-integer": "^1.6.52", "blakejs": "1.2.1", "cachedir": "^2.4.0", "isomorphic-fetch": "^3.0.0", @@ -2359,14 +2358,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.52", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", - "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", - "engines": { - "node": ">=0.6" - } - }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", diff --git a/package.json b/package.json index 824a4c4201..78b5592fef 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,6 @@ "typescript": "5.1" }, "dependencies": { - "big-integer": "^1.6.52", "blakejs": "1.2.1", "cachedir": "^2.4.0", "isomorphic-fetch": "^3.0.0", diff --git a/src/examples/crypto/rsa/rsa.test.ts b/src/examples/crypto/rsa/rsa.test.ts index bff9d72c40..437621f64d 100644 --- a/src/examples/crypto/rsa/rsa.test.ts +++ b/src/examples/crypto/rsa/rsa.test.ts @@ -1,4 +1,3 @@ -import bigInt from 'big-integer'; import { Bigint2048, rsaVerify65537 } from './rsa.js'; import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; import { expect } from 'expect'; @@ -30,28 +29,16 @@ describe('RSA65537 verification tests', () => { }; const message = Bigint2048.from(13n); - const rsaSig = Bigint2048.from( - BigInt('0x' + bigInt(13n).modPow(params.d, params.n).toString(16)) - ); + const rsaSig = Bigint2048.from(rsaSign(13n, params.d, params.n)); const modul = Bigint2048.from(params.n); rsaVerify65537(message, rsaSig, modul); }); - it('should accept RSA signature with randomly generated parameters: 512-bits', () => { + it('should accept RSA signature with randomly generated parameters: 512-bits (20 iterations)', () => { const input = generateDigestBigint('hello there!'); - const params = generateRsaParams(512); - const message = Bigint2048.from(input); - const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); - const modulus = Bigint2048.from(params.n); - - rsaVerify65537(message, signature, modulus); - }); - - it.skip('should accept RSA signature with randomly generated parameters: 512-bits - 100 iterations', () => { - for (let i = 0; i < 100; i++) { - const input = generateDigestBigint('hello there!'); + for (let i = 0; i < 20; i++) { const params = generateRsaParams(512); const message = Bigint2048.from(input); @@ -62,20 +49,10 @@ describe('RSA65537 verification tests', () => { } }); - it('should accept RSA signature with randomly generated parameters: 1024-bits', () => { + it('should accept RSA signature with randomly generated parameters: 1024-bits (10 iterations)', () => { const input = generateDigestBigint('how is it going!'); - const params = generateRsaParams(1024); - const message = Bigint2048.from(input); - const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); - const modulus = Bigint2048.from(params.n); - - rsaVerify65537(message, signature, modulus); - }); - - it.skip('should accept RSA signature with randomly generated parameters: 1024-bits - 100 iterations', () => { - for (let i = 0; i < 50; i++) { - const input = generateDigestBigint('how is it going!'); + for (let i = 0; i < 10; i++) { const params = generateRsaParams(1024); const message = Bigint2048.from(input); @@ -86,20 +63,10 @@ describe('RSA65537 verification tests', () => { } }); - it('should accept RSA signature with randomly generated parameters: 2048-bits', () => { + it('should accept RSA signature with randomly generated parameters: 2048-bits (5 iterations)', () => { const input = generateDigestBigint('how are you!'); - const params = generateRsaParams(2048); - - const message = Bigint2048.from(input); - const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); - const modulus = Bigint2048.from(params.n); - - rsaVerify65537(message, signature, modulus); - }); - it.skip('should accept RSA signature with randomly generated parameters: 2048-bits - 50 iterations', () => { - for (let i = 0; i < 25; i++) { - const input = generateDigestBigint('how are you!'); + for (let i = 0; i < 5; i++) { const params = generateRsaParams(2048); const message = Bigint2048.from(input); diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts index ab6971614c..02e4ab204a 100644 --- a/src/examples/crypto/rsa/run.ts +++ b/src/examples/crypto/rsa/run.ts @@ -23,7 +23,6 @@ let rsaZkProgram = ZkProgram({ let { verifyRsa65537 } = await rsaZkProgram.analyzeMethods(); console.log(verifyRsa65537.summary()); -// console.log('rows', verifyRsa65537.rows); console.time('compile'); const forceRecompileEnabled = false; From 77e85f63c8c00603f581543644d68b9b9f697a46 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 09:15:23 +0200 Subject: [PATCH 31/36] cleanup --- src/examples/crypto/README.md | 4 ++-- src/examples/crypto/rsa/{rsa.test.ts => test.ts} | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) rename src/examples/crypto/rsa/{rsa.test.ts => test.ts} (99%) diff --git a/src/examples/crypto/README.md b/src/examples/crypto/README.md index 4962272875..929ec320d2 100644 --- a/src/examples/crypto/README.md +++ b/src/examples/crypto/README.md @@ -5,6 +5,6 @@ These examples show how to use some of the crypto primitives that are supported - Non-native field arithmetic: `foreign-field.ts` - Non-native ECDSA verification: `ecdsa.ts` -As a more low-level example, you will also fine an RSA implementation using a custom bigint type implemented using efficient o1js range checks: +As a more low-level example, you will also find an RSA implementation using a custom bigint type implemented using efficient o1js range checks: -- RSA signature verification: `rsa.ts` +- RSA signature verification: `rsa/` diff --git a/src/examples/crypto/rsa/rsa.test.ts b/src/examples/crypto/rsa/test.ts similarity index 99% rename from src/examples/crypto/rsa/rsa.test.ts rename to src/examples/crypto/rsa/test.ts index 437621f64d..8c3036bfd2 100644 --- a/src/examples/crypto/rsa/rsa.test.ts +++ b/src/examples/crypto/rsa/test.ts @@ -3,7 +3,6 @@ import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; import { expect } from 'expect'; import { it, describe } from 'node:test'; -//TODO Refactor tests describe('RSA65537 verification tests', () => { it('should accept a simple RSA signature', () => { const message = Bigint2048.from(4n); From 759f85f67aaa441db65b185e6a5cf97d9ad682a4 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 09:49:43 +0200 Subject: [PATCH 32/36] get rid of nodejs stdlib dependency --- src/examples/crypto/rsa/utils.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/examples/crypto/rsa/utils.ts b/src/examples/crypto/rsa/utils.ts index 162559e3b8..3b505f201c 100644 --- a/src/examples/crypto/rsa/utils.ts +++ b/src/examples/crypto/rsa/utils.ts @@ -1,6 +1,4 @@ -import { createHash } from 'node:crypto'; - -export { generateDigestBigint, generateRsaParams, rsaSign }; +export { sha256Bigint, generateRsaParams, rsaSign, randomPrime }; /** * Generates an RSA signature for the given message using the private key and modulus. @@ -19,26 +17,24 @@ function rsaSign(message: bigint, privateKey: bigint, modulus: bigint): bigint { * @param message - The input message to be hashed. * @returns The SHA-256 hash of the input message as a native bigint. */ -function generateDigestBigint(message: string) { - const digest = createHash('sha256').update(message, 'utf8').digest('hex'); - return BigInt('0x' + digest); +async function sha256Bigint(message: string) { + let messageBytes = new TextEncoder().encode(message); + let digestBytes = new Uint8Array( + await crypto.subtle.digest('SHA-256', messageBytes) + ); + return bytesToBigint(digestBytes); } /** * Generates RSA parameters including prime numbers, public exponent, and private exponent. - * @param primeSize - The bit size of the prime numbers used for generating the RSA parameters. + * @param bitSize - The bit size of the prime numbers used for generating the RSA parameters. * @returns An object containing the RSA parameters: - * - p (prime), - * - q (prime), - * - n (modulus), - * - phiN (Euler's totient function), - * - e (public exponent), - * - d (private exponent). + * `n` (modulus), `e` (public exponent), `d` (private exponent). */ -function generateRsaParams(primeSize: number) { +function generateRsaParams(bitSize: number) { // Generate two random prime numbers - const p = randomPrime(primeSize / 2); - const q = randomPrime(primeSize / 2); + const p = randomPrime(bitSize / 2); + const q = randomPrime(bitSize / 2); // Public exponent const e = 65537n; @@ -49,7 +45,7 @@ function generateRsaParams(primeSize: number) { // Private exponent const d = inverse(e, phiN); - return { p, q, n: p * q, phiN, e, d }; + return { n: p * q, e, d }; } // random primes From 10beba5d32f44eba0094f7eb1d0e7d486219cd82 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 09:49:54 +0200 Subject: [PATCH 33/36] more circuit cleanup --- src/examples/crypto/rsa/rsa.ts | 60 ++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/src/examples/crypto/rsa/rsa.ts b/src/examples/crypto/rsa/rsa.ts index 92a2bdc800..8d54376426 100644 --- a/src/examples/crypto/rsa/rsa.ts +++ b/src/examples/crypto/rsa/rsa.ts @@ -1,7 +1,14 @@ /** * RSA signature verification with o1js */ -import { Field, Gadgets, Provable, Struct, provable } from 'o1js'; +import { + Field, + Gadgets, + Provable, + Struct, + Unconstrained, + provable, +} from 'o1js'; export { Bigint2048, rsaVerify65537 }; @@ -12,17 +19,20 @@ const mask = (1n << 116n) - 1n; */ const Field18 = Provable.Array(Field, 18); -class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { - modmul(x: Bigint2048, y: Bigint2048) { +class Bigint2048 extends Struct({ + fields: Field18, + value: Unconstrained.provable as Provable>, +}) { + modMul(x: Bigint2048, y: Bigint2048) { return multiply(x, y, this); } - modsquare(x: Bigint2048) { + modSquare(x: Bigint2048) { return multiply(x, x, this, { isSquare: true }); } - toBigInt() { - return this.value; + toBigint() { + return this.value.get(); } static from(x: bigint) { @@ -32,7 +42,7 @@ class Bigint2048 extends Struct({ fields: Field18, value: BigInt }) { fields.push(Field(x & mask)); x >>= 116n; } - return new Bigint2048({ fields, value }); + return new Bigint2048({ fields, value: Unconstrained.from(value) }); } static check(x: { fields: Field[] }) { @@ -58,56 +68,56 @@ function multiply( let { q, r } = Provable.witness( provable({ q: Bigint2048, r: Bigint2048 }), () => { - let xy = x.toBigInt() * y.toBigInt(); - let p0 = p.toBigInt(); + let xy = x.toBigint() * y.toBigint(); + let p0 = p.toBigint(); let q = xy / p0; let r = xy - q * p0; return { q: Bigint2048.from(q), r: Bigint2048.from(r) }; } ); - // compute res = xy - qp - r - // we can use a sum of native field products for each result limb, because + // compute delta = xy - qp - r + // we can use a sum of native field products for each limb, because // input limbs are range-checked to 116 bits, and 2*116 + log(2*18-1) = 232 + 6 fits the native field. - let res: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); + let delta: Field[] = Array.from({ length: 2 * 18 - 1 }, () => Field(0)); let [X, Y, Q, R, P] = [x.fields, y.fields, q.fields, r.fields, p.fields]; for (let i = 0; i < 18; i++) { // when squaring, we can save constraints by not computing xi * xj twice if (isSquare) { for (let j = 0; j < i; j++) { - res[i + j] = res[i + j].add(X[i].mul(X[j]).mul(2n)); + delta[i + j] = delta[i + j].add(X[i].mul(X[j]).mul(2n)); } - res[2 * i] = res[2 * i].add(X[i].mul(X[i])); + delta[2 * i] = delta[2 * i].add(X[i].mul(X[i])); } else { for (let j = 0; j < 18; j++) { - res[i + j] = res[i + j].add(X[i].mul(Y[j])); + delta[i + j] = delta[i + j].add(X[i].mul(Y[j])); } } for (let j = 0; j < 18; j++) { - res[i + j] = res[i + j].sub(Q[i].mul(P[j])); + delta[i + j] = delta[i + j].sub(Q[i].mul(P[j])); } - res[i] = res[i].sub(R[i]); + delta[i] = delta[i].sub(R[i]).seal(); } - // perform carrying on res to show that it is zero + // perform carrying on the difference to show that it is zero let carry = Field(0); for (let i = 0; i < 2 * 18 - 2; i++) { - let res_i = res[i].add(carry); + let deltaPlusCarry = delta[i].add(carry).seal(); - carry = Provable.witness(Field, () => res_i.div(1n << 116n)); + carry = Provable.witness(Field, () => deltaPlusCarry.div(1n << 116n)); rangeCheck128Signed(carry); // (xy - qp - r)_i + c_(i-1) === c_i * 2^116 // proves that bits i*116 to (i+1)*116 of res are zero - res_i.assertEquals(carry.mul(1n << 116n)); + deltaPlusCarry.assertEquals(carry.mul(1n << 116n)); } - // last carry is 0 ==> all of res is 0 ==> x*y = q*p + r as integers - res[2 * 18 - 2].add(carry).assertEquals(0n); + // last carry is 0 ==> all of diff is 0 ==> x*y = q*p + r as integers + delta[2 * 18 - 2].add(carry).assertEquals(0n); return r; } @@ -127,10 +137,10 @@ function rsaVerify65537( // square 16 times let x = signature; for (let i = 0; i < 16; i++) { - x = modulus.modsquare(x); + x = modulus.modSquare(x); } // multiply by signature - x = modulus.modmul(x, signature); + x = modulus.modMul(x, signature); // check that x == message Provable.assertEqual(Bigint2048, message, x); From d7ed2f05807e085ce38afc0d0cbaa56da5ba1dbe Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 09:50:06 +0200 Subject: [PATCH 34/36] adapt tests --- src/examples/crypto/rsa/run.ts | 9 +++--- src/examples/crypto/rsa/test.ts | 50 ++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/examples/crypto/rsa/run.ts b/src/examples/crypto/rsa/run.ts index 02e4ab204a..3d27fb7ed9 100644 --- a/src/examples/crypto/rsa/run.ts +++ b/src/examples/crypto/rsa/run.ts @@ -1,6 +1,6 @@ import { ZkProgram } from 'o1js'; import { Bigint2048, rsaVerify65537 } from './rsa.js'; -import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; +import { sha256Bigint, generateRsaParams, rsaSign } from './utils.js'; let rsaZkProgram = ZkProgram({ name: 'rsa-verify', @@ -29,14 +29,13 @@ const forceRecompileEnabled = false; await rsaZkProgram.compile({ forceRecompile: forceRecompileEnabled }); console.timeEnd('compile'); -const input = generateDigestBigint('How are you!'); +console.time('generate RSA parameters and inputs (2048 bits)'); +const input = await sha256Bigint('How are you!'); const params = generateRsaParams(2048); - -console.time('generate RSA parameters: 2048 bits'); const message = Bigint2048.from(input); const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); const modulus = Bigint2048.from(params.n); -console.timeEnd('generate RSA parameters: 2048 bits'); +console.timeEnd('generate RSA parameters and inputs (2048 bits)'); console.time('prove'); let proof = await rsaZkProgram.verifyRsa65537(message, signature, modulus); diff --git a/src/examples/crypto/rsa/test.ts b/src/examples/crypto/rsa/test.ts index 8c3036bfd2..41c920ef2c 100644 --- a/src/examples/crypto/rsa/test.ts +++ b/src/examples/crypto/rsa/test.ts @@ -1,5 +1,10 @@ import { Bigint2048, rsaVerify65537 } from './rsa.js'; -import { generateDigestBigint, generateRsaParams, rsaSign } from './utils.js'; +import { + sha256Bigint, + generateRsaParams, + rsaSign, + randomPrime, +} from './utils.js'; import { expect } from 'expect'; import { it, describe } from 'node:test'; @@ -9,7 +14,6 @@ describe('RSA65537 verification tests', () => { const rsaSig = Bigint2048.from(31n); const modul = Bigint2048.from(33n); - // Provable RSA verification rsaVerify65537(message, rsaSig, modul); }); @@ -34,8 +38,8 @@ describe('RSA65537 verification tests', () => { rsaVerify65537(message, rsaSig, modul); }); - it('should accept RSA signature with randomly generated parameters: 512-bits (20 iterations)', () => { - const input = generateDigestBigint('hello there!'); + it('should accept RSA signature with randomly generated parameters: 512-bits (20 iterations)', async () => { + const input = await sha256Bigint('hello there!'); for (let i = 0; i < 20; i++) { const params = generateRsaParams(512); @@ -48,8 +52,8 @@ describe('RSA65537 verification tests', () => { } }); - it('should accept RSA signature with randomly generated parameters: 1024-bits (10 iterations)', () => { - const input = generateDigestBigint('how is it going!'); + it('should accept RSA signature with randomly generated parameters: 1024-bits (10 iterations)', async () => { + const input = await sha256Bigint('how is it going!'); for (let i = 0; i < 10; i++) { const params = generateRsaParams(1024); @@ -62,8 +66,8 @@ describe('RSA65537 verification tests', () => { } }); - it('should accept RSA signature with randomly generated parameters: 2048-bits (5 iterations)', () => { - const input = generateDigestBigint('how are you!'); + it('should accept RSA signature with randomly generated parameters: 2048-bits (5 iterations)', async () => { + const input = await sha256Bigint('how are you!'); for (let i = 0; i < 5; i++) { const params = generateRsaParams(2048); @@ -76,8 +80,8 @@ describe('RSA65537 verification tests', () => { } }); - it('should reject RSA signature with randomly generated parameters larger than 2048-bits', () => { - const input = generateDigestBigint('how are you!'); + it('should reject RSA signature with randomly generated parameters larger than 2048 bits', async () => { + const input = await sha256Bigint('how are you!'); const params = generateRsaParams(2560); const message = Bigint2048.from(input); @@ -87,20 +91,20 @@ describe('RSA65537 verification tests', () => { expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); }); - it('should reject RSA signature with non-compliant modulus: 1024-bits', () => { - const input = generateDigestBigint('hello!'); - const params = generateRsaParams(1024); + it('should reject RSA signature with non-compliant modulus: 2048 bits', async () => { + const input = await sha256Bigint('hello!'); + const params = generateRsaParams(2048); const message = Bigint2048.from(input); const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); - const modulus = Bigint2048.from(params.phiN); // Tamper with modulus + const modulus = Bigint2048.from(randomPrime(2048)); // Tamper with modulus expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); }); - it('should reject RSA signature with non-compliant input: 1024-bits', () => { - const input = generateDigestBigint('hello!'); - const params = generateRsaParams(1024); + it('should reject RSA signature with non-compliant input: 2048 bits', async () => { + const input = await sha256Bigint('hello!'); + const params = generateRsaParams(2048); const message = Bigint2048.from(35n); // Tamper with input const signature = Bigint2048.from(rsaSign(input, params.d, params.n)); @@ -109,9 +113,9 @@ describe('RSA65537 verification tests', () => { expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); }); - it('should reject non compliant RSA signature: false private key: 1024-bits', () => { - const input = generateDigestBigint('hello!'); - const params = generateRsaParams(1024); + it('should reject non compliant RSA signature: false private key: 2048 bits', async () => { + const input = await sha256Bigint('hello!'); + const params = generateRsaParams(2048); const message = Bigint2048.from(input); const signature = Bigint2048.from(rsaSign(input, params.e, params.n)); // Tamper with private key @@ -120,9 +124,9 @@ describe('RSA65537 verification tests', () => { expect(() => rsaVerify65537(message, signature, modulus)).toThrowError(); }); - it('should reject non-compliant RSA signature: false signature modulus : 1024-bits', () => { - const input = generateDigestBigint('hello!'); - const params = generateRsaParams(1024); + it('should reject non-compliant RSA signature: false signature modulus : 2048 bits', async () => { + const input = await sha256Bigint('hello!'); + const params = generateRsaParams(2048); const message = Bigint2048.from(input); const signature = Bigint2048.from(rsaSign(input, params.d, 1223n)); // Tamper with signature modulus From 286fb7d4f19ffe1ed633522f7529d6bacf629481 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 09:55:40 +0200 Subject: [PATCH 35/36] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 451d35c0d0..8ab8020721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,13 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Exposed sideloaded verification keys https://github.com/o1-labs/o1js/pull/1606 [@rpanic](https://github.com/rpanic) - Added Proof type `DynamicProof` that allows verification through specifying a verification key in-circuit +- `Provable.witnessFields()` to easily witness a tuple of field elements https://github.com/o1-labs/o1js/pull/1229 +- Example for implementing RSA verification in o1js https://github.com/o1-labs/o1js/pull/1229 [@Shigoto-dev19](https://github.com/Shigoto-dev19) + - Check out https://github.com/o1-labs/o1js/blob/main/src/examples/crypto/rsa/rsa.ts and tests in the same folder + +### Changed + +- `Gadgets.rangeCheck64()` now returns individual range-checked limbs for advanced use cases https://github.com/o1-labs/o1js/pull/1229 ## [1.0.1](https://github.com/o1-labs/o1js/compare/1b6fd8b8e...02c5e8d4d) - 2024-04-22 From 9c9feec23929d10bce8bca76f12da0460a6013b9 Mon Sep 17 00:00:00 2001 From: Gregor Date: Mon, 29 Apr 2024 10:00:10 +0200 Subject: [PATCH 36/36] minor --- src/examples/crypto/rsa/utils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/examples/crypto/rsa/utils.ts b/src/examples/crypto/rsa/utils.ts index 3b505f201c..96534ebcda 100644 --- a/src/examples/crypto/rsa/utils.ts +++ b/src/examples/crypto/rsa/utils.ts @@ -75,6 +75,7 @@ function millerRabinTest(n: bigint): 'composite' | 'probably prime' { const k = 10; if (n === 2n || n === 3n) return 'probably prime'; if (n < 2n) return 'composite'; + // check if divisible by one of first few primes for (let p of knownPrimes) { if (n % p === 0n && n > p) return 'composite'; @@ -87,7 +88,6 @@ function millerRabinTest(n: bigint): 'composite' | 'probably prime' { WitnessLoop: for (let i = 0; i < k; i++) { let a = randomBigintRange(2n, n - 2n); - // let x = a ** d % n; // too large let x = power(a, d, n); if (x === 1n || x === n - 1n) continue; for (let i = 0; i + 1 < r; i++) { @@ -144,8 +144,8 @@ function power(a: bigint, n: bigint, p: bigint) { } // modular inverse, 1/a in Z_p -function inverse(a_: bigint, p: bigint) { - let a = mod(a_, p); +function inverse(a: bigint, p: bigint) { + a = mod(a, p); if (a === 0n) throw Error('modular inverse: division by 0'); let b = p; let [x, y, u, v] = [0n, 1n, 1n, 0n];