New OFAC updates (#47)

Co-authored-by: nicoshark <i.am.nicoshark@gmail.com>
This commit is contained in:
turboblitz
2025-02-12 13:21:01 -08:00
committed by GitHub
parent 5902d7c253
commit 6d4c211c51
54 changed files with 1243 additions and 661 deletions

View File

@@ -12,7 +12,9 @@ import { generateCircuitInputsVCandDisclose } from '../../../common/src/utils/ci
import crypto from 'crypto';
import { genMockPassportData } from '../../../common/src/utils/passports/genMockPassportData';
import { SMT } from '@openpassport/zk-kit-smt';
import namejson from '../../../common/ofacdata/outputs/nameSMT.json';
import nameAndDobjson from '../../../common/ofacdata/outputs/nameAndDobSMT.json';
import nameAndYobjson from '../../../common/ofacdata/outputs/nameAndYobSMT.json';
import passportNojson from '../../../common/ofacdata/outputs/passportNoAndNationalitySMT.json';
import {
formatAndUnpackReveal,
formatAndUnpackForbiddenCountriesList,
@@ -37,8 +39,33 @@ describe('Disclose', function () {
'300101'
);
passportData = initPassportDataParsing(passportData);
let tree: any;
let forbidden_countries_list: any;
const forbidden_countries_list = ['ALG', 'DZA'];
const secret = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
const majority = '18';
const user_identifier = crypto.randomUUID();
const selector_dg1 = Array(88).fill('1');
const selector_older_than = '1';
const scope = '@coboyApp';
const attestation_id = PASSPORT_ATTESTATION_ID;
// compute the commitment and insert it in the tree
const commitment = generateCommitment(secret, attestation_id, passportData);
console.log('commitment in js ', commitment);
const tree: any = new LeanIMT((a, b) => poseidon2([a, b]), []);
tree.insert(BigInt(commitment));
const passportNo_smt = new SMT(poseidon2, true);
passportNo_smt.import(passportNojson);
const nameAndDob_smt = new SMT(poseidon2, true);
nameAndDob_smt.import(nameAndDobjson);
const nameAndYob_smt = new SMT(poseidon2, true);
nameAndYob_smt.import(nameAndYobjson);
const selector_ofac = 1;
before(async () => {
circuit = await wasm_tester(
path.join(__dirname, '../../circuits/disclose/vc_and_disclose.circom'),
@@ -51,26 +78,6 @@ describe('Disclose', function () {
}
);
const secret = BigInt(Math.floor(Math.random() * Math.pow(2, 254))).toString();
const majority = '18';
const user_identifier = crypto.randomUUID();
const selector_dg1 = Array(88).fill('1');
const selector_older_than = '1';
const scope = '@coboyApp';
const attestation_id = PASSPORT_ATTESTATION_ID;
// compute the commitment and insert it in the tree
const commitment = generateCommitment(secret, attestation_id, passportData);
console.log('commitment in js ', commitment);
tree = new LeanIMT((a, b) => poseidon2([a, b]), []);
tree.insert(BigInt(commitment));
let smt = new SMT(poseidon2, true);
smt.import(namejson);
const selector_ofac = 1;
forbidden_countries_list = ['ALG', 'DZA'];
inputs = generateCircuitInputsVCandDisclose(
secret,
PASSPORT_ATTESTATION_ID,
@@ -80,7 +87,9 @@ describe('Disclose', function () {
selector_older_than,
tree,
majority,
smt,
passportNo_smt,
nameAndDob_smt,
nameAndYob_smt,
selector_ofac,
forbidden_countries_list,
user_identifier
@@ -199,4 +208,190 @@ describe('Disclose', function () {
expect(reveal_unpacked[88]).to.equal('\x00');
expect(reveal_unpacked[89]).to.equal('\x00');
});
describe('OFAC disclosure', function () {
it('should allow disclosing OFAC check result when selector is 1', async function () {
w = await circuit.calculateWitness(inputs);
const revealedData_packed = await circuit.getOutput(w, ['revealedData_packed[3]']);
const reveal_unpacked = formatAndUnpackReveal(revealedData_packed);
console.log('reveal_unpacked', reveal_unpacked);
// OFAC result is stored at index 90 in the revealed data
const ofac_results = reveal_unpacked.slice(90, 93);
console.log('ofac_results', ofac_results);
expect(ofac_results).to.deep.equal(
['\x01', '\x01', '\x01'],
'OFAC result bits should be [1, 1, 1]'
);
expect(ofac_results).to.not.equal(['\x00', '\x00', '\x00'], 'OFAC result should be revealed');
});
it('should not disclose OFAC check result when selector is 0', async function () {
w = await circuit.calculateWitness({
...inputs,
selector_ofac: '0',
});
const revealedData_packed = await circuit.getOutput(w, ['revealedData_packed[3]']);
const reveal_unpacked = formatAndUnpackReveal(revealedData_packed);
// OFAC result should be hidden (null byte)
const ofac_result = reveal_unpacked[90];
expect(ofac_result).to.equal('\x00', 'OFAC result should not be revealed');
});
it('should show different levels of OFAC matching', async function () {
// Test cases for different matching scenarios
const testCases = [
{
desc: 'No details match',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'USA',
'010101',
'300101',
'DIF123456',
'DIFFERENT NAME',
'DIFFERENT SURNAME'
),
expectedBits: ['\x01', '\x01', '\x01'],
},
{
desc: 'Only passport number matches',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'ESP', // different nationality
'000101',
'300101',
'98lh90556', // Matching passport number
'DIFFERENT NAME',
'DIFFERENT SURNAME'
),
expectedBits: ['\x01', '\x01', '\x01'],
},
{
desc: 'Only nationality matches',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'FRA',
'991231',
'300101',
'DIF123456', // different passport number
'DIFFERENT NAME',
'DIFFERENT SURNAME'
),
expectedBits: ['\x01', '\x01', '\x01'],
},
{
desc: 'Only passport number and nationality matches',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'FRA',
'991231',
'300101',
'98lh90556',
'DIFFERENT NAME',
'DIFFERENT SURNAME'
),
expectedBits: ['\x00', '\x01', '\x01'],
},
{
desc: 'Name and DOB matches (so YOB matches too)',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'FRA',
'541007',
'300101',
'DIF123456',
'HENAO MONTOYA',
'ARCANGEL DE JESUS'
),
expectedBits: ['\x01', '\x00', '\x00'],
},
{
desc: 'Only name and YOB match',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'FRA',
'541299',
'300101', // Same year (54) different month/day
'DIF123456',
'HENAO MONTOYA',
'ARCANGEL DE JESUS'
),
expectedBits: ['\x01', '\x01', '\x00'],
},
{
desc: 'All details match',
data: genMockPassportData(
'sha256',
'sha256',
'rsa_sha256_65537_2048',
'FRA',
'541007',
'300101',
'98lh90556',
'HENAO MONTOYA',
'ARCANGEL DE JESUS'
),
expectedBits: ['\x00', '\x00', '\x00'],
},
];
for (const testCase of testCases) {
console.log(`Testing: ${testCase.desc}`);
const passportData = initPassportDataParsing(testCase.data);
const sanctionedCommitment = generateCommitment(
secret,
PASSPORT_ATTESTATION_ID,
passportData
);
tree.insert(BigInt(sanctionedCommitment));
const testInputs = generateCircuitInputsVCandDisclose(
secret,
PASSPORT_ATTESTATION_ID,
passportData,
scope,
Array(88).fill('0'), // selector_dg1
selector_older_than,
tree,
majority,
passportNo_smt,
nameAndDob_smt,
nameAndYob_smt,
'1', // selector_ofac
forbidden_countries_list,
user_identifier
);
w = await circuit.calculateWitness(testInputs);
const revealedData_packed = await circuit.getOutput(w, ['revealedData_packed[3]']);
const reveal_unpacked = formatAndUnpackReveal(revealedData_packed);
const ofac_results = reveal_unpacked.slice(90, 93);
console.log(`${testCase.desc} - OFAC bits:`, ofac_results);
expect(ofac_results).to.deep.equal(
testCase.expectedBits,
`Failed matching pattern for: ${testCase.desc}`
);
}
});
});
});

View File

@@ -4,14 +4,14 @@ import { wasm as wasm_tester } from 'circom_tester';
import { generateCircuitInputsOfac } from '../../../common/src/utils/circuits/generateInputs';
import { SMT } from '@openpassport/zk-kit-smt';
import { poseidon2 } from 'poseidon-lite';
import passportNojson from '../../../common/ofacdata/outputs/passportNoSMT.json';
import nameDobjson from '../../../common/ofacdata/outputs/nameDobSMT.json';
import namejson from '../../../common/ofacdata/outputs/nameSMT.json';
import passportNoAndNationalityjson from '../../../common/ofacdata/outputs/passportNoAndNationalitySMT.json';
import nameAndDobjson from '../../../common/ofacdata/outputs/nameAndDobSMT.json';
import nameAndYobjson from '../../../common/ofacdata/outputs/nameAndYobSMT.json';
import { genMockPassportData } from '../../../common/src/utils/passports/genMockPassportData';
let circuit: any;
// Mock passport added in ofac list to test circuits
// Mock passport not added in ofac list
const passportData = genMockPassportData(
'sha256',
'sha256',
@@ -20,7 +20,7 @@ const passportData = genMockPassportData(
'040211',
'300101'
);
// Mock passport not added in ofac list to test circuits
// Mock passport in ofac list
const passportDataInOfac = genMockPassportData(
'sha256',
'sha256',
@@ -39,19 +39,16 @@ const passportDataInOfac = genMockPassportData(
// 2. Invalid proof : Correct path and corresponding closest leaf AND leaf == pasport_hash ; Valid prove of membership ; Hence non-membership proof would fail
// 3. Invalid proof : Correct path but wrong corresponding siblings ; fails due to calculatedRoot != smt_root
// Level 3: Passport number match in OfacList
describe('OFAC - Passport number match', function () {
// Level 3: Passport number and Nationality match in OfacList
describe('OFAC - Passport number and Nationality match', function () {
this.timeout(0);
let passno_smt = new SMT(poseidon2, true);
let passNoAndNationality_smt = new SMT(poseidon2, true);
let memSmtInputs: any;
let nonMemSmtInputs: any;
before(async () => {
circuit = await wasm_tester(
path.join(
__dirname,
'../../circuits/ofac/../../circuits/tests/ofac/ofac_passport_number_tester.circom'
),
path.join(__dirname, '../../circuits/tests/ofac/ofac_passport_number_tester.circom'),
{
include: [
'node_modules',
@@ -61,11 +58,17 @@ describe('OFAC - Passport number match', function () {
}
);
passno_smt.import(passportNojson);
const proofLevel = 3;
memSmtInputs = generateCircuitInputsOfac(passportDataInOfac, passno_smt, proofLevel);
passNoAndNationality_smt.import(passportNoAndNationalityjson);
memSmtInputs = generateCircuitInputsOfac(
passportDataInOfac,
passNoAndNationality_smt,
proofLevel
);
// console.log('memSmtInputs', memSmtInputs);
nonMemSmtInputs = generateCircuitInputsOfac(passportData, passno_smt, proofLevel);
nonMemSmtInputs = generateCircuitInputsOfac(passportData, passNoAndNationality_smt, proofLevel);
// console.log('nonMemSmtInputs', nonMemSmtInputs);
});
// Compile circuit
@@ -73,7 +76,7 @@ describe('OFAC - Passport number match', function () {
expect(circuit).to.not.be.undefined;
});
// Correct siblings and closest leaf : Everything correct as a proof
// Correct siblings and closest leaf: Everything correct as a proof
it('should pass without errors, all conditions satisfied', async function () {
let w = await circuit.calculateWitness(nonMemSmtInputs);
const ofacCheckResult = (await circuit.getOutput(w, ['ofacCheckResult'])).ofacCheckResult;
@@ -87,7 +90,7 @@ describe('OFAC - Passport number match', function () {
expect(ofacCheckResult).to.equal('0');
});
// Give wrong closest leaf but correct siblings array : Fail of SMT Verification
// Give wrong closest leaf but correct siblings array: Fail of SMT Verification
it('should pass - wrong merkleroot, level 3', async function () {
const wrongInputs = {
...nonMemSmtInputs,
@@ -118,7 +121,7 @@ describe('OFAC - Name and DOB match', function () {
}
);
namedob_smt.import(nameDobjson);
namedob_smt.import(nameAndDobjson);
const proofLevel = 2;
memSmtInputs = generateCircuitInputsOfac(
// proof of membership
@@ -167,8 +170,8 @@ describe('OFAC - Name and DOB match', function () {
});
});
// Level 1: Name match in OfacList
describe('OFAC - Name match', function () {
// Level 1: Name and YOB match in OfacList
describe('OFAC - Name and YOB match', function () {
this.timeout(0);
let name_smt = new SMT(poseidon2, true);
let memSmtInputs: any;
@@ -176,7 +179,7 @@ describe('OFAC - Name match', function () {
before(async () => {
circuit = await wasm_tester(
path.join(__dirname, '../../circuits/tests/ofac/ofac_name_tester.circom'),
path.join(__dirname, '../../circuits/tests/ofac/ofac_name_yob_tester.circom'),
{
include: [
'node_modules',
@@ -186,7 +189,7 @@ describe('OFAC - Name match', function () {
}
);
name_smt.import(namejson);
name_smt.import(nameAndYobjson);
const proofLevel = 1;
memSmtInputs = generateCircuitInputsOfac(
// proof of membership
@@ -233,3 +236,101 @@ describe('OFAC - Name match', function () {
expect(ofacCheckResult).to.equal('0');
});
});
describe.only('OFAC - SMT Security Tests', function () {
this.timeout(0);
let passNoAndNationality_smt = new SMT(poseidon2, true);
let circuit: any;
let baseInputs: any;
before(async () => {
circuit = await wasm_tester(
path.join(__dirname, '../../circuits/tests/ofac/ofac_passport_number_tester.circom'),
{
include: [
'node_modules',
'./node_modules/@zk-kit/binary-merkle-root.circom/src',
'./node_modules/circomlib/circuits',
],
}
);
passNoAndNationality_smt.import(passportNoAndNationalityjson);
const proofLevel = 3;
baseInputs = generateCircuitInputsOfac(passportData, passNoAndNationality_smt, proofLevel);
});
it('should reject proof with invalid siblings length', async function () {
const overflowInputs = {
...baseInputs,
smt_siblings: Array(65)
.fill('0')
.map((x) => x.toString()), // More siblings than tree depth
};
try {
await circuit.calculateWitness(overflowInputs);
expect.fail('Should have thrown error');
} catch (err) {
expect(err.toString()).to.include('Too many values for input signal');
}
});
it('should reject proof with malformed path bits', async function () {
const malformedPathInputs = {
...baseInputs,
// Modify one of the siblings to be an invalid value
smt_siblings: baseInputs.smt_siblings.map((x: string, i: number) =>
i === 0 ? '2'.repeat(254) : x
),
};
try {
await circuit.calculateWitness(malformedPathInputs);
expect.fail('Should have thrown error');
} catch (err) {
expect(err.toString()).to.include('Error');
}
});
// Test against zero value attack
it('should handle zero values in siblings array correctly', async function () {
const zeroSiblingsInputs = {
...baseInputs,
smt_siblings: Array(64)
.fill('0')
.map((x) => x.toString()),
};
let w = await circuit.calculateWitness(zeroSiblingsInputs);
const ofacCheckResult = (await circuit.getOutput(w, ['ofacCheckResult'])).ofacCheckResult;
expect(ofacCheckResult).to.equal('0');
});
// Test against incorrect tree height
it('should reject proof with incorrect number of siblings', async function () {
const wrongHeightInputs = {
...baseInputs,
smt_siblings: baseInputs.smt_siblings.slice(0, 32), // Only half the siblings
};
try {
await circuit.calculateWitness(wrongHeightInputs);
expect.fail('Should have thrown error');
} catch (err) {
expect(err.toString()).to.include('Not enough values for input signal');
}
});
// Test against invalid root
it('should reject proof with invalid merkle root', async function () {
const invalidRootInputs = {
...baseInputs,
smt_root: (BigInt(baseInputs.smt_root) ^ 1n).toString(), // Modify smt_root by one bit
};
let w = await circuit.calculateWitness(invalidRootInputs);
const ofacCheckResult = (await circuit.getOutput(w, ['ofacCheckResult'])).ofacCheckResult;
expect(ofacCheckResult).to.equal('0');
});
});