* feat: selfrica circuit and tests

* chore: remove unused code

* feat: test for ofac,date and olderthan

* fix: public signal constant

* feat: add contract tests

* feat: helper function to gen TEE input

* feat: gen circuit inputs with signature

* feat: seralized base64

* fix: DateIsLessFullYear componenet

* feat: register circuit for selfrica

* feat: selfrica disclose circuit and test

* fix: common module error

* feat: add more test and fix constant

* fix: commitment calculation

* feat: selfrica contracts

* test: selfrica register using unified circuit

* feat: register persona and selfrica circuit

* feat: selfrica circuit and tests

* chore: remove unused code

* feat: test for ofac,date and olderthan

* fix: public signal constant

* feat: add contract tests

* feat: helper function to gen TEE input

* feat: gen circuit inputs with signature

* feat: seralized base64

* fix: DateIsLessFullYear componenet

* feat: register circuit for selfrica

* feat: selfrica disclose circuit and test

* fix: common module error

* feat: add more test and fix constant

* fix: commitment calculation

* feat: selfrica contracts

* test: selfrica register using unified circuit

* feat: register persona and selfrica circuit

* refactor: contract size reduction for IdentityVerificationHubImplV2

export function logic to external libs, reduce compiler runs to 200, update deploy scripts to link new libs

* feat: disclose circuit for persona

* feat: update  persona ofac trees

* feat; register circuit for selfper

* feat: disclose test for selfper

* chore: refactor

* chore : remove unused circuits

* chore: rename selfper to kyc

* chore: update comments

* feat: constrain s to be 251 bit

* feat: add range check on majority ASCII and comments

* feat: range check on neg_r_inv

* chore: remove is pk zero constrain

* merge dev

* feat: add registerPubkey function to Selfrica with GCPJWT Verification

* test: add testing for GCPJWT verification on Selfrica

* fix: script that calls register_selfrica circuits (ptau:14 -> ptau:15)

* fix: get remaining Selfrica tests working with proper import paths

* refactor: store pubkeys as string

also add some comment code for registerPubkey function

* refactor: remove registerPubkeyCommitment function

some tests now skipped as awaiting changes to how pubkeys are stored (string instead of uint256)

* feat: use hex decoding for the pubkey commitment

* test: adjust tests for pubkey being string again

* fix: remove old references to registerPubkey

* docs: add full natspec for IdentityRegistrySelfricaImplV1

* docs: update files in rest of the repo for Selfrica attestation type

* test: fix broken tests

* fix: builds and move to kyc from selfrica

* fix: constrain r_inv, Rx, s, T

* feat: eddsa

* feat: add onlyTEE check to registerPubkeyCommitment

onlyOwner is able to change onlyTEE

* refactor: update gcpRootCAPubkeyHash to be changeable by owner

* feat: add events for update functions

* style: move functions to be near other similar functions

* fix: kyc happy flow

* fix: all contract tests passing

| fix: timestamp conversion with Date(), migrate to V2 for endToEnd test, scope formatting, fix register aadhaar issue by using block.timestamp instead of Date.now(), fix changed getter function name, enable MockGCPJWTVerifier with updated file paths, add missing LeanIMT import, fix user identifier format

* audit: bind key offset-value offset and ensure image_digest only occurs once in the payload

* fix: constrain bracket

* chore: update comment

* audit: hardcode attestation id

* audit: make sure R and pubkey are on the curve

* audit: ensure pubkey is within bounds

* fix: all contract tests passing

* feat: change max length to 99 from 74

* audit: don't check sha256 padding

* audit: check the last window as well

* audit: single occurance for eat_nonce and image_digest

* audit: check if the certs are expired

* audit: add the timestamp check to the contract

* audit: make sure the person is less than 255 years of age

* audit fixes

* chore: yarn.lock

* fix: build fixes

* fix: aadhaar timestamp

* lint

* fix: types

* format

---------

Co-authored-by: vishal <vishalkoolkarni0045@gmail.com>
Co-authored-by: Evi Nova <tranquil_flow@protonmail.com>
This commit is contained in:
Nesopie
2026-01-19 15:54:37 +05:30
committed by GitHub
parent 5b5110925a
commit e77247f372
99 changed files with 6885 additions and 701 deletions

View File

@@ -3,6 +3,7 @@ export type document_type = 'passport' | 'id_card';
export type hashAlgosTypes = 'sha512' | 'sha384' | 'sha256' | 'sha224' | 'sha1';
export const AADHAAR_ATTESTATION_ID = '3';
export const API_URL = 'https://api.self.xyz';
export const KYC_ATTESTATION_ID = '4';
export const API_URL_STAGING = 'https://api.staging.self.xyz';

View File

@@ -20,6 +20,8 @@ export {
IDENTITY_TREE_URL_STAGING_ID_CARD,
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
AADHAAR_ATTESTATION_ID,
KYC_ATTESTATION_ID,
PCR0_MANAGER_ADDRESS,
REDIRECT_URL,
RPC_URL,

View File

@@ -11,9 +11,6 @@ async function build_aadhaar_ofac_smt() {
// -----PASSPORT DATA-----
console.log(`Reading data from ${baseInputPath}`);
const passports = JSON.parse(
fs.readFileSync(`${baseInputPath}passports.json`) as unknown as string
);
// -----Aadhaar DATA-----
console.log('\nBuilding Aadhaar Card SMTs...');

View File

@@ -22,9 +22,73 @@ import type { AadhaarData, Environment, IDDocument, OfacTree } from '../../utils
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import { KycField } from '../kyc/constants.js';
export { generateCircuitInputsRegister } from './generateInputs.js';
// export function generateTEEInputsKycDisclose( secret: string,
// kycData: KycData,
// selfApp: SelfApp,
// getTree: <T extends 'ofac' | 'commitment'>(
// doc: DocumentCategory,
// tree: T
// ) => T extends 'ofac' ? OfacTree : any
// ) {
// const {generateKycInputWithOutSig} = require('../kyc/generateInputs.js');
// const { scope, disclosures, userId, userDefinedData, chainID } = selfApp;
// const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
// // Map SelfAppDisclosureConfig to KycField array
// const mapDisclosuresToKycFields = (config: SelfAppDisclosureConfig): KycField[] => {
// const mapping: [keyof SelfAppDisclosureConfig, KycField][] = [
// ['issuing_state', 'ADDRESS'],
// ['nationality', 'COUNTRY'],
// ['name', 'FULL_NAME'],
// ['passport_number', 'ID_NUMBER'],
// ['date_of_birth', 'DOB'],
// ['gender', 'GENDER'],
// ['expiry_date', 'EXPIRY_DATE'],
// ];
// return mapping.filter(([key]) => config[key]).map(([_, field]) => field);
// };
// const ofac_trees = getTree('kyc', 'ofac');
// if (!ofac_trees) {
// throw new Error('OFAC trees not loaded');
// }
// if (!ofac_trees.nameAndDob || !ofac_trees.nameAndYob) {
// throw new Error('Invalid OFAC tree structure: missing required fields');
// }
// const nameAndDobSMT = new SMT(poseidon2, true);
// const nameAndYobSMT = new SMT(poseidon2, true);
// nameAndDobSMT.import(ofac_trees.nameAndDob);
// nameAndYobSMT.import(ofac_trees.nameAndYob);
// const inputs = generateKycInputWithOutSig(
// kycData.serializedRealData,
// nameAndDobSMT,
// nameAndYobSMT,
// disclosures.ofac,
// scope,
// userIdentifierHash.toString(),
// mapDisclosuresToKycFields(disclosures),
// disclosures.excludedCountries,
// disclosures.minimumAge
// );
// return {
// inputs,
// circuitName: 'vc_and_disclose_kyc',
// endpointType: selfApp.endpointType,
// endpoint: selfApp.endpoint,
// };
// }
export function generateTEEInputsAadhaarDisclose(
secret: string,
aadhaarData: AadhaarData,
@@ -175,6 +239,15 @@ export function generateTEEInputsDiscloseStateless(
);
return { inputs, circuitName, endpointType, endpoint };
}
// if (passportData.documentCategory === 'kyc') {
// const { inputs, circuitName, endpointType, endpoint } = generateTEEInputsKycDisclose(
// secret,
// passportData,
// selfApp,
// getTree
// );
// return { inputs, circuitName, endpointType, endpoint };
// }
const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp;
const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
const scope_hash = hashEndpointWithScope(endpoint, scope);
@@ -253,6 +326,10 @@ export async function generateTEEInputsRegister(
return { inputs, circuitName, endpointType, endpoint };
}
// if (passportData.documentCategory === 'kyc') {
// throw new Error('Kyc does not support registration');
// }
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree as string);
const circuitName = getCircuitNameFromPassportData(passportData, 'register');
const endpointType = env === 'stg' ? 'staging_celo' : 'celo';

View File

@@ -0,0 +1,179 @@
export const KYC_COUNTRY_INDEX = 0;
export const KYC_COUNTRY_LENGTH = 3;
export const KYC_ID_TYPE_INDEX = KYC_COUNTRY_INDEX + KYC_COUNTRY_LENGTH;
export const KYC_ID_TYPE_LENGTH = 27;
export const KYC_ID_NUMBER_INDEX = KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH;
export const KYC_ID_NUMBER_LENGTH = 32; // Updated: max(20, 32) = 32
export const KYC_ISSUANCE_DATE_INDEX = KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH;
export const KYC_ISSUANCE_DATE_LENGTH = 8;
export const KYC_EXPIRY_DATE_INDEX = KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH;
export const KYC_EXPIRY_DATE_LENGTH = 8;
export const KYC_FULL_NAME_INDEX = KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH;
export const KYC_FULL_NAME_LENGTH = 64; // Updated: max(40, 64) = 64
export const KYC_DOB_INDEX = KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH;
export const KYC_DOB_LENGTH = 8;
export const KYC_PHOTO_HASH_INDEX = KYC_DOB_INDEX + KYC_DOB_LENGTH;
export const KYC_PHOTO_HASH_LENGTH = 32;
export const KYC_PHONE_NUMBER_INDEX = KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH;
export const KYC_PHONE_NUMBER_LENGTH = 12;
export const KYC_DOCUMENT_INDEX = KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH;
export const KYC_DOCUMENT_LENGTH = 32; // Updated: max(2, 32) = 32
export const KYC_GENDER_INDEX = KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH;
export const KYC_GENDER_LENGTH = 6;
export const KYC_ADDRESS_INDEX = KYC_GENDER_INDEX + KYC_GENDER_LENGTH;
export const KYC_ADDRESS_LENGTH = 100;
export const KYC_MAX_LENGTH = KYC_ADDRESS_INDEX + KYC_ADDRESS_LENGTH;
// ------------------------------
// Field lengths for selector bits
// ------------------------------
export const KYC_FIELD_LENGTHS = {
COUNTRY: KYC_COUNTRY_LENGTH, // 3
ID_TYPE: KYC_ID_TYPE_LENGTH, // 27
ID_NUMBER: KYC_ID_NUMBER_LENGTH, // 32 (updated)
ISSUANCE_DATE: KYC_ISSUANCE_DATE_LENGTH, // 8
EXPIRY_DATE: KYC_EXPIRY_DATE_LENGTH, // 8
FULL_NAME: KYC_FULL_NAME_LENGTH, // 64 (updated)
DOB: KYC_DOB_LENGTH, // 8
PHOTO_HASH: KYC_PHOTO_HASH_LENGTH, // 32
PHONE_NUMBER: KYC_PHONE_NUMBER_LENGTH, // 12
DOCUMENT: KYC_DOCUMENT_LENGTH, // 32 (updated)
GENDER: KYC_GENDER_LENGTH, // 6
ADDRESS: KYC_ADDRESS_LENGTH, // 100
} as const;
// ------------------------------
// Reveal data indices for selector bits
// ------------------------------
export const KYC_REVEAL_DATA_INDICES = {
COUNTRY: 0,
ID_TYPE: KYC_COUNTRY_LENGTH, // 3
ID_NUMBER: KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH, // 30
ISSUANCE_DATE: KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH, // 62 (updated)
EXPIRY_DATE: KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH, // 70 (updated)
FULL_NAME: KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH, // 78 (updated)
DOB: KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH, // 142 (updated)
PHOTO_HASH: KYC_DOB_INDEX + KYC_DOB_LENGTH, // 150 (updated)
PHONE_NUMBER: KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH, // 182 (updated)
DOCUMENT: KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH, // 194 (updated)
GENDER: KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH, // 226 (updated)
ADDRESS: KYC_GENDER_INDEX + KYC_GENDER_LENGTH, // 232 (updated)
} as const;
// ------------------------------
// Selector bit positions for each field
// ------------------------------
export const KYC_SELECTOR_BITS = {
COUNTRY: Array.from({ length: KYC_COUNTRY_LENGTH }, (_, i) => i) as number[], // 0-2
ID_TYPE: Array.from({ length: KYC_ID_TYPE_LENGTH }, (_, i) => i + KYC_COUNTRY_LENGTH) as number[], // 3-29
ID_NUMBER: Array.from(
{ length: KYC_ID_NUMBER_LENGTH },
(_, i) => i + KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH
) as number[], // 30-61 (updated)
ISSUANCE_DATE: Array.from(
{ length: KYC_ISSUANCE_DATE_LENGTH },
(_, i) => i + KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
) as number[], // 62-69 (updated)
EXPIRY_DATE: Array.from(
{ length: KYC_EXPIRY_DATE_LENGTH },
(_, i) => i + KYC_ISSUANCE_DATE_INDEX + KYC_ISSUANCE_DATE_LENGTH
) as number[], // 70-77 (updated)
FULL_NAME: Array.from(
{ length: KYC_FULL_NAME_LENGTH },
(_, i) => i + KYC_EXPIRY_DATE_INDEX + KYC_EXPIRY_DATE_LENGTH
) as number[], // 78-141 (updated)
DOB: Array.from(
{ length: KYC_DOB_LENGTH },
(_, i) => i + KYC_FULL_NAME_INDEX + KYC_FULL_NAME_LENGTH
) as number[], // 142-149 (updated)
PHOTO_HASH: Array.from(
{ length: KYC_PHOTO_HASH_LENGTH },
(_, i) => i + KYC_DOB_INDEX + KYC_DOB_LENGTH
) as number[], // 150-181 (updated)
PHONE_NUMBER: Array.from(
{ length: KYC_PHONE_NUMBER_LENGTH },
(_, i) => i + KYC_PHOTO_HASH_INDEX + KYC_PHOTO_HASH_LENGTH
) as number[], // 182-193 (updated)
DOCUMENT: Array.from(
{ length: KYC_DOCUMENT_LENGTH },
(_, i) => i + KYC_PHONE_NUMBER_INDEX + KYC_PHONE_NUMBER_LENGTH
) as number[], // 194-225 (updated)
GENDER: Array.from(
{ length: KYC_GENDER_LENGTH },
(_, i) => i + KYC_DOCUMENT_INDEX + KYC_DOCUMENT_LENGTH
) as number[], // 226-231 (updated)
ADDRESS: Array.from(
{ length: KYC_ADDRESS_LENGTH },
(_, i) => i + KYC_GENDER_INDEX + KYC_GENDER_LENGTH
) as number[], // 232-331 (updated)
} as const;
export type KycField = keyof typeof KYC_FIELD_LENGTHS;
// ------------------------------
// Public Signals Indices
// ------------------------------
export const KYC_PUBLIC_SIGNALS_ATTESTATION_ID = 0;
export const KYC_PUBLIC_SIGNALS_REVEALED_DATA_PACKED = 1;
export const KYC_PUBLIC_SIGNALS_REVEALED_DATA_PACKED_LENGTH = 9;
export const KYC_PUBLIC_SIGNALS_FORBIDDEN_COUNTRIES_PACKED = 10;
export const KYC_PUBLIC_SIGNALS_FORBIDDEN_COUNTRIES_PACKED_LENGTH = 4;
export const KYC_PUBLIC_SIGNALS_NULLIFIER = 14;
export const KYC_PUBLIC_SIGNALS_SCOPE = 15;
export const KYC_PUBLIC_SIGNALS_USER_IDENTIFIER = 16;
export const KYC_PUBLIC_SIGNALS_CURRENT_DATE = 17;
export const KYC_PUBLIC_SIGNALS_CURRENT_DATE_LENGTH = 8;
export const KYC_PUBLIC_SIGNALS_OFAC_NAME_DOB_SMT_ROOT = 25;
export const KYC_PUBLIC_SIGNALS_OFAC_NAME_YOB_SMT_ROOT = 26;
// ------------------------------
// Helper functions for selector bits
// ------------------------------
export function createKycSelector(fieldsToReveal: KycField[]): [bigint, bigint] {
const bits = Array(KYC_MAX_LENGTH).fill(0);
for (const field of fieldsToReveal) {
const selectorBits = KYC_SELECTOR_BITS[field];
for (const bit of selectorBits) {
bits[bit] = 1;
}
}
let lowResult = 0n;
let highResult = 0n;
const splitPoint = Math.floor(KYC_MAX_LENGTH / 2);
for (let i = 0; i < splitPoint; i++) {
if (bits[i]) {
lowResult += 1n << BigInt(i);
}
}
for (let i = splitPoint; i < KYC_MAX_LENGTH; i++) {
if (bits[i]) {
highResult += 1n << BigInt(i - splitPoint);
}
}
return [lowResult, highResult];
}

View File

@@ -0,0 +1,121 @@
import { poseidon5 } from 'poseidon-lite';
import { modulus } from './utils.js';
import { addPoint, Base8, mulPointEscalar, Point, subOrder } from '@zk-kit/baby-jubjub';
import { EdDSAPoseidon, Signature } from '@zk-kit/eddsa-poseidon';
import { packBytesAndPoseidon } from '../../hash.js';
export function signEdDSA(key: bigint, msg: number[]): [Signature, Point<bigint>] {
key = modulus(key, subOrder);
const msgHash = BigInt(packBytesAndPoseidon(msg));
const eddsaFactory = new EdDSAPoseidon(key.toString());
const signature = eddsaFactory.signMessage(msgHash.toString());
console.assert(
verifyEdDSAZkKit(eddsaFactory.publicKey, signature, msg) == true,
'Invalid signature'
);
return [signature, eddsaFactory.publicKey];
}
export const verifyEdDSAZkKit = (pubkey: Point<bigint>, sig: Signature, msgArr: number[]) => {
let msg = BigInt(packBytesAndPoseidon(msgArr));
let challenge = poseidon5([sig.R8[0], sig.R8[1], pubkey[0], pubkey[1], msg]);
let S = mulPointEscalar(Base8, BigInt(sig.S));
let c_Pk = mulPointEscalar(pubkey, modulus(challenge * 8n, subOrder));
let R_plus_c_Pk = addPoint([BigInt(sig.R8[0]), BigInt(sig.R8[1])], c_Pk);
let minus_R_plus_c_Pk = mulPointEscalar(R_plus_c_Pk, modulus(-1n, subOrder));
let V_plus_minus_R_plus_c_Pk = addPoint(S, minus_R_plus_c_Pk);
let final = mulPointEscalar(V_plus_minus_R_plus_c_Pk, 8n);
return final[0] == 0n && final[1] == 1n;
};
export function buffer2bits(buff) {
const res = [];
for (let i = 0; i < buff.length; i++) {
for (let j = 0; j < 8; j++) {
if ((buff[i] >> j) & 1) {
res.push(1n);
} else {
res.push(0n);
}
}
}
return res;
}
// export const verifyEdDSA = (pubkey: Point<bigint>, sig: Signature, msgStr: string) => {
// let msg = modulus(BigInt(packBytesAndPoseidon(msgStr.split('').map(c => c.charCodeAt(0)))), subOrder);
// let challenge = modulus(poseidon5([
// sig.R[0],
// sig.R[1],
// pubkey[0],
// pubkey[1],
// msg
// ]), subOrder);
// let S = mulPointEscalar(Base8, sig.s);
// let c_Pk = mulPointEscalar(pubkey, challenge);
// let R_plus_c_Pk = addPoint(sig.R, c_Pk);
// let minus_R_plus_c_Pk = mulPointEscalar(R_plus_c_Pk, modulus(-1n, subOrder));
// let V_plus_minus_R_plus_c_Pk = addPoint(S, minus_R_plus_c_Pk);
// //for the taceo library
// let final = mulPointEscalar(V_plus_minus_R_plus_c_Pk, 8n);
// return final[0] == 0n && final[1] == 1n;
// }
//TODO: zk-kit/baby-jubjub uses affine which involses Fr.div which makes process slower , try to implement the function using PointProjective
// export function signECDSA(key: bigint, msg: number[]): Signature {
// key = modulus(key, subOrder);
// const msgHash = getECDSAMessageHash(msg);
// // Deterministically generate the nonce k and reduce it modulo the subgroup order
// const k = modulus(poseidon2([msgHash, key]), subOrder);
// const R = mulPointEscalar(Base8, k);
// const kInv = modInv(k, subOrder);
// // Compute s = k_inv * (msg_hash + r * key) mod n
// const s = modulus(
// kInv * (msgHash + R[0] * key),
// subOrder
// );
// return { R, s };
// }
// export function verifyECDSA(msg: number[], sig: Signature, pk: Point<bigint>): boolean {
// const msgHash = getECDSAMessageHash(msg);
// const sInv = modInv(sig.s, subOrder);
// // u1 = msg_hash * s_inv mod n
// const u1 = modulus((msgHash * sInv), subOrder);
// // u2 = r * s_inv mod n
// const u2 = modulus(sig.R[0] * sInv, subOrder);
// // R = u1*G + u2*pk
// const u1G = mulPointEscalar(Base8, u1);
// const u2Pk = mulPointEscalar(pk, u2);
// let R = addPoint(u1G, u2Pk);
// return R[0] == sig.R[0]
// }
export function verifyEffECDSA(
s: bigint,
T: Point<bigint>,
U: Point<bigint>,
pk: Point<bigint>
): boolean {
// Check if s*T + U == pk
const sT = mulPointEscalar(T, s);
const calPk = addPoint(sT, U);
const xvalid = calPk[0] == pk[0];
const yvalid = calPk[1] == pk[1];
return xvalid && yvalid == true;
}

View File

@@ -0,0 +1,75 @@
import { Base8, mulPointEscalar, Point, subOrder } from '@zk-kit/baby-jubjub';
import { Signature } from '../types.js';
import { packBytesAndPoseidon } from '../../hash.js';
/**
* Compute the hash of a message using the ECDSA algorithm
* @param msg
* @returns hash as a hex string
*/
export const getECDSAMessageHash = (msg: number[]): bigint => {
const msgHash = BigInt(packBytesAndPoseidon(msg));
return modulus(msgHash, subOrder);
};
export const modulus = (a: bigint, m: bigint): bigint => {
return ((a % m) + m) % m;
};
export function modInv(a: bigint, m: bigint): bigint {
let m0 = m;
let y = 0n,
x = 1n;
if (m === 1n) return 0n;
while (a > 1n) {
const q = a / m;
let t = m;
// m is remainder now
m = a % m;
a = t;
t = y;
// Update x and y
y = x - q * y;
x = t;
}
// Make x positive
if (x < 0n) x += m0;
return x;
}
export function getEffECDSAArgs(
msg: number[],
sig: Signature
): { T: Point<bigint>; U: Point<bigint> } {
const msgHash = getECDSAMessageHash(msg);
const rInv = modInv(sig.R[0], subOrder);
// T = R * r_inv, where R is the signature's R point
const T = mulPointEscalar(sig.R, rInv);
// U = G * (-r_inv * msg_hash mod n), where G is the generator
const rInvNeg = modulus(-rInv, subOrder);
const U = mulPointEscalar(Base8, modulus(rInvNeg * msgHash, subOrder));
return { T, U };
}
export const generateRandomsg = (): number[] => {
const randomNumbers: number[] = Array.from({ length: 298 }, () =>
Math.floor(Math.random() * 128)
);
return randomNumbers;
};
//TODO: Recheck the logic
export function bigintTo64bitLimbs(x: bigint): bigint[] {
const mask = (1n << 64n) - 1n;
const limbs: bigint[] = [];
for (let i = 0; i < 4; i++) {
limbs.push(x & mask);
x >>= 64n;
}
return limbs;
}

View File

@@ -0,0 +1,182 @@
import { SMT } from '@openpassport/zk-kit-smt';
import {
generateMerkleProof,
generateSMTProof,
getNameDobLeafKyc,
getNameYobLeafKyc,
} from '../trees.js';
import { KycDiscloseInput, KycRegisterInput, serializeKycData, KycData } from './types.js';
import { findIndexInTree, formatInput } from '../circuits/generateInputs.js';
import { createKycSelector, KYC_MAX_LENGTH, KycField } from './constants.js';
import { poseidon2 } from 'poseidon-lite';
import { Base8, inCurve, mulPointEscalar, subOrder } from '@zk-kit/baby-jubjub';
import { signEdDSA } from './ecdsa/ecdsa.js';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { packBytesAndPoseidon } from '../hash.js';
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
export const OFAC_DUMMY_INPUT: KycData = {
country: 'KEN',
idType: 'NATIONAL ID',
idNumber: '12345678901234567890123456789012', //32 digits
issuanceDate: '20200101',
expiryDate: '20290101',
fullName: 'ABBAS ABU',
dob: '19481210',
photoHash: '1234567890',
phoneNumber: '1234567890',
document: 'ID',
gender: 'Male',
address: '1234567890',
user_identifier: '1234567890',
current_date: '20250101',
majority_age_ASCII: '20',
selector_older_than: '1',
};
export const NON_OFAC_DUMMY_INPUT: KycData = {
country: 'KEN',
idType: 'NATIONAL ID',
idNumber: '12345678901234567890123456789012', //32 digits
issuanceDate: '20200101',
expiryDate: '20290101',
fullName: 'John Doe',
dob: '19900101',
photoHash: '1234567890',
phoneNumber: '1234567890',
document: 'ID',
gender: 'Male',
address: '1234567890',
user_identifier: '1234567890',
current_date: '20250101',
majority_age_ASCII: '20',
selector_older_than: '1',
};
export const createKycDiscloseSelFromFields = (fieldsToReveal: KycField[]): string[] => {
const [lowResult, highResult] = createKycSelector(fieldsToReveal);
return [lowResult.toString(), highResult.toString()];
};
export const generateMockKycRegisterInput = async (
secretKey?: bigint,
ofac?: boolean,
secret?: string
) => {
const kycData = ofac ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
const serializedData = serializeKycData(kycData).padEnd(KYC_MAX_LENGTH, '\0');
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const sk = secretKey ? secretKey : BigInt(Math.floor(Math.random() * Number(subOrder - 2n))) + 1n;
const pk = mulPointEscalar(Base8, sk);
console.assert(inCurve(pk), 'Point pk not on curve');
console.assert(pk[0] != 0n && pk[1] != 0n, 'pk is zero');
const [sig, pubKey] = signEdDSA(sk, msgPadded);
console.assert(BigInt(sig.S) < subOrder, ' s is greater than scalar field');
const kycRegisterInput: KycRegisterInput = {
data_padded: msgPadded.map((x) => Number(x)),
s: BigInt(sig.S),
R: sig.R8 as [bigint, bigint],
pubKey,
secret: secret || '1234',
};
return kycRegisterInput;
};
export const generateCircuitInputsOfac = (data: KycData, smt: SMT, proofLevel: number) => {
const name = data.fullName;
const dob = data.dob;
const yob = data.dob.slice(0, 4);
const nameDobLeaf = getNameDobLeafKyc(name, dob);
const nameYobLeaf = getNameYobLeafKyc(name, yob);
let root, closestleaf, siblings;
if (proofLevel == 2) {
({ root, closestleaf, siblings } = generateSMTProof(smt, nameDobLeaf));
} else if (proofLevel == 1) {
({ root, closestleaf, siblings } = generateSMTProof(smt, nameYobLeaf));
} else {
throw new Error('Invalid proof level');
}
return {
smt_root: formatInput(root),
smt_leaf_key: formatInput(closestleaf),
smt_siblings: formatInput(siblings),
};
};
export const generateKycDiscloseInput = (
ofac_input: boolean,
nameDobSmt: SMT,
nameYobSmt: SMT,
identityTree: LeanIMT,
ofac: boolean,
scope: string,
userIdentifier: string,
fieldsToReveal?: KycField[],
forbiddenCountriesList?: string[],
minimumAge?: number,
updateTree?: boolean,
secret: string = '1234'
) => {
const data = ofac_input ? OFAC_DUMMY_INPUT : NON_OFAC_DUMMY_INPUT;
const serializedData = serializeKycData(data).padEnd(KYC_MAX_LENGTH, '\0');
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const commitment = poseidon2([secret, packBytesAndPoseidon(msgPadded)]);
if (updateTree) {
identityTree.insert(commitment);
}
const index = findIndexInTree(identityTree, commitment);
const {
siblings,
path: merkle_path,
leaf_depth,
} = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH);
const nameDobInputs = generateCircuitInputsOfac(data, nameDobSmt, 2);
const nameYobInputs = generateCircuitInputsOfac(data, nameYobSmt, 1);
const fieldsToRevealFinal = fieldsToReveal || [];
const compressed_disclose_sel = createKycDiscloseSelFromFields(fieldsToRevealFinal);
const majorityAgeASCII = minimumAge
? minimumAge
.toString()
.padStart(3, '0')
.split('')
.map((x) => x.charCodeAt(0))
: ['0', '0', '0'].map((x) => x.charCodeAt(0));
const currentDate = new Date().toISOString().split('T')[0].replace(/-/g, '').split('');
const circuitInput: KycDiscloseInput = {
data_padded: formatInput(msgPadded),
compressed_disclose_sel: compressed_disclose_sel,
scope: scope,
merkle_root: formatInput(BigInt(identityTree.root)),
leaf_depth: formatInput(leaf_depth),
path: formatInput(merkle_path),
siblings: formatInput(siblings),
forbidden_countries_list: forbiddenCountriesList || [...Array(120)].map((x) => '0'),
ofac_name_dob_smt_leaf_key: nameDobInputs.smt_leaf_key,
ofac_name_dob_smt_root: nameDobInputs.smt_root,
ofac_name_dob_smt_siblings: nameDobInputs.smt_siblings,
ofac_name_yob_smt_leaf_key: nameYobInputs.smt_leaf_key,
ofac_name_yob_smt_root: nameYobInputs.smt_root,
ofac_name_yob_smt_siblings: nameYobInputs.smt_siblings,
selector_ofac: ofac ? ['1'] : ['0'],
user_identifier: userIdentifier,
current_date: currentDate,
majority_age_ASCII: majorityAgeASCII,
secret: secret,
};
return circuitInput;
};

View File

@@ -0,0 +1,87 @@
import { Point } from '@zk-kit/baby-jubjub';
import * as constants from './constants.js';
export type KycData = {
country: string;
idType: string;
idNumber: string;
issuanceDate: string;
expiryDate: string;
fullName: string;
dob: string;
photoHash: string;
phoneNumber: string;
document: string;
gender: string;
address: string;
user_identifier: string;
current_date: string;
majority_age_ASCII: string;
selector_older_than: string;
};
export const serializeKycData = (kycData: KycData) => {
//ensure max length of each field
let serializedData = '';
serializedData += kycData.country.toUpperCase().padEnd(constants.KYC_COUNTRY_LENGTH, '\0');
serializedData += kycData.idType.toUpperCase().padEnd(constants.KYC_ID_TYPE_LENGTH, '\0');
serializedData += kycData.idNumber.padEnd(constants.KYC_ID_NUMBER_LENGTH, '\0');
serializedData += kycData.issuanceDate.padEnd(constants.KYC_ISSUANCE_DATE_LENGTH, '\0');
serializedData += kycData.expiryDate.padEnd(constants.KYC_EXPIRY_DATE_LENGTH, '\0');
serializedData += kycData.fullName.padEnd(constants.KYC_FULL_NAME_LENGTH, '\0');
serializedData += kycData.dob.padEnd(constants.KYC_DOB_LENGTH, '\0');
serializedData += kycData.photoHash.padEnd(constants.KYC_PHOTO_HASH_LENGTH, '\0');
serializedData += kycData.phoneNumber.padEnd(constants.KYC_PHONE_NUMBER_LENGTH, '\0');
serializedData += kycData.document.padEnd(constants.KYC_DOCUMENT_LENGTH, '\0');
serializedData += kycData.gender.padEnd(constants.KYC_GENDER_LENGTH, '\0');
serializedData += kycData.address.padEnd(constants.KYC_ADDRESS_LENGTH, '\0');
return serializedData;
};
export type KycRegisterInput = {
data_padded: number[];
s: bigint;
R: [bigint, bigint];
pubKey: [bigint, bigint];
secret: string;
};
export type KycDiscloseInput = {
data_padded: string[];
compressed_disclose_sel: string[];
merkle_root: string[];
leaf_depth: string[];
path: string[];
siblings: string[];
scope: string;
forbidden_countries_list: string[];
ofac_name_dob_smt_leaf_key: string[];
ofac_name_dob_smt_root: string[];
ofac_name_dob_smt_siblings: string[];
ofac_name_yob_smt_leaf_key: string[];
ofac_name_yob_smt_root: string[];
ofac_name_yob_smt_siblings: string[];
selector_ofac: string[];
user_identifier: string;
current_date: string[];
majority_age_ASCII: number[];
secret: string;
};
export type KycDisclosePublicInput = {
attestation_id: string;
revealedData_packed: string[];
forbidden_countries_list_packed: string[];
nullifier: string;
scope: string;
user_identifier: string;
current_date: string[];
ofac_name_dob_smt_root: string;
ofac_name_yob_smt_root: string;
};
export type Signature = {
R: Point<bigint>;
s: bigint;
};

View File

@@ -9,6 +9,8 @@ import {
poseidon10,
poseidon12,
poseidon13,
poseidon4,
poseidon8,
} from 'poseidon-lite';
import {
@@ -704,3 +706,140 @@ export function getPassportNumberAndNationalityLeaf(
console.log('err : passport', err, i, passport);
}
}
export function buildKycSMT(field: any[], treetype: string): [number, number, SMT] {
let count = 0;
let startTime = performance.now();
const providerName = 'KYC';
const hash2 = (childNodes: ChildNodes) =>
childNodes.length === 2 ? poseidon2(childNodes) : poseidon3(childNodes);
const tree = new SMT(hash2, true);
for (let i = 0; i < field.length; i++) {
const entry = field[i];
if (i !== 0) {
console.log(`Processing ${providerName}`, treetype, 'number', i, 'out of', field.length);
}
let leaf = BigInt(0);
if (treetype == 'name_and_dob') {
leaf = processNameAndDobKyc(entry, i);
} else if (treetype == 'name_and_yob') {
leaf = processNameAndYobKyc(entry, i);
}
if (leaf == BigInt(0) || tree.createProof(leaf).membership) {
console.log('This entry already exists in the tree, skipping...');
continue;
}
count += 1;
tree.add(leaf, BigInt(1));
}
console.log(`Total ${providerName}`, treetype, 'parsed are : ', count, ' over ', field.length);
console.log(`${providerName}`, treetype, 'tree built in', performance.now() - startTime, 'ms');
return [count, performance.now() - startTime, tree];
}
const processNameAndDobKyc = (entry: any, i: number): bigint => {
const firstName = entry.First_Name;
const lastName = entry.Last_Name;
const day = entry.day;
const month = entry.month;
const year = entry.year;
if (day == null || month == null || year == null) {
console.log('dob is null', i, entry);
return BigInt(0);
}
const nameHash = processNameKyc(firstName, lastName, i);
const dobHash = processDobKyc(day, month, year, i);
return generateSmallKey(poseidon2([dobHash, nameHash]));
};
export const getNameDobLeafKyc = (name: string, dob: string) => {
const namePaddingLength = 64;
const paddedName = name
.padEnd(namePaddingLength, '\0')
.split('')
.map((char) => char.charCodeAt(0));
const nameHash = BigInt(packBytesAndPoseidon(paddedName));
const dobHash = BigInt(poseidon8(stringToAsciiBigIntArray(dob)));
return generateSmallKey(poseidon2([dobHash, nameHash]));
};
const processNameKyc = (firstName: string, lastName: string, i: number): bigint => {
const namePaddingLength = 64;
firstName = firstName.replace(/'/g, '');
firstName = firstName.replace(/\./g, '');
firstName = firstName.replace(/[- ]/g, '<');
lastName = lastName.replace(/'/g, '');
lastName = lastName.replace(/[- ]/g, '<');
lastName = lastName.replace(/\./g, '');
//TODO: check if smile id does first name and last name || last name and first name
const nameArr = (lastName + ' ' + firstName)
.padEnd(namePaddingLength, '\0')
.split('')
.map((char) => char.charCodeAt(0));
return BigInt(packBytesAndPoseidon(nameArr));
};
const processDobKyc = (day: string, month: string, year: string, i: number): bigint => {
const monthMap: { [key: string]: string } = {
jan: '01',
feb: '02',
mar: '03',
apr: '04',
may: '05',
jun: '06',
jul: '07',
aug: '08',
sep: '09',
oct: '10',
nov: '11',
dec: '12',
};
month = monthMap[month.toLowerCase()];
const dob = year + month + day;
let arr = stringToAsciiBigIntArray(dob);
return BigInt(poseidon8(arr));
};
export const getNameYobLeafKyc = (name: string, yob: string) => {
const namePaddingLength = 64;
const paddedName = name
.padEnd(namePaddingLength, '\0')
.split('')
.map((char) => char.charCodeAt(0));
const nameHash = BigInt(packBytesAndPoseidon(paddedName));
const yearHash = processYearKyc(yob, 0);
return generateSmallKey(poseidon2([yearHash, nameHash]));
};
const processNameAndYobKyc = (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 = processNameKyc(firstName, lastName, i);
const yearHash = processYearKyc(year, i);
return generateSmallKey(poseidon2([yearHash, nameHash]));
};
const processYearKyc = (year: string, i: number): bigint => {
const yearArr = stringToAsciiBigIntArray(year);
return BigInt(poseidon4(yearArr));
};

View File

@@ -1,6 +1,7 @@
import type { ExtractedQRData } from './aadhaar/utils.js';
import type { CertificateData } from './certificate_parsing/dataStructure.js';
import type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js';
import { KycField } from './kyc/constants.js';
// Base interface for common fields
interface BaseIDData {
@@ -21,6 +22,12 @@ export interface AadhaarData extends BaseIDData {
photoHash?: string;
}
// export interface KycData extends BaseIDData {
// documentCategory: 'kyc';
// serializedRealData: string;
// kycFields: KycField[];
// }
export type DeployedCircuits = {
REGISTER: string[];
REGISTER_ID: string[];
@@ -34,7 +41,7 @@ export interface DocumentCatalog {
selectedDocumentId?: string; // This is now a contentHash
}
export type DocumentCategory = 'passport' | 'id_card' | 'aadhaar';
export type DocumentCategory = 'passport' | 'id_card' | 'aadhaar' | 'kyc';
export interface DocumentMetadata {
id: string; // contentHash as ID for deduplication