SELF-1938 sumsub integration (#1661)

* Sumsub: Update keychain and types

* sumsub: ProvingMachine changes - WIP

* fix: remove duplicate identifier

* update proving machine

* Refactor && Continue onchain registration if user left the app

* fix register flow

* Add hooks to KycSuccessScreen

* Integrate KycVerifiedScreen (#1686)

* Integrate KycVerifiedScreen & Fix race conditions

* yarn lint

* lint

* lint

* add mock kyc

* fix disclose flow

* yarn lint

* Feat/add kyc home screen card design (#1708)

* feat: add new designs to the kycIdCard

* refactor: Update KycIdCard design to match IdCard styling

* feat: update document cards + dev document

* feat: update empty id card for new design

* feat: update pending document card design

* feat: update expired doc + unregistered doc cards from new design

* fix: unregisted id card button links to continue registration screen

* fix: logo design on document cards

* feat: add 6 different backgrounds for ids

deterministically shows 1 of 6 backgrounds for each document | fix: fixed document designs not displaying correctly.

* chore: trigger CI rebuild

* feat: Integrate PendingIdCard to Homescreen

* fix KycIdCard.tsx

---------

Co-authored-by: seshanthS <seshanth@protonmail.com>

* lint

* fix tests

* fix: cleanup only on unmount

* coderabbit comments

* fix: cleanup unused code

* fix: edge case for German Passports with D<< nationality code

* fix tests

* review comments

* review comments

* lint

* Hide duplicated cards in Homescreen

* remove console.log

* fix patch

* remove unused vars

* agent updates

* agent feedback

* abstract colors and formatting

* agent feedback

* Regenerate Sumsub patch-package patch

* fix: handle malformed kyc payload in card background selector

* re-add for clean up

---------

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: Evi Nova <tranquil_flow@protonmail.com>
Co-authored-by: Justin Hernandez <justin.hernandez@self.xyz>
This commit is contained in:
Seshanth.S
2026-02-12 03:21:10 +05:30
committed by GitHub
parent 0bece5edd0
commit 886e02f53d
74 changed files with 3647 additions and 1606 deletions

View File

@@ -26,6 +26,7 @@ export type { Environment } from './src/utils/types.js';
// Utils exports
export {
AADHAAR_ATTESTATION_ID,
API_URL,
API_URL_STAGING,
CSCA_TREE_URL,
@@ -42,9 +43,8 @@ export {
IDENTITY_TREE_URL_STAGING,
IDENTITY_TREE_URL_STAGING_ID_CARD,
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
AADHAAR_ATTESTATION_ID,
KYC_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
PCR0_MANAGER_ADDRESS,
REDIRECT_URL,
RPC_URL,
@@ -102,6 +102,23 @@ export {
stringToBigInt,
} from './src/utils/index.js';
export {
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_LENGTH,
KYC_MAX_LENGTH,
} from './src/utils/kyc/constants.js';
export type { KycData } from './src/utils/kyc/types.js';
export { serializeKycData } from './src/utils/kyc/types.js';
export {
NON_OFAC_DUMMY_INPUT,
OFAC_DUMMY_INPUT,
generateKycDiscloseInput,
generateKycRegisterInput,
generateMockKycRegisterInput,
} from './src/utils/kyc/generateInputs.js';
// Crypto polyfill for cross-platform compatibility
export {
createHash,
@@ -121,10 +138,11 @@ export {
hash,
packBytesAndPoseidon,
} from './src/utils/hash.js';
export { deserializeApplicantInfo } from './src/utils/kyc/api.js';
export { generateTestData, testCustomData } from './src/utils/aadhaar/utils.js';
export { isAadhaarDocument, isMRZDocument } from './src/utils/index.js';
export { isAadhaarDocument, isKycDocument, isMRZDocument } from './src/utils/index.js';
export {
prepareAadhaarDiscloseData,
@@ -132,19 +150,3 @@ export {
prepareAadhaarRegisterData,
prepareAadhaarRegisterTestData,
} from './src/utils/aadhaar/mockData.js';
export {
generateKycDiscloseInput,
generateMockKycRegisterInput,
NON_OFAC_DUMMY_INPUT,
OFAC_DUMMY_INPUT,
generateKycRegisterInput,
} from './src/utils/kyc/generateInputs.js';
export {
KYC_MAX_LENGTH,
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_LENGTH,
} from './src/utils/kyc/constants.js';
export { serializeKycData, KycData } from './src/utils/kyc/types.js';

View File

@@ -1,4 +1,4 @@
import type { IDDocument, PassportData } from '../types.js';
import { type IDDocument, isKycDocument, type PassportData } from '../types.js';
export function getCircuitNameFromPassportData(
passportData: IDDocument,
@@ -14,6 +14,10 @@ export function getCircuitNameFromPassportData(
function getDSCircuitNameFromPassportData(passportData: IDDocument) {
console.log('Getting DSC circuit name from passport data...');
if (isKycDocument(passportData)) {
throw new Error('KYC documents do not have a DSC circuit');
}
if (passportData.documentCategory === 'aadhaar') {
throw new Error('Aadhaar does not have a DSC circuit');
}
@@ -87,6 +91,10 @@ function getRegisterNameFromPassportData(passportData: IDDocument) {
return 'register_aadhaar';
}
if (isKycDocument(passportData)) {
return 'register_kyc';
}
if (!passportData.passportMetadata) {
console.error('Passport metadata is missing');
throw new Error('Passport data are not parsed');

View File

@@ -18,77 +18,24 @@ import {
getCircuitNameFromPassportData,
hashEndpointWithScope,
} from '../../utils/index.js';
import type { AadhaarData, Environment, IDDocument, OfacTree } from '../../utils/types.js';
import type {
AadhaarData,
Environment,
IDDocument,
KycData as KycIDData,
OfacTree,
} from '../../utils/types.js';
import { KycField } from '../kyc/constants.js';
import {
generateKycDiscloseInputFromData,
generateKycRegisterInput,
} from '../kyc/generateInputs.js';
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,
@@ -182,45 +129,6 @@ export function generateTEEInputsDSC(
return { inputs, circuitName, endpointType, endpoint };
}
/*** DISCLOSURE ***/
function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) {
switch (document) {
case 'passport':
return getSelectorDg1Passport(disclosures);
case 'id_card':
return getSelectorDg1IdCard(disclosures);
}
}
function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(88).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(90).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
export function generateTEEInputsDiscloseStateless(
secret: string,
passportData: IDDocument,
@@ -239,15 +147,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 };
// }
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);
@@ -310,6 +218,111 @@ export function generateTEEInputsDiscloseStateless(
};
}
/*** DISCLOSURE ***/
function getSelectorDg1(document: DocumentCategory, disclosures: SelfAppDisclosureConfig) {
switch (document) {
case 'passport':
return getSelectorDg1Passport(disclosures);
case 'id_card':
return getSelectorDg1IdCard(disclosures);
}
}
function getSelectorDg1Passport(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(88).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition[attribute as keyof typeof attributeToPosition];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
function getSelectorDg1IdCard(disclosures: SelfAppDisclosureConfig) {
const selector_dg1 = Array(90).fill('0');
Object.entries(disclosures).forEach(([attribute, reveal]) => {
if (['ofac', 'excludedCountries', 'minimumAge'].includes(attribute)) {
return;
}
if (reveal) {
const [start, end] = attributeToPosition_ID[attribute as keyof typeof attributeToPosition_ID];
selector_dg1.fill('1', start, end + 1);
}
});
return selector_dg1;
}
export function generateTEEInputsKycDisclose(
secret: string,
kycData: KycIDData,
selfApp: SelfApp,
getTree: <T extends 'ofac' | 'commitment'>(
doc: DocumentCategory,
tree: T
) => T extends 'ofac' ? OfacTree : any
) {
const { scope, disclosures, endpoint, userId, userDefinedData, chainID } = selfApp;
const userIdentifierHash = calculateUserIdentifierHash(chainID, userId, userDefinedData);
const scope_hash = hashEndpointWithScope(endpoint, scope);
// 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 serialized_tree = getTree('kyc', 'commitment');
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serialized_tree);
const inputs = generateKycDiscloseInputFromData(
kycData.serializedApplicantInfo,
secret,
nameAndDobSMT,
nameAndYobSMT,
tree,
disclosures.ofac ?? false,
scope_hash,
userIdentifierHash.toString(),
mapDisclosuresToKycFields(disclosures),
disclosures.excludedCountries,
disclosures.minimumAge
);
return {
inputs,
circuitName: 'vc_and_disclose_kyc',
endpointType: selfApp.endpointType,
endpoint: selfApp.endpoint,
};
}
export async function generateTEEInputsRegister(
secret: string,
passportData: IDDocument,
@@ -326,11 +339,26 @@ export async function generateTEEInputsRegister(
return { inputs, circuitName, endpointType, endpoint };
}
// if (passportData.documentCategory === 'kyc') {
// throw new Error('Kyc does not support registration');
// }
if (passportData.documentCategory === 'kyc') {
const inputs = await generateKycRegisterInput(
passportData.serializedApplicantInfo,
passportData.signature,
[passportData.pubkey[0].toString(), passportData.pubkey[1].toString()],
secret
);
return {
inputs,
circuitName: getCircuitNameFromPassportData(passportData, 'register'),
endpointType: env === 'stg' ? 'staging_celo' : 'celo',
endpoint: 'https://self.xyz',
};
}
const inputs = generateCircuitInputsRegister(secret, passportData, dscTree as string);
const inputs = generateCircuitInputsRegister(
secret,
passportData as PassportData,
dscTree as string
);
const circuitName = getCircuitNameFromPassportData(passportData, 'register');
const endpointType = env === 'stg' ? 'staging_celo' : 'celo';
const endpoint = 'https://self.xyz';

View File

@@ -5,6 +5,7 @@ export type {
DocumentCategory,
DocumentMetadata,
IDDocument,
KycData,
OfacTree,
PassportData,
} from './types.js';
@@ -70,6 +71,6 @@ export {
export { getCircuitNameFromPassportData } from './circuits/circuitsName.js';
export { getSKIPEM } from './csca.js';
export { initElliptic } from './certificate_parsing/elliptic.js';
export { isAadhaarDocument, isMRZDocument } from './types.js';
export { isAadhaarDocument, isKycDocument, isMRZDocument } from './types.js';
export { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple.js';
export { parseDscCertificateData } from './passports/passport_parsing/parseDscCertificateData.js';

View File

@@ -1,5 +1,7 @@
//Helper function to destructure the kyc data from the api response
import { Point } from '@zk-kit/baby-jubjub';
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import {
KYC_ADDRESS_INDEX,
KYC_ADDRESS_LENGTH,
@@ -26,11 +28,7 @@ import {
} from './constants.js';
import { KycData } from './types.js';
//accepts a base64 signature and returns a signature object
export function deserializeSignature(signature: string): { R: Point<bigint>; s: bigint } {
const [Rx, Ry, s] = Buffer.from(signature, 'base64').toString('utf-8').split(',').map(BigInt);
return { R: [Rx, Ry] as Point<bigint>, s };
}
import { Point } from '@zk-kit/baby-jubjub';
//accepts a base64 applicant info and returns a kyc data object
export function deserializeApplicantInfo(
@@ -88,3 +86,9 @@ export function deserializeApplicantInfo(
address,
};
}
//accepts a base64 signature and returns a signature object
export function deserializeSignature(signature: string): { R: Point<bigint>; s: bigint } {
const [Rx, Ry, s] = Buffer.from(signature, 'base64').toString('utf-8').split(',').map(BigInt);
return { R: [Rx, Ry] as Point<bigint>, s };
}

View File

@@ -1,38 +1,23 @@
import { SMT } from '@openpassport/zk-kit-smt';
import { poseidon2 } from 'poseidon-lite';
import { COMMITMENT_TREE_DEPTH } from '../../constants/constants.js';
import { formatCountriesList } from '../circuits/formatInputs.js';
import { findIndexInTree, formatInput } from '../circuits/generateInputs.js';
import { packBytesAndPoseidon } from '../hash.js';
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';
import { deserializeApplicantInfo, deserializeSignature } from './api.js';
import { createKycSelector, KYC_MAX_LENGTH, KycField } from './constants.js';
import { signEdDSA } from './ecdsa/ecdsa.js';
import { KycData, KycDiscloseInput, KycRegisterInput, serializeKycData } from './types.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',
gender: 'M',
address: '1234567890',
user_identifier: '1234567890',
current_date: '20250101',
majority_age_ASCII: '20',
selector_older_than: '1',
};
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import { SMT } from '@openpassport/zk-kit-smt';
import { Base8, inCurve, mulPointEscalar, subOrder } from '@zk-kit/baby-jubjub';
export const NON_OFAC_DUMMY_INPUT: KycData = {
country: 'KEN',
@@ -52,66 +37,29 @@ export const NON_OFAC_DUMMY_INPUT: KycData = {
selector_older_than: '1',
};
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',
gender: 'M',
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 generateKycRegisterInput = async (
applicantInfoBase64: string,
signatureBase64: string,
pubkeyStr: [string, string],
secret: string
) => {
const applicantInfo = deserializeApplicantInfo(applicantInfoBase64);
const signature = deserializeSignature(signatureBase64);
const pubkey = [BigInt(pubkeyStr[0]), BigInt(pubkeyStr[1])] as [bigint, bigint];
const serializedData = serializeKycData(applicantInfo);
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const kycRegisterInput: KycRegisterInput = {
data_padded: msgPadded.map((x) => Number(x)),
s: signature.s,
R: signature.R,
pubKey: pubkey,
secret,
};
return kycRegisterInput;
};
export const generateCircuitInputsOfac = (data: KycData, smt: SMT, proofLevel: number) => {
const name = data.fullName;
const dob = data.dob;
@@ -195,7 +143,9 @@ export const generateKycDiscloseInput = (
leaf_depth: formatInput(leaf_depth),
path: formatInput(merkle_path),
siblings: formatInput(siblings),
forbidden_countries_list: forbiddenCountriesList || [...Array(120)].map((x) => '0'),
forbidden_countries_list: forbiddenCountriesList
? formatInput(formatCountriesList(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,
@@ -211,3 +161,141 @@ export const generateKycDiscloseInput = (
return circuitInput;
};
export const generateKycDiscloseInputFromData = (
serializedApplicantInfo: string,
secret: string,
nameDobSmt: SMT,
nameYobSmt: SMT,
identityTree: LeanIMT,
ofac: boolean,
scope: string,
userIdentifier: string,
fieldsToReveal?: KycField[],
forbiddenCountriesList?: string[],
minimumAge?: number
): KycDiscloseInput => {
// Decode base64 applicant info to get raw padded bytes for the circuit
const rawData = Buffer.from(serializedApplicantInfo, 'base64').toString('utf-8');
const serializedData = rawData.padEnd(KYC_MAX_LENGTH, '\0');
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
// Compute commitment
const commitment = poseidon2([secret, packBytesAndPoseidon(msgPadded)]);
// Find in tree and generate merkle proof
const index = findIndexInTree(identityTree, commitment);
const {
siblings,
path: merkle_path,
leaf_depth,
} = generateMerkleProof(identityTree, index, COMMITMENT_TREE_DEPTH);
// Deserialize to get individual fields for OFAC lookups
const applicantData = deserializeApplicantInfo(serializedApplicantInfo);
const ofacData = {
...applicantData,
user_identifier: '',
current_date: '',
majority_age_ASCII: '',
selector_older_than: '',
} as KycData;
const nameDobInputs = generateCircuitInputsOfac(ofacData, nameDobSmt, 2);
const nameYobInputs = generateCircuitInputsOfac(ofacData, nameYobSmt, 1);
// Build disclosure selector
const fieldsToRevealFinal = fieldsToReveal || [];
const compressed_disclose_sel = createKycDiscloseSelFromFields(fieldsToRevealFinal);
// Age and date
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
? formatInput(formatCountriesList(forbiddenCountriesList))
: [...Array(120)].map(() => '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;
};
export const generateKycRegisterInput = async (
applicantInfoBase64: string,
signatureBase64: string,
pubkeyStr: [string, string],
secret: string
) => {
const applicantInfo = deserializeApplicantInfo(applicantInfoBase64);
const signature = deserializeSignature(signatureBase64);
const pubkey = [BigInt(pubkeyStr[0]), BigInt(pubkeyStr[1])] as [bigint, bigint];
const serializedData = serializeKycData(applicantInfo).padEnd(KYC_MAX_LENGTH, '\0');
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const kycRegisterInput: KycRegisterInput = {
data_padded: msgPadded,
s: signature.s,
R: signature.R,
pubKey: pubkey,
secret,
};
return kycRegisterInput;
};
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;
};

View File

@@ -0,0 +1,43 @@
import { poseidon2 } from 'poseidon-lite';
import { packBytesAndPoseidon } from '../hash.js';
import { IDDocument, isKycDocument } from '../types.js';
import { deserializeApplicantInfo } from './api.js';
import {
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_LENGTH,
KYC_ID_TYPE_INDEX,
KYC_ID_TYPE_LENGTH,
} from './constants.js';
import { serializeKycData } from './types.js';
export const generateKycCommitment = (passportData: IDDocument, secret: string) => {
if (isKycDocument(passportData)) {
const applicantInfo = deserializeApplicantInfo(passportData.serializedApplicantInfo);
const serializedData = serializeKycData(applicantInfo);
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const dataPadded = msgPadded.map((x) => Number(x));
const commitment = poseidon2([secret, packBytesAndPoseidon(dataPadded)]);
return commitment.toString();
}
};
export const generateKycNullifier = (passportData: IDDocument) => {
if (isKycDocument(passportData)) {
const applicantInfo = deserializeApplicantInfo(passportData.serializedApplicantInfo);
const serializedData = serializeKycData(applicantInfo);
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const dataPadded = msgPadded.map((x) => Number(x));
const idNumber = dataPadded.slice(
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
);
const nullifierInputs = [
...'sumsub'.split('').map((x) => x.charCodeAt(0)),
...idNumber,
...dataPadded.slice(KYC_ID_TYPE_INDEX, KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH),
];
const nullifier = packBytesAndPoseidon(nullifierInputs);
return nullifier;
}
};

View File

@@ -29,14 +29,22 @@ import {
import { formatInput } from '../circuits/generateInputs.js';
import { findStartIndex, findStartIndexEC } from '../csca.js';
import { hash, packBytesAndPoseidon } from '../hash.js';
import { deserializeApplicantInfo } from '../kyc/api.js';
import {
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_LENGTH,
KYC_ID_TYPE_INDEX,
KYC_ID_TYPE_LENGTH,
} from '../kyc/constants.js';
import { serializeKycData } from '../kyc/types.js';
import { sha384_512Pad, shaPad } from '../shaPad.js';
import { getLeafDscTree } from '../trees.js';
import type { DocumentCategory, IDDocument, PassportData, SignatureAlgorithm } from '../types.js';
import { AadhaarData, isAadhaarDocument, isMRZDocument } from '../types.js';
import { AadhaarData, isAadhaarDocument, isKycDocument, isMRZDocument } from '../types.js';
import { formatMrz } from './format.js';
import { parsePassportData } from './passport_parsing/parsePassportData.js';
export function calculateContentHash(passportData: PassportData | AadhaarData): string {
export function calculateContentHash(passportData: IDDocument): string {
if (isMRZDocument(passportData) && passportData.eContent) {
// eContent is likely a buffer or array, convert to string properly
const eContentStr =
@@ -47,6 +55,13 @@ export function calculateContentHash(passportData: PassportData | AadhaarData):
return sha256(eContentStr);
}
if (isKycDocument(passportData)) {
const serializedData = passportData.serializedApplicantInfo;
const parsedApplicantInfo = deserializeApplicantInfo(serializedData);
const stableFields = `${parsedApplicantInfo.fullName}${parsedApplicantInfo.dob}${parsedApplicantInfo.country}${parsedApplicantInfo.idType}`;
return sha256(stableFields);
}
// For MRZ documents without eContent, hash core stable fields
const stableData = {
documentType: passportData.documentType,
@@ -193,6 +208,23 @@ export function generateNullifier(passportData: IDDocument) {
if (isAadhaarDocument(passportData)) {
return nullifierHash(passportData.extractedFields);
}
if (isKycDocument(passportData)) {
const applicantInfo = deserializeApplicantInfo(passportData.serializedApplicantInfo);
const serializedData = serializeKycData(applicantInfo);
const msgPadded = Array.from(serializedData, (x) => x.charCodeAt(0));
const dataPadded = msgPadded.map((x) => Number(x));
const idNumber = dataPadded.slice(
KYC_ID_NUMBER_INDEX,
KYC_ID_NUMBER_INDEX + KYC_ID_NUMBER_LENGTH
);
const nullifierInputs = [
...'sumsub'.split('').map((x) => x.charCodeAt(0)),
...idNumber,
...dataPadded.slice(KYC_ID_TYPE_INDEX, KYC_ID_TYPE_INDEX + KYC_ID_TYPE_LENGTH),
];
const nullifier = packBytesAndPoseidon(nullifierInputs);
return nullifier;
}
const signedAttr_shaBytes = hash(
passportData.passportMetadata.signedAttrHashFunction,
@@ -318,6 +350,8 @@ export function getSignatureAlgorithmFullName(
export function inferDocumentCategory(documentType: string): DocumentCategory {
if (documentType.includes('passport')) {
return 'passport' as DocumentCategory;
} else if (documentType.includes('kyc')) {
return 'kyc' as DocumentCategory;
} else if (documentType.includes('id')) {
return 'id_card' as DocumentCategory;
} else if (documentType.includes('aadhaar')) {

View File

@@ -22,12 +22,15 @@ import {
nullifierHash,
processQRDataSimple,
} from '../aadhaar/mockData.js';
import { generateKycCommitment, generateKycNullifier } from '../kyc/utils.js';
import {
AadhaarData,
AttestationIdHex,
type DeployedCircuits,
type DocumentCategory,
IDDocument,
isKycDocument,
KycData,
type PassportData,
} from '../types.js';
import { generateCommitment, generateNullifier } from './passport.js';
@@ -49,7 +52,8 @@ function validateRegistrationCircuit(
circuitNameRegister &&
(deployedCircuits.REGISTER.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_ID.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_AADHAAR.includes(circuitNameRegister));
deployedCircuits.REGISTER_AADHAAR.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_KYC.includes(circuitNameRegister));
return { isValid: !!isValid, circuitName: circuitNameRegister };
}
@@ -82,7 +86,7 @@ export async function checkDocumentSupported(
details: string;
}> {
const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory);
if (passportData.documentCategory === 'aadhaar') {
if (passportData.documentCategory === 'aadhaar' || passportData.documentCategory === 'kyc') {
const { isValid, circuitName } = validateRegistrationCircuit(passportData, deployedCircuits);
if (!isValid) {
@@ -241,7 +245,9 @@ export async function isDocumentNullified(passportData: IDDocument) {
? AttestationIdHex.passport
: passportData.documentCategory === 'aadhaar'
? AttestationIdHex.aadhaar
: AttestationIdHex.id_card;
: passportData.documentCategory === 'kyc'
? AttestationIdHex.kyc
: AttestationIdHex.id_card;
console.log('checking for nullifier', nullifierHex, attestationId);
const baseUrl = passportData.mock === false ? API_URL : API_URL_STAGING;
const controller = new AbortController();
@@ -270,7 +276,7 @@ export async function isDocumentNullified(passportData: IDDocument) {
}
export async function isUserRegistered(
documentData: PassportData | AadhaarData,
documentData: IDDocument,
secret: string,
getCommitmentTree: (docCategory: DocumentCategory) => string
) {
@@ -281,7 +287,9 @@ export async function isUserRegistered(
const document: DocumentCategory = documentData.documentCategory;
let commitment: string;
if (document === 'aadhaar') {
if (isKycDocument(documentData)) {
commitment = generateKycCommitment(documentData, secret);
} else if (document === 'aadhaar') {
const aadhaarData = documentData as AadhaarData;
const nullifier = nullifierHash(aadhaarData.extractedFields);
const packedCommitment = computePackedCommitment(aadhaarData.extractedFields);
@@ -327,6 +335,11 @@ export async function isUserRegisteredWithAlternativeCSCA(
let commitment_list: string[];
let csca_list: string[];
if (document === 'kyc') {
const isRegistered = await isUserRegistered(passportData, secret, getCommitmentTree);
return { isRegistered, csca: null };
}
if (document === 'aadhaar') {
// For Aadhaar, use public keys from protocol store instead of CSCA
const publicKeys = getAltCSCA(document);

View File

@@ -1,5 +1,5 @@
import forge from 'node-forge';
import { Buffer } from 'buffer';
import forge from 'node-forge';
import { WS_DB_RELAYER, WS_DB_RELAYER_STAGING } from '../constants/index.js';
import { initElliptic } from '../utils/certificate_parsing/elliptic.js';
@@ -34,9 +34,9 @@ export const ec = new EC('p256');
// eslint-disable-next-line -- clientKey is created from ec so must be second
export const clientKey = ec.genKeyPair();
type RegisterSuffixes = '' | '_id' | '_aadhaar';
type RegisterSuffixes = '' | '_id' | '_aadhaar' | '_kyc';
type DscSuffixes = '' | '_id';
type DiscloseSuffixes = '' | '_id' | '_aadhaar';
type DiscloseSuffixes = '' | '_id' | '_aadhaar' | '_kyc';
type ProofTypes = 'register' | 'dsc' | 'disclose';
type RegisterProofType = `${Extract<ProofTypes, 'register'>}${RegisterSuffixes}`;
type DscProofType = `${Extract<ProofTypes, 'dsc'>}${DscSuffixes}`;
@@ -59,6 +59,10 @@ export function encryptAES256GCM(plaintext: string, key: forge.util.ByteStringBu
};
}
function bigIntReplacer(_key: string, value: unknown): unknown {
return typeof value === 'bigint' ? value.toString() : value;
}
export function getPayload(
inputs: any,
circuitType: RegisterProofType | DscProofType | DiscloseProofType,
@@ -75,7 +79,9 @@ export function getPayload(
? 'disclose'
: circuitName === 'vc_and_disclose_aadhaar'
? 'disclose_aadhaar'
: 'disclose_id';
: circuitName === 'vc_and_disclose_kyc'
? 'disclose_kyc'
: 'disclose_id';
const payload: TEEPayloadDisclose = {
type,
endpointType: endpointType,
@@ -83,7 +89,7 @@ export function getPayload(
onchain: endpointType === 'celo' ? true : false,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
inputs: JSON.stringify(inputs, bigIntReplacer),
},
version,
userDefinedData,
@@ -91,14 +97,19 @@ export function getPayload(
};
return payload;
} else {
const type = circuitName === 'register_aadhaar' ? 'register_aadhaar' : circuitType;
const type =
circuitName === 'register_aadhaar'
? 'register_aadhaar'
: circuitName === 'register_kyc'
? 'register_kyc'
: circuitType;
const payload: TEEPayload = {
type: type as RegisterProofType | DscProofType,
onchain: true,
endpointType: endpointType,
circuit: {
name: circuitName,
inputs: JSON.stringify(inputs),
inputs: JSON.stringify(inputs, bigIntReplacer),
},
};
return payload;

View File

@@ -1,7 +1,7 @@
import type { ExtractedQRData } from './aadhaar/utils.js';
import type { CertificateData } from './certificate_parsing/dataStructure.js';
import type { KycField } from './kyc/constants.js';
import type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js';
import { KycField } from './kyc/constants.js';
// Base interface for common fields
interface BaseIDData {
@@ -22,16 +22,11 @@ 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[];
REGISTER_AADHAAR: string[];
REGISTER_KYC: string[];
DSC: string[];
DSC_ID: string[];
};
@@ -51,19 +46,28 @@ export interface DocumentMetadata {
mock: boolean; // whether this is a mock document
isRegistered?: boolean; // whether the document is registered onChain
registeredAt?: number; // timestamp (epoch ms) when document was registered
idType?: string; // for KYC documents: the ID type used (e.g. "passport", "drivers_licence")
}
export type DocumentType =
| 'passport'
| 'id_card'
| 'aadhaar'
| 'drivers_licence'
| 'mock_passport'
| 'mock_id_card'
| 'mock_aadhaar';
export type Environment = 'prod' | 'stg';
export type IDDocument = AadhaarData | PassportData;
export type IDDocument = AadhaarData | KycData | PassportData;
export interface KycData extends BaseIDData {
documentCategory: 'kyc';
serializedApplicantInfo: string;
signature: string;
pubkey: string[];
}
export type OfacTree = {
passportNoAndNationality: any;
@@ -85,6 +89,20 @@ export interface PassportData extends BaseIDData {
passportMetadata?: PassportMetadata;
}
// pending - pending sumsub verification
// processing - sumsub verification completed and pending onchain confirmation
// failed - sumsub verification failed
export type PendingKycStatus = 'pending' | 'processing' | 'failed';
export interface PendingKycVerification {
userId: string; // Correlation key from fetchAccessToken()
createdAt: number; // Timestamp when verification started
status: PendingKycStatus; // Current status
errorMessage?: string; // Error message if failed
timeoutAt: number; // When to consider timed out
documentId?: string; // Content hash of stored KYC document
}
export type Proof = {
proof: {
a: [string, string];
@@ -156,6 +174,7 @@ export enum AttestationIdHex {
passport = '0x0000000000000000000000000000000000000000000000000000000000000001',
id_card = '0x0000000000000000000000000000000000000000000000000000000000000002',
aadhaar = '0x0000000000000000000000000000000000000000000000000000000000000003',
kyc = '0x0000000000000000000000000000000000000000000000000000000000000004',
}
export function castCSCAProof(proof: any): Proof {
@@ -169,15 +188,15 @@ export function castCSCAProof(proof: any): Proof {
};
}
export function isAadhaarDocument(
passportData: PassportData | AadhaarData
): passportData is AadhaarData {
export function isAadhaarDocument(passportData: IDDocument): passportData is AadhaarData {
return passportData.documentCategory === 'aadhaar';
}
export function isMRZDocument(
passportData: PassportData | AadhaarData
): passportData is PassportData {
export function isKycDocument(passportData: IDDocument): passportData is KycData {
return passportData.documentCategory === 'kyc';
}
export function isMRZDocument(passportData: IDDocument): passportData is PassportData {
return (
passportData.documentCategory === 'passport' || passportData.documentCategory === 'id_card'
);