UNPKG

@0xpolygonid/js-sdk

Version:
450 lines (449 loc) 22.6 kB
import { DID, getDateFromUnixTimestamp } from '@iden3/js-iden3-core'; import { getDocumentLoader, Path } from '@iden3/js-jsonld-merklization'; import { byteEncoder, isGenesisState } from '../../utils'; import { calculateCoreSchemaHash, ProofType } from '../../verifiable'; import { AtomicQueryMTPV2PubSignals } from '../../circuits/atomic-query-mtp-v2'; import { AtomicQuerySigV2PubSignals } from '../../circuits/atomic-query-sig-v2'; import { AtomicQueryV3PubSignals } from '../../circuits/atomic-query-v3'; import { AuthV2PubSignals } from '../../circuits/auth-v2'; import { BaseConfig } from '../../circuits/common'; import { LinkedMultiQueryPubSignals, LinkedMultiQueryInputs } from '../../circuits/linked-multi-query'; import { CircuitId } from '../../circuits/models'; import { checkQueryRequest, fieldValueFromVerifiablePresentation, validateDisclosureV2Circuit, validateEmptyCredentialSubjectV2Circuit, validateOperators, verifyFieldValueInclusionV2, validateDisclosureNativeSDSupport, validateEmptyCredentialSubjectNoopNativeSupport, verifyFieldValueInclusionNativeExistsSupport, checkCircuitOperator } from './query'; import { parseQueriesMetadata } from '../common'; import { Operators } from '../../circuits'; import { calculateQueryHashV3 } from './query-hash'; import { PROTOCOL_CONSTANTS } from '../../iden3comm'; export const userStateError = new Error(`user state is not valid`); const zeroInt = 0n; /** * PubSignalsVerifier provides verify method * @public * @class PubSignalsVerifier */ export class PubSignalsVerifier { /** * Creates an instance of PubSignalsVerifier. * @param {DocumentLoader} _documentLoader document loader * @param {IStateStorage} _stateStorage state storage */ constructor(_documentLoader, _stateStorage) { this._documentLoader = _documentLoader; this._stateStorage = _stateStorage; this.credentialAtomicQueryMTPV2Verify = async ({ query, verifiablePresentation, sender, challenge, pubSignals, opts }) => { let mtpv2PubSignals = new AtomicQueryMTPV2PubSignals(); mtpv2PubSignals = mtpv2PubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals))); if (!mtpv2PubSignals.userID) { throw new Error('user id is not presented in proof public signals'); } if (!mtpv2PubSignals.requestID) { throw new Error('requestId is not presented in proof public signals'); } this.userId = mtpv2PubSignals.userID; this.challenge = mtpv2PubSignals.requestID; // verify query const outs = { issuerId: mtpv2PubSignals.issuerID, schemaHash: mtpv2PubSignals.claimSchema, slotIndex: mtpv2PubSignals.slotIndex, operator: mtpv2PubSignals.operator, value: mtpv2PubSignals.value, timestamp: mtpv2PubSignals.timestamp, merklized: mtpv2PubSignals.merklized, claimPathKey: mtpv2PubSignals.claimPathKey, claimPathNotExists: mtpv2PubSignals.claimPathNotExists, valueArraySize: mtpv2PubSignals.getValueArrSize(), isRevocationChecked: mtpv2PubSignals.isRevocationChecked }; await this.checkQueryV2Circuits(CircuitId.AtomicQueryMTPV2, query, outs, opts, verifiablePresentation); // verify state await this.checkStateExistenceForId(mtpv2PubSignals.issuerID, mtpv2PubSignals.issuerClaimIdenState); if (mtpv2PubSignals.isRevocationChecked !== 0) { await this.checkRevocationState(mtpv2PubSignals.issuerID, mtpv2PubSignals.issuerClaimNonRevState, opts); } // verify ID ownership this.verifyIdOwnership(sender, challenge); return mtpv2PubSignals; }; this.credentialAtomicQuerySigV2Verify = async ({ query, verifiablePresentation, sender, challenge, pubSignals, opts }) => { let sigV2PubSignals = new AtomicQuerySigV2PubSignals(); sigV2PubSignals = sigV2PubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals))); this.userId = sigV2PubSignals.userID; this.challenge = sigV2PubSignals.requestID; // verify query const outs = { issuerId: sigV2PubSignals.issuerID, schemaHash: sigV2PubSignals.claimSchema, slotIndex: sigV2PubSignals.slotIndex, operator: sigV2PubSignals.operator, value: sigV2PubSignals.value, timestamp: sigV2PubSignals.timestamp, merklized: sigV2PubSignals.merklized, claimPathKey: sigV2PubSignals.claimPathKey, claimPathNotExists: sigV2PubSignals.claimPathNotExists, valueArraySize: sigV2PubSignals.getValueArrSize(), isRevocationChecked: sigV2PubSignals.isRevocationChecked }; await this.checkQueryV2Circuits(CircuitId.AtomicQuerySigV2, query, outs, opts, verifiablePresentation); // verify state await this.checkStateExistenceForId(sigV2PubSignals.issuerID, sigV2PubSignals.issuerAuthState); if (sigV2PubSignals.isRevocationChecked !== 0) { await this.checkRevocationState(sigV2PubSignals.issuerID, sigV2PubSignals.issuerClaimNonRevState, opts); } // verify Id ownership this.verifyIdOwnership(sender, challenge); return sigV2PubSignals; }; this.credentialAtomicQueryV3Verify = async ({ query, verifiablePresentation, sender, challenge, pubSignals, opts, params }) => { let v3PubSignals = new AtomicQueryV3PubSignals(); v3PubSignals = v3PubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals))); this.userId = v3PubSignals.userID; this.challenge = v3PubSignals.requestID; // verify query const outs = { issuerId: v3PubSignals.issuerID, schemaHash: v3PubSignals.claimSchema, slotIndex: v3PubSignals.slotIndex, operator: v3PubSignals.operator, value: v3PubSignals.value, timestamp: v3PubSignals.timestamp, merklized: v3PubSignals.merklized, claimPathKey: v3PubSignals.claimPathKey, valueArraySize: v3PubSignals.getValueArrSize(), operatorOutput: v3PubSignals.operatorOutput, isRevocationChecked: v3PubSignals.isRevocationChecked }; if (!query.type) { throw new Error(`proof query type is undefined`); } const loader = this._documentLoader ?? getDocumentLoader(); // validate schema let context; try { context = (await loader(query.context ?? '')).document; } catch (e) { throw new Error(`can't load schema for request query`); } const queriesMetadata = await parseQueriesMetadata(query.type, JSON.stringify(context), query.credentialSubject, { documentLoader: loader }); const circuitId = CircuitId.AtomicQueryV3; await checkQueryRequest(query, queriesMetadata, context, outs, circuitId, this._documentLoader, opts); const queryMetadata = queriesMetadata[0]; // only one query is supported checkCircuitOperator(circuitId, outs.operator); // validate selective disclosure if (queryMetadata.operator === Operators.SD) { try { await validateDisclosureNativeSDSupport(queryMetadata, outs, verifiablePresentation, loader); } catch (e) { throw new Error(`failed to validate selective disclosure: ${e.message}`); } } else if (!queryMetadata.fieldName && queryMetadata.operator == Operators.NOOP) { try { await validateEmptyCredentialSubjectNoopNativeSupport(outs); } catch (e) { throw new Error(`failed to validate operators: ${e.message}`); } } else { try { await validateOperators(queryMetadata, outs); } catch (e) { throw new Error(`failed to validate operators: ${e.message}`); } } // verify field inclusion / non-inclusion verifyFieldValueInclusionNativeExistsSupport(outs, queryMetadata); const { proofType, verifierID, nullifier, nullifierSessionID, linkID } = v3PubSignals; switch (query.proofType) { case ProofType.BJJSignature: if (proofType !== 1) { throw new Error('wrong proof type for BJJSignature'); } break; case ProofType.Iden3SparseMerkleTreeProof: if (proofType !== 2) { throw new Error('wrong proof type for Iden3SparseMerkleTreeProof'); } break; default: throw new Error('invalid proof type'); } const nSessionId = BigInt(params?.nullifierSessionId ?? 0); if (nSessionId !== 0n) { if (BigInt(nullifier ?? 0) === 0n) { throw new Error('nullifier should be provided for nullification and should not be 0'); } // verify nullifier information const verifierDIDParam = params?.verifierDid; if (!verifierDIDParam) { throw new Error('verifierDid is required'); } const id = DID.idFromDID(verifierDIDParam); if (verifierID.bigInt() != id.bigInt()) { throw new Error('wrong verifier is used for nullification'); } if (nullifierSessionID !== nSessionId) { throw new Error(`wrong verifier session id is used for nullification, expected ${nSessionId}, got ${nullifierSessionID}`); } } else if (nullifierSessionID !== 0n) { throw new Error(`Nullifier id is generated but wasn't requested`); } if (!query.groupId && linkID !== 0n) { throw new Error(`proof contains link id, but group id is not provided`); } if (query.groupId && linkID === 0n) { throw new Error("proof doesn't contain link id, but group id is provided"); } // verify state await this.checkStateExistenceForId(v3PubSignals.issuerID, v3PubSignals.issuerState); if (v3PubSignals.isRevocationChecked !== 0) { await this.checkRevocationState(v3PubSignals.issuerID, v3PubSignals.issuerClaimNonRevState, opts); } this.verifyIdOwnership(sender, challenge); return v3PubSignals; }; this.authV2Verify = async ({ sender, challenge, pubSignals, opts }) => { let authV2PubSignals = new AuthV2PubSignals(); authV2PubSignals = authV2PubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals))); this.userId = authV2PubSignals.userID; this.challenge = authV2PubSignals.challenge; // no query verification // verify state const gist = await this.checkGlobalState(authV2PubSignals.GISTRoot, this.userId); let acceptedStateTransitionDelay = PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY; if (opts?.acceptedStateTransitionDelay) { acceptedStateTransitionDelay = opts.acceptedStateTransitionDelay; } if (!gist.latest) { const timeDiff = Date.now() - getDateFromUnixTimestamp(Number(gist.transitionTimestamp)).getTime(); if (timeDiff > acceptedStateTransitionDelay) { throw new Error('global state is outdated'); } } // verify Id ownership this.verifyIdOwnership(sender, challenge); return new BaseConfig(); }; this.linkedMultiQuery10Verify = async ({ query, verifiablePresentation, pubSignals }) => { let multiQueryPubSignals = new LinkedMultiQueryPubSignals(); multiQueryPubSignals = multiQueryPubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals))); // verify query let schema; const ldOpts = { documentLoader: this._documentLoader }; try { schema = (await ldOpts.documentLoader(query.context || '')).document; } catch (e) { throw new Error(`can't load schema for request query`); } const ldContextJSON = JSON.stringify(schema); const credentialSubject = query.credentialSubject; const schemaId = await Path.getTypeIDFromContext(ldContextJSON, query.type || '', ldOpts); const schemaHash = calculateCoreSchemaHash(byteEncoder.encode(schemaId)); const queriesMetadata = await parseQueriesMetadata(query.type || '', ldContextJSON, credentialSubject, ldOpts); const request = []; const merklized = queriesMetadata[0]?.merklizedSchema ? 1 : 0; for (let i = 0; i < LinkedMultiQueryInputs.queryCount; i++) { const queryMeta = queriesMetadata[i]; const values = queryMeta?.values ?? []; const valArrSize = values.length; const queryHash = calculateQueryHashV3(values, schemaHash, queryMeta?.slotIndex ?? 0, queryMeta?.operator ?? 0, queryMeta?.claimPathKey.toString() ?? 0, valArrSize, merklized, 0, 0, 0); request.push({ queryHash, queryMeta }); } const queryHashCompare = (a, b) => { if (a.queryHash < b.queryHash) return -1; if (a.queryHash > b.queryHash) return 1; return 0; }; const pubSignalsMeta = multiQueryPubSignals.circuitQueryHash.map((queryHash, index) => ({ queryHash, operatorOutput: multiQueryPubSignals.operatorOutput[index] })); pubSignalsMeta.sort(queryHashCompare); request.sort(queryHashCompare); for (let i = 0; i < LinkedMultiQueryInputs.queryCount; i++) { if (request[i].queryHash != pubSignalsMeta[i].queryHash) { throw new Error('query hashes do not match'); } if (request[i].queryMeta?.operator === Operators.SD) { const disclosedValue = await fieldValueFromVerifiablePresentation(request[i].queryMeta.fieldName, verifiablePresentation, this._documentLoader); if (disclosedValue != pubSignalsMeta[i].operatorOutput) { throw new Error('disclosed value is not in the proof outputs'); } } } return multiQueryPubSignals; }; this.verifyIdOwnership = (sender, challenge) => { const senderId = DID.idFromDID(DID.parse(sender)); if (senderId.string() !== this.userId.string()) { throw new Error(`sender id is not used for proof creation, expected ${sender}, user from public signals: ${this.userId.string()}`); } if (challenge !== this.challenge) { throw new Error(`challenge is not used for proof creation, expected ${challenge}, challenge from public signals: ${this.challenge} `); } }; this.checkStateExistenceForId = async (userId, userState) => { await this.resolve(userId, userState.bigInt()); }; this.checkGlobalState = async (state, id) => { return this.rootResolve(state.bigInt(), id.bigInt()); }; this.checkRevocationStateForId = async (issuerId, issuerClaimNonRevState) => { const issuerNonRevStateResolved = await this.resolve(issuerId, issuerClaimNonRevState.bigInt()); return issuerNonRevStateResolved; }; this.checkRevocationState = async (issuerID, issuerClaimNonRevState, opts) => { const issuerNonRevStateResolved = await this.checkRevocationStateForId(issuerID, issuerClaimNonRevState); const acceptedStateTransitionDelay = opts?.acceptedStateTransitionDelay ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY; if (!issuerNonRevStateResolved.latest) { const timeDiff = Date.now() - getDateFromUnixTimestamp(Number(issuerNonRevStateResolved.transitionTimestamp)).getTime(); if (timeDiff > acceptedStateTransitionDelay) { throw new Error('issuer state is outdated'); } } }; } /** * verify public signals * * @param {string} circuitId circuit id * @param {VerifyContext} ctx verification parameters * @returns `Promise<BaseConfig>` */ async verify(circuitId, ctx) { const fnName = `${circuitId.split('-')[0]}Verify`; const fn = this[fnName]; if (!fn) { throw new Error(`public signals verifier for ${circuitId} not found`); } return fn(ctx); } async checkQueryV2Circuits(circuitId, query, outs, opts, verifiablePresentation) { if (!query.type) { throw new Error(`proof query type is undefined`); } const loader = this._documentLoader ?? getDocumentLoader(); // validate schema let context; try { context = (await loader(query.context ?? '')).document; } catch (e) { throw new Error(`can't load schema for request query`); } const queriesMetadata = await parseQueriesMetadata(query.type, JSON.stringify(context), query.credentialSubject, { documentLoader: loader }); await checkQueryRequest(query, queriesMetadata, context, outs, circuitId, this._documentLoader, opts); const queryMetadata = queriesMetadata[0]; // only one query is supported checkCircuitOperator(circuitId, outs.operator); // validate selective disclosure if (queryMetadata.operator === Operators.SD) { try { await validateDisclosureV2Circuit(queryMetadata, outs, verifiablePresentation, loader); } catch (e) { throw new Error(`failed to validate selective disclosure: ${e.message}`); } } else if (!queryMetadata.fieldName && queryMetadata.operator == Operators.NOOP) { try { await validateEmptyCredentialSubjectV2Circuit(queryMetadata, outs); } catch (e) { throw new Error(`failed to validate operators: ${e.message}`); } } else { try { await validateOperators(queryMetadata, outs); } catch (e) { throw new Error(`failed to validate operators: ${e.message}`); } } // verify field inclusion verifyFieldValueInclusionV2(outs, queryMetadata); } async resolve(id, state) { const idBigInt = id.bigInt(); const did = DID.parseFromId(id); // check if id is genesis const isGenesis = isGenesisState(did, state); let contractState; try { contractState = await this._stateStorage.getStateInfoByIdAndState(idBigInt, state); } catch (e) { const stateNotExistErr = (e?.errorArgs ?? [])[0]; const errMsg = stateNotExistErr || e.message; if (errMsg.includes('State does not exist')) { if (isGenesis) { return { latest: true, transitionTimestamp: 0 }; } throw new Error('State is not genesis and not registered in the smart contract'); } throw e; } if (!contractState.id || contractState.id.toString() !== idBigInt.toString()) { throw new Error(`state was recorded for another identity`); } if (!contractState.state || contractState.state.toString() !== state.toString()) { if (!contractState.replacedAtTimestamp || contractState.replacedAtTimestamp.toString() === zeroInt.toString()) { throw new Error(`no information about state transition`); } return { latest: false, transitionTimestamp: contractState.replacedAtTimestamp.toString() }; } return { latest: !contractState.replacedAtTimestamp || contractState.replacedAtTimestamp.toString() === zeroInt.toString(), transitionTimestamp: contractState.replacedAtTimestamp?.toString() ?? 0 }; } async rootResolve(state, id) { let globalStateInfo; try { globalStateInfo = await this._stateStorage.getGISTRootInfo(state, id); } catch (e) { if (e.errorArgs[0] === 'Root does not exist') { throw new Error('GIST root does not exist in the smart contract'); } throw e; } if (globalStateInfo.root.toString() !== state.toString()) { throw new Error(`gist info contains invalid state`); } if (globalStateInfo.replacedByRoot.toString() !== zeroInt.toString()) { if (globalStateInfo.replacedAtTimestamp.toString() === zeroInt.toString()) { throw new Error(`state was replaced, but replaced time unknown`); } return { latest: false, transitionTimestamp: globalStateInfo.replacedAtTimestamp.toString() }; } return { latest: true, transitionTimestamp: 0 }; } }