Files
self/circuits/tests/utils/ecdsa.test.ts
2025-02-19 10:32:32 -08:00

320 lines
9.3 KiB
TypeScript

import { wasm as wasmTester } from 'circom_tester';
import * as crypto from 'crypto';
import { initElliptic } from '../../../common/src/utils/certificate_parsing/elliptic';
import * as path from 'path';
import { splitToWords } from '../../../common/src/utils/bytes';
const elliptic = initElliptic();
const testSuite = [
{
hash: 'sha512',
curve: 'brainpoolP256r1',
n: 64,
k: 4,
reason: 'when hash is greater than curve bits',
},
];
const fullTestSuite = [
{
hash: 'sha1',
curve: 'brainpoolP224r1',
n: 32,
k: 7,
reason: 'when hash is lesser than curve bits',
},
{
hash: 'sha512',
curve: 'brainpoolP256r1',
n: 64,
k: 4,
reason: 'when hash is greater than curve bits',
},
{
hash: 'sha384',
curve: 'brainpoolP384r1',
n: 64,
k: 6,
reason: 'when hash bits are the same as curve bits',
},
{
hash: 'sha512',
curve: 'brainpoolP512r1',
n: 64,
k: 8,
reason: 'when hash bits are the same as curve bits',
},
{
hash: 'sha256',
curve: 'p256',
n: 64,
k: 4,
reason: 'when hash bits are the same as curve bits',
},
{
hash: 'sha384',
curve: 'p384',
n: 64,
k: 6,
reason: 'when hash bits are the same as curve bits',
},
{
hash: 'sha512',
curve: 'p521',
n: 66,
k: 8,
reason: 'when hash bits are less than the curve bits',
},
];
describe('ecdsa', () => {
testSuite.forEach(({ hash, curve, n, k, reason }) => {
const message = crypto.randomBytes(32);
(
[
[true, 'should verify'],
[false, 'should not verify'],
] as [boolean, string][]
).forEach(([shouldVerify, shouldVerifyReason]) => {
describe(shouldVerifyReason, function () {
this.timeout(0);
const inputs = sign(message, curve, hash, k, n);
if (!shouldVerify) {
inputs.hashParsed.map((x) => 0);
}
it(reason, async () => {
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'],
}
);
try {
const witness = await circuit.calculateWitness(inputs);
await circuit.checkConstraints(witness);
if (!shouldVerify) {
throw new Error('Test failed: Invalid signature was verified.');
}
} 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('Num2Bits')) {
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;
}
});
});
function sign(message: Uint8Array, curve: string, hash: string, n: number, k: number) {
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(''))
);
return {
signature: [
...splitToWords(BigInt(signature.r), k, n),
...splitToWords(BigInt(signature.s), k, n),
],
pubKey: [
splitToWords(BigInt(pubkey.getX().toString()), k, n),
splitToWords(BigInt(pubkey.getY().toString()), k, n),
],
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,
};
}