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

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;