diff --git a/circuits/circuits/tests/utils/utils/isNBitsEqual.circom b/circuits/circuits/tests/utils/utils/isNBitsEqual.circom new file mode 100644 index 000000000..5455084a7 --- /dev/null +++ b/circuits/circuits/tests/utils/utils/isNBitsEqual.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../../utils/crypto/utils/isNBits.circom"; + +component main = isNBits(64); diff --git a/circuits/circuits/tests/utils/utils/isNBitsGreater.circom b/circuits/circuits/tests/utils/utils/isNBitsGreater.circom new file mode 100644 index 000000000..5aa1ca560 --- /dev/null +++ b/circuits/circuits/tests/utils/utils/isNBitsGreater.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../../utils/crypto/utils/isNBits.circom"; + +component main = isNBits(65); diff --git a/circuits/circuits/tests/utils/utils/isNBitsLesser.circom b/circuits/circuits/tests/utils/utils/isNBitsLesser.circom new file mode 100644 index 000000000..7794c5992 --- /dev/null +++ b/circuits/circuits/tests/utils/utils/isNBitsLesser.circom @@ -0,0 +1,5 @@ +pragma circom 2.1.6; + +include "../../../utils/crypto/utils/isNBits.circom"; + +component main = isNBits(64); diff --git a/circuits/circuits/utils/crypto/bigInt/bigInt.circom b/circuits/circuits/utils/crypto/bigInt/bigInt.circom index 2bd7d0418..e0603f90c 100644 --- a/circuits/circuits/utils/crypto/bigInt/bigInt.circom +++ b/circuits/circuits/utils/crypto/bigInt/bigInt.circom @@ -40,8 +40,8 @@ template BigMultModP(CHUNK_SIZE, CHUNK_NUMBER_GREATER, CHUNK_NUMBER_LESS, CHUNK_ for (var i = 0; i < CHUNK_NUMBER_DIV; i++){ div[i] <-- long_division[0][i]; - } + component modChecks[CHUNK_NUMBER_MODULUS]; for (var i = 0; i < CHUNK_NUMBER_MODULUS; i++){ mod[i] <-- long_division[1][i]; @@ -79,6 +79,36 @@ template BigMultModP(CHUNK_SIZE, CHUNK_NUMBER_GREATER, CHUNK_NUMBER_LESS, CHUNK_ } } +// in[0] < in[1] +template BigLessThan(CHUNK_SIZE, CHUNK_NUMBER){ + signal input in[2][CHUNK_NUMBER]; + + signal output out; + + component lessThan[CHUNK_NUMBER]; + component isEqual[CHUNK_NUMBER - 1]; + signal result[CHUNK_NUMBER - 1]; + for (var i = 0; i < CHUNK_NUMBER; i++){ + lessThan[i] = LessThan(CHUNK_SIZE); + lessThan[i].in[0] <== in[0][i]; + lessThan[i].in[1] <== in[1][i]; + + if (i != 0){ + isEqual[i - 1] = IsEqual(); + isEqual[i - 1].in[0] <== in[0][i]; + isEqual[i - 1].in[1] <== in[1][i]; + } + } + + for (var i = 1; i < CHUNK_NUMBER; i++){ + if (i == 1){ + result[i - 1] <== lessThan[i].out + isEqual[i - 1].out * lessThan[i - 1].out; + } else { + result[i - 1] <== lessThan[i].out + isEqual[i - 1].out * result[i - 2]; + } + } + out <== result[CHUNK_NUMBER - 2]; +} // in[0] <= in[1] template BigLessEqThan(CHUNK_SIZE, CHUNK_NUMBER){ @@ -100,7 +130,7 @@ template BigLessEqThan(CHUNK_SIZE, CHUNK_NUMBER){ } for (var i = 0; i < CHUNK_NUMBER; i++){ - if (i == 0){ + if (i == 0){ result[i] <== lessThan[i].out + isEqual[i].out; } else { result[i] <== lessThan[i].out + isEqual[i].out * result[i - 1]; @@ -108,7 +138,6 @@ template BigLessEqThan(CHUNK_SIZE, CHUNK_NUMBER){ } out <== result[CHUNK_NUMBER - 1]; - } // in[0] > in[1] @@ -122,6 +151,25 @@ template BigGreaterThan(CHUNK_SIZE, CHUNK_NUMBER){ out <== 1 - lessEqThan.out; } +// lowerbound <= value < upperbound +template BigRangeCheck(CHUNK_SIZE, CHUNK_NUMBER) { + signal input value[CHUNK_NUMBER]; + signal input lowerBound[CHUNK_NUMBER]; + signal input upperBound[CHUNK_NUMBER]; + + signal output out; + + component greaterThanLower = BigLessThan(CHUNK_SIZE, CHUNK_NUMBER); + greaterThanLower.in[0] <== value; + greaterThanLower.in[1] <== lowerBound; + + component lessThanUpper = BigLessThan(CHUNK_SIZE, CHUNK_NUMBER); + lessThanUpper.in[0] <== value; + lessThanUpper.in[1] <== upperBound; + + out <== (1 - greaterThanLower.out) * lessThanUpper.out; +} + // calculates in ^ (-1) % modulus; // in, modulus has CHUNK_NUMBER template BigModInv(CHUNK_SIZE, CHUNK_NUMBER) { diff --git a/circuits/circuits/utils/crypto/signature/ecdsa/ecdsa.circom b/circuits/circuits/utils/crypto/signature/ecdsa/ecdsa.circom index e4c471deb..0705dbe0f 100644 --- a/circuits/circuits/utils/crypto/signature/ecdsa/ecdsa.circom +++ b/circuits/circuits/utils/crypto/signature/ecdsa/ecdsa.circom @@ -30,10 +30,31 @@ template verifyECDSABits(CHUNK_SIZE, CHUNK_NUMBER, A, B, P, ALGO){ } hashedChunked[CHUNK_NUMBER - 1 - i] <== bits2Num[i].out; } + + signal one[CHUNK_NUMBER]; + one[0] <== 1; + for (var i = 1; i < CHUNK_NUMBER; i++){ + one[i] <== 0; + } component getOrder = EllipicCurveGetOrder(CHUNK_SIZE,CHUNK_NUMBER, A, B, P); signal order[CHUNK_NUMBER]; order <== getOrder.order; + + // check if 1 <= r < order + component rangeChecks[2]; + rangeChecks[0] = BigRangeCheck(CHUNK_SIZE, CHUNK_NUMBER); + rangeChecks[0].value <== signature[0]; + rangeChecks[0].lowerBound <== one; + rangeChecks[0].upperBound <== order; + rangeChecks[0].out === 1; + + //check if 1 <= s < order + rangeChecks[1] = BigRangeCheck(CHUNK_SIZE, CHUNK_NUMBER); + rangeChecks[1].value <== signature[1]; + rangeChecks[1].lowerBound <== one; + rangeChecks[1].upperBound <== order; + rangeChecks[1].out === 1; // s_inv = s ^ -1 mod n signal sinv[CHUNK_NUMBER]; @@ -69,9 +90,14 @@ template verifyECDSABits(CHUNK_SIZE, CHUNK_NUMBER, A, B, P, ALGO){ component add = EllipticCurveAdd(CHUNK_SIZE, CHUNK_NUMBER, A, B, P); add.in1 <== scalarMult1.out; add.in2 <== scalarMult2.out; + + component addModN = BigMultModP(CHUNK_SIZE, CHUNK_NUMBER, CHUNK_NUMBER, CHUNK_NUMBER); + addModN.in1 <== add.out[0]; + addModN.in2 <== one; + addModN.modulus <== order; // x1 === r for (var i = 0; i < CHUNK_NUMBER; i++){ - add.out[0][i] === signature[0][i]; + addModN.mod[i] === signature[0][i]; } } \ No newline at end of file diff --git a/circuits/circuits/utils/crypto/signature/ecdsa/ecdsaVerifier.circom b/circuits/circuits/utils/crypto/signature/ecdsa/ecdsaVerifier.circom index e1ff2e4be..e65d86b53 100644 --- a/circuits/circuits/utils/crypto/signature/ecdsa/ecdsaVerifier.circom +++ b/circuits/circuits/utils/crypto/signature/ecdsa/ecdsaVerifier.circom @@ -1,13 +1,14 @@ pragma circom 2.1.9; include "../../../passport/signatureAlgorithm.circom"; +include "../../utils/isNBits.circom"; include "ecdsa.circom"; /// @title EcdsaVerifier /// @notice Verifies an ECDSA signature for a given signature algorithm, public key, and message hash /// @param signatureAlgorithm The hashing/signature algorithm as defined in `signatureAlgorithm.circom` -/// @param n The number of chunks used to represent integers (e.g., public key components and signature) -/// @param k The base chunk size, scaled based on the signature algorithm +/// @param n The base chunk size, scaled based on the signature algorithm +/// @param k The number of chunks used to represent integers (e.g., public key components and signature) /// @input signature The [R, S] component in an array /// @input pubKey The public key to verify the signature /// @input hashParsed The hash of the message to be verified @@ -53,6 +54,19 @@ template EcdsaVerifier(signatureAlgorithm, n, k) { } signal pubkey_xy[2][k] <== [pubKey_x, pubKey_y]; + component rangeCheck[4 * k]; + for (var i = 0; i < k; i++) { + rangeCheck[4 * i + 0] = isNBits(n); + rangeCheck[4 * i + 1] = isNBits(n); + rangeCheck[4 * i + 2] = isNBits(n); + rangeCheck[4 * i + 3] = isNBits(n); + + rangeCheck[4 * i + 0].in <== signature_r[i]; + rangeCheck[4 * i + 1].in <== signature_s[i]; + rangeCheck[4 * i + 2].in <== pubKey_x[i]; + rangeCheck[4 * i + 3].in <== pubKey_y[i]; + } + var a[k] = get_a(signatureAlgorithm); var b[k] = get_b(signatureAlgorithm); var p[k] = get_p(signatureAlgorithm); diff --git a/circuits/circuits/utils/crypto/utils/isNBits.circom b/circuits/circuits/utils/crypto/utils/isNBits.circom new file mode 100644 index 000000000..031676ce9 --- /dev/null +++ b/circuits/circuits/utils/crypto/utils/isNBits.circom @@ -0,0 +1,23 @@ +pragma circom 2.1.6; + +include "circomlib/circuits/bitify.circom"; + +/// @title isNBits +/// @notice Checks whether an input number can be represented using at most `n` bits. +/// @param n The maximum number of bits allowed for the input value. +/// @input in The integer input to be checked. +template isNBits(n) { + signal input in; + + component n2b = Num2Bits(254); + n2b.in <== in; + + signal check[254 - n]; + check[0] <== n2b.out[n]; + + for (var i = n + 1; i < 254; i++) { + check[i - n] <== check[i - n - 1] + n2b.out[i]; + } + + check[254 - n - 1] === 0; +} \ No newline at end of file diff --git a/circuits/tests/utils/ecdsa.test.ts b/circuits/tests/utils/ecdsa.test.ts index 2daae5b3f..31260146c 100644 --- a/circuits/tests/utils/ecdsa.test.ts +++ b/circuits/tests/utils/ecdsa.test.ts @@ -54,11 +54,10 @@ const testSuite = [ describe('ecdsa', () => { testSuite.forEach(({ hash, curve, n, k, reason }) => { const message = crypto.randomBytes(32); - ( [ - [true, 'should verify correctly'], - [false, 'should not verify correctly'], + [true, 'should verify'], + [false, 'should not verify'], ] as [boolean, string][] ).forEach(([shouldVerify, shouldVerifyReason]) => { describe(shouldVerifyReason, function () { @@ -84,12 +83,155 @@ describe('ecdsa', () => { } } catch (error) { if (shouldVerify) { + console.log(error); throw new Error('Test failed: Valid signature was not verified.'); } } }); }); }); + it('should not verify if either signature component is greater than the order', async function () { + this.timeout(0); + // takes way too long to find a valid input for these + if (['p256', 'p384'].includes(curve)) { + return; + } + const circuit = await wasmTester( + path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_${curve}.circom`), + { + include: ['node_modules', './node_modules/@zk-kit/binary-merkle-root.circom/src'], + } + ); + + for (const item of [true, false]) { + try { + let inputs; + while (true) { + try { + inputs = signOverflow(message, curve, hash, k, n, item); + break; + } catch (err) {} + } + const witness = await circuit.calculateWitness(inputs); + await circuit.checkConstraints(witness); + throw new Error('Test failed: Invalid signature was verified.'); + } catch (error) {} + } + }); + }); + it('should not accept invalid chunks in the signature', async function () { + this.timeout(0); + const circuit = await wasmTester( + path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_p256.circom`), + { + include: ['node_modules', './node_modules/@zk-kit/binary-merkle-root.circom/src'], + } + ); + + const inputs = { + signature: [ + [ + '11897043862654108222', + '6687976630675743167', + '6842677606991059234', + '3933303995770833589', + ], + [ + '10364704208062614840', + '21394470794141451286901280378935131115', + '0', + '15812853153589603704', + ], + ], + pubKey: [ + [ + '1647443686294582730', + '7524809848328723651', + '2690299118416708846', + '2230381215521625212', + ], + [ + '12063856007545978738', + '2856046104882309217', + '14084651496056034469', + '2603012891351374004', + ], + ], + hashParsed: [ + 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, + 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, + 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, + 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, + 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, + 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, + 1, 0, 1, 1, 1, 0, 1, 0, + ], + }; + + try { + const witness = await circuit.calculateWitness(inputs); + await circuit.checkConstraints(witness); + throw new Error('Test failed: Invalid signature was verified.'); + } catch (err) { + if (!(err as Error).message.includes('isNBits')) { + throw err; + } + } + }); + + it('should reduce the final signature addition mod n', async function () { + this.timeout(0); + const circuit = await wasmTester( + path.join(__dirname, `../../circuits/tests/utils/ecdsa/test_p256.circom`), + { + include: ['node_modules', './node_modules/@zk-kit/binary-merkle-root.circom/src'], + } + ); + + const inputs = { + signature: [ + ['884452912994769579', '4834901530490986875', '0', '0'], + [ + '17562291160714782030', + '13611842547513532036', + '18446744073709551615', + '18446744069414584320', + ], + ], + pubKey: [ + [ + '12004473255778836739', + '5567425807485590512', + '4612562821672420442', + '781819838238377577', + ], + [ + '2517678904895060574', + '13415238991415823444', + '5824794594647846510', + '14195660962316692941', + ], + ], + hashParsed: [ + 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, + 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, + 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, + 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, + 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 1, 1, + ], + }; + try { + const witness = await circuit.calculateWitness(inputs); + await circuit.checkConstraints(witness); + } catch (err) { + throw err; + } }); }); @@ -119,3 +261,43 @@ function sign(message: Uint8Array, curve: string, hash: string, n: number, k: nu hashParsed, }; } + +function signOverflow( + message: Uint8Array, + curve: string, + hash: string, + n: number, + k: number, + overflowS: boolean +) { + const ec = new elliptic.ec(curve); + + const key = ec.genKeyPair(); + + const messageHash = crypto.createHash(hash).update(message).digest(); + + const signature = key.sign(messageHash, 'hex'); + const pubkey = key.getPublic(); + const hashParsed = []; + Array.from(messageHash).forEach((x) => + hashParsed.push(...x.toString(2).padStart(8, '0').split('')) + ); + + let r = BigInt(signature.r); + let s = BigInt(signature.s); + + if (overflowS) { + s = s + BigInt(ec.n); + } else { + r = r + BigInt(ec.n); + } + + return { + signature: [...splitToWords(r, k, n), ...splitToWords(s, k, n)], + pubKey: [ + splitToWords(BigInt(pubkey.getX().toString()), k, n), + splitToWords(BigInt(pubkey.getY().toString()), k, n), + ], + hashParsed, + }; +} diff --git a/circuits/tests/utils/isNBits.test.ts b/circuits/tests/utils/isNBits.test.ts new file mode 100644 index 000000000..caf3e7a7f --- /dev/null +++ b/circuits/tests/utils/isNBits.test.ts @@ -0,0 +1,41 @@ +import path from 'path'; +import { wasm as wasmTester } from 'circom_tester'; +import { generateCircuitInputsCountryVerifier } from '../../../common/src/utils/generateInputs'; + +const testSuite = [ + { + type: 'Greater', + in: 1n << 64n, + }, + { + type: 'Equal', + in: (1n << 64n) - 1n, + }, + { + type: 'Lesser', + in: (1n << 64n) - 2n, + }, +]; + +describe('isNBits should work when input is', () => { + testSuite.map(({ type, in: input }) => { + it(type + ' than 64 bits', async () => { + const circuit = await wasmTester( + path.join(__dirname, `../../circuits/tests/utils/utils/isNBits${type}.circom`), + { + include: ['node_modules', './node_modules/@zk-kit/binary-merkle-root.circom/src'], + } + ); + const inputs = { + in: input, + }; + try { + const witness = await circuit.calculateWitness(inputs); + await circuit.checkConstraints(witness); + } catch (err) { + console.error(err); + throw err; + } + }); + }); +});