-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1229 from o1-labs/feature/advanced-range-check
RSA example
- Loading branch information
Showing
10 changed files
with
606 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.