UNPKG

@0xpolygonid/js-sdk

Version:
715 lines (631 loc) 23.1 kB
import { BytesHelper, DID, MerklizedRootPosition, getDateFromUnixTimestamp } from '@iden3/js-iden3-core'; import { AuthV2Inputs, AuthV2PubSignals, AuthV3Inputs, AuthV3PubSignals, CircuitId, Operators, Query, TreeState, ValueProof } from '../circuits'; import { ICredentialWallet } from '../credentials'; import { IIdentityWallet } from '../identity'; import { createVerifiablePresentation, ProofQuery, RevocationStatus, VerifiableConstants, W3CCredential } from '../verifiable'; import { PreparedCredential, QueryMetadata, parseCredentialSubject, parseQueryMetadata, toGISTProof, transformQueryValueToBigInts } from './common'; import { IZKProver, NativeProver } from './provers/prover'; import { Merklizer, Options, getDocumentLoader } from '@iden3/js-jsonld-merklization'; import { ZKProof } from '@iden3/js-jwz'; import { Signer } from 'ethers'; import { StateVerificationOpts, JSONObject, ZeroKnowledgeProofRequest, ZeroKnowledgeProofResponse, PROTOCOL_CONSTANTS, VerifiablePresentation, JsonDocumentObject, ZeroKnowledgeProofAuthResponse } from '../iden3comm'; import { cacheLoader } from '../schema-processor'; import { ICircuitStorage, IProofStorage, IStateStorage } from '../storage'; import { byteDecoder, byteEncoder } from '../utils/encoding'; import { AuthProofGenerationOptions, InputGenerator, ProofGenerationOptions, ProofInputsParams } from './provers/inputs-generator'; import { PubSignalsVerifier, VerifyContext } from './verifiers/pub-signals-verifier'; import { VerifyOpts } from './verifiers'; export interface QueryWithFieldName { query: Query; fieldName: string; rawValue?: unknown; isSelectiveDisclosure?: boolean; } /** * Metadata that returns on verification * @type VerificationResultMetadata */ export type VerificationResultMetadata = { linkID?: bigint; }; /** * List of options to customize ProofService */ export type ProofServiceOptions = Options & { prover?: IZKProver; proofsCacheStorage?: IProofStorage; }; export interface ProofVerifyOpts { query: ProofQuery; sender: string; opts?: VerifyOpts; params?: JSONObject; } export interface IProofService { /** * Verification of zkp proof for given circuit id * * @param {ZKProof} zkp - proof to verify * @param {CircuitId} circuitId - circuit id * @returns `{Promise<boolean>}` */ verifyProof(zkp: ZKProof, circuitName: CircuitId): Promise<boolean>; /** * Verification of zkp proof and pub signals for given circuit id * * @param {ZeroKnowledgeProofResponse} response - zero knowledge proof response * @param {ProofVerifyOpts} opts - proof verification options * @returns `{Promise<VerificationResultMetadata>}` */ verifyZKPResponse( proofResp: ZeroKnowledgeProofResponse, opts: ProofVerifyOpts ): Promise<VerificationResultMetadata>; /** * Generate proof from given identity and credential for protocol proof request * * @param {ZeroKnowledgeProofRequest} proofReq - protocol zkp request * @param {DID} identifier - did that will generate proof * @param {W3CCredential} credential - credential that will be used for proof generation * @param {ProofGenerationOptions} opts - options that will be used for proof generation * * @returns `Promise<ZeroKnowledgeProofResponse>` */ generateProof( proofReq: ZeroKnowledgeProofRequest, identifier: DID, opts?: ProofGenerationOptions ): Promise<ZeroKnowledgeProofResponse>; /** * @deprecated, use generateAuthInputs with CircuitId.AuthV2 instead * generates auth inputs * * @param {Uint8Array} hash - challenge that will be signed * @param {DID} did - identity that will generate a proof * @param {CircuitId} circuitId - circuit id for authentication * @returns `Promise<Uint8Array>` */ generateAuthV2Inputs(hash: Uint8Array, did: DID, circuitId: CircuitId): Promise<Uint8Array>; /** * generates Auth inputs * * @param {Uint8Array} hash - challenge that will be signed * @param {DID} did - identity that will generate a proof * @param {CircuitId} circuitId - circuit id for authentication * @returns `Promise<Uint8Array>` */ generateAuthInputs(hash: Uint8Array, did: DID, circuitId: CircuitId): Promise<Uint8Array>; /** * @deprecated, use generateAuthProof with CircuitId.AuthV2 instead * generates auth v2 proof from given identity * * @param {Uint8Array} hash - challenge that will be signed * @param {DID} did - identity that will generate a proof * @returns `Promise<ZKProof>` */ generateAuthV2Proof(hash: Uint8Array, did: DID): Promise<ZKProof>; /** * Generate auth proof from given identity with generic params * * @param {CircuitId} circuitId - circuitId for the proof generation * @param {DID} identifier - did that will generate proof * @param {ProofGenerationOptions} opts - options that will be used for proof generation * * @returns `Promise<ZeroKnowledgeProofResponse>` */ generateAuthProof( circuitId: CircuitId, identifier: DID, opts?: AuthProofGenerationOptions ): Promise<ZeroKnowledgeProofAuthResponse>; /** * state verification function * * @param {string} circuitId - id of authentication circuit * @param {Array<string>} pubSignals - public signals of authentication circuit * @returns `Promise<boolean>` */ verifyState(circuitId: string, pubSignals: Array<string>): Promise<boolean>; /** * transitState is done always to the latest state * * Generates a state transition proof and publishes state to the blockchain * * @param {DID} did - identity that will transit state * @param {TreeState} oldTreeState - previous tree state * @param {boolean} isOldStateGenesis - is a transition state is done from genesis state * @param {Signer} ethSigner - signer for transaction * @returns `{Promise<string>}` - transaction hash is returned */ transitState( did: DID, oldTreeState: TreeState, isOldStateGenesis: boolean, stateStorage: IStateStorage, ethSigner: Signer ): Promise<string>; findCredentialByProofQuery( did: DID, query: ProofQuery, opts?: { skipClaimRevocationCheck: boolean } ): Promise<{ cred: W3CCredential; revStatus: RevocationStatus | undefined }>; } /** * Proof service is an implementation of IProofService * that works with a native groth16 prover * * @public * @class ProofService * @implements implements IProofService interface */ export class ProofService implements IProofService { private readonly _prover: IZKProver; private readonly _ldOptions: Options; private readonly _inputsGenerator: InputGenerator; private readonly _pubSignalsVerifier: PubSignalsVerifier; private readonly _proofsCacheStorage?: IProofStorage; /** * Creates an instance of ProofService. * @param {IIdentityWallet} _identityWallet - identity wallet * @param {ICredentialWallet} _credentialWallet - credential wallet * @param {ICircuitStorage} _circuitStorage - circuit storage to load proving / verification files * @param {IStateStorage} _stateStorage - state storage to get GIST proof / publish state */ constructor( private readonly _identityWallet: IIdentityWallet, private readonly _credentialWallet: ICredentialWallet, _circuitStorage: ICircuitStorage, private readonly _stateStorage: IStateStorage, opts?: ProofServiceOptions ) { this._prover = opts?.prover ?? new NativeProver(_circuitStorage); this._ldOptions = { ...opts, documentLoader: opts?.documentLoader ?? cacheLoader(opts) }; this._inputsGenerator = new InputGenerator(_identityWallet, _credentialWallet, _stateStorage); this._pubSignalsVerifier = new PubSignalsVerifier( opts?.documentLoader ?? cacheLoader(opts), _stateStorage ); this._proofsCacheStorage = opts?.proofsCacheStorage; } /** {@inheritdoc IProofService.verifyProof} */ async verifyProof(zkp: ZKProof, circuitId: CircuitId): Promise<boolean> { return this._prover.verify(zkp, circuitId); } /** {@inheritdoc IProofService.verify} */ async verifyZKPResponse( proofResp: ZeroKnowledgeProofResponse, opts: ProofVerifyOpts ): Promise<VerificationResultMetadata> { const proofValid = await this._prover.verify(proofResp, proofResp.circuitId); if (!proofValid) { throw Error( `Proof with circuit id ${proofResp.circuitId} and request id ${proofResp.id} is not valid` ); } const verifyContext: VerifyContext = { pubSignals: proofResp.pub_signals, query: opts.query, verifiablePresentation: proofResp.vp, sender: opts.sender, challenge: BigInt(proofResp.id), opts: opts.opts, params: opts.params }; const pubSignals = await this._pubSignalsVerifier.verify(proofResp.circuitId, verifyContext); return { linkID: (pubSignals as unknown as { linkID?: bigint }).linkID }; } /** {@inheritdoc IProofService.generateProof} */ async generateProof( proofReq: ZeroKnowledgeProofRequest, identifier: DID, opts?: ProofGenerationOptions ): Promise<ZeroKnowledgeProofResponse> { if (!opts) { opts = { skipRevocation: false, challenge: 0n }; } let credentialWithRevStatus: { cred: W3CCredential | undefined; revStatus: RevocationStatus | undefined; } = { cred: opts.credential, revStatus: opts.credentialRevocationStatus }; if (!opts.credential) { credentialWithRevStatus = await this.findCredentialByProofQuery(identifier, proofReq.query); } if (opts.credential && !opts.credentialRevocationStatus && !opts.skipRevocation) { const revStatus = await this._credentialWallet.getRevocationStatusFromCredential( opts.credential ); credentialWithRevStatus = { cred: opts.credential, revStatus }; } if (!credentialWithRevStatus.cred) { throw new Error( VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_QUERY + ` ${JSON.stringify(proofReq.query)}` ); } if (this._proofsCacheStorage && !opts?.bypassCache) { const cachedProof = await this._proofsCacheStorage.getProof( identifier, credentialWithRevStatus.cred.id, proofReq ); if (cachedProof) { return cachedProof; } } const credentialCoreClaim = await this._identityWallet.getCoreClaimFromCredential( credentialWithRevStatus.cred ); const { nonce: authProfileNonce, genesisDID } = await this._identityWallet.getGenesisDIDMetadata(identifier); const preparedCredential: PreparedCredential = { credential: credentialWithRevStatus.cred, credentialCoreClaim, revStatus: credentialWithRevStatus.revStatus }; const subjectDID = DID.parse(preparedCredential.credential.credentialSubject['id'] as string); const { nonce: credentialSubjectProfileNonce, genesisDID: subjectGenesisDID } = await this._identityWallet.getGenesisDIDMetadata(subjectDID); if (subjectGenesisDID.string() !== genesisDID.string()) { throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_PROFILE_GENESIS_DID_MISMATCH); } const propertiesMetadata = parseCredentialSubject( proofReq.query.credentialSubject as JsonDocumentObject ); if (!propertiesMetadata.length) { throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_QUERIES_IN_ZKP_REQUEST); } const mtPosition = preparedCredential.credentialCoreClaim.getMerklizedPosition(); let mk: Merklizer | undefined; if (mtPosition !== MerklizedRootPosition.None) { mk = await preparedCredential.credential.merklize(this._ldOptions); } const context = proofReq.query['context'] as string; const groupId = proofReq.query['groupId'] as number; const ldContext = await this.loadLdContext(context); const credentialType = proofReq.query['type'] as string; const queriesMetadata: QueryMetadata[] = []; const circuitQueries: Query[] = []; for (const propertyMetadata of propertiesMetadata) { const queryMetadata = await parseQueryMetadata( propertyMetadata, byteDecoder.decode(ldContext), credentialType, this._ldOptions ); queriesMetadata.push(queryMetadata); const circuitQuery = await this.toCircuitsQuery( preparedCredential.credential, queryMetadata, mk ); circuitQueries.push(circuitQuery); } const inputs = await this.generateInputs( preparedCredential, genesisDID, proofReq, { ...opts, authProfileNonce, credentialSubjectProfileNonce, linkNonce: groupId ? opts.linkNonce : 0n }, circuitQueries ); const sdQueries = queriesMetadata.filter((q) => q.operator === Operators.SD); let vp: VerifiablePresentation | undefined; if (sdQueries.length) { vp = createVerifiablePresentation( context, credentialType, preparedCredential.credential, sdQueries ); } const { proof, pub_signals } = await this._prover.generate(inputs, proofReq.circuitId); const zkpRes = { id: proofReq.id, circuitId: proofReq.circuitId, vp, proof, pub_signals }; if (this._proofsCacheStorage) { await this._proofsCacheStorage.storeProof( identifier, credentialWithRevStatus.cred.id, proofReq, zkpRes ); } return zkpRes; } /** {@inheritdoc IProofService.generateAuthProof} */ async generateAuthProof( circuitId: CircuitId, identifier: DID, opts?: AuthProofGenerationOptions ): Promise<ZeroKnowledgeProofAuthResponse> { if ( circuitId !== CircuitId.AuthV2 && circuitId !== CircuitId.AuthV3 && circuitId !== CircuitId.AuthV3_8_32 ) { throw new Error('CircuitId is not supported'); } if (!opts) { opts = { challenge: 0n }; } const challenge = opts.challenge ? BytesHelper.intToBytes(opts.challenge).reverse() : new Uint8Array(32); const authInputs = await this.generateAuthInputs(challenge, identifier, circuitId); const zkProof = await this._prover.generate(authInputs, circuitId); return { circuitId: circuitId, proof: zkProof.proof, pub_signals: zkProof.pub_signals }; } /** {@inheritdoc IProofService.transitState} */ async transitState( did: DID, oldTreeState: TreeState, isOldStateGenesis: boolean, stateStorage: IStateStorage, // for compatibility with previous versions we leave this parameter ethSigner: Signer ): Promise<string> { return this._identityWallet.transitState( did, oldTreeState, isOldStateGenesis, ethSigner, this._prover ); } private async generateInputs( preparedCredential: PreparedCredential, identifier: DID, proofReq: ZeroKnowledgeProofRequest, params: ProofInputsParams, circuitQueries: Query[] ): Promise<Uint8Array> { return this._inputsGenerator.generateInputs({ preparedCredential, identifier, proofReq, params, circuitQueries }); } private async toCircuitsQuery( credential: W3CCredential, queryMetadata: QueryMetadata, merklizedCredential?: Merklizer ): Promise<Query> { if (queryMetadata.merklizedSchema && !merklizedCredential) { throw new Error('merklized root position is set to None for merklized schema'); } if (!queryMetadata.merklizedSchema && merklizedCredential) { throw new Error('merklized root position is not set to None for non-merklized schema'); } const query = new Query(); query.slotIndex = queryMetadata.slotIndex; query.operator = queryMetadata.operator; query.values = queryMetadata.values; if (queryMetadata.merklizedSchema && merklizedCredential) { const { proof, value: mtValue } = await merklizedCredential.proof(queryMetadata.path); query.valueProof = new ValueProof(); query.valueProof.mtp = proof; query.valueProof.path = queryMetadata.claimPathKey; const mtEntry = (await mtValue?.mtEntry()) ?? 0n; query.valueProof.value = mtEntry; if (!queryMetadata.fieldName) { query.values = [mtEntry]; return query; } } if (queryMetadata.operator === Operators.SD) { const [first, ...rest] = queryMetadata.fieldName.split('.'); let v = credential.credentialSubject[first]; for (const part of rest) { v = (v as JsonDocumentObject)[part]; } if (typeof v === 'undefined') { throw new Error(`credential doesn't contain value for field ${queryMetadata.fieldName}`); } query.values = await transformQueryValueToBigInts(v, queryMetadata.datatype); } return query; } private async loadLdContext(context: string): Promise<Uint8Array> { const loader = getDocumentLoader(this._ldOptions); let ldSchema: object; try { ldSchema = (await loader(context)).document; } catch (e) { throw new Error(`can't load ld context from url ${context}`); } return byteEncoder.encode(JSON.stringify(ldSchema)); } /** {@inheritdoc IProofService.generateAuthV2Inputs} */ async generateAuthV2Inputs( hash: Uint8Array, did: DID, circuitId: CircuitId ): Promise<Uint8Array> { if (circuitId !== CircuitId.AuthV2) { throw new Error('CircuitId is not supported'); } const { nonce: authProfileNonce, genesisDID } = await this._identityWallet.getGenesisDIDMetadata(did); const challenge = BytesHelper.bytesToInt(hash.reverse()); const authPrepared = await this._inputsGenerator.prepareAuthBJJCredential(genesisDID); const signature = await this._identityWallet.signChallenge(challenge, authPrepared.credential); const id = DID.idFromDID(genesisDID); const stateProof = await this._stateStorage.getGISTProof(id.bigInt()); const gistProof = toGISTProof(stateProof); const authInputs = new AuthV2Inputs(); authInputs.genesisID = id; authInputs.profileNonce = BigInt(authProfileNonce); authInputs.authClaim = authPrepared.coreClaim; authInputs.authClaimIncMtp = authPrepared.incProof.proof; authInputs.authClaimNonRevMtp = authPrepared.nonRevProof.proof; authInputs.treeState = authPrepared.incProof.treeState; authInputs.signature = signature; authInputs.challenge = challenge; authInputs.gistProof = gistProof; return authInputs.inputsMarshal(); } /** {@inheritdoc IProofService.generateAuthInputs} */ async generateAuthInputs(hash: Uint8Array, did: DID, circuitId: CircuitId): Promise<Uint8Array> { if ( circuitId !== CircuitId.AuthV2 && circuitId !== CircuitId.AuthV3 && circuitId !== CircuitId.AuthV3_8_32 ) { throw new Error('CircuitId is not supported'); } const { nonce: authProfileNonce, genesisDID } = await this._identityWallet.getGenesisDIDMetadata(did); const challenge = BytesHelper.bytesToInt(hash.reverse()); const authPrepared = await this._inputsGenerator.prepareAuthBJJCredential(genesisDID); const signature = await this._identityWallet.signChallenge(challenge, authPrepared.credential); const id = DID.idFromDID(genesisDID); const stateProof = await this._stateStorage.getGISTProof(id.bigInt()); const gistProof = toGISTProof(stateProof); const authInputs = new AuthV3Inputs(); // works for both v3 and v2 if (circuitId === CircuitId.AuthV3_8_32) { authInputs.mtLevel = 8; authInputs.mtLevelOnChain = 32; } authInputs.genesisID = id; authInputs.profileNonce = BigInt(authProfileNonce); authInputs.authClaim = authPrepared.coreClaim; authInputs.authClaimIncMtp = authPrepared.incProof.proof; authInputs.authClaimNonRevMtp = authPrepared.nonRevProof.proof; authInputs.treeState = authPrepared.incProof.treeState; authInputs.signature = signature; authInputs.challenge = challenge; authInputs.gistProof = gistProof; return authInputs.inputsMarshal(); } /** {@inheritdoc IProofService.generateAuthV2Proof} */ async generateAuthV2Proof(challenge: Uint8Array, did: DID): Promise<ZKProof> { const authInputs = await this.generateAuthInputs(challenge, did, CircuitId.AuthV2); const zkProof = await this._prover.generate(authInputs, CircuitId.AuthV2); return zkProof; } async verifyState( circuitId: string, pubSignals: string[], opts: StateVerificationOpts = { acceptedStateTransitionDelay: PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY } ): Promise<boolean> { if ( circuitId !== CircuitId.AuthV2 && circuitId !== CircuitId.AuthV3 && circuitId !== CircuitId.AuthV3_8_32 ) { throw new Error(`CircuitId is not supported ${circuitId}`); } let gistRoot, userId; if (circuitId === CircuitId.AuthV2) { const authV2PubSignals = new AuthV2PubSignals().pubSignalsUnmarshal( byteEncoder.encode(JSON.stringify(pubSignals)) ); gistRoot = authV2PubSignals.GISTRoot.bigInt(); userId = authV2PubSignals.userID.bigInt(); } else { const authV3PubSignals = new AuthV3PubSignals().pubSignalsUnmarshal( byteEncoder.encode(JSON.stringify(pubSignals)) ); gistRoot = authV3PubSignals.GISTRoot.bigInt(); userId = authV3PubSignals.userID.bigInt(); } const globalStateInfo = await this._stateStorage.getGISTRootInfo(gistRoot, userId); if (globalStateInfo.root !== gistRoot) { throw new Error(`gist info contains invalid state`); } if (globalStateInfo.replacedByRoot !== 0n) { if (globalStateInfo.replacedAtTimestamp === 0n) { throw new Error(`state was replaced, but replaced time unknown`); } const timeDiff = Date.now() - getDateFromUnixTimestamp(Number(globalStateInfo.replacedAtTimestamp)).getTime(); if ( timeDiff > (opts?.acceptedStateTransitionDelay ?? PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY) ) { throw new Error('global state is outdated'); } } return true; } async findCredentialByProofQuery( did: DID, query: ProofQuery ): Promise<{ cred: W3CCredential; revStatus: RevocationStatus | undefined }> { const credentials = await this._identityWallet.findOwnedCredentialsByDID(did, query); if (!credentials.length) { throw new Error( VerifiableConstants.ERRORS.PROOF_SERVICE_NO_CREDENTIAL_FOR_IDENTITY_OR_PROFILE ); } // For EQ / IN / NIN / LT / GT operations selective if credential satisfies query - we can get any. // TODO: choose credential for selective credentials const credential = query.skipClaimRevocationCheck ? { cred: credentials[0], revStatus: undefined } : await this._credentialWallet.findNonRevokedCredential(credentials); return credential; } }