Skip to content

Commit

Permalink
Merge pull request #1229 from o1-labs/feature/advanced-range-check
Browse files Browse the repository at this point in the history
RSA example
  • Loading branch information
mitschabaude authored Apr 29, 2024
2 parents 29d40e5 + 9c9feec commit 3b3bd10
Show file tree
Hide file tree
Showing 10 changed files with 606 additions and 6 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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

Expand Down
4 changes: 4 additions & 0 deletions src/examples/crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 find an RSA implementation using a custom bigint type implemented using efficient o1js range checks:

- RSA signature verification: `rsa/`
181 changes: 181 additions & 0 deletions src/examples/crypto/rsa/rsa.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/**
* RSA signature verification with o1js
*/
import {
Field,
Gadgets,
Provable,
Struct,
Unconstrained,
provable,
} from 'o1js';

export { Bigint2048, rsaVerify65537 };

const mask = (1n << 116n) - 1n;

/**
* We use 116-bit limbs, which means 18 limbs for 2048-bit numbers as used in RSA.
*/
const Field18 = Provable.Array(Field, 18);

class Bigint2048 extends Struct({
fields: Field18,
value: Unconstrained.provable as Provable<Unconstrained<bigint>>,
}) {
modMul(x: Bigint2048, y: Bigint2048) {
return multiply(x, y, this);
}

modSquare(x: Bigint2048) {
return multiply(x, x, this, { isSquare: true });
}

toBigint() {
return this.value.get();
}

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: Unconstrained.from(value) });
}

static check(x: { fields: Field[] }) {
for (let i = 0; i < 18; i++) {
rangeCheck116(x.fields[i]);
}
}
}

/**
* x*y mod p
*/
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(
provable({ q: Bigint2048, r: Bigint2048 }),
() => {
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 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 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++) {
delta[i + j] = delta[i + j].add(X[i].mul(X[j]).mul(2n));
}
delta[2 * i] = delta[2 * i].add(X[i].mul(X[i]));
} else {
for (let j = 0; j < 18; j++) {
delta[i + j] = delta[i + j].add(X[i].mul(Y[j]));
}
}

for (let j = 0; j < 18; j++) {
delta[i + j] = delta[i + j].sub(Q[i].mul(P[j]));
}

delta[i] = delta[i].sub(R[i]).seal();
}

// 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 deltaPlusCarry = delta[i].add(carry).seal();

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
deltaPlusCarry.assertEquals(carry.mul(1n << 116n));
}

// 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;
}

/**
* 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.modSquare(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, x in [0, 2^116)
*/
function rangeCheck116(x: Field) {
let [x0, x1] = Provable.witnessFields(2, () => [
x.toBigInt() & ((1n << 64n) - 1n),
x.toBigInt() >> 64n,
]);

Gadgets.rangeCheck64(x0);
let [x52] = Gadgets.rangeCheck64(x1);
x52.assertEquals(0n); // => x1 is 52 bits
// 64 + 52 = 116
x0.add(x1.mul(1n << 64n)).assertEquals(x);
}

/**
* Custom range check for carries, x in [-2^127, 2^127)
*/
function rangeCheck128Signed(xSigned: Field) {
let x = xSigned.add(1n << 127n);

let [x0, x1] = Provable.witnessFields(2, () => {
const x0 = x.toBigInt() & ((1n << 64n) - 1n);
const x1 = x.toBigInt() >> 64n;
return [x0, x1];
});

Gadgets.rangeCheck64(x0);
Gadgets.rangeCheck64(x1);

x0.add(x1.mul(1n << 64n)).assertEquals(x);
}
46 changes: 46 additions & 0 deletions src/examples/crypto/rsa/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ZkProgram } from 'o1js';
import { Bigint2048, rsaVerify65537 } from './rsa.js';
import { sha256Bigint, generateRsaParams, rsaSign } from './utils.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.time('compile');
const forceRecompileEnabled = false;
await rsaZkProgram.compile({ forceRecompile: forceRecompileEnabled });
console.timeEnd('compile');

console.time('generate RSA parameters and inputs (2048 bits)');
const input = await sha256Bigint('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);
console.timeEnd('generate RSA parameters and inputs (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');
137 changes: 137 additions & 0 deletions src/examples/crypto/rsa/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Bigint2048, rsaVerify65537 } from './rsa.js';
import {
sha256Bigint,
generateRsaParams,
rsaSign,
randomPrime,
} from './utils.js';
import { expect } from 'expect';
import { it, describe } from 'node:test';

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);

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: 0x0086fa9ba066685845fc03833a9699c8baefb53cfbf19052a7f10f1eaa30488cec1ceb752bdff2df9fad6c64b3498956e7dbab4035b4823c99a44cc57088a23783n,
e: 65537n,
d: 0x5d2f0dd982596ef781affb1cab73a77c46985c6da2aafc252cea3f4546e80f40c0e247d7d9467750ea1321cc5aa638871b3ed96d19dcc124916b0bcb296f35e1n,
p: 0x00c59419db615e56b9805cc45673a32d278917534804171edcf925ab1df203927fn,
q: 0x00aee3f86b66087abc069b8b1736e38ad6af624f7ea80e70b95f4ff2bf77cd90fdn,
dmp1: 0x008112f5a969fcb56f4e3a4c51a60dcdebec157ee4a7376b843487b53844e8ac85n,
dmq1: 0x1a7370470e0f8a4095df40922a430fe498720e03e1f70d257c3ce34202249d21n,
coeff:
0x00b399675e5e81506b729a777cc03026f0b2119853dfc5eb124610c0ab82999e45n,
};

const message = Bigint2048.from(13n);
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 (20 iterations)', async () => {
const input = await sha256Bigint('hello there!');

for (let i = 0; i < 20; i++) {
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 (10 iterations)', async () => {
const input = await sha256Bigint('how is it going!');

for (let i = 0; i < 10; i++) {
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 (5 iterations)', async () => {
const input = await sha256Bigint('how are you!');

for (let i = 0; i < 5; i++) {
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', async () => {
const input = await sha256Bigint('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: 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(randomPrime(2048)); // Tamper with modulus

expect(() => rsaVerify65537(message, signature, modulus)).toThrowError();
});

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));
const modulus = Bigint2048.from(params.n);

expect(() => rsaVerify65537(message, signature, modulus)).toThrowError();
});

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
const modulus = Bigint2048.from(params.n);

expect(() => rsaVerify65537(message, signature, modulus)).toThrowError();
});

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
const modulus = Bigint2048.from(params.n);

expect(() => rsaVerify65537(message, signature, modulus)).toThrowError();
});
});
Loading

0 comments on commit 3b3bd10

Please sign in to comment.