mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
import { ethers } from 'ethers';
|
|
import { hashEndpointWithScope } from '@selfxyz/common/utils/scope';
|
|
import {
|
|
AadhaarVerifier,
|
|
AadhaarVerifier__factory,
|
|
IdentityVerificationHubImpl,
|
|
IdentityVerificationHubImpl__factory,
|
|
Registry__factory,
|
|
Verifier,
|
|
Verifier__factory,
|
|
} from './typechain-types/index.js';
|
|
import { discloseIndices } from './utils/constants.js';
|
|
import { formatRevealedDataPacked } from './utils/id.js';
|
|
import { AttestationId, VcAndDiscloseProof, VerificationConfig } from './types/types.js';
|
|
import { Country3LetterCode } from '@selfxyz/common/constants/countries';
|
|
import { calculateUserIdentifierHash } from './utils/hash.js';
|
|
import { castToUserIdentifier, UserIdType } from '@selfxyz/common/utils/circuits/uuid';
|
|
import {
|
|
ConfigMismatch,
|
|
ConfigMismatchError,
|
|
RegistryContractError,
|
|
VerifierContractError,
|
|
} from './errors/index.js';
|
|
import { IConfigStorage } from './store/interface.js';
|
|
import { unpackForbiddenCountriesList } from './utils/utils.js';
|
|
import { BigNumberish } from 'ethers';
|
|
|
|
const CELO_MAINNET_RPC_URL = 'https://forno.celo.org';
|
|
const CELO_TESTNET_RPC_URL = 'https://forno.celo-sepolia.celo-testnet.org';
|
|
|
|
const IDENTITY_VERIFICATION_HUB_ADDRESS = '0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF';
|
|
const IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING = '0x16ECBA51e18a4a7e61fdC417f0d47AFEeDfbed74';
|
|
|
|
export class SelfBackendVerifier {
|
|
protected scope: string;
|
|
protected identityVerificationHubContract: IdentityVerificationHubImpl;
|
|
protected configStorage: IConfigStorage;
|
|
protected provider: ethers.JsonRpcProvider;
|
|
protected allowedIds: Map<AttestationId, boolean>;
|
|
protected userIdentifierType: UserIdType;
|
|
|
|
constructor(
|
|
scope: string,
|
|
endpoint: string,
|
|
mockPassport: boolean = false,
|
|
allowedIds: Map<AttestationId, boolean>,
|
|
configStorage: IConfigStorage,
|
|
userIdentifierType: UserIdType
|
|
) {
|
|
const rpcUrl = mockPassport ? CELO_TESTNET_RPC_URL : CELO_MAINNET_RPC_URL;
|
|
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
const identityVerificationHubAddress = mockPassport
|
|
? IDENTITY_VERIFICATION_HUB_ADDRESS_STAGING
|
|
: IDENTITY_VERIFICATION_HUB_ADDRESS;
|
|
this.identityVerificationHubContract = IdentityVerificationHubImpl__factory.connect(
|
|
identityVerificationHubAddress,
|
|
provider
|
|
);
|
|
this.provider = provider;
|
|
this.scope = hashEndpointWithScope(endpoint, scope);
|
|
this.allowedIds = allowedIds;
|
|
this.configStorage = configStorage;
|
|
this.userIdentifierType = userIdentifierType;
|
|
}
|
|
|
|
public async verify(
|
|
attestationId: AttestationId,
|
|
proof: VcAndDiscloseProof,
|
|
pubSignals: BigNumberish[],
|
|
userContextData: string
|
|
) {
|
|
//check if attestation id is allowed
|
|
const allowedId = this.allowedIds.get(attestationId);
|
|
let issues: Array<{ type: ConfigMismatch; message: string }> = [];
|
|
if (!allowedId) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidId,
|
|
message: 'Attestation ID is not allowed, received: ' + attestationId,
|
|
});
|
|
}
|
|
|
|
const publicSignals = pubSignals
|
|
.map(String)
|
|
.map((x) => (/[a-f]/g.test(x) && x.length > 0 ? '0x' + x : x));
|
|
//check if user context hash matches
|
|
const userContextHashInCircuit = BigInt(
|
|
publicSignals[discloseIndices[attestationId].userIdentifierIndex]
|
|
);
|
|
const userContextHash = BigInt(
|
|
calculateUserIdentifierHash(Buffer.from(userContextData, 'hex'))
|
|
);
|
|
|
|
if (userContextHashInCircuit !== userContextHash) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidUserContextHash,
|
|
message:
|
|
'User context hash does not match with the one in the circuit\nCircuit: ' +
|
|
userContextHashInCircuit +
|
|
'\nUser context hash: ' +
|
|
userContextHash,
|
|
});
|
|
}
|
|
|
|
//check if scope matches
|
|
const isValidScope = this.scope === publicSignals[discloseIndices[attestationId].scopeIndex];
|
|
if (!isValidScope) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidScope,
|
|
message:
|
|
'Scope does not match with the one in the circuit\nCircuit: ' +
|
|
publicSignals[discloseIndices[attestationId].scopeIndex] +
|
|
'\nScope: ' +
|
|
this.scope,
|
|
});
|
|
}
|
|
|
|
//check the root
|
|
try {
|
|
const registryAddress = await this.identityVerificationHubContract.registry(
|
|
'0x' + attestationId.toString(16).padStart(64, '0')
|
|
);
|
|
if (registryAddress === '0x0000000000000000000000000000000000000000') {
|
|
throw new RegistryContractError('Registry contract not found');
|
|
}
|
|
const registryContract = Registry__factory.connect(registryAddress, this.provider);
|
|
const currentRoot = await registryContract.checkIdentityCommitmentRoot(
|
|
publicSignals[discloseIndices[attestationId].merkleRootIndex]
|
|
);
|
|
if (!currentRoot) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidRoot,
|
|
message:
|
|
'Onchain root does not exist, received: ' +
|
|
publicSignals[discloseIndices[attestationId].merkleRootIndex],
|
|
});
|
|
}
|
|
} catch (error) {
|
|
throw new RegistryContractError('Registry contract not found');
|
|
}
|
|
|
|
//check if attestation id matches
|
|
const isValidAttestationId =
|
|
attestationId.toString() === publicSignals[discloseIndices[attestationId].attestationIdIndex];
|
|
if (!isValidAttestationId) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidAttestationId,
|
|
message: 'Attestation ID does not match with the one in the circuit',
|
|
});
|
|
}
|
|
|
|
const userIdentifier = castToUserIdentifier(
|
|
BigInt('0x' + userContextData.slice(64, 128)),
|
|
this.userIdentifierType
|
|
);
|
|
const userDefinedData = userContextData.slice(128);
|
|
const configId = await this.configStorage.getActionId(userIdentifier, userDefinedData);
|
|
if (!configId) {
|
|
issues.push({
|
|
type: ConfigMismatch.ConfigNotFound,
|
|
message: 'Config Id not found',
|
|
});
|
|
}
|
|
|
|
let verificationConfig: VerificationConfig | null;
|
|
try {
|
|
verificationConfig = await this.configStorage.getConfig(configId);
|
|
} catch (error) {
|
|
issues.push({
|
|
type: ConfigMismatch.ConfigNotFound,
|
|
message: `Config not found for ${configId}`,
|
|
});
|
|
} finally {
|
|
if (!verificationConfig) {
|
|
issues.push({
|
|
type: ConfigMismatch.ConfigNotFound,
|
|
message: `Config not found for ${configId}`,
|
|
});
|
|
throw new ConfigMismatchError(issues);
|
|
}
|
|
}
|
|
|
|
//check if forbidden countries list matches
|
|
const forbiddenCountriesList: string[] = unpackForbiddenCountriesList(
|
|
[0, 1, 2, 3].map(
|
|
(x) => publicSignals[discloseIndices[attestationId].forbiddenCountriesListPackedIndex + x]
|
|
)
|
|
);
|
|
const forbiddenCountriesListVerificationConfig = verificationConfig.excludedCountries || [];
|
|
|
|
const isForbiddenCountryListValid = forbiddenCountriesListVerificationConfig.every((country) =>
|
|
forbiddenCountriesList.includes(country as Country3LetterCode)
|
|
);
|
|
if (!isForbiddenCountryListValid) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidForbiddenCountriesList,
|
|
message:
|
|
'Forbidden countries list in config does not match with the one in the circuit\nCircuit: ' +
|
|
forbiddenCountriesList.join(', ') +
|
|
'\nConfig: ' +
|
|
forbiddenCountriesListVerificationConfig.join(', '),
|
|
});
|
|
}
|
|
|
|
const genericDiscloseOutput = formatRevealedDataPacked(attestationId, publicSignals);
|
|
//check if minimum age matches
|
|
const isMinimumAgeValid =
|
|
verificationConfig.minimumAge !== undefined
|
|
? verificationConfig.minimumAge === Number.parseInt(genericDiscloseOutput.minimumAge, 10) ||
|
|
genericDiscloseOutput.minimumAge === '00'
|
|
: true;
|
|
if (!isMinimumAgeValid) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidMinimumAge,
|
|
message:
|
|
'Minimum age in config does not match with the one in the circuit\nCircuit: ' +
|
|
genericDiscloseOutput.minimumAge +
|
|
'\nConfig: ' +
|
|
verificationConfig.minimumAge,
|
|
});
|
|
}
|
|
|
|
let circuitTimestampYy: number[];
|
|
let circuitTimestampMm: number[];
|
|
let circuitTimestampDd: number[];
|
|
if (attestationId === 3) {
|
|
circuitTimestampYy = String(publicSignals[discloseIndices[attestationId].currentDateIndex])
|
|
.split('')
|
|
.map(Number);
|
|
circuitTimestampMm = String(
|
|
publicSignals[discloseIndices[attestationId].currentDateIndex + 1]
|
|
)
|
|
.split('')
|
|
.map(Number);
|
|
circuitTimestampDd = String(
|
|
publicSignals[discloseIndices[attestationId].currentDateIndex + 2]
|
|
)
|
|
.split('')
|
|
.map(Number);
|
|
} else {
|
|
circuitTimestampYy = [
|
|
2,
|
|
0,
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex],
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex + 1],
|
|
];
|
|
circuitTimestampMm = [
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex + 2],
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex + 3],
|
|
];
|
|
circuitTimestampDd = [
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex + 4],
|
|
+publicSignals[discloseIndices[attestationId].currentDateIndex + 5],
|
|
];
|
|
}
|
|
|
|
const circuitTimestamp = new Date(
|
|
Number(circuitTimestampYy.join('')),
|
|
Number(circuitTimestampMm.join('')) - 1,
|
|
Number(circuitTimestampDd.join(''))
|
|
);
|
|
const currentTimestamp = new Date();
|
|
|
|
//check if timestamp is in the future
|
|
const oneDayAhead = new Date(currentTimestamp.getTime() + 24 * 60 * 60 * 1000);
|
|
if (circuitTimestamp > oneDayAhead) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidTimestamp,
|
|
message: 'Circuit timestamp is in the future',
|
|
});
|
|
}
|
|
|
|
//check if timestamp is 1 day in the past
|
|
const circuitTimestampEOD = new Date(
|
|
circuitTimestamp.getTime() + 23 * 60 * 60 * 1e3 + 59 * 60 * 1e3 + 59 * 1e3
|
|
);
|
|
const oneDayAgo = new Date(currentTimestamp.getTime() - 24 * 60 * 60 * 1000);
|
|
if (circuitTimestampEOD < oneDayAgo) {
|
|
issues.push({
|
|
type: ConfigMismatch.InvalidTimestamp,
|
|
message: 'Circuit timestamp is too old',
|
|
});
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
throw new ConfigMismatchError(issues);
|
|
}
|
|
|
|
let verifierContract: Verifier | AadhaarVerifier;
|
|
try {
|
|
const verifierAddress = await this.identityVerificationHubContract.discloseVerifier(
|
|
'0x' + attestationId.toString(16).padStart(64, '0')
|
|
);
|
|
if (verifierAddress === '0x0000000000000000000000000000000000000000') {
|
|
throw new VerifierContractError('Verifier contract not found');
|
|
}
|
|
if (attestationId === 3) {
|
|
verifierContract = AadhaarVerifier__factory.connect(verifierAddress, this.provider);
|
|
} else {
|
|
verifierContract = Verifier__factory.connect(verifierAddress, this.provider);
|
|
}
|
|
} catch (error) {
|
|
throw new VerifierContractError('Verifier contract not found');
|
|
}
|
|
|
|
let isValid = false;
|
|
try {
|
|
isValid = await verifierContract.verifyProof(
|
|
proof.a,
|
|
[
|
|
[proof.b[0][1], proof.b[0][0]],
|
|
[proof.b[1][1], proof.b[1][0]],
|
|
],
|
|
proof.c,
|
|
publicSignals
|
|
);
|
|
} catch (error) {
|
|
isValid = false;
|
|
}
|
|
|
|
const cumulativeOfac = genericDiscloseOutput.ofac.reduce((acc, curr) => acc || curr, false);
|
|
|
|
return {
|
|
attestationId,
|
|
isValidDetails: {
|
|
isValid,
|
|
isMinimumAgeValid:
|
|
verificationConfig.minimumAge !== undefined
|
|
? verificationConfig.minimumAge <= Number.parseInt(genericDiscloseOutput.minimumAge, 10)
|
|
: true,
|
|
isOfacValid:
|
|
//isOfacValid is true when a person is in OFAC list
|
|
verificationConfig.ofac !== undefined && verificationConfig.ofac ? cumulativeOfac : false,
|
|
},
|
|
forbiddenCountriesList,
|
|
discloseOutput: genericDiscloseOutput,
|
|
userData: {
|
|
userIdentifier,
|
|
userDefinedData,
|
|
},
|
|
};
|
|
}
|
|
}
|