mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 06:38:09 -05:00
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:
4
.gitleaksignore
Normal file
4
.gitleaksignore
Normal 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
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
193
common/src/utils/passports/validate.test.ts
Normal file
193
common/src/utils/passports/validate.test.ts
Normal file
File diff suppressed because one or more lines are too long
256
common/src/utils/passports/validate.ts
Normal file
256
common/src/utils/passports/validate.ts
Normal 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;
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
bigIntToString,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['tests/**/*.test.ts'],
|
||||
include: ['**/*.test.ts'],
|
||||
setupFiles: ['./tests/setup.ts'],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user