@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
704 lines (620 loc) • 21.8 kB
text/typescript
import { DID, getDateFromUnixTimestamp, Id } from '@iden3/js-iden3-core';
import { DocumentLoader, getDocumentLoader, Path } from '@iden3/js-jsonld-merklization';
import { Hash } from '@iden3/js-merkletree';
import { IStateStorage, RootInfo } from '../../storage';
import { byteEncoder } from '../../utils';
import { calculateCoreSchemaHash, ProofQuery, 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,
ClaimOutputs,
VerifyOpts,
fieldValueFromVerifiablePresentation,
validateDisclosureV2Circuit,
validateEmptyCredentialSubjectV2Circuit,
validateOperators,
verifyFieldValueInclusionV2,
validateDisclosureNativeSDSupport,
validateEmptyCredentialSubjectNoopNativeSupport,
verifyFieldValueInclusionNativeExistsSupport,
checkCircuitOperator
} from './query';
import { parseQueriesMetadata, QueryMetadata } from '../common';
import { Operators } from '../../circuits';
import { calculateQueryHashV3 } from './query-hash';
import { JsonLd } from 'jsonld/jsonld-spec';
import {
PROTOCOL_CONSTANTS,
JSONObject,
VerifiablePresentation,
JsonDocumentObject
} from '../../iden3comm';
import { isRootDoesNotExistError } from '../../storage/blockchain/errors';
/**
* Verify Context - params for pub signal verification
* @type VerifyContext
*/
export type VerifyContext = {
pubSignals: string[];
query: ProofQuery;
verifiablePresentation?: VerifiablePresentation;
sender: string;
challenge: bigint;
opts?: VerifyOpts;
params?: JSONObject;
};
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(
private readonly _documentLoader: DocumentLoader,
private readonly _stateStorage: IStateStorage
) {}
/**
* verify public signals
*
* @param {string} circuitId circuit id
* @param {VerifyContext} ctx verification parameters
* @returns `Promise<BaseConfig>`
*/
async verify(circuitId: string, ctx: VerifyContext): Promise<BaseConfig> {
const fnName = `${circuitId.split('-')[0]}Verify`;
const fn = (this as unknown as { [k: string]: (ctx: VerifyContext) => Promise<BaseConfig> })[
fnName
];
if (!fn) {
throw new Error(`public signals verifier for ${circuitId} not found`);
}
return fn(ctx);
}
private credentialAtomicQueryMTPV2Verify = async ({
query,
verifiablePresentation,
sender,
challenge,
pubSignals,
opts
}: VerifyContext): Promise<BaseConfig> => {
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');
}
// verify query
const outs: ClaimOutputs = {
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, mtpv2PubSignals.userID, mtpv2PubSignals.requestID);
return mtpv2PubSignals;
};
private credentialAtomicQuerySigV2Verify = async ({
query,
verifiablePresentation,
sender,
challenge,
pubSignals,
opts
}: VerifyContext): Promise<BaseConfig> => {
let sigV2PubSignals = new AtomicQuerySigV2PubSignals();
sigV2PubSignals = sigV2PubSignals.pubSignalsUnmarshal(
byteEncoder.encode(JSON.stringify(pubSignals))
);
// verify query
const outs: ClaimOutputs = {
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, sigV2PubSignals.userID, sigV2PubSignals.requestID);
return sigV2PubSignals;
};
private credentialAtomicQueryV3Verify = async ({
query,
verifiablePresentation,
sender,
challenge,
pubSignals,
opts,
params
}: VerifyContext): Promise<BaseConfig> => {
let v3PubSignals = new AtomicQueryV3PubSignals();
v3PubSignals = v3PubSignals.pubSignalsUnmarshal(byteEncoder.encode(JSON.stringify(pubSignals)));
// verify query
const outs: ClaimOutputs = {
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: JsonLd;
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 as JsonDocumentObject,
{
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 as Error).message}`);
}
} else if (!queryMetadata.fieldName && queryMetadata.operator == Operators.NOOP) {
try {
await validateEmptyCredentialSubjectNoopNativeSupport(outs);
} catch (e: unknown) {
throw new Error(`failed to validate operators: ${(e as Error).message}`);
}
} else {
try {
await validateOperators(queryMetadata, outs);
} catch (e) {
throw new Error(`failed to validate operators: ${(e as Error).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:
// if proof type is not specified in query any proof type in signals is OK.
}
const nSessionId = BigInt((params?.nullifierSessionId as string) ?? 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 as DID);
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
);
}
// verify Id ownership
this.verifyIdOwnership(sender, challenge, v3PubSignals.userID, v3PubSignals.requestID);
return v3PubSignals;
};
private authV2Verify = async ({
sender,
challenge,
pubSignals,
opts
}: VerifyContext): Promise<BaseConfig> => {
let authV2PubSignals = new AuthV2PubSignals();
authV2PubSignals = authV2PubSignals.pubSignalsUnmarshal(
byteEncoder.encode(JSON.stringify(pubSignals))
);
// no query verification
// verify state
const gist = await this.checkGlobalState(authV2PubSignals.GISTRoot, authV2PubSignals.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, authV2PubSignals.userID, authV2PubSignals.challenge);
return new BaseConfig();
};
private linkedMultiQuery10Verify = async ({
query,
verifiablePresentation,
pubSignals
}: VerifyContext): Promise<BaseConfig> => {
let multiQueryPubSignals = new LinkedMultiQueryPubSignals();
multiQueryPubSignals = multiQueryPubSignals.pubSignalsUnmarshal(
byteEncoder.encode(JSON.stringify(pubSignals))
);
// verify query
let schema: JsonDocumentObject;
const ldOpts = { documentLoader: this._documentLoader };
try {
schema = (await ldOpts.documentLoader(query.context || '')).document as JsonDocumentObject;
} catch (e) {
throw new Error(`can't load schema for request query`);
}
const ldContextJSON = JSON.stringify(schema);
const credentialSubject = query.credentialSubject as JsonDocumentObject;
const schemaId: string = await Path.getTypeIDFromContext(
ldContextJSON,
query.type || '',
ldOpts
);
const schemaHash = calculateCoreSchemaHash(byteEncoder.encode(schemaId));
const queriesMetadata = await parseQueriesMetadata(
query.type || '',
ldContextJSON,
credentialSubject,
ldOpts
);
const request: { queryHash: bigint; queryMeta: QueryMetadata }[] = [];
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: { queryHash: bigint }, b: { queryHash: bigint }): number => {
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 as unknown as BaseConfig;
};
private verifyIdOwnership = (
sender: string,
challenge: bigint,
expectedUserId: Id,
expectedChallenge: bigint
): void => {
const senderId = DID.idFromDID(DID.parse(sender));
if (senderId.string() !== expectedUserId.string()) {
throw new Error(
`sender id is not used for proof creation, expected ${sender}, user from public signals: ${expectedUserId.string()}`
);
}
if (challenge !== expectedChallenge) {
throw new Error(
`challenge is not used for proof creation, expected ${challenge}, challenge from public signals: ${expectedChallenge} `
);
}
};
private async checkQueryV2Circuits(
circuitId: CircuitId.AtomicQueryMTPV2 | CircuitId.AtomicQuerySigV2,
query: ProofQuery,
outs: ClaimOutputs,
opts: VerifyOpts | undefined,
verifiablePresentation: VerifiablePresentation | undefined
) {
if (!query.type) {
throw new Error(`proof query type is undefined`);
}
const loader = this._documentLoader ?? getDocumentLoader();
// validate schema
let context: JsonLd;
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 as JsonDocumentObject,
{
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 as Error).message}`);
}
} else if (!queryMetadata.fieldName && queryMetadata.operator == Operators.NOOP) {
try {
await validateEmptyCredentialSubjectV2Circuit(queryMetadata, outs);
} catch (e: unknown) {
throw new Error(`failed to validate operators: ${(e as Error).message}`);
}
} else {
try {
await validateOperators(queryMetadata, outs);
} catch (e) {
throw new Error(`failed to validate operators: ${(e as Error).message}`);
}
}
// verify field inclusion
verifyFieldValueInclusionV2(outs, queryMetadata);
}
private async resolve(
id: Id,
state: bigint
): Promise<{
latest: boolean;
transitionTimestamp: number | string;
}> {
const idBigInt = id.bigInt();
const contractState = await this._stateStorage.getStateInfoByIdAndState(idBigInt, state);
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
};
}
private async rootResolve(
state: bigint,
id: bigint
): Promise<{
latest: boolean;
transitionTimestamp: number | string;
}> {
let globalStateInfo: RootInfo;
try {
globalStateInfo = await this._stateStorage.getGISTRootInfo(state, id);
} catch (e: unknown) {
if (isRootDoesNotExistError(e)) {
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
};
}
private checkStateExistenceForId = async (userId: Id, userState: Hash): Promise<void> => {
await this.resolve(userId, userState.bigInt());
};
private checkGlobalState = async (
state: Hash,
id: Id
): Promise<{
latest: boolean;
transitionTimestamp: number | string;
}> => {
return this.rootResolve(state.bigInt(), id.bigInt());
};
private checkRevocationStateForId = async (
issuerId: Id,
issuerClaimNonRevState: Hash
): Promise<{
latest: boolean;
transitionTimestamp: number | string;
}> => {
const issuerNonRevStateResolved = await this.resolve(issuerId, issuerClaimNonRevState.bigInt());
return issuerNonRevStateResolved;
};
private checkRevocationState = async (
issuerID: Id,
issuerClaimNonRevState: Hash,
opts: VerifyOpts | undefined
) => {
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');
}
}
};
}