audit: ecdsa (#64)

This commit is contained in:
Nesopie
2025-02-13 18:53:33 +05:30
committed by GitHub
parent efbe84e599
commit 66a257c53d
5 changed files with 272 additions and 6 deletions

View File

@@ -5,6 +5,7 @@ include "circomlib/circuits/bitify.circom";
include "./bigIntFunc.circom";
include "./bigIntOverflow.circom";
include "../int/arithmetic.circom";
include "@openpassport/zk-email-circuits/lib/bigint.circom";
// What BigInt in this lib means
// We represent big number as array of chunks with some shunk_size (will be explained later)
@@ -122,6 +123,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.a <== value;
greaterThanLower.b <== lowerBound;
component lessThanUpper = BigLessThan(CHUNK_SIZE, CHUNK_NUMBER);
lessThanUpper.a <== value;
lessThanUpper.b <== upperBound;
out <== (1 - greaterThanLower.out) * lessThanUpper.out;
}
// calculates in ^ (-1) % modulus;
// in, modulus has CHUNK_NUMBER
template BigModInv(CHUNK_SIZE, CHUNK_NUMBER) {

View File

@@ -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];
}
}

View File

@@ -1,13 +1,15 @@
pragma circom 2.1.9;
include "../../../passport/signatureAlgorithm.circom";
include "../../bigInt/bigInt.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
@@ -60,6 +62,19 @@ template EcdsaVerifier(signatureAlgorithm, n, k) {
// verify eContentHash signature
component ecdsa_verify = verifyECDSABits(n, k, a, b, p, n * k);
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];
}
ecdsa_verify.pubkey <== pubkey_xy;
ecdsa_verify.signature <== [signature_r, signature_s];
ecdsa_verify.hashed <== hash;

View File

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

View File

@@ -70,11 +70,10 @@ const fullTestSuite = [
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 () {
@@ -100,12 +99,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;
}
});
});
@@ -135,3 +277,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,
};
}