@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
341 lines (304 loc) • 11.2 kB
text/typescript
import { DID, getDateFromUnixTimestamp, Id, SchemaHash } from '@iden3/js-iden3-core';
import { DocumentLoader, Merklizer, MtValue, Path } from '@iden3/js-jsonld-merklization';
import { Proof } from '@iden3/js-merkletree';
import { byteEncoder } from '../../utils';
import { getOperatorNameByValue, Operators, QueryOperators } from '../../circuits/comparer';
import { CircuitId } from '../../circuits/models';
import { calculateCoreSchemaHash, ProofQuery, VerifiableConstants } from '../../verifiable';
import { QueryMetadata } from '../common';
import { circuitValidator } from '../provers';
import { JsonLd } from 'jsonld/jsonld-spec';
import { VerifiablePresentation } from '../../iden3comm';
import { ethers } from 'ethers';
/**
* Options to verify state
*/
export type VerifyOpts = {
// acceptedStateTransitionDelay is the period of time in milliseconds that a revoked state remains valid.
acceptedStateTransitionDelay?: number;
// acceptedProofGenerationDelay is the period of time in milliseconds that a generated proof remains valid.
acceptedProofGenerationDelay?: number;
};
const defaultProofGenerationDelayOpts = 24 * 60 * 60 * 1000; // 24 hours
// ClaimOutputs fields that are used in proof generation
export interface ClaimOutputs {
issuerId: Id;
schemaHash: SchemaHash;
slotIndex?: number;
operator: number;
operatorOutput?: bigint;
value: bigint[];
timestamp: number;
merklized: number;
claimPathKey?: bigint;
claimPathNotExists?: number;
valueArraySize: number;
isRevocationChecked: number;
}
export async function checkQueryRequest(
query: ProofQuery,
queriesMetadata: QueryMetadata[],
ldContext: JsonLd,
outputs: ClaimOutputs,
circuitId: CircuitId,
schemaLoader?: DocumentLoader,
opts?: VerifyOpts
): Promise<void> {
// validate issuer
const userDID = DID.parseFromId(outputs.issuerId);
const issuerAllowed =
!query.allowedIssuers ||
query.allowedIssuers?.some((issuer) => issuer === '*' || issuer === userDID.string());
if (!issuerAllowed) {
throw new Error('issuer is not in allowed list');
}
if (!query.type) {
throw new Error('query type is missing');
}
const schemaId: string = await Path.getTypeIDFromContext(JSON.stringify(ldContext), query.type, {
documentLoader: schemaLoader
});
const schemaHash = calculateCoreSchemaHash(byteEncoder.encode(schemaId));
if (schemaHash.bigInt() !== outputs.schemaHash.bigInt()) {
throw new Error(`schema that was used is not equal to requested in query`);
}
if (!query.skipClaimRevocationCheck && outputs.isRevocationChecked === 0) {
throw new Error(`check revocation is required`);
}
checkCircuitQueriesLength(circuitId, queriesMetadata);
// verify timestamp
let acceptedProofGenerationDelay = defaultProofGenerationDelayOpts;
if (opts?.acceptedProofGenerationDelay) {
acceptedProofGenerationDelay = opts.acceptedProofGenerationDelay;
}
const timeDiff = Date.now() - getDateFromUnixTimestamp(Number(outputs.timestamp)).getTime();
if (timeDiff > acceptedProofGenerationDelay) {
throw new Error('generated proof is outdated');
}
return;
}
export function checkCircuitQueriesLength(circuitId: CircuitId, queriesMetadata: QueryMetadata[]) {
const circuitValidationData = circuitValidator[circuitId];
if (queriesMetadata.length > circuitValidationData.maxQueriesCount) {
throw new Error(
`circuit ${circuitId} supports only ${
circuitValidator[circuitId as CircuitId].maxQueriesCount
} queries`
);
}
}
export function checkCircuitOperator(circuitId: CircuitId, operator: number) {
const circuitValidationData = circuitValidator[circuitId];
if (!circuitValidationData.supportedOperations.includes(operator)) {
throw new Error(
`circuit ${circuitId} not support ${getOperatorNameByValue(operator)} operator`
);
}
}
export function verifyFieldValueInclusionV2(outputs: ClaimOutputs, metadata: QueryMetadata) {
if (outputs.operator == QueryOperators.$noop) {
return;
}
if (outputs.merklized === 1) {
if (outputs.claimPathNotExists === 1) {
throw new Error(`proof doesn't contains target query key`);
}
if (outputs.claimPathKey !== metadata.claimPathKey) {
throw new Error(`proof was generated for another path`);
}
} else {
if (outputs.slotIndex !== metadata.slotIndex) {
throw new Error(`wrong claim slot was used in claim`);
}
}
}
export function verifyFieldValueInclusionNativeExistsSupport(
outputs: ClaimOutputs,
metadata: QueryMetadata
) {
if (outputs.operator == Operators.NOOP) {
return;
}
if (outputs.operator === Operators.EXISTS && !outputs.merklized) {
throw new Error('$exists operator is not supported for non-merklized credential');
}
if (outputs.merklized === 1) {
if (outputs.claimPathKey !== metadata.claimPathKey) {
throw new Error(`proof was generated for another path`);
}
} else {
if (outputs.slotIndex !== metadata.slotIndex) {
throw new Error(`wrong claim slot was used in claim`);
}
}
}
export async function validateEmptyCredentialSubjectV2Circuit(
cq: QueryMetadata,
outputs: ClaimOutputs
) {
if (outputs.operator !== Operators.EQ) {
throw new Error('empty credentialSubject request available only for equal operation');
}
for (let index = 1; index < outputs.value.length; index++) {
if (outputs.value[index] !== 0n) {
throw new Error(`empty credentialSubject request not available for array of values`);
}
}
const path = Path.newPath([VerifiableConstants.CREDENTIAL_SUBJECT_PATH]);
const subjectEntry = await path.mtEntry();
if (outputs.claimPathKey !== subjectEntry) {
throw new Error(`proof doesn't contain credentialSubject in claimPathKey`);
}
return;
}
export async function validateOperators(cq: QueryMetadata, outputs: ClaimOutputs) {
if (outputs.operator !== cq.operator) {
throw new Error(`operator that was used is not equal to request`);
}
if (outputs.operator === Operators.NOOP) {
// for noop operator slot and value are not used in this case
return;
}
for (let index = 0; index < outputs.value.length; index++) {
if (outputs.value[index] !== cq.values[index]) {
if (outputs.value[index] === 0n && cq.values[index] === undefined) {
continue;
}
throw new Error(`comparison value that was used is not equal to requested in query`);
}
}
}
export async function validateDisclosureV2Circuit(
cq: QueryMetadata,
outputs: ClaimOutputs,
verifiablePresentation?: VerifiablePresentation,
ldLoader?: DocumentLoader
) {
const bi = await fieldValueFromVerifiablePresentation(
cq.fieldName,
verifiablePresentation,
ldLoader
);
if (bi !== outputs.value[0]) {
throw new Error(`value that was used is not equal to requested in query`);
}
if (outputs.operator !== Operators.EQ) {
throw new Error(`operator for selective disclosure must be $eq`);
}
for (let index = 1; index < outputs.value.length; index++) {
if (outputs.value[index] !== 0n) {
throw new Error(`selective disclosure not available for array of values`);
}
}
}
export async function validateDisclosureNativeSDSupport(
cq: QueryMetadata,
outputs: ClaimOutputs,
verifiablePresentation?: VerifiablePresentation,
ldLoader?: DocumentLoader
) {
const bi = await fieldValueFromVerifiablePresentation(
cq.fieldName,
verifiablePresentation,
ldLoader
);
if (bi !== outputs.operatorOutput) {
throw new Error(`operator output should be equal to disclosed value`);
}
if (outputs.operator !== Operators.SD) {
throw new Error(`operator for selective disclosure must be $sd`);
}
for (let index = 0; index < outputs.value.length; index++) {
if (outputs.value[index] !== 0n) {
throw new Error(`public signal values must be zero`);
}
}
}
export async function validateEmptyCredentialSubjectNoopNativeSupport(outputs: ClaimOutputs) {
if (outputs.operator !== Operators.NOOP) {
throw new Error('empty credentialSubject request available only for $noop operation');
}
for (let index = 1; index < outputs.value.length; index++) {
if (outputs.value[index] !== 0n) {
throw new Error(`empty credentialSubject request not available for array of values`);
}
}
}
export const fieldValueFromVerifiablePresentation = async (
fieldName: string,
verifiablePresentation?: VerifiablePresentation,
ldLoader?: DocumentLoader
): Promise<bigint> => {
if (!verifiablePresentation) {
throw new Error(`verifiablePresentation is required for selective disclosure request`);
}
let mz: Merklizer;
const strVerifiablePresentation: string = JSON.stringify(verifiablePresentation);
try {
mz = await Merklizer.merklizeJSONLD(strVerifiablePresentation, {
documentLoader: ldLoader
});
} catch (e) {
throw new Error(`can't merklize verifiablePresentation`);
}
let merklizedPath: Path;
try {
const p = `verifiableCredential.credentialSubject.${fieldName}`;
merklizedPath = await Path.fromDocument(null, strVerifiablePresentation, p, {
documentLoader: ldLoader
});
} catch (e) {
throw new Error(`can't build path to '${fieldName}' key`);
}
let proof: Proof;
let value: MtValue | undefined;
try {
({ proof, value } = await mz.proof(merklizedPath));
} catch (e) {
throw new Error(`can't get value by path '${fieldName}'`);
}
if (!value) {
throw new Error(`can't get merkle value for field '${fieldName}'`);
}
if (!proof.existence) {
throw new Error(
`path [${merklizedPath.parts}] doesn't exist in verifiablePresentation document`
);
}
return await value.mtEntry();
};
export function calculateGroupId(requestIds: bigint[]): bigint {
const types = Array(requestIds.length).fill('uint256');
const groupID =
BigInt(ethers.keccak256(ethers.solidityPacked(types, requestIds))) &
// It should fit in a field number in the circuit (max 253 bits). With this we truncate to 252 bits for the group ID
BigInt('0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF');
return groupID;
}
export function calculateRequestId(requestParams: string, creatorAddress: string): bigint {
// 0x0000000000000000FFFF...FF. Reserved first 8 bytes for the request Id type and future use
// 0x00010000000000000000...00. First 2 bytes for the request Id type
// - 0x0000... for old request Ids with uint64
// - 0x0001... for new request Ids with uint256
const requestId =
(BigInt(
ethers.keccak256(ethers.solidityPacked(['bytes', 'address'], [requestParams, creatorAddress]))
) &
BigInt('0x0000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF')) +
BigInt('0x0001000000000000000000000000000000000000000000000000000000000000');
return requestId;
}
export function calculateMultiRequestId(
requestIds: bigint[],
groupIds: bigint[],
creatorAddress: string
): bigint {
return BigInt(
ethers.keccak256(
ethers.solidityPacked(
['uint256[]', 'uint256[]', 'address'],
[requestIds, groupIds, creatorAddress]
)
)
);
}