moves validateDocument functions into the common package. (#977)

* moves validateDocument functions into the common package.

* fix build issues and lint

* handle bad connections better in nullifiier

* add an abort controler to nullifer fetcher,  ignore fals positives

* import types separately

* take it as an arg
This commit is contained in:
Aaron DeRuvo
2025-08-29 17:28:42 +02:00
committed by GitHub
parent 18697f0211
commit ac745bbf8f
23 changed files with 560 additions and 293 deletions

4
.gitleaksignore Normal file
View File

@@ -0,0 +1,4 @@
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:54
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:55
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:73
1b461a626e0a4a93d4e1c727e7aed8c955aa728c:common/src/utils/passports/validate.test.ts:generic-api-key:74

View File

@@ -144,7 +144,7 @@ module.exports = {
// General JavaScript Rules
// Warn on common issues but don't block development
'no-console': 'warn',
'no-console': 'off',
'no-empty-pattern': 'off',
'prefer-const': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',

View File

@@ -104,6 +104,10 @@ const extraNodeModules = {
commonPath,
'dist/esm/src/utils/passports/format.js',
),
'@selfxyz/common/utils/passports/validate': path.resolve(
commonPath,
'dist/esm/src/utils/passports/validate.js',
),
'@selfxyz/common/utils/passportMock': path.resolve(
commonPath,
'dist/esm/src/utils/passports/mock.js',

View File

@@ -59,12 +59,8 @@ import type {
DocumentMetadata,
PassportData,
} from '@selfxyz/common/utils/types';
import {
DocumentsAdapter,
getAllDocuments,
SelfClient,
useSelfClient,
} from '@selfxyz/mobile-sdk-alpha';
import type { DocumentsAdapter, SelfClient } from '@selfxyz/mobile-sdk-alpha';
import { getAllDocuments, useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { unsafe_getPrivateKey, useAuth } from '@/providers/authProvider';

View File

@@ -6,6 +6,7 @@ import React, { useCallback, useState } from 'react';
import { Separator, View, XStack, YStack } from 'tamagui';
import { useNavigation } from '@react-navigation/native';
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -23,10 +24,10 @@ import {
loadPassportDataAndSecret,
reStorePassportDataWithRightCSCA,
} from '@/providers/passportDataProvider';
import { useProtocolStore } from '@/stores/protocolStore';
import { useSettingStore } from '@/stores/settingStore';
import { STORAGE_NAME, useBackupMnemonic } from '@/utils/cloudBackup';
import { black, slate500, slate600, white } from '@/utils/colors';
import { isUserRegisteredWithAlternativeCSCA } from '@/utils/proving/validateDocument';
const AccountRecoveryChoiceScreen: React.FC = () => {
const { trackEvent } = useSelfClient();
@@ -60,6 +61,14 @@ const AccountRecoveryChoiceScreen: React.FC = () => {
const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA(
passportData,
secret,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},
);
if (!isRegistered) {
console.warn(

View File

@@ -9,6 +9,7 @@ import { Text, TextArea, View, XStack, YStack } from 'tamagui';
import Clipboard from '@react-native-clipboard/clipboard';
import { useNavigation } from '@react-navigation/native';
import { isUserRegisteredWithAlternativeCSCA } from '@selfxyz/common/utils/passports/validate';
import { useSelfClient } from '@selfxyz/mobile-sdk-alpha';
import { BackupEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -20,6 +21,7 @@ import {
loadPassportDataAndSecret,
reStorePassportDataWithRightCSCA,
} from '@/providers/passportDataProvider';
import { useProtocolStore } from '@/stores/protocolStore';
import {
black,
slate300,
@@ -28,7 +30,6 @@ import {
slate700,
white,
} from '@/utils/colors';
import { isUserRegisteredWithAlternativeCSCA } from '@/utils/proving/validateDocument';
const RecoverWithPhraseScreen: React.FC = () => {
const navigation = useNavigation();
@@ -65,6 +66,14 @@ const RecoverWithPhraseScreen: React.FC = () => {
const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA(
passportData,
secret as string,
{
getCommitmentTree(docCategory) {
return useProtocolStore.getState()[docCategory].commitment_tree;
},
getAltCSCA(docCategory) {
return useProtocolStore.getState()[docCategory].alternative_csca;
},
},
);
if (!isRegistered) {
console.warn(

View File

@@ -20,7 +20,7 @@ import {
IDENTITY_TREE_URL_STAGING,
IDENTITY_TREE_URL_STAGING_ID_CARD,
} from '@selfxyz/common/constants';
import type { OfacTree } from '@selfxyz/common/utils/types';
import type { DeployedCircuits, OfacTree } from '@selfxyz/common/utils/types';
import { fetchOfacTrees } from '@/utils/ofac';
@@ -29,7 +29,7 @@ interface ProtocolState {
commitment_tree: any;
dsc_tree: any;
csca_tree: string[][] | null;
deployed_circuits: any;
deployed_circuits: DeployedCircuits | null;
circuits_dns_mapping: any;
alternative_csca: Record<string, string>;
ofac_trees: OfacTree | null;
@@ -49,7 +49,7 @@ interface ProtocolState {
commitment_tree: any;
dsc_tree: any;
csca_tree: string[][] | null;
deployed_circuits: any;
deployed_circuits: DeployedCircuits | null;
circuits_dns_mapping: any;
alternative_csca: Record<string, string>;
ofac_trees: OfacTree | null;

View File

@@ -16,7 +16,4 @@ export {
// From loadingScreenStateText - used in loading screen
export { getLoadingScreenText } from '@/utils/proving/loadingScreenStateText';
// From validateDocument - used in recovery and splash screens
export { isUserRegisteredWithAlternativeCSCA } from '@/utils/proving/validateDocument';
export { useProvingStore } from '@/utils/proving/provingMachine';

View File

@@ -17,6 +17,13 @@ import {
getSolidityPackedUserContextData,
} from '@selfxyz/common/utils';
import { getPublicKey, verifyAttestation } from '@selfxyz/common/utils/attest';
import {
checkDocumentSupported,
checkIfPassportDscIsInTree,
isDocumentNullified,
isUserRegistered,
isUserRegisteredWithAlternativeCSCA,
} from '@selfxyz/common/utils/passports/validate';
import {
clientKey,
clientPublicKeyHex,
@@ -50,13 +57,6 @@ import {
generateTEEInputsDSC,
generateTEEInputsRegister,
} from '@/utils/proving/provingInputs';
import {
checkIfPassportDscIsInTree,
checkPassportSupported,
isDocumentNullified,
isUserRegistered,
isUserRegisteredWithAlternativeCSCA,
} from '@/utils/proving/validateDocument';
const { trackEvent } = analytics();
@@ -711,7 +711,10 @@ export const useProvingStore = create<ProvingState>((set, get) => {
if (!passportData) {
throw new Error('PassportData is not available');
}
const isSupported = await checkPassportSupported(passportData);
const isSupported = await checkDocumentSupported(passportData, {
getDeployedCircuits: (documentCategory: DocumentCategory) =>
useProtocolStore.getState()[documentCategory].deployed_circuits!,
});
if (isSupported.status !== 'passport_supported') {
console.error(
'Passport not supported:',
@@ -726,13 +729,15 @@ export const useProvingStore = create<ProvingState>((set, get) => {
actor!.send({ type: 'PASSPORT_NOT_SUPPORTED' });
return;
}
const getCommitmentTree = (documentCategory: DocumentCategory) =>
useProtocolStore.getState()[documentCategory].commitment_tree;
/// disclosure
if (circuitType === 'disclose') {
// check if the user is registered using the csca from the passport data.
const isRegisteredWithLocalCSCA = await isUserRegistered(
passportData,
secret as string,
getCommitmentTree,
);
if (isRegisteredWithLocalCSCA) {
trackEvent(ProofEvents.VALIDATION_SUCCESS);
@@ -750,6 +755,11 @@ export const useProvingStore = create<ProvingState>((set, get) => {
await isUserRegisteredWithAlternativeCSCA(
passportData,
secret as string,
{
getCommitmentTree,
getAltCSCA: docType =>
useProtocolStore.getState()[docType].alternative_csca,
},
);
if (isRegistered) {
reStorePassportDataWithRightCSCA(passportData, csca as string);

View File

@@ -2,26 +2,8 @@
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
import { poseidon2, poseidon5 } from 'poseidon-lite';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
import {
API_URL,
API_URL_STAGING,
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
} from '@selfxyz/common/constants';
import type { DocumentCategory, PassportData } from '@selfxyz/common/types';
import { parseCertificateSimple } from '@selfxyz/common/utils/certificates/parseSimple';
import { getCircuitNameFromPassportData } from '@selfxyz/common/utils/circuitNames';
import { packBytesAndPoseidon } from '@selfxyz/common/utils/hash/poseidon';
import { hash } from '@selfxyz/common/utils/hash/sha';
import { formatMrz } from '@selfxyz/common/utils/passportFormat';
import {
generateCommitment,
generateNullifier,
} from '@selfxyz/common/utils/passports';
import { getLeafDscTree } from '@selfxyz/common/utils/trees';
import type { PassportData } from '@selfxyz/common/types';
import { isUserRegistered } from '@selfxyz/common/utils/passports/validate';
import type { PassportValidationCallbacks } from '@selfxyz/mobile-sdk-alpha';
import { isPassportDataValid } from '@selfxyz/mobile-sdk-alpha';
import { DocumentEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics';
@@ -39,13 +21,6 @@ import analytics from '@/utils/analytics';
const { trackEvent } = analytics();
export type PassportSupportStatus =
| 'passport_metadata_missing'
| 'csca_not_found'
| 'registration_circuit_not_supported'
| 'dsc_circuit_not_supported'
| 'passport_supported';
/**
* This function checks and updates registration states for all documents and updates the `isRegistered`.
*/
@@ -152,7 +127,11 @@ export async function checkAndUpdateRegistrationStates(): Promise<void> {
}
const { secret } = JSON.parse(passportDataAndSecret);
const isRegistered = await isUserRegistered(migratedPassportData, secret);
const isRegistered = await isUserRegistered(
migratedPassportData,
secret,
docType => useProtocolStore.getState()[docType].commitment_tree,
);
// Update the registration state in the document metadata
await updateDocumentRegistrationState(documentId, isRegistered);
@@ -183,225 +162,7 @@ export async function checkAndUpdateRegistrationStates(): Promise<void> {
if (__DEV__) console.log('Registration state check and update completed');
}
export async function checkIfPassportDscIsInTree(
passportData: PassportData,
dscTree: string,
): Promise<boolean> {
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
const tree = LeanIMT.import(hashFunction, dscTree);
const leaf = getLeafDscTree(
passportData.dsc_parsed!,
passportData.csca_parsed!,
);
const index = tree.indexOf(BigInt(leaf));
if (index === -1) {
console.warn('DSC not found in the tree');
return false;
}
return true;
}
export async function checkPassportSupported(
passportData: PassportData,
): Promise<{
status: PassportSupportStatus;
details: string;
}> {
const passportMetadata = passportData.passportMetadata;
const document: DocumentCategory = passportData.documentCategory;
if (!passportMetadata) {
console.warn('Passport metadata is null');
return { status: 'passport_metadata_missing', details: passportData.dsc };
}
if (!passportMetadata.cscaFound) {
console.warn('CSCA not found');
return { status: 'csca_not_found', details: passportData.dsc };
}
const circuitNameRegister = getCircuitNameFromPassportData(
passportData,
'register',
);
const deployedCircuits =
useProtocolStore.getState()[document].deployed_circuits; // change this to the document type
if (
!circuitNameRegister ||
!(
deployedCircuits.REGISTER.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_ID.includes(circuitNameRegister)
)
) {
return {
status: 'registration_circuit_not_supported',
details: circuitNameRegister,
};
}
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
if (
!circuitNameDsc ||
!(
deployedCircuits.DSC.includes(circuitNameDsc) ||
deployedCircuits.DSC_ID.includes(circuitNameDsc)
)
) {
console.warn('DSC circuit not supported:', circuitNameDsc);
return { status: 'dsc_circuit_not_supported', details: circuitNameDsc };
}
return { status: 'passport_supported', details: 'null' };
}
export function generateCommitmentInApp(
secret: string,
attestation_id: string,
passportData: PassportData,
alternativeCSCA: Record<string, string>,
) {
const dg1_packed_hash = packBytesAndPoseidon(formatMrz(passportData.mrz));
const eContent_packed_hash = packBytesAndPoseidon(
(
hash(
passportData.passportMetadata!.eContentHashFunction,
Array.from(passportData.eContent),
'bytes',
) as number[]
)
// eslint-disable-next-line no-bitwise
.map(byte => byte & 0xff),
);
const csca_list: string[] = [];
const commitment_list: string[] = [];
for (const [cscaKey, cscaValue] of Object.entries(alternativeCSCA)) {
try {
const formattedCsca = formatCSCAPem(cscaValue);
const cscaParsed = parseCertificateSimple(formattedCsca);
const commitment = poseidon5([
secret,
attestation_id,
dg1_packed_hash,
eContent_packed_hash,
getLeafDscTree(passportData.dsc_parsed!, cscaParsed),
]).toString();
csca_list.push(formatCSCAPem(cscaValue));
commitment_list.push(commitment);
} catch (error) {
console.warn(
`Failed to parse CSCA certificate for key ${cscaKey}:`,
error,
);
}
}
if (commitment_list.length === 0) {
console.error('No valid CSCA certificates found in alternativeCSCA');
}
return { commitment_list, csca_list };
}
function formatCSCAPem(cscaPem: string): string {
let cleanedPem = cscaPem.trim();
if (!cleanedPem.includes('-----BEGIN CERTIFICATE-----')) {
cleanedPem = cleanedPem.replace(/[^A-Za-z0-9+/=]/g, '');
try {
Buffer.from(cleanedPem, 'base64');
} catch (error) {
throw new Error(`Invalid base64 certificate data: ${error}`);
}
cleanedPem = `-----BEGIN CERTIFICATE-----\n${cleanedPem}\n-----END CERTIFICATE-----`;
}
return cleanedPem;
}
export async function isDocumentNullified(passportData: PassportData) {
const nullifier = generateNullifier(passportData);
const nullifierHex = `0x${BigInt(nullifier).toString(16)}`;
const attestationId =
passportData.documentCategory === 'passport'
? '0x0000000000000000000000000000000000000000000000000000000000000001'
: '0x0000000000000000000000000000000000000000000000000000000000000002';
console.log('checking for nullifier', nullifierHex, attestationId);
const baseUrl = passportData.mock === false ? API_URL : API_URL_STAGING;
const response = await fetch(
`${baseUrl}/is-nullifier-onchain-with-attestation-id`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nullifier: nullifierHex,
attestation_id: attestationId,
}),
},
);
const data = await response.json();
console.log('isDocumentNullified', data);
return data.data;
}
export async function isUserRegistered(
passportData: PassportData,
secret: string,
) {
if (!passportData) {
return false;
}
const attestationId =
passportData.documentCategory === 'passport'
? PASSPORT_ATTESTATION_ID
: ID_CARD_ATTESTATION_ID;
const commitment = generateCommitment(secret, attestationId, passportData);
const document: DocumentCategory = passportData.documentCategory;
const serializedTree = useProtocolStore.getState()[document].commitment_tree;
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
const index = tree.indexOf(BigInt(commitment));
return index !== -1;
}
export async function isUserRegisteredWithAlternativeCSCA(
passportData: PassportData,
secret: string,
): Promise<{ isRegistered: boolean; csca: string | null }> {
if (!passportData) {
console.error('Passport data is null');
return { isRegistered: false, csca: null };
}
const document: DocumentCategory = passportData.documentCategory;
const alternativeCSCA =
useProtocolStore.getState()[document].alternative_csca;
const { commitment_list, csca_list } = generateCommitmentInApp(
secret,
document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID,
passportData,
alternativeCSCA,
);
if (commitment_list.length === 0) {
console.error(
'No valid CSCA certificates could be parsed from alternativeCSCA',
);
return { isRegistered: false, csca: null };
}
const serializedTree = useProtocolStore.getState()[document].commitment_tree;
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
for (let i = 0; i < commitment_list.length; i++) {
const commitment = commitment_list[i];
const index = tree.indexOf(BigInt(commitment));
if (index !== -1) {
return { isRegistered: true, csca: csca_list[i] };
}
}
console.warn(
'None of the following CSCA correspond to the commitment:',
csca_list,
);
return { isRegistered: false, csca: null };
}
// UNUSED?
interface MigratedPassportData extends Omit<PassportData, 'documentType'> {
documentType?: string;

View File

@@ -9,6 +9,7 @@
"@env": ["./env.ts"],
"@selfxyz/common": ["../common"],
"@selfxyz/common/*": ["../common/*"],
"@selfxyz/common/utils/passports/*": ["../common/utils/passports/*"],
"@selfxyz/mobile-sdk-alpha": [
"../packages/mobile-sdk-alpha/src",
"../packages/mobile-sdk-alpha/dist"

View File

@@ -271,6 +271,11 @@
"import": "./dist/esm/src/utils/passports/passport.js",
"require": "./dist/cjs/src/utils/passports/passport.cjs"
},
"./utils/passports/validate": {
"types": "./dist/esm/src/utils/passports/validate.d.ts",
"import": "./dist/esm/src/utils/passports/validate.js",
"require": "./dist/cjs/src/utils/passports/validate.cjs"
},
"./utils/passports/genMockIdDoc": {
"types": "./dist/esm/src/utils/passports/genMockIdDoc.d.ts",
"import": "./dist/esm/src/utils/passports/genMockIdDoc.js",

View File

@@ -1,6 +1,7 @@
import { writeFileSync, mkdirSync, readFileSync } from 'node:fs';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { shimConfigs } from './shimConfigs.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -25,6 +26,7 @@ const distPackageJson = {
'.': './esm/index.js',
'./constants': './esm/src/constants/index.js',
'./utils': './esm/src/utils/index.js',
'./utils/passports/validate': './esm/src/utils/passports/validate.js',
'./types': './esm/src/types/index.js',
},
};

View File

@@ -214,6 +214,11 @@ export const shimConfigs = [
targetPath: '../../../esm/src/utils/passports/mockDsc.js',
name: 'utils/passports/mockDsc',
},
{
shimPath: 'utils/passports/validate',
targetPath: '../../../esm/src/utils/passports/validate.js',
name: 'utils/passports/validate',
},
{
shimPath: 'utils/passports/mockGeneration',
targetPath: '../../../esm/src/utils/passports/mockGeneration.js',

View File

@@ -21,8 +21,8 @@ export {
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
PCR0_MANAGER_ADDRESS,
RPC_URL,
REDIRECT_URL,
RPC_URL,
RegisterVerifierId,
SignatureAlgorithmIndex,
TREE_URL,

View File

@@ -6,7 +6,13 @@ import type { UserIdType } from './circuits/uuid.js';
import { validateUserId } from './circuits/uuid.js';
import { formatEndpoint } from './scope.js';
export interface DeferredLinkingTokenResponse {
campaign_id: string;
campaign_user_id: string;
self_app: string; // SelfApp is serialized as a string
}
export type EndpointType = 'https' | 'celo' | 'staging_celo' | 'staging_https';
export type Mode = 'register' | 'dsc' | 'vc_and_disclose';
export interface SelfApp {
@@ -121,9 +127,3 @@ export class SelfAppBuilder {
export function getUniversalLink(selfApp: SelfApp): string {
return `${REDIRECT_URL}?selfApp=${encodeURIComponent(JSON.stringify(selfApp))}`;
}
export interface DeferredLinkingTokenResponse {
campaign_id: string;
campaign_user_id: string;
self_app: string; // SelfApp is serialized as a string
}

View File

@@ -27,12 +27,21 @@ export {
hash,
packBytesAndPoseidon,
} from './hash.js';
export {
clientKey,
clientPublicKeyHex,
ec,
encryptAES256GCM,
getPayload,
getWSDbRelayerUrl,
} from './proving.js';
export {
findStartPubKeyIndex,
generateCommitment,
generateNullifier,
initPassportDataParsing,
} from './passports/passport.js';
export type { TEEPayload, TEEPayloadBase, TEEPayloadDisclose } from './proving.js';
export { formatMrz } from './passports/format.js';
export { genAndInitMockPassportData } from './passports/genMockPassportData.js';
export {
@@ -50,12 +59,3 @@ export { getSKIPEM } from './csca.js';
export { initElliptic } from './certificate_parsing/elliptic.js';
export { parseCertificateSimple } from './certificate_parsing/parseCertificateSimple.js';
export { parseDscCertificateData } from './passports/passport_parsing/parseDscCertificateData.js';
export {
clientKey,
clientPublicKeyHex,
ec,
encryptAES256GCM,
getPayload,
getWSDbRelayerUrl,
} from './proving.js';
export type { TEEPayload, TEEPayloadBase, TEEPayloadDisclose } from './proving.js';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,256 @@
// 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 { poseidon2, poseidon5 } from 'poseidon-lite';
import {
API_URL,
API_URL_STAGING,
ID_CARD_ATTESTATION_ID,
PASSPORT_ATTESTATION_ID,
} from '../../constants/index.js';
import { parseCertificateSimple } from '../../utils/certificate_parsing/parseSimple.js';
import { getCircuitNameFromPassportData } from '../../utils/circuits/circuitsName.js';
import { packBytesAndPoseidon } from '../../utils/hash/poseidon.js';
import { hash } from '../../utils/hash/sha.js';
import { formatMrz } from '../../utils/passports/format.js';
import { getLeafDscTree } from '../../utils/trees.js';
import {
AttestationIdHex,
type DeployedCircuits,
type DocumentCategory,
type PassportData,
} from '../types.js';
import { generateCommitment, generateNullifier } from './passport.js';
import { LeanIMT } from '@openpassport/zk-kit-lean-imt';
export type PassportSupportStatus =
| 'passport_metadata_missing'
| 'csca_not_found'
| 'registration_circuit_not_supported'
| 'dsc_circuit_not_supported'
| 'passport_supported';
export async function checkDocumentSupported(
passportData: PassportData,
opts: {
getDeployedCircuits: (docCategory: DocumentCategory) => DeployedCircuits;
}
): Promise<{
status: PassportSupportStatus;
details: string;
}> {
const passportMetadata = passportData.passportMetadata;
const document: DocumentCategory = passportData.documentCategory;
if (!passportMetadata) {
console.warn('Passport metadata is null');
return { status: 'passport_metadata_missing', details: passportData.dsc };
}
if (!passportMetadata.cscaFound) {
console.warn('CSCA not found');
return { status: 'csca_not_found', details: passportData.dsc };
}
const circuitNameRegister = getCircuitNameFromPassportData(passportData, 'register');
const deployedCircuits = opts.getDeployedCircuits(passportData.documentCategory);
if (
!circuitNameRegister ||
!(
deployedCircuits.REGISTER.includes(circuitNameRegister) ||
deployedCircuits.REGISTER_ID.includes(circuitNameRegister)
)
) {
return {
status: 'registration_circuit_not_supported',
details: circuitNameRegister,
};
}
const circuitNameDsc = getCircuitNameFromPassportData(passportData, 'dsc');
if (
!circuitNameDsc ||
!(
deployedCircuits.DSC.includes(circuitNameDsc) ||
deployedCircuits.DSC_ID.includes(circuitNameDsc)
)
) {
console.warn('DSC circuit not supported:', circuitNameDsc);
return { status: 'dsc_circuit_not_supported', details: circuitNameDsc };
}
return { status: 'passport_supported', details: 'null' };
}
export async function checkIfPassportDscIsInTree(
passportData: PassportData,
dscTree: string
): Promise<boolean> {
const hashFunction = (a: bigint, b: bigint) => poseidon2([a, b]);
const tree = LeanIMT.import(hashFunction, dscTree);
const leaf = getLeafDscTree(passportData.dsc_parsed!, passportData.csca_parsed!);
const index = tree.indexOf(BigInt(leaf));
if (index === -1) {
console.warn('DSC not found in the tree');
return false;
}
return true;
}
type AlternativeCSCA = Record<string, string>;
export function generateCommitmentInApp(
secret: string,
attestation_id: string,
passportData: PassportData,
alternativeCSCA: AlternativeCSCA
) {
const dg1_packed_hash = packBytesAndPoseidon(formatMrz(passportData.mrz));
const eContent_packed_hash = packBytesAndPoseidon(
(
hash(
passportData.passportMetadata!.eContentHashFunction,
Array.from(passportData.eContent),
'bytes'
) as number[]
)
// eslint-disable-next-line no-bitwise
.map((byte) => byte & 0xff)
);
const csca_list: string[] = [];
const commitment_list: string[] = [];
for (const [cscaKey, cscaValue] of Object.entries(alternativeCSCA)) {
try {
const formattedCsca = formatCSCAPem(cscaValue);
const cscaParsed = parseCertificateSimple(formattedCsca);
const commitment = poseidon5([
secret,
attestation_id,
dg1_packed_hash,
eContent_packed_hash,
getLeafDscTree(passportData.dsc_parsed!, cscaParsed),
]).toString();
csca_list.push(formatCSCAPem(cscaValue));
commitment_list.push(commitment);
} catch (error) {
console.warn(`Failed to parse CSCA certificate for key ${cscaKey}:`, error);
}
}
if (commitment_list.length === 0) {
console.error('No valid CSCA certificates found in alternativeCSCA');
}
return { commitment_list, csca_list };
}
export async function isDocumentNullified(passportData: PassportData) {
const nullifier = generateNullifier(passportData);
const nullifierHex = `0x${BigInt(nullifier).toString(16)}`;
const attestationId =
passportData.documentCategory === 'passport'
? AttestationIdHex.passport
: AttestationIdHex.id_card;
console.log('checking for nullifier', nullifierHex, attestationId);
const baseUrl = passportData.mock === false ? API_URL : API_URL_STAGING;
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(`${baseUrl}/is-nullifier-onchain-with-attestation-id`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nullifier: nullifierHex, attestation_id: attestationId }),
signal: controller.signal,
});
clearTimeout(t);
if (!response.ok) {
throw new Error(`isDocumentNullified non-OK response: ${response.status}`);
}
const data = await response.json();
return Boolean(data?.data);
} catch (e) {
const erorr = e instanceof Error ? e : new Error(String(e));
clearTimeout(t);
// re throw so our catcher can get this
throw new Error(
`isDocumentNullified request failed: ${erorr.name} ${erorr.message} \n ${erorr.stack}`
);
}
}
export async function isUserRegistered(
passportData: PassportData,
secret: string,
getCommitmentTree: (docCategory: DocumentCategory) => string
) {
if (!passportData) {
return false;
}
const attestationId =
passportData.documentCategory === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID;
const commitment = generateCommitment(secret, attestationId, passportData);
const document: DocumentCategory = passportData.documentCategory;
const serializedTree = getCommitmentTree(document);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
const index = tree.indexOf(BigInt(commitment));
return index !== -1;
}
export async function isUserRegisteredWithAlternativeCSCA(
passportData: PassportData,
secret: string,
{
getCommitmentTree,
getAltCSCA,
}: {
getCommitmentTree: (docCategory: DocumentCategory) => string;
getAltCSCA: (docCategory: DocumentCategory) => AlternativeCSCA;
}
): Promise<{ isRegistered: boolean; csca: string | null }> {
if (!passportData) {
console.error('Passport data is null');
return { isRegistered: false, csca: null };
}
const document: DocumentCategory = passportData.documentCategory;
const alternativeCSCA = getAltCSCA(document);
const { commitment_list, csca_list } = generateCommitmentInApp(
secret,
document === 'passport' ? PASSPORT_ATTESTATION_ID : ID_CARD_ATTESTATION_ID,
passportData,
alternativeCSCA
);
if (commitment_list.length === 0) {
console.error('No valid CSCA certificates could be parsed from alternativeCSCA');
return { isRegistered: false, csca: null };
}
const serializedTree = getCommitmentTree(document);
const tree = LeanIMT.import((a, b) => poseidon2([a, b]), serializedTree);
for (let i = 0; i < commitment_list.length; i++) {
const commitment = commitment_list[i];
const index = tree.indexOf(BigInt(commitment));
if (index !== -1) {
return { isRegistered: true, csca: csca_list[i] };
}
}
console.warn('None of the following CSCA correspond to the commitment:', csca_list);
return { isRegistered: false, csca: null };
}
function formatCSCAPem(cscaPem: string): string {
let cleanedPem = cscaPem.trim();
if (!cleanedPem.includes('-----BEGIN CERTIFICATE-----')) {
cleanedPem = cleanedPem.replace(/[^A-Za-z0-9+/=]/g, '');
try {
Buffer.from(cleanedPem, 'base64');
} catch (error) {
throw new Error(`Invalid base64 certificate data: ${error}`);
}
cleanedPem = `-----BEGIN CERTIFICATE-----\n${cleanedPem}\n-----END CERTIFICATE-----`;
}
return cleanedPem;
}

View File

@@ -1,15 +1,20 @@
import type { CertificateData } from './certificate_parsing/dataStructure.js';
import type { PassportMetadata } from './passports/passport_parsing/parsePassportData.js';
export type DocumentCategory = 'passport' | 'id_card';
export type DocumentType = 'passport' | 'id_card' | 'mock_passport' | 'mock_id_card';
export type DeployedCircuits = {
REGISTER: string[];
REGISTER_ID: string[];
DSC: string[];
DSC_ID: string[];
};
export interface DocumentCatalog {
documents: DocumentMetadata[];
selectedDocumentId?: string; // This is now a contentHash
}
export type DocumentCategory = 'passport' | 'id_card';
export interface DocumentMetadata {
id: string; // contentHash as ID for deduplication
documentType: string; // passport, mock_passport, id_card, etc.
@@ -19,6 +24,8 @@ export interface DocumentMetadata {
isRegistered?: boolean; // whether the document is registered onChain
}
export type DocumentType = 'passport' | 'id_card' | 'mock_passport' | 'mock_id_card';
export type OfacTree = {
passportNoAndNationality: any;
nameAndDob: any;
@@ -100,6 +107,13 @@ export type SignatureAlgorithm =
| 'ecdsa_sha384_brainpoolP512r1_512'
| 'ecdsa_sha512_brainpoolP512r1_512';
// keys should match DocumentCategory
export enum AttestationIdHex {
invalid = '0x0000000000000000000000000000000000000000000000000000000000000000',
passport = '0x0000000000000000000000000000000000000000000000000000000000000001',
id_card = '0x0000000000000000000000000000000000000000000000000000000000000002',
}
export function castCSCAProof(proof: any): Proof {
return {
proof: {

View File

@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import {
bigIntToString,

View File

@@ -32,6 +32,7 @@ const entry = {
'src/utils/passports/index': 'src/utils/passports/index.ts',
'src/utils/passports/format': 'src/utils/passports/format.ts',
'src/utils/passports/mock': 'src/utils/passports/mock.ts',
'src/utils/passports/validate': 'src/utils/passports/validate.ts',
'src/utils/passports/dg1': 'src/utils/passports/dg1.ts',
'src/utils/passports/genMockPassportData': 'src/utils/passports/genMockPassportData.ts',
'src/utils/passports/genMockIdDoc': 'src/utils/passports/genMockIdDoc.ts',

View File

@@ -4,7 +4,7 @@ export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],
include: ['**/*.test.ts'],
setupFiles: ['./tests/setup.ts'],
},
});