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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -22,6 +22,7 @@
"country-iso-3-to-2": "^1.1.1",
"elliptic": "^6.5.5",
"fs": "^0.0.1-security",
"i18n-iso-countries": "^7.13.0",
"js-sha1": "^0.7.0",
"js-sha256": "^0.11.0",
"js-sha512": "^0.9.0",

View File

@@ -30,6 +30,8 @@ export const saltLengths = [64, 48, 32];
export const MAX_FORBIDDEN_COUNTRIES_LIST_LENGTH = 10;
export const OFAC_TREE_LEVELS = 64;
export const MAX_PADDED_ECONTENT_LEN: Partial<Record<(typeof hashAlgos)[number], number>> = {
sha1: 384,
sha224: 512,

View File

@@ -1,28 +0,0 @@
// Constant Values in OneTimeSBT.sol
export const ATTRIBUTE_ISSUING_STATE_INDEX = 0;
export const ATTRIBUTE_NAME_INDEX = 1;
export const ATTRIBUTE_PASSPORT_NUMBER_INDEX = 2;
export const ATTRIBUTE_NATIONALITY_INDEX = 3;
export const ATTRIBUTE_DATE_OF_BIRTH_INDEX = 4;
export const ATTRIBUTE_GENDER_INDEX = 5;
export const ATTRIBUTE_EXPIRY_DATE_INDEX = 6;
export const ATTRIBUTE_OLDER_THAN_INDEX = 7;
export const PROVE_RSA_NULLIFIER_INDEX = 0;
export const PROVE_RSA_REVEALED_DATA_PACKED_INDEX = 1;
export const PROVE_RSA_OLDER_THAN_INDEX = 4;
export const PROVE_RSA_PUBKEY_DISCLOSED_INDEX = 6;
export const PROVE_RSA_FORBIDDEN_COUNTRIES_LIST_PACKED_DISCLOSED_INDEX = 38;
export const PROVE_RSA_OFAC_RESULT_INDEX = 40;
export const PROVE_RSA_COMMITMENT_INDEX = 41;
export const PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX = 42;
export const PROVE_RSA_CURRENT_DATE_INDEX = 43;
export const PROVE_RSA_USER_IDENTIFIER_INDEX = 49;
export const PROVE_RSA_SCOPE_INDEX = 50;
export const DSC_BLINDED_DSC_COMMITMENT_INDEX = 0;
export const DSC_MERKLE_ROOT_INDEX = 1;
// Enum in VerifiersManager.sol
export const VERIFICATION_TYPE_ENUM_PROVE = 0;
export const VERIFICATION_TYPE_ENUM_DSC = 1;

View File

@@ -45,7 +45,7 @@ export function unpackReveal(revealedData_packed: string | string[]): string[] {
? revealedData_packed
: [revealedData_packed];
const bytesCount = [31, 31, 29]; // nb of bytes in each of the first three field elements
const bytesCount = [31, 31, 31]; // nb of bytes in each of the first three field elements
const bytesArray = packedArray.flatMap((element: string, index: number) => {
const bytes = bytesCount[index] || 31; // Use 31 as default if index is out of range
const elementBigInt = BigInt(element);

View File

@@ -7,7 +7,7 @@ import {
} from '../../constants/constants';
import { PassportData } from '../types';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { getCountryLeaf, getNameLeaf, getNameDobLeaf, getPassportNumberLeaf, getLeafCscaTree, getLeaf, getLeafDscTree } from '../trees';
import { getCountryLeaf, getNameDobLeaf, getPassportNumberAndNationalityLeaf, getLeafCscaTree, getLeaf, getLeafDscTree, getNameYobLeaf } from '../trees';
import { SMT } from '@openpassport/zk-kit-smt';
import {
extractSignatureFromDSC,
@@ -167,7 +167,9 @@ export function generateCircuitInputsVCandDisclose(
selector_older_than: string | number,
merkletree: LeanIMT,
majority: string,
name_smt: SMT,
passportNo_smt: SMT,
nameAndDob_smt: SMT,
nameAndYob_smt: SMT,
selector_ofac: string | number,
forbidden_countries_list: string[],
user_identifier: string
@@ -199,13 +201,28 @@ export function generateCircuitInputsVCandDisclose(
const formattedMajority = majority.length === 1 ? `0${majority}` : majority;
const majority_ascii = formattedMajority.split('').map((char) => char.charCodeAt(0));
// SMT - OFAC
const name_leaf = getNameLeaf(formattedMrz.slice(10, 49)); // [6-44] + 5 shift
// SMT - OFAC
const passportNo_leaf = getPassportNumberAndNationalityLeaf(formattedMrz.slice(49, 58), formattedMrz.slice(59, 62));
const namedob_leaf = getNameDobLeaf(formattedMrz.slice(10, 49), formattedMrz.slice(62, 68)); // [57-62] + 5 shift
const name_leaf = getNameYobLeaf(formattedMrz.slice(10, 49), formattedMrz.slice(62, 64));
const {
root: smt_root,
closestleaf: smt_leaf_key,
siblings: smt_siblings,
} = generateSMTProof(name_smt, name_leaf);
root: passportNo_smt_root,
closestleaf: passportNo_smt_leaf_key,
siblings: passportNo_smt_siblings,
} = generateSMTProof(passportNo_smt, passportNo_leaf);
const {
root: nameAndDob_smt_root,
closestleaf: nameAndDob_smt_leaf_key,
siblings: nameAndDob_smt_siblings,
} = generateSMTProof(nameAndDob_smt, namedob_leaf);
const {
root: nameAndYob_smt_root,
closestleaf: nameAndYob_smt_leaf_key,
siblings: nameAndYob_smt_siblings,
} = generateSMTProof(nameAndYob_smt, name_leaf);
return {
secret: formatInput(secret),
@@ -223,9 +240,15 @@ export function generateCircuitInputsVCandDisclose(
current_date: formatInput(getCurrentDateYYMMDD()),
majority: formatInput(majority_ascii),
user_identifier: formatInput(castFromUUID(user_identifier)),
smt_root: formatInput(smt_root),
smt_leaf_key: formatInput(smt_leaf_key),
smt_siblings: formatInput(smt_siblings),
ofac_passportno_smt_root: formatInput(passportNo_smt_root),
ofac_passportno_smt_leaf_key: formatInput(passportNo_smt_leaf_key),
ofac_passportno_smt_siblings: formatInput(passportNo_smt_siblings),
ofac_namedob_smt_root: formatInput(nameAndDob_smt_root),
ofac_namedob_smt_leaf_key: formatInput(nameAndDob_smt_leaf_key),
ofac_namedob_smt_siblings: formatInput(nameAndDob_smt_siblings),
ofac_nameyob_smt_root: formatInput(nameAndYob_smt_root),
ofac_nameyob_smt_leaf_key: formatInput(nameAndYob_smt_leaf_key),
ofac_nameyob_smt_siblings: formatInput(nameAndYob_smt_siblings),
selector_ofac: formatInput(selector_ofac),
forbidden_countries_list: formatInput(formatCountriesList(forbidden_countries_list)),
};
@@ -237,9 +260,11 @@ export function generateCircuitInputsOfac(
proofLevel: number
) {
const mrz_bytes = formatMrz(passportData.mrz);
const passport_leaf = getPassportNumberLeaf(mrz_bytes.slice(49, 58));
console.log('mrz_bytes', mrz_bytes);
console.log('mrz_bytes.slice(59, 62)', mrz_bytes.slice(59, 62).map((byte) => String.fromCharCode(byte)));
const passport_leaf = getPassportNumberAndNationalityLeaf(mrz_bytes.slice(49, 58), mrz_bytes.slice(59, 62));
const namedob_leaf = getNameDobLeaf(mrz_bytes.slice(10, 49), mrz_bytes.slice(62, 68)); // [57-62] + 5 shift
const name_leaf = getNameLeaf(mrz_bytes.slice(10, 49)); // [6-44] + 5 shift
const name_leaf = getNameYobLeaf(mrz_bytes.slice(10, 49), mrz_bytes.slice(62, 64));
let root, closestleaf, siblings;
if (proofLevel == 3) {

View File

@@ -1,154 +0,0 @@
import {
PROVE_RSA_NULLIFIER_INDEX,
PROVE_RSA_REVEALED_DATA_PACKED_INDEX,
PROVE_RSA_OLDER_THAN_INDEX,
PROVE_RSA_PUBKEY_DISCLOSED_INDEX,
PROVE_RSA_FORBIDDEN_COUNTRIES_LIST_PACKED_DISCLOSED_INDEX,
PROVE_RSA_OFAC_RESULT_INDEX,
PROVE_RSA_COMMITMENT_INDEX,
PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX,
PROVE_RSA_CURRENT_DATE_INDEX,
PROVE_RSA_USER_IDENTIFIER_INDEX,
PROVE_RSA_SCOPE_INDEX,
DSC_BLINDED_DSC_COMMITMENT_INDEX,
} from '../../../constants/contractConstants';
import { Proof } from '../../types';
export function generateMockRSAProveVerifierInputs({
nullifier = '1',
revealedData_packed = ['2', '3', '4'],
older_than = ['49', '56'],
pubkey_disclosed = [
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
'0',
],
forbidden_contries_list_packed_disclose = ['8', '9'],
ofac_result = '10',
commitment = '11',
blinded_dsc_commitment = '12',
current_date = new Date(),
user_identifier = '13',
scope = '14',
}: {
nullifier?: string;
revealedData_packed?: string[];
older_than?: string[];
pubkey_disclosed?: string[];
forbidden_contries_list_packed_disclose?: string[];
ofac_result?: string;
commitment?: string;
blinded_dsc_commitment?: string;
current_date?: Date;
user_identifier?: string;
scope?: string;
}): Proof {
let pub_signals: string[] = [];
pub_signals[PROVE_RSA_NULLIFIER_INDEX] = nullifier;
pub_signals.splice(PROVE_RSA_REVEALED_DATA_PACKED_INDEX, 0, ...revealedData_packed);
pub_signals.splice(PROVE_RSA_OLDER_THAN_INDEX, 0, ...older_than);
pub_signals.splice(PROVE_RSA_PUBKEY_DISCLOSED_INDEX, 0, ...pubkey_disclosed);
pub_signals.splice(
PROVE_RSA_FORBIDDEN_COUNTRIES_LIST_PACKED_DISCLOSED_INDEX,
0,
...forbidden_contries_list_packed_disclose
);
pub_signals[PROVE_RSA_OFAC_RESULT_INDEX] = ofac_result;
pub_signals[PROVE_RSA_COMMITMENT_INDEX] = commitment;
pub_signals[PROVE_RSA_BLINDED_DSC_COMMITMENT_INDEX] = blinded_dsc_commitment;
pub_signals.splice(PROVE_RSA_CURRENT_DATE_INDEX, 0, ...getDateNum(current_date));
pub_signals[PROVE_RSA_USER_IDENTIFIER_INDEX] = user_identifier;
pub_signals[PROVE_RSA_SCOPE_INDEX] = scope;
let proof: Proof = {
proof: {
a: ['1', '1'],
b: [
['1', '2'],
['3', '4'],
],
c: ['5', '6'],
},
pub_signals: pub_signals,
};
return proof;
}
export function generateMockDSCVerifierInputs({
blinded_dsc_commitment = '12',
}: {
blinded_dsc_commitment?: string;
}): Proof {
let pub_signals: string[] = [];
pub_signals[DSC_BLINDED_DSC_COMMITMENT_INDEX] = blinded_dsc_commitment;
let proof: Proof = {
proof: {
a: ['1', '1'],
b: [
['1', '2'],
['3', '4'],
],
c: ['5', '6'],
},
pub_signals: pub_signals,
};
return proof;
}
function getDateNum(date: Date = new Date()): string[] {
const year = date.getUTCFullYear() % 100;
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const dateNum = [
Math.floor(year / 10),
year % 10,
Math.floor(month / 10),
month % 10,
Math.floor(day / 10),
day % 10,
];
return dateNum.map((num) => num.toString());
}
export function convertProofTypeIntoInput(proof: Proof) {
return {
a: proof.proof.a,
b: proof.proof.b,
c: proof.proof.c,
pubSignals: proof.pub_signals,
};
}

View File

@@ -1,4 +1,4 @@
import { poseidon9, poseidon3, poseidon2, poseidon6, poseidon13 } from 'poseidon-lite';
import { poseidon9, poseidon3, poseidon2, poseidon6, poseidon13, poseidon12 } from 'poseidon-lite';
import { ChildNodes, SMT } from '@openpassport/zk-kit-smt';
import { stringToAsciiBigIntArray } from './circuits/uuid';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
@@ -8,12 +8,15 @@ import {
import { packBytesAndPoseidon } from './hash';
import { DscCertificateMetaData, parseDscCertificateData } from './passports/passport_parsing/parseDscCertificateData';
import { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple';
import { CSCA_TREE_DEPTH, DSC_TREE_DEPTH, max_csca_bytes } from '../constants/constants';
import { CSCA_TREE_DEPTH, DSC_TREE_DEPTH, max_csca_bytes, OFAC_TREE_LEVELS } from '../constants/constants';
import { max_dsc_bytes } from '../constants/constants';
import serialized_csca_tree from '../../pubkeys/serialized_csca_tree.json';
import serialized_dsc_tree from '../../pubkeys/serialized_dsc_tree.json';
import { IMT } from '@openpassport/zk-kit-imt';
import { pad } from './passports/passport';
import countries from "i18n-iso-countries";
import en from "i18n-iso-countries/langs/en.json";
countries.registerLocale(en);
export async function fetchTreeFromUrl(url: string): Promise<LeanIMT> {
const response = await fetch(url);
@@ -75,6 +78,7 @@ export function getTreeInclusionProof(leaf: string, type: 'csca' | 'dsc') {
throw new Error('Invalid tree type');
}
}
function getDscTreeInclusionProof(leaf: string): [string, number[], bigint[], number] {
const hashFunction = (a: any, b: any) => poseidon2([a, b]);
const tree = LeanIMT.import(hashFunction, serialized_dsc_tree);
@@ -96,14 +100,13 @@ function getCscaTreeInclusionProof(leaf: string) {
const proof = tree.createProof(index);
return [tree.root, proof.pathIndices.map(index => index.toString()), proof.siblings.flat().map(sibling => sibling.toString())];
}
export function getCscaTreeRoot() {
let tree = new IMT(poseidon2, CSCA_TREE_DEPTH, 0, 2);
tree.setNodes(serialized_csca_tree);
return tree.root;
}
export function formatRoot(root: string): string {
let rootHex = BigInt(root).toString(16);
return rootHex.length % 2 === 0 ? '0x' + rootHex : '0x0' + rootHex;
@@ -130,7 +133,7 @@ export function generateSMTProof(smt: SMT, leaf: bigint) {
// PATH, SIBLINGS manipulation as per binary tree in the circuit
siblings.reverse();
while (siblings.length < 256) siblings.push(BigInt(0));
while (siblings.length < OFAC_TREE_LEVELS) siblings.push(BigInt(0));
// ----- Useful for debugging hence leaving as comments -----
// const binary = entry[0].toString(2)
@@ -178,13 +181,10 @@ export function generateMerkleProof(imt: LeanIMT, _index: number, maxleaf_depth:
return { siblings, path, leaf_depth };
}
// SMT trees for 3 levels :
// 1. Passport tree : level 3 (Absolute Match)
// 2. Names and dob combo tree : level 2 (High Probability Match)
// 3. Names tree : level 1 (Partial Match)
// SMT trees for 3 levels of matching :
// 1. Passport Number and Nationality tree : level 3 (Absolute Match)
// 2. Name and date of birth combo tree : level 2 (High Probability Match)
// 3. Name and year of birth combo tree : level 1 (Partial Match)
export function buildSMT(field: any[], treetype: string): [number, number, SMT] {
let count = 0;
let startTime = performance.now();
@@ -201,12 +201,12 @@ export function buildSMT(field: any[], treetype: string): [number, number, SMT]
}
let leaf = BigInt(0);
if (treetype == 'passport') {
leaf = processPassport(entry.Pass_No, i);
} else if (treetype == 'name_dob') {
leaf = processNameDob(entry, i);
} else if (treetype == 'name') {
leaf = processName(entry.First_Name, entry.Last_Name, i);
if (treetype == 'passport_no_and_nationality') {
leaf = processPassportNoAndNationality(entry.Pass_No, entry.Pass_Country, i);
} else if (treetype == 'name_and_dob') {
leaf = processNameAndDob(entry, i);
} else if (treetype == 'name_and_yob') {
leaf = processNameAndYob(entry, i);
} else if (treetype == 'country') {
const keys = Object.keys(entry);
leaf = processCountry(keys[0], entry[keys[0]], i);
@@ -226,24 +226,60 @@ export function buildSMT(field: any[], treetype: string): [number, number, SMT]
return [count, performance.now() - startTime, tree];
}
function processPassport(passno: string, index: number): bigint {
function processPassportNoAndNationality(passno: string, nationality: string, index: number): bigint {
if (passno.length > 9) {
console.log('passport length is greater than 9:', index, passno);
console.log('passport number length is greater than 9:', index, passno);
} else if (passno.length < 9) {
while (passno.length != 9) {
passno += '<';
}
}
const leaf = getPassportNumberLeaf(stringToAsciiBigIntArray(passno));
const countryCode = getCountryCode(nationality);
if (!countryCode) {
console.log('Error getting country code', index, nationality);
return BigInt(0);
}
console.log('nationality and countryCode', nationality, countryCode);
const leaf = getPassportNumberAndNationalityLeaf(
stringToAsciiBigIntArray(passno),
stringToAsciiBigIntArray(countryCode),
index
);
if (!leaf) {
console.log('Error creating leaf value', index, passno);
console.log('Error creating leaf value', index, passno, nationality);
return BigInt(0);
}
return leaf;
}
function processNameDob(entry: any, i: number): bigint {
// this is a temporary workaround for some of the country name,
// will be removed once we parse the OFAC list better, starting from the XML file.
const normalizeCountryName = (country: string): string => {
const mapping: Record<string, string> = {
"palestinian": "Palestine",
"korea, north": "North Korea",
"korea, south": "Korea, Republic of",
"united kingdom": "United Kingdom",
"syria": "Syrian Arab Republic",
"burma": "Myanmar",
"cabo verde": "Cape Verde",
"congo, democratic republic of the": "Democratic Republic of the Congo",
"macau": "Macao",
};
return mapping[country.toLowerCase()] || country;
};
const getCountryCode = (countryName: string): string | undefined => {
return countries.getAlpha3Code(normalizeCountryName(countryName), "en");
};
function generateSmallKey(input: bigint): bigint {
return input % (BigInt(1) << BigInt(OFAC_TREE_LEVELS));
}
function processNameAndDob(entry: any, i: number): bigint {
const firstName = entry.First_Name;
const lastName = entry.Last_Name;
const day = entry.day;
@@ -255,8 +291,30 @@ function processNameDob(entry: any, i: number): bigint {
}
const nameHash = processName(firstName, lastName, i);
const dobHash = processDob(day, month, year, i);
const leaf = poseidon2([dobHash, nameHash]);
return leaf;
return generateSmallKey(poseidon2([dobHash, nameHash]));
}
function processNameAndYob(entry: any, i: number): bigint {
const firstName = entry.First_Name;
const lastName = entry.Last_Name;
const year = entry.year;
if (year == null) {
console.log('year is null', i, entry);
return BigInt(0);
}
const nameHash = processName(firstName, lastName, i);
const yearHash = processYear(year, i);
return generateSmallKey(poseidon2([yearHash, nameHash]));
}
function processYear(year: string, i: number): bigint {
year = year.slice(-2);
const yearArr = stringToAsciiBigIntArray(year);
return getYearLeaf(yearArr);
}
function getYearLeaf(yearArr: (bigint | number)[]): bigint {
return poseidon2(yearArr);
}
function processName(firstName: string, lastName: string, i: number): bigint {
@@ -337,13 +395,18 @@ export function getCountryLeaf(
}
}
export function getPassportNumberLeaf(passport: (bigint | number)[], i?: number): bigint {
export function getPassportNumberAndNationalityLeaf(passport: (bigint | number)[], nationality: (bigint | number)[], i?: number): bigint {
if (passport.length !== 9) {
console.log('parsed passport length is not 9:', i, passport);
return;
}
if (nationality.length !== 3) {
console.log('parsed nationality length is not 3:', i, nationality);
return;
}
try {
return poseidon9(passport);
const fullHash = poseidon12(passport.concat(nationality));
return generateSmallKey(fullHash);
} catch (err) {
console.log('err : passport', err, i, passport);
}
@@ -354,7 +417,15 @@ export function getNameDobLeaf(
dobMrz: (bigint | number)[],
i?: number
): bigint {
return poseidon2([getDobLeaf(dobMrz), getNameLeaf(nameMrz)]);
return generateSmallKey(poseidon2([getDobLeaf(dobMrz), getNameLeaf(nameMrz)]));
}
export function getNameYobLeaf(
nameMrz: (bigint | number)[],
yobMrz: (bigint | number)[],
i?: number
): bigint {
return generateSmallKey(poseidon2([getYearLeaf(yobMrz), getNameLeaf(nameMrz)]));
}
export function getNameLeaf(nameMrz: (bigint | number)[], i?: number): bigint {

View File

@@ -782,6 +782,13 @@ __metadata:
languageName: node
linkType: hard
"diacritics@npm:1.3.0":
version: 1.3.0
resolution: "diacritics@npm:1.3.0"
checksum: 10c0/bc99c3d2e64315b1830f1573eafe1f7b06fd5dbc9687f35ea8e2e25ce8618d1444d0a2c8313b98467b0aff1d0ee35b8f9f67ef214e56e810b37da3cdb29785ac
languageName: node
linkType: hard
"diff@npm:^3.1.0":
version: 3.5.0
resolution: "diff@npm:3.5.0"
@@ -1499,6 +1506,15 @@ __metadata:
languageName: node
linkType: hard
"i18n-iso-countries@npm:^7.13.0":
version: 7.13.0
resolution: "i18n-iso-countries@npm:7.13.0"
dependencies:
diacritics: "npm:1.3.0"
checksum: 10c0/7b9bb3d64e575857d7bcf0543fd86b50b9233689140c3e9b2dc9c125bbb71f0cbb800b9546a287ea6e9d745a3c446dc272db66863fa6d28b70add88cc0fa4fda
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
@@ -2397,6 +2413,7 @@ __metadata:
country-iso-3-to-2: "npm:^1.1.1"
elliptic: "npm:^6.5.5"
fs: "npm:^0.0.1-security"
i18n-iso-countries: "npm:^7.13.0"
js-sha1: "npm:^0.7.0"
js-sha256: "npm:^0.11.0"
js-sha512: "npm:^0.9.0"