diff --git a/package-lock.json b/package-lock.json index a4a62e5e9..c9ff9cc43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3320,9 +3320,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz", - "integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==", + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4976,9 +4976,9 @@ "peer": true }, "node_modules/loupe": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", - "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", "dev": true, "license": "MIT" }, @@ -6859,15 +6859,15 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz", - "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz", + "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" diff --git a/src/iden3comm/types/protocol/auth.ts b/src/iden3comm/types/protocol/auth.ts index b9ecf9e7b..3f6b972a3 100644 --- a/src/iden3comm/types/protocol/auth.ts +++ b/src/iden3comm/types/protocol/auth.ts @@ -54,16 +54,25 @@ export type ZeroKnowledgeProofRequest = { }; /** ZeroKnowledgeProofQuery represents structure of zkp request query object */ -export type ZeroKnowledgeProofQuery = { +export type ZeroKnowledgeProofQuery = W3CV1ProofQueryFields & { allowedIssuers: string[]; context: string; credentialSubject?: JsonDocumentObject; + credentialSubjectFullDisclosure?: boolean; proofType?: ProofType; skipClaimRevocationCheck?: boolean; groupId?: number; type: string; }; +/** W3CV1ProofQueryFields represents fields for W3C v1 ZKP proof query */ +export type W3CV1ProofQueryFields = { + expirationDate?: JsonDocumentObject; + issuanceDate?: JsonDocumentObject; + credentialStatus?: JsonDocumentObject; + credentialStatusFullDisclosure?: boolean; +}; + export type ZeroKnowledgeInvokeResponse = { responses: ZeroKnowledgeProofResponse[]; crossChainProof?: CrossChainProof; @@ -88,6 +97,11 @@ export type VerifiablePresentation = { '@context': string | string[]; type: string | string[]; credentialSubject: JsonDocumentObject; + credentialStatus?: { + id?: string; + type?: string; + revocationNonce?: number; + }; }; }; diff --git a/src/proof/common.ts b/src/proof/common.ts index cee4c43f1..a7f58b8c5 100644 --- a/src/proof/common.ts +++ b/src/proof/common.ts @@ -7,20 +7,23 @@ import { Operators, QueryOperators } from '../circuits'; -import { StateProof } from '../storage/entities/state'; import { MerkleTreeProofWithTreeState, RevocationStatus, W3CCredential, buildFieldPath, getSerializationAttrFromContext, - getFieldSlotIndex + getFieldSlotIndex, + VerifiableConstants, + ProofQuery, + CredentialStatusType } from '../verifiable'; import { Merklizer, Options, Path } from '@iden3/js-jsonld-merklization'; import { byteEncoder } from '../utils'; -import { JsonDocumentObject } from '../iden3comm'; +import { JsonDocumentObject, VerifiablePresentation, ZeroKnowledgeProofQuery } from '../iden3comm'; import { Claim } from '@iden3/js-iden3-core'; import { poseidon } from '@iden3/js-crypto'; +import { StateProof } from '../storage'; export type PreparedCredential = { credential: W3CCredential; @@ -101,8 +104,11 @@ export type PropertyQuery = { fieldName: string; operator: Operators; operatorValue?: unknown; + kind?: PropertyQueryKind; }; +export type PropertyQueryKind = 'credentialSubject' | 'w3cV1'; + export type QueryMetadata = PropertyQuery & { slotIndex: number; values: bigint[]; @@ -112,14 +118,144 @@ export type QueryMetadata = PropertyQuery & { merklizedSchema: boolean; }; -export const parseCredentialSubject = (credentialSubject?: JsonDocumentObject): PropertyQuery[] => { - // credentialSubject is empty - if (!credentialSubject) { - return [{ operator: QueryOperators.$noop, fieldName: '' }]; +export const parseZKPQuery = (query: ZeroKnowledgeProofQuery): PropertyQuery[] => { + const propertiesMetadata = parseCredentialSubject(query.credentialSubject as JsonDocumentObject); + if (query.credentialSubjectFullDisclosure) { + propertiesMetadata.push({ + operator: QueryOperators.$sd, + fieldName: 'credentialSubject', + kind: 'w3cV1' + }); + } + if (query.expirationDate) { + const expirationDate = parseJsonDocumentObject( + { expirationDate: query.expirationDate }, + 'w3cV1' + ); + propertiesMetadata.push(...expirationDate); + } + if (query.issuanceDate) { + const issuanceDate = parseJsonDocumentObject({ issuanceDate: query.issuanceDate }, 'w3cV1'); + propertiesMetadata.push(...issuanceDate); + } + if (query.credentialStatus) { + const flattenedObject = flattenNestedObject( + query.credentialStatus as Record, + 'credentialStatus' + ); + propertiesMetadata.push(...parseJsonDocumentObject(flattenedObject, 'w3cV1')); + if (query.credentialStatusFullDisclosure) { + propertiesMetadata.push({ + operator: QueryOperators.$sd, + fieldName: 'credentialStatus', + kind: 'w3cV1' + }); + } + } + return propertiesMetadata; +}; + +const flattenNestedObject = ( + input: Record, + parentKey: string +): Record => { + const result: Record = {}; + + for (const [key, value] of Object.entries(input)) { + if (value !== undefined) { + result[`${parentKey}.${key}`] = value; + } + } + return result; +}; + +const parseCredentialStatus = ( + document?: JsonDocumentObject, + vp?: VerifiablePresentation +): PropertyQuery[] => { + const fieldName = 'credentialStatus'; + const kind = 'w3cV1'; + if (!document) { + return [{ operator: QueryOperators.$noop, fieldName: '', kind }]; + } + // if document is empty, full disclosure is needed + if (Object.entries(document).length === 0) { + if (!vp) { + throw new Error(`VerifiablePresentation is required for full disclosure of credentialStatus`); + } + const queries: PropertyQuery[] = []; + queries.push({ operator: QueryOperators.$sd, fieldName, kind }); + const flattened = flattenToQueryShape( + (vp.verifiableCredential as Record)[fieldName], + fieldName + ); + queries.push(...parseJsonDocumentObject(flattened, kind)); + return queries; + } + const flattenedObject = flattenNestedObject( + document as Record, + fieldName + ); + return parseJsonDocumentObject(flattenedObject, kind); +}; + +const parseCredentialSubjectWithVP = ( + document?: JsonDocumentObject, + vp?: VerifiablePresentation +): PropertyQuery[] => { + const fieldName = 'credentialSubject'; + const kind = 'credentialSubject'; + if (!document) { + return [{ operator: QueryOperators.$noop, fieldName: '', kind }]; + } + // if document is empty, full disclosure is needed + if (Object.entries(document).length === 0) { + if (!vp) { + throw new Error( + `VerifiablePresentation is required for full disclosure of credentialSubject` + ); + } + const queries: PropertyQuery[] = []; + queries.push({ operator: QueryOperators.$sd, fieldName, kind: 'w3cV1' }); + const flattened = flattenToQueryShape( + (vp.verifiableCredential as Record)[fieldName] + ); + queries.push(...parseJsonDocumentObject(flattened, kind)); + return queries; + } + return parseJsonDocumentObject(document, kind); +}; + +export const flattenToQueryShape = ( + obj: Record, + parentKey = '' +): JsonDocumentObject => { + const result: JsonDocumentObject = {}; + for (const [key, value] of Object.entries(obj)) { + if (key === 'id' || key === 'type') { + continue; + } + const fullKey = parentKey ? `${parentKey}.${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + Object.assign(result, flattenToQueryShape(value, fullKey)); + } else { + result[fullKey] = {}; + } + } + return result; +}; + +export const parseJsonDocumentObject = ( + document?: JsonDocumentObject, + kind?: PropertyQueryKind +): PropertyQuery[] => { + // document is empty + if (!document) { + return [{ operator: QueryOperators.$noop, fieldName: '', kind }]; } const queries: PropertyQuery[] = []; - const entries = Object.entries(credentialSubject); + const entries = Object.entries(document); if (!entries.length) { throw new Error(`query must have at least 1 predicate`); } @@ -130,7 +266,7 @@ export const parseCredentialSubject = (credentialSubject?: JsonDocumentObject): const isSelectiveDisclosure = fieldReqEntries.length === 0; if (isSelectiveDisclosure) { - queries.push({ operator: QueryOperators.$sd, fieldName: fieldName }); + queries.push({ operator: QueryOperators.$sd, fieldName, kind }); continue; } @@ -139,18 +275,32 @@ export const parseCredentialSubject = (credentialSubject?: JsonDocumentObject): throw new Error(`operator is not supported by lib`); } const operator = QueryOperators[operatorName as keyof typeof QueryOperators]; - queries.push({ operator, fieldName, operatorValue }); + queries.push({ operator, fieldName, operatorValue, kind }); } } return queries; }; +export const parseCredentialSubject = (credentialSubject?: JsonDocumentObject): PropertyQuery[] => { + return parseJsonDocumentObject(credentialSubject, 'credentialSubject'); +}; + export const parseQueryMetadata = async ( propertyQuery: PropertyQuery, ldContextJSON: string, credentialType: string, options: Options ): Promise => { + const replacedFieldName = propertyQuery.fieldName; + if (propertyQuery?.kind === 'w3cV1') { + if (Object.values(CredentialStatusType).includes(credentialType as CredentialStatusType)) { + propertyQuery.fieldName = propertyQuery.fieldName.replace('credentialStatus.', ''); + ldContextJSON = VerifiableConstants.JSONLD_SCHEMA.IDEN3_PROOFS_DEFINITION_DOCUMENT; + } else { + ldContextJSON = VerifiableConstants.JSONLD_SCHEMA.W3C_VC_DOCUMENT_2018; + credentialType = VerifiableConstants.CREDENTIAL_TYPE.W3C_VERIFIABLE_CREDENTIAL; + } + } const query: QueryMetadata = { ...propertyQuery, slotIndex: 0, @@ -197,6 +347,7 @@ export const parseQueryMetadata = async ( ldContextJSON, credentialType, propertyQuery.fieldName, + propertyQuery.kind, options ); query.claimPathKey = await path.mtEntry(); @@ -234,9 +385,46 @@ export const parseQueryMetadata = async ( } query.values = values; } + query.fieldName = replacedFieldName; return query; }; +export const parseProofQueryMetadata = async ( + credentialType: string, + ldContextJSON: string, + query: ProofQuery, + options: Options, + vp?: VerifiablePresentation +): Promise => { + const propertyQuery = parseCredentialSubjectWithVP(query.credentialSubject, vp); + if (query.expirationDate) { + propertyQuery.push( + ...parseJsonDocumentObject({ expirationDate: query.expirationDate }, 'w3cV1') + ); + } + if (query.issuanceDate) { + propertyQuery.push(...parseJsonDocumentObject({ issuanceDate: query.issuanceDate }, 'w3cV1')); + } + + if (query.credentialStatus) { + const credSubject = parseCredentialStatus(query.credentialStatus, vp); + propertyQuery.push(...credSubject); + } + + return Promise.all( + propertyQuery.map((p) => { + let credType = credentialType; + if (p?.kind === 'w3cV1' && p.fieldName.startsWith('credentialStatus.')) { + if (!vp?.verifiableCredential?.credentialStatus?.type) { + throw new Error('credentialStatus.type is required for w3cV1 queries'); + } + credType = vp?.verifiableCredential?.credentialStatus?.type; + } + return parseQueryMetadata(p, ldContextJSON, credType, options); + }) + ); +}; + export const parseQueriesMetadata = async ( credentialType: string, ldContextJSON: string, diff --git a/src/proof/proof-service.ts b/src/proof/proof-service.ts index f7f169ce3..7f628a89e 100644 --- a/src/proof/proof-service.ts +++ b/src/proof/proof-service.ts @@ -25,8 +25,9 @@ import { import { PreparedCredential, QueryMetadata, - parseCredentialSubject, + flattenToQueryShape, parseQueryMetadata, + parseZKPQuery, toGISTProof, transformQueryValueToBigInts } from './common'; @@ -316,9 +317,9 @@ export class ProofService implements IProofService { throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_PROFILE_GENESIS_DID_MISMATCH); } - const propertiesMetadata = parseCredentialSubject( - proofReq.query.credentialSubject as JsonDocumentObject - ); + proofReq = this.preprocessZeroKnowledgeProofRequest(proofReq, preparedCredential.credential); + + const propertiesMetadata = parseZKPQuery(proofReq.query); if (!propertiesMetadata.length) { throw new Error(VerifiableConstants.ERRORS.PROOF_SERVICE_NO_QUERIES_IN_ZKP_REQUEST); } @@ -335,18 +336,24 @@ export class ProofService implements IProofService { const ldContext = await this.loadLdContext(context); - const credentialType = proofReq.query['type'] as string; const queriesMetadata: QueryMetadata[] = []; const circuitQueries: Query[] = []; for (const propertyMetadata of propertiesMetadata) { + let credentialType = proofReq.query['type'] as string; + // todo: check if we can move this to the parseQueryMetadata function + if ( + propertyMetadata?.kind === 'w3cV1' && + propertyMetadata.fieldName.startsWith('credentialStatus.') + ) { + credentialType = preparedCredential.credential.credentialStatus.type; + } const queryMetadata = await parseQueryMetadata( propertyMetadata, byteDecoder.decode(ldContext), credentialType, this._ldOptions ); - queriesMetadata.push(queryMetadata); const circuitQuery = await this.toCircuitsQuery( preparedCredential.credential, @@ -369,16 +376,14 @@ export class ProofService implements IProofService { circuitQueries ); + const credentialType = proofReq.query['type']; const sdQueries = queriesMetadata.filter((q) => q.operator === Operators.SD); - let vp: VerifiablePresentation | undefined; - if (sdQueries.length) { - vp = createVerifiablePresentation( - context, - credentialType, - preparedCredential.credential, - sdQueries - ); - } + const vp = createVerifiablePresentation( + context, + credentialType, + preparedCredential.credential, + sdQueries + ); const { proof, pub_signals } = await this._prover.generate(inputs, proofReq.circuitId); @@ -489,7 +494,18 @@ export class ProofService implements IProofService { if (queryMetadata.operator === Operators.SD) { const [first, ...rest] = queryMetadata.fieldName.split('.'); - let v = credential.credentialSubject[first]; + let v; + if (queryMetadata?.kind === 'w3cV1') { + v = credential[first as keyof W3CCredential]; + if ( + queryMetadata.fieldName === 'credentialStatus' || + queryMetadata.fieldName === 'credentialSubject' + ) { + v = (v as JsonDocumentObject).id; + } + } else { + v = credential.credentialSubject[first]; + } for (const part of rest) { v = (v as JsonDocumentObject)[part]; } @@ -513,6 +529,23 @@ export class ProofService implements IProofService { return byteEncoder.encode(JSON.stringify(ldSchema)); } + // for full object SD + private preprocessZeroKnowledgeProofRequest( + request: ZeroKnowledgeProofRequest, + cred: W3CCredential + ): ZeroKnowledgeProofRequest { + const { credentialStatus, credentialSubject } = request.query; + if (credentialSubject && Object.keys(credentialSubject).length === 0) { + request.query.credentialSubjectFullDisclosure = true; + request.query.credentialSubject = flattenToQueryShape(cred.credentialSubject); + } + if (credentialStatus && Object.keys(credentialStatus).length === 0) { + request.query.credentialStatusFullDisclosure = true; + request.query.credentialStatus = flattenToQueryShape(cred.credentialStatus); + } + return request; + } + /** {@inheritdoc IProofService.generateAuthV2Inputs} */ async generateAuthV2Inputs( hash: Uint8Array, diff --git a/src/proof/verifiers/pub-signals-verifier.ts b/src/proof/verifiers/pub-signals-verifier.ts index 8c356f2af..cd879f74e 100644 --- a/src/proof/verifiers/pub-signals-verifier.ts +++ b/src/proof/verifiers/pub-signals-verifier.ts @@ -3,7 +3,12 @@ import { DocumentLoader, getDocumentLoader, Path } from '@iden3/js-jsonld-merkli import { Hash } from '@iden3/js-merkletree'; import { IStateStorage, RootInfo, StateInfo } from '../../storage'; import { byteEncoder, isGenesisState } from '../../utils'; -import { calculateCoreSchemaHash, ProofQuery, ProofType } from '../../verifiable'; +import { + calculateCoreSchemaHash, + CredentialStatusType, + 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'; @@ -28,7 +33,7 @@ import { verifyFieldValueInclusionNativeExistsSupport, checkCircuitOperator } from './query'; -import { parseQueriesMetadata, QueryMetadata } from '../common'; +import { parseProofQueryMetadata, parseQueriesMetadata, QueryMetadata } from '../common'; import { Operators } from '../../circuits'; import { calculateQueryHashV3 } from './query-hash'; import { JsonLd } from 'jsonld/jsonld-spec'; @@ -437,7 +442,6 @@ export class PubSignalsVerifier { 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 || '', @@ -445,11 +449,12 @@ export class PubSignalsVerifier { ); const schemaHash = calculateCoreSchemaHash(byteEncoder.encode(schemaId)); - const queriesMetadata = await parseQueriesMetadata( + const queriesMetadata = await parseProofQueryMetadata( query.type || '', ldContextJSON, - credentialSubject, - ldOpts + query, + ldOpts, + verifiablePresentation ); const request: { queryHash: bigint; queryMeta: QueryMetadata }[] = []; @@ -496,6 +501,7 @@ export class PubSignalsVerifier { if (request[i].queryMeta?.operator === Operators.SD) { const disclosedValue = await fieldValueFromVerifiablePresentation( request[i].queryMeta.fieldName, + request[i].queryMeta.kind, verifiablePresentation, this._documentLoader ); diff --git a/src/proof/verifiers/query.ts b/src/proof/verifiers/query.ts index 4cce344da..40d523513 100644 --- a/src/proof/verifiers/query.ts +++ b/src/proof/verifiers/query.ts @@ -5,7 +5,7 @@ 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 { PropertyQueryKind, QueryMetadata } from '../common'; import { circuitValidator } from '../provers'; import { JsonLd } from 'jsonld/jsonld-spec'; import { VerifiablePresentation } from '../../iden3comm'; @@ -196,6 +196,7 @@ export async function validateDisclosureV2Circuit( ) { const bi = await fieldValueFromVerifiablePresentation( cq.fieldName, + cq.kind, verifiablePresentation, ldLoader ); @@ -222,6 +223,7 @@ export async function validateDisclosureNativeSDSupport( ) { const bi = await fieldValueFromVerifiablePresentation( cq.fieldName, + cq.kind, verifiablePresentation, ldLoader ); @@ -252,6 +254,7 @@ export async function validateEmptyCredentialSubjectNoopNativeSupport(outputs: C export const fieldValueFromVerifiablePresentation = async ( fieldName: string, + kind: PropertyQueryKind = 'credentialSubject', verifiablePresentation?: VerifiablePresentation, ldLoader?: DocumentLoader ): Promise => { @@ -271,7 +274,12 @@ export const fieldValueFromVerifiablePresentation = async ( let merklizedPath: Path; try { - const p = `verifiableCredential.credentialSubject.${fieldName}`; + let p; + if (kind === 'w3cV1') { + p = `verifiableCredential.${fieldName}`; + } else { + p = `verifiableCredential.credentialSubject.${fieldName}`; + } merklizedPath = await Path.fromDocument(null, strVerifiablePresentation, p, { documentLoader: ldLoader }); diff --git a/src/storage/filters/jsonQuery.ts b/src/storage/filters/jsonQuery.ts index f258dafe2..9edcd8b91 100644 --- a/src/storage/filters/jsonQuery.ts +++ b/src/storage/filters/jsonQuery.ts @@ -319,6 +319,43 @@ export const StandardJSONCredentialsQueryFilter = (query: ProofQuery): FilterQue return acc.concat(reqFilters); } + case 'credentialStatus': { + const reqFilters = Object.keys(queryValue).reduce((acc: FilterQuery[], fieldKey) => { + const fieldParams = queryValue[fieldKey]; + if (typeof fieldParams === 'object' && Object.keys(fieldParams).length === 0) { + return acc.concat([ + new FilterQuery(`credentialStatus.${fieldKey}`, comparatorOptions.$noop, null) + ]); + } + const res = Object.keys(fieldParams).map((comparator) => { + const value = fieldParams[comparator]; + const path = `credentialStatus.${fieldKey}`; + return new FilterQuery( + path, + comparatorOptions[comparator as keyof typeof comparatorOptions], + value + ); + }); + return acc.concat(res); + }, []); + + return acc.concat(reqFilters); + } + case 'expirationDate': + case 'issuanceDate': { + if (Object.keys(queryValue).length === 0) { + return acc.concat([new FilterQuery(queryKey, comparatorOptions.$noop, null)]); + } + const res = Object.keys(queryValue).map((comparator) => { + const value = queryValue[comparator]; + return new FilterQuery( + queryKey, + comparatorOptions[comparator as keyof typeof comparatorOptions], + value + ); + }); + return acc.concat(res); + } case 'proofType': case 'groupId': case 'skipClaimRevocationCheck': { diff --git a/src/verifiable/constants.ts b/src/verifiable/constants.ts index ec3134880..895033028 100644 --- a/src/verifiable/constants.ts +++ b/src/verifiable/constants.ts @@ -53,6 +53,7 @@ export const VerifiableConstants = Object.freeze({ W3C_VERIFIABLE_PRESENTATION: 'VerifiablePresentation' }, CREDENTIAL_SUBJECT_PATH: 'https://www.w3.org/2018/credentials#credentialSubject', + CREDENTIAL_STATUS_PATH: 'https://www.w3.org/2018/credentials#credentialStatus', JSONLD_SCHEMA: { // JSONLDSchemaIden3Credential is a schema for context with Iden3Credential type IDEN3_CREDENTIAL: 'https://schema.iden3.io/core/jsonld/iden3proofs.jsonld', diff --git a/src/verifiable/presentation.ts b/src/verifiable/presentation.ts index 34f1017ba..670e20c6b 100644 --- a/src/verifiable/presentation.ts +++ b/src/verifiable/presentation.ts @@ -1,7 +1,7 @@ -import { VerifiableConstants } from './constants'; +import { CredentialStatusType, VerifiableConstants } from './constants'; import { Options, Path } from '@iden3/js-jsonld-merklization'; import { W3CCredential } from './credential'; -import { QueryMetadata } from '../proof'; +import { PropertyQueryKind, QueryMetadata } from '../proof'; import { VerifiablePresentation, JsonDocumentObject } from '../iden3comm'; export const stringByPath = (obj: { [key: string]: unknown }, path: string): string => { @@ -25,6 +25,7 @@ export const buildFieldPath = async ( ldSchema: string, contextType: string, field: string, + kind: PropertyQueryKind = 'credentialSubject', opts?: Options ): Promise => { let path = new Path(); @@ -32,13 +33,29 @@ export const buildFieldPath = async ( if (field) { path = await Path.getContextPathKey(ldSchema, contextType, field, opts); } - path.prepend([VerifiableConstants.CREDENTIAL_SUBJECT_PATH]); + if (kind === 'credentialSubject') { + path.prepend([VerifiableConstants.CREDENTIAL_SUBJECT_PATH]); + } + + if (Object.values(CredentialStatusType).includes(contextType as CredentialStatusType)) { + path.prepend([VerifiableConstants.CREDENTIAL_STATUS_PATH]); + } return path; }; -export const findValue = (fieldName: string, credential: W3CCredential): JsonDocumentObject => { +export const findValue = ( + fieldName: string, + credential: W3CCredential, + kind: PropertyQueryKind = 'credentialSubject' +): JsonDocumentObject => { const [first, ...rest] = fieldName.split('.'); - let v = credential.credentialSubject[first]; + let v; + if (kind === 'credentialSubject') { + v = credential.credentialSubject[first]; + } else { + v = credential[first as keyof W3CCredential]; + } + for (const part of rest) { v = (v as JsonDocumentObject)[part]; } @@ -51,9 +68,6 @@ export const createVerifiablePresentation = ( credential: W3CCredential, queries: QueryMetadata[] ): VerifiablePresentation => { - const baseContext = [VerifiableConstants.JSONLD_SCHEMA.W3C_CREDENTIAL_2018]; - const ldContext = baseContext[0] === context ? baseContext : [...baseContext, context]; - const vc = VerifiableConstants.CREDENTIAL_TYPE.W3C_VERIFIABLE_CREDENTIAL; const vcTypes = [vc]; if (tp !== vc) { @@ -61,10 +75,10 @@ export const createVerifiablePresentation = ( } const skeleton = { - '@context': baseContext, + '@context': [VerifiableConstants.JSONLD_SCHEMA.W3C_CREDENTIAL_2018], type: VerifiableConstants.CREDENTIAL_TYPE.W3C_VERIFIABLE_PRESENTATION, verifiableCredential: { - '@context': ldContext, + '@context': credential['@context'], type: vcTypes, credentialSubject: { type: tp @@ -73,6 +87,9 @@ export const createVerifiablePresentation = ( }; let result: JsonDocumentObject = {}; + let w3cResult: JsonDocumentObject = { + credentialStatus: {} + }; for (const query of queries) { const parts = query.fieldName.split('.'); const current: JsonDocumentObject = parts.reduceRight( @@ -82,16 +99,31 @@ export const createVerifiablePresentation = ( } return { [part]: acc }; }, - findValue(query.fieldName, credential) as JsonDocumentObject + findValue(query.fieldName, credential, query.kind) as JsonDocumentObject ); - result = { ...result, ...current }; + if (!query.kind || query.kind === 'credentialSubject') { + result = { ...result, ...current }; + } else { + w3cResult = { ...w3cResult, ...current }; + } } + w3cResult.credentialStatus = { + ...(w3cResult.credentialStatus as object), + id: credential.credentialStatus.id, + type: credential.credentialStatus.type + }; + skeleton.verifiableCredential.credentialSubject = { ...skeleton.verifiableCredential.credentialSubject, ...result }; + skeleton.verifiableCredential = { + ...skeleton.verifiableCredential, + ...w3cResult + }; + return skeleton; }; diff --git a/src/verifiable/proof.ts b/src/verifiable/proof.ts index 4aa194f59..d81bab027 100644 --- a/src/verifiable/proof.ts +++ b/src/verifiable/proof.ts @@ -230,6 +230,9 @@ export interface ProofQuery { proofType?: string; groupId?: number; params?: JSONObject; + expirationDate?: JsonDocumentObject; + issuanceDate?: JsonDocumentObject; + credentialStatus?: JsonDocumentObject; } /** diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index f9dd44c82..7033feb9d 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -50,7 +50,13 @@ import { RootInfo } from '../../src'; import { ProvingMethodAlg, Token } from '@iden3/js-jwz'; -import { Blockchain, DID, DidMethod, NetworkId } from '@iden3/js-iden3-core'; +import { + Blockchain, + DID, + DidMethod, + NetworkId, + getDateFromUnixTimestamp +} from '@iden3/js-iden3-core'; import { describe, expect, it, beforeEach } from 'vitest'; import { ethers } from 'ethers'; import * as uuid from 'uuid'; @@ -2667,4 +2673,349 @@ describe.sequential('auth', () => { 'no packer with profile which meets `accept` header requirements' ); }); + + it('credential status + credential subject full SD', async () => { + const profileDID = await idWallet.createProfile(userDID, 777, issuerDID.string()); + const basicPersonCred: CredentialRequest = { + credentialSchema: 'ipfs://QmTojMfyzxehCJVw7aUrdWuxdF68R7oLYooGHCUr9wwsef', + type: 'BasicPerson', + credentialSubject: { + id: profileDID.string(), + fullName: 'John Doe', + firstName: 'John', + familyName: 'Doe', + dateOfBirth: 838531598, + governmentIdentifier: 'RRRRR', + governmentIdentifierType: 'passport', + placeOfBirth: { + countryCode: 'UA-ua' + } + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL, + nonce: 2837597946 + } + }; + const employeeCred = await idWallet.issueCredential(issuerDID, basicPersonCred, merklizeOpts); + + await credWallet.saveAll([employeeCred]); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'BasicPerson', + context: 'ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL', + credentialStatus: {}, + credentialSubject: {} + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + const tokenStr = authRes.token; + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + + const vpCredentialSubject = + authRes.authResponse.body.scope[0].vp?.verifiableCredential.credentialSubject; + expect(vpCredentialSubject).to.toMatchObject(basicPersonCred.credentialSubject); + + const vpCredentialStatus = + authRes.authResponse.body.scope[0].vp?.verifiableCredential.credentialStatus; + expect(vpCredentialStatus?.revocationNonce).to.equal(basicPersonCred.revocationOpts.nonce); + expect(vpCredentialStatus?.type).to.equal(basicPersonCred.revocationOpts.type); + + await authHandler.handleAuthorizationResponse( + authRes.authResponse, + authReq, + TEST_VERIFICATION_OPTS + ); + }); + + it('credential status + credential subject full SD (more than 10 queries)', async () => { + const profileDID = await idWallet.createProfile(userDID, 777, issuerDID.string()); + const basicPersonCred: CredentialRequest = { + credentialSchema: 'ipfs://QmTojMfyzxehCJVw7aUrdWuxdF68R7oLYooGHCUr9wwsef', + type: 'BasicPerson', + credentialSubject: { + id: profileDID.string(), + fullName: 'John Doe', + firstName: 'John', + familyName: 'Doe', + dateOfBirth: 838531598, + governmentIdentifier: 'RRRRR', + governmentIdentifierType: 'passport', + placeOfBirth: { + countryCode: 'UA-ua', + region: 'Kyiv' + } + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL, + nonce: 2837597946 + } + }; + const employeeCred = await idWallet.issueCredential(issuerDID, basicPersonCred, merklizeOpts); + + await credWallet.saveAll([employeeCred]); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'BasicPerson', + context: 'ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL', + credentialStatus: {}, + credentialSubject: {} + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + await expect(authHandler.handleAuthorizationRequest(userDID, msgBytes)).rejects.toThrow( + 'circuit linkedMultiQuery10-beta.1 supports only 10 queries' + ); + }); + + it('w3c field request: revocation nonce $eq, expirationDate $eq, issuanceDate SD', async () => { + const profileDID = await idWallet.createProfile(userDID, 777, issuerDID.string()); + + const basicPersonCredRequest: CredentialRequest = { + credentialSchema: 'ipfs://QmTojMfyzxehCJVw7aUrdWuxdF68R7oLYooGHCUr9wwsef', + type: 'BasicPerson', + credentialSubject: { + id: profileDID.string(), + fullName: 'John Doe', + firstName: 'John', + familyName: 'Doe', + dateOfBirth: 838531598, + governmentIdentifier: 'RRRRR', + governmentIdentifierType: 'passport', + placeOfBirth: { + countryCode: 'UA-ua' + } + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL, + nonce: 2837597946 + } + }; + const employeeCred = await idWallet.issueCredential( + issuerDID, + basicPersonCredRequest, + merklizeOpts + ); + + await credWallet.saveAll([employeeCred]); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'BasicPerson', + context: 'ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL', + credentialStatus: { + revocationNonce: { + $eq: 2837597946 + } + }, + expirationDate: { + $eq: getDateFromUnixTimestamp(2793526400).toISOString() + }, + issuanceDate: {}, + credentialSubject: { + 'placeOfBirth.countryCode': {} + } + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + const tokenStr = authRes.token; + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + + await authHandler.handleAuthorizationResponse( + authRes.authResponse, + authReq, + TEST_VERIFICATION_OPTS + ); + }); + + it('w3c field request: revocation nonce SD, expirationDate SD, issuanceDate SD', async () => { + const profileDID = await idWallet.createProfile(userDID, 777, issuerDID.string()); + + const basicPersonCredRequest: CredentialRequest = { + credentialSchema: 'ipfs://QmTojMfyzxehCJVw7aUrdWuxdF68R7oLYooGHCUr9wwsef', + type: 'BasicPerson', + credentialSubject: { + id: profileDID.string(), + fullName: 'John Doe', + firstName: 'John', + familyName: 'Doe', + dateOfBirth: 838531598, + governmentIdentifier: 'RRRRR', + governmentIdentifierType: 'passport', + placeOfBirth: { + countryCode: 'UA-ua' + } + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL, + nonce: 2837597946 + } + }; + const employeeCred = await idWallet.issueCredential( + issuerDID, + basicPersonCredRequest, + merklizeOpts + ); + + await credWallet.saveAll([employeeCred]); + + const proofReqs: ZeroKnowledgeProofRequest[] = [ + { + id: 1, + circuitId: CircuitId.LinkedMultiQuery10, + optional: false, + query: { + groupId: 1, + proofType: ProofType.BJJSignature, + allowedIssuers: ['*'], + type: 'BasicPerson', + context: 'ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL', + credentialStatus: { + revocationNonce: {} + }, + expirationDate: {}, + issuanceDate: {}, + credentialSubject: { + 'placeOfBirth.countryCode': {} + } + } + } + ]; + + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + scope: proofReqs + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); + const tokenStr = authRes.token; + expect(tokenStr).to.be.a('string'); + const token = await Token.parse(tokenStr); + expect(token).to.be.a('object'); + + const vpCredentialStatus = + authRes.authResponse.body.scope[0].vp?.verifiableCredential.credentialStatus; + expect(vpCredentialStatus?.revocationNonce).to.equal( + basicPersonCredRequest.revocationOpts.nonce + ); + expect(authRes.authResponse.body.scope[0].vp?.verifiableCredential['expirationDate']).to.equal( + getDateFromUnixTimestamp(basicPersonCredRequest.expiration as number).toISOString() + ); + expect(authRes.authResponse.body.scope[0].vp?.verifiableCredential['issuanceDate']).to.be.a( + 'string' + ); + expect(vpCredentialStatus?.type).to.equal(basicPersonCredRequest.revocationOpts.type); + + await authHandler.handleAuthorizationResponse( + authRes.authResponse, + authReq, + TEST_VERIFICATION_OPTS + ); + }); }); diff --git a/tests/mocks/schema.ts b/tests/mocks/schema.ts index 36d72ae52..2ab0e0b2d 100644 --- a/tests/mocks/schema.ts +++ b/tests/mocks/schema.ts @@ -43,6 +43,10 @@ const testSchemas = [ url: 'ipfs://QmcvoKLc742CyVH2Cnw6X95b4c8VdABqNPvTyAHEeaK1aP', doc: '{"@context":[{"@protected":true,"@version":1.1,"id":"@id","type":"@type","types123":{"@context":{"@propagate":true,"@protected":true,"polygon-vocab":"urn:uuid:f2606686-53bf-499e-86f1-fe5abbc11763#","xsd":"http://www.w3.org/2001/XMLSchema#","string":{"@id":"polygon-vocab:string","@type":"xsd:string"},"int":{"@id":"polygon-vocab:int","@type":"xsd:integer"},"double":{"@id":"polygon-vocab:double","@type":"xsd:double"},"bol":{"@id":"polygon-vocab:bol","@type":"xsd:boolean"},"dt":{"@id":"polygon-vocab:dt","@type":"xsd:dateTime"}},"@id":"urn:uuid:c22db6e7-6234-4dcc-8488-02a21ccb88ed"}}]}' }, + { + url: 'ipfs://QmTojMfyzxehCJVw7aUrdWuxdF68R7oLYooGHCUr9wwsef', + doc: `{"$metadata":{"type":"BasicPerson","uris":{"jsonLdContext":"ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL"},"version":"1.43"},"$schema":"https://json-schema.org/draft/2020-12/schema","description":"A schema that defines basic fields for identifying a person, can be used in combination with other schemas for KYC purposes. This schema is part of the Privado ID Common Schemas Initiative. The schema aligns with the Decentralized Identity Foundation (DIF) Credential Schema standard for Basic Person, although not all fields are supported due to protocol limitations. Basic Person Schema spec at DIF: https://identity.foundation/credential-schemas/#basic-person-schema","title":"Basic Person Schema","properties":{"@context":{"type":["string","array","object"]},"expirationDate":{"format":"date-time","type":"string"},"id":{"type":"string"},"issuanceDate":{"format":"date-time","type":"string"},"issuer":{"type":["string","object"],"format":"uri","properties":{"id":{"format":"uri","type":"string"}},"required":["id"]},"type":{"type":["string","array"],"items":{"type":"string"}},"credentialSubject":{"description":"Stores the data of the credential","title":"Credential subject","properties":{"id":{"description":"Stores the DID of the subject that owns the credential","title":"Credential subject ID","format":"uri","type":"string"},"fullName":{"description":"End-User's full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User's locale and preferences.","title":"Full Name","minLength":1,"type":"string"},"firstName":{"description":"Current first name(s) or given names of the credential subject","title":"First Name","minLength":1,"type":"string"},"familyName":{"description":"Current family name(s) of the credential subject","title":"Family Name","type":"string"},"middleName":{"description":"Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used.","title":"Middle Name","type":"string"},"alsoKnownAs":{"description":"Stage name, religious name or any other type of alias/pseudonym with which a person is known in a specific context besides its legal name. This must be part of the applicable legislation and thus the trust framework (e.g., be an attribute on the identity card).","title":"Also Known As","type":"string"},"dateOfBirth":{"description":"Date of birth of the credential subject.","title":"Date Of Birth","type":"integer"},"governmentIdentifier":{"description":"The unique government or national identifier of the credential subject (constructed by the sending Member State in accordance with the technical specifications for the purposes of cross-border identification and which is as persistent as possible in time).","title":"Government Identifier","type":"string"},"governmentIdentifierType":{"description":"Type of government or national identifier, allowed values: passport, national id document, tax id, drivers license, social service number (ssn, social issurance number, or health service id), other.","enum":["passport","national id document","tax id","drivers license","social service number","other"],"title":"Government identifier type","type":"string"},"gender":{"description":"Gender that the credential subject identifies as. Some reference values (non-exhaustive list): male, female, transgender male, transgender female, non-binary….","title":"Gender","type":"string"},"email":{"description":"End-User's preferred e-mail address. Its value MUST conform to the RFC 5322 [RFC5322] addr-spec syntax.","title":"Email","format":"idn-email","type":"string"},"sex":{"description":"Biological sex of the individual at birth. Allowed values: male, female.","title":"sex","type":"string"},"phoneNumber":{"description":"Primary contact number of the user, it should include the country code. The phone number must be formatted according to ITU-T recommendation [E.164], e.g., 1999550123 or 50688785073","title":"Phone Number","type":"number"},"phoneNumberVerified":{"description":"True if the End-User's phone number has been verified; otherwise false. When this Claim Value is true, this means that the OP took affirmative steps to ensure that this phone number was controlled by the End-User at the time the verification was performed. The means by which a phone number is verified is context-specific, and dependent upon the trust framework or contractual agreements within which the parties are operating.","title":"Phone Number Verified (Boolean)","type":"boolean"},"title":{"description":"Title of the credential subject e.g., Dr. or Sir","title":"title","type":"string"},"salutation":{"description":"Salutation of the credential subject e.g., Mr. or Miss.","title":"salutation","type":"string"},"documentExpirationDate":{"description":"Expiration date of the identity document used.","title":"Document Expiration Date","type":"integer"},"nameAndFamilyNameAtBirth":{"description":"Defines the first and the family name(s) of the credential subject at the time of their birth. Structured as a json object.","title":"Name and Family Name at birth","properties":{"firstName":{"description":"First name(s) or given names of the credential subject at birth.","title":"First Name","type":"string"},"familyName":{"description":"Family name(s) of the credential subject at birth.","title":"Family Name","type":"string"}},"required":[],"type":"object"},"placeOfBirth":{"description":"Defines the place where the credential subject is born. The value of this is a JSON object.","title":"Place of Birth","properties":{"locality":{"description":"Locality: String representing city or locality component.","title":"Locality","type":"string"},"region":{"description":"region: String representing state, province, prefecture, or region component. This field might be required in some jurisdictions.","title":"Region","type":"string"},"countryCode":{"description":"Country: String representing country in ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Country Code","minLength":3,"type":"string"},"countryCodeNumber":{"description":"ISO 3166-1 numeric three-digit country code, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Country Code Number","type":"integer"}},"required":[],"type":"object"},"addresses":{"description":"Various addresses associated with the credential subject.","title":"Addresses List","properties":{"primaryAddress":{"description":"Primary address of the credential subject.","title":"Primary Address","properties":{"addressLine1":{"description":"Address Line 1, usually the street address or major indication for the address.","title":"Address Line 1","type":"string"},"addressLine2":{"description":"Address Line 2, with apartment or suite number or additional indications.","title":"Address Line 2","type":"string"},"locality":{"description":"locality: String representing city or locality component.","title":"Locality","type":"string"},"region":{"description":"region: String representing state, province, prefecture, or region component.","title":"Region","type":"string"},"countryCode":{"description":"country: String representing country in ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Country Code","minLength":3,"type":"string"},"postalCode":{"description":"Postal code (also known as postcode, post code, PIN or ZIP Code).","title":"Postal Code","type":"string"},"countryCodeNumber":{"description":"ISO 3166-1 numeric three-digit country code, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Country Code Number","type":"integer"},"unstructuredAddress":{"description":"Optional one line address field since not all addresses may be structured in the person's KYC source information.","title":"Unstructured Address","type":"string"}},"required":[],"type":"object"},"homeAddress":{"description":"Home address of the credential subject.","title":"Home Address","properties":{"addressLine1":{"description":"Address Line 1, usually the street address or major indication for the address.","title":"addressLine1","type":"string"},"addressLine2":{"description":"Address Line 2, with apartment or suite number or additional indications.","title":"addressLine2","type":"string"},"locality":{"description":"locality: String representing city or locality component.","title":"Locality","type":"string"},"region":{"description":"region: String representing state, province, prefecture, or region component.","title":"Region","type":"string"},"countryCode":{"description":"country: String representing country in ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Country Code","minLength":3,"type":"string"},"postalCode":{"description":"Postal code (also known as postcode, post code, PIN or ZIP Code).","title":"Postal Code","type":"string"},"countryCodeNumber":{"description":"ISO 3166-1 numeric three-digit country code, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Country Code Number","type":"integer"},"unstructuredAddress":{"description":"Optional one line address field since not all addresses may be structured in the person's KYC source information.","title":"Unstructured Address","type":"string"}},"required":[],"type":"object"},"businessAddress":{"description":"Business address of the credential subject.","title":"Business Address","properties":{"addressLine1":{"description":"Address Line 1, usually the street address or major indication for the address","title":"Address Line 1","type":"string"},"addressLine2":{"description":"Address Line 2, with apartment or suite number or additional indications","title":"Address Line 2","type":"string"},"locality":{"description":"locality: String representing city or locality component.","title":"Locality","type":"string"},"region":{"description":"region: String representing state, province, prefecture, or region component.","title":"Region","type":"string"},"countryCode":{"description":"country: String representing country in ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Country Code","minLength":3,"type":"string"},"postalCode":{"description":"Postal code (also known as postcode, post code, PIN or ZIP Code).","title":"Postal Code","type":"string"},"countryCodeNumber":{"description":"ISO 3166-1 numeric three-digit country code, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Country Code Number","type":"integer"},"unstructuredAddress":{"description":"Optional one line address field since not all addresses may be structured in the person's KYC source information.","title":"Unstructured Address","type":"string"}},"required":[],"type":"object"},"mailingAddress":{"description":"Address through which credential subject can be sent correspondence via postal mail.","title":"Mailing Address","properties":{"addressLine1":{"description":"Address Line 1, usually the street address or major indication for the address.","title":"Address Line 1","type":"string"},"addressLine2":{"description":"Address Line 2, with apartment or suite number or additional indications.","title":"Address Line 2","type":"string"},"locality":{"description":"locality: String representing city or locality component.","title":"Locality","type":"string"},"region":{"description":"region: String representing state, province, prefecture, or region component.","title":"Region","type":"string"},"countryCode":{"description":"country: String representing country in ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Country Code","minLength":3,"type":"string"},"postalCode":{"description":"Postal code (also known as postcode, post code, PIN or ZIP Code).","title":"Postal Code","type":"string"},"countryCodeNumber":{"description":"ISO 3166-1 numeric three-digit country code, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Country Code Number","type":"integer"},"unstructuredAddress":{"description":"Optional one line address field since not all addresses may be structured in the person's KYC source information.","title":"Unstructured Address","type":"string"}},"required":[],"type":"object"}},"required":[],"type":"object"},"nationalities":{"description":"Credential subject’s nationalities.","title":"Nationalities","properties":{"nationality1CountryCode":{"description":"Primary nationality of the credential subject using ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Nationality 1 Country Code","minLength":3,"type":"string"},"nationality2CountryCode":{"description":"Additional nationality of the credential subject using using ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Nationality 2 Country Code","minLength":3,"type":"string"},"nationality3CountryCode":{"description":"Additional nationality of the credential subject using using ISO 3166-1 alpha-3 codes (e.g. FRA, USA, CRC).","title":"Nationality 3 Country Code","minLength":3,"type":"string"},"nationality1CountryCodeNumber":{"description":"Primary nationality of the credential subject using ISO 3166-1 numeric three-digit country codes, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Nationality 1 Country Code Number","type":"integer"},"nationality2CountryCodeNumber":{"description":"Additional nationality of the credential subject using ISO 3166-1 numeric three-digit country codes, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Nationality 2 Country Code Number","type":"integer"},"nationality3CountryCodeNumber":{"description":"Additional nationality of the credential subject using ISO 3166-1 numeric three-digit country codes, eg: 250 for France, 840 for United States, 188 for Costa Rica.","title":"Nationality 3 Country Code Number","type":"integer"}},"required":[],"type":"object"},"customFields":{"description":"Optional custom fields that can be added by the credential issuer when required. They would be included inside this json object.","title":"Custom Fields","properties":{"string1":{"description":"optional field for elements not included in the main schema","title":"Custom String Field 1","type":"string"},"string2":{"description":"optional field for elements not included in the main schema","title":"Custom String Field 2","type":"string"},"string3":{"description":"optional field for elements not included in the main schema","title":"Custom String Field 3","type":"string"},"number1":{"description":"optional field for elements not included in the main schema","title":"Custom Number Field 1","type":"number"},"number2":{"description":"optional field for elements not included in the main schema","title":"Custom Number Field 2","type":"number"},"number3":{"description":"optional field for elements not included in the main schema","title":"Custom Number Field 3","type":"number"},"boolean1":{"description":"optional field for elements not included in the main schema","title":"Custom Boolean Field 1","type":"boolean"},"boolean2":{"description":"optional field for elements not included in the main schema","title":"Custom Boolean Field 2","type":"boolean"},"boolean3":{"description":"optional field for elements not included in the main schema","title":"Custom Boolean Field 3","type":"boolean"}},"required":[],"type":"object"}},"required":["fullName","firstName","dateOfBirth","governmentIdentifier","governmentIdentifierType"],"type":"object"},"credentialSchema":{"properties":{"id":{"format":"uri","type":"string"},"type":{"type":"string"}},"required":["id","type"],"type":"object"}},"required":["@context","id","issuanceDate","issuer","type","credentialSubject","credentialSchema"],"type":"object"}` + }, // https { url: 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v4.jsonld', @@ -107,6 +111,10 @@ const testSchemas = [ { url: 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v4.json', doc: '{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","$metadata":{"uris":{"jsonLdContext":"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-v4.jsonld","jsonSchema":"https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/KYCAgeCredential-v4.json"}},"required":["@context","id","type","issuanceDate","credentialSubject","credentialSchema","credentialStatus","issuer"],"properties":{"@context":{"type":["string","array","object"]},"id":{"type":"string"},"type":{"type":["string","array"],"items":{"type":"string"}},"issuer":{"type":["string","object"],"format":"uri","required":["id"],"properties":{"id":{"type":"string","format":"uri"}}},"issuanceDate":{"type":"string","format":"date-time"},"expirationDate":{"type":"string","format":"date-time"},"credentialSchema":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string","format":"uri"},"type":{"type":"string"}}},"subjectPosition":{"type":"string","enum":["none","index","value"]},"merklizationRootPosition":{"type":"string","enum":["none","index","value"]},"revNonce":{"type":"integer"},"version":{"type":"integer"},"updatable":{"type":"boolean"},"credentialSubject":{"type":"object","required":["id","birthday","documentType"],"properties":{"id":{"title":"Credential Subject ID","type":"string","format":"uri"},"birthday":{"type":"integer"},"documentType":{"type":"integer"}}}}}' + }, + { + url: 'ipfs://QmZbsTnRwtCmbdg3r9o7Txid37LmvPcvmzVi1Abvqu1WKL', + doc: `{"@context":[{"@protected":true,"@version":1.1,"id":"@id","type":"@type","BasicPerson":{"@context":{"@propagate":true,"@protected":true,"polygon-vocab":"urn:uuid:bd42bba2-1caa-48b9-ab85-9509d33016bc#","xsd":"http://www.w3.org/2001/XMLSchema#","fullName":{"@id":"polygon-vocab:fullName","@type":"xsd:string"},"firstName":{"@id":"polygon-vocab:firstName","@type":"xsd:string"},"familyName":{"@id":"polygon-vocab:familyName","@type":"xsd:string"},"middleName":{"@id":"polygon-vocab:middleName","@type":"xsd:string"},"alsoKnownAs":{"@id":"polygon-vocab:alsoKnownAs","@type":"xsd:string"},"dateOfBirth":{"@id":"polygon-vocab:dateOfBirth","@type":"xsd:integer"},"governmentIdentifier":{"@id":"polygon-vocab:governmentIdentifier","@type":"xsd:string"},"governmentIdentifierType":{"@id":"polygon-vocab:governmentIdentifierType","@type":"xsd:string"},"gender":{"@id":"polygon-vocab:gender","@type":"xsd:string"},"email":{"@id":"polygon-vocab:email","@type":"xsd:string"},"sex":{"@id":"polygon-vocab:sex","@type":"xsd:string"},"phoneNumber":{"@id":"polygon-vocab:phoneNumber","@type":"xsd:double"},"phoneNumberVerified":{"@id":"polygon-vocab:phoneNumberVerified","@type":"xsd:boolean"},"title":{"@id":"polygon-vocab:title","@type":"xsd:string"},"salutation":{"@id":"polygon-vocab:salutation","@type":"xsd:string"},"documentExpirationDate":{"@id":"polygon-vocab:documentExpirationDate","@type":"xsd:integer"},"nameAndFamilyNameAtBirth":{"@context":{"firstName":{"@id":"polygon-vocab:firstName","@type":"xsd:string"},"familyName":{"@id":"polygon-vocab:familyName","@type":"xsd:string"}},"@id":"polygon-vocab:nameAndFamilyNameAtBirth"},"placeOfBirth":{"@context":{"locality":{"@id":"polygon-vocab:locality","@type":"xsd:string"},"region":{"@id":"polygon-vocab:region","@type":"xsd:string"},"countryCode":{"@id":"polygon-vocab:countryCode","@type":"xsd:string"},"countryCodeNumber":{"@id":"polygon-vocab:countryCodeNumber","@type":"xsd:integer"}},"@id":"polygon-vocab:placeOfBirth"},"addresses":{"@context":{"primaryAddress":{"@context":{"addressLine1":{"@id":"polygon-vocab:addressLine1","@type":"xsd:string"},"addressLine2":{"@id":"polygon-vocab:addressLine2","@type":"xsd:string"},"locality":{"@id":"polygon-vocab:locality","@type":"xsd:string"},"region":{"@id":"polygon-vocab:region","@type":"xsd:string"},"countryCode":{"@id":"polygon-vocab:countryCode","@type":"xsd:string"},"postalCode":{"@id":"polygon-vocab:postalCode","@type":"xsd:string"},"countryCodeNumber":{"@id":"polygon-vocab:countryCodeNumber","@type":"xsd:integer"},"unstructuredAddress":{"@id":"polygon-vocab:unstructuredAddress","@type":"xsd:string"}},"@id":"polygon-vocab:primaryAddress"},"homeAddress":{"@context":{"addressLine1":{"@id":"polygon-vocab:addressLine1","@type":"xsd:string"},"addressLine2":{"@id":"polygon-vocab:addressLine2","@type":"xsd:string"},"locality":{"@id":"polygon-vocab:locality","@type":"xsd:string"},"region":{"@id":"polygon-vocab:region","@type":"xsd:string"},"countryCode":{"@id":"polygon-vocab:countryCode","@type":"xsd:string"},"postalCode":{"@id":"polygon-vocab:postalCode","@type":"xsd:string"},"countryCodeNumber":{"@id":"polygon-vocab:countryCodeNumber","@type":"xsd:integer"},"unstructuredAddress":{"@id":"polygon-vocab:unstructuredAddress","@type":"xsd:string"}},"@id":"polygon-vocab:homeAddress"},"businessAddress":{"@context":{"addressLine1":{"@id":"polygon-vocab:addressLine1","@type":"xsd:string"},"addressLine2":{"@id":"polygon-vocab:addressLine2","@type":"xsd:string"},"locality":{"@id":"polygon-vocab:locality","@type":"xsd:string"},"region":{"@id":"polygon-vocab:region","@type":"xsd:string"},"countryCode":{"@id":"polygon-vocab:countryCode","@type":"xsd:string"},"postalCode":{"@id":"polygon-vocab:postalCode","@type":"xsd:string"},"countryCodeNumber":{"@id":"polygon-vocab:countryCodeNumber","@type":"xsd:integer"},"unstructuredAddress":{"@id":"polygon-vocab:unstructuredAddress","@type":"xsd:string"}},"@id":"polygon-vocab:businessAddress"},"mailingAddress":{"@context":{"addressLine1":{"@id":"polygon-vocab:addressLine1","@type":"xsd:string"},"addressLine2":{"@id":"polygon-vocab:addressLine2","@type":"xsd:string"},"locality":{"@id":"polygon-vocab:locality","@type":"xsd:string"},"region":{"@id":"polygon-vocab:region","@type":"xsd:string"},"countryCode":{"@id":"polygon-vocab:countryCode","@type":"xsd:string"},"postalCode":{"@id":"polygon-vocab:postalCode","@type":"xsd:string"},"countryCodeNumber":{"@id":"polygon-vocab:countryCodeNumber","@type":"xsd:integer"},"unstructuredAddress":{"@id":"polygon-vocab:unstructuredAddress","@type":"xsd:string"}},"@id":"polygon-vocab:mailingAddress"}},"@id":"polygon-vocab:addresses"},"nationalities":{"@context":{"nationality1CountryCode":{"@id":"polygon-vocab:nationality1CountryCode","@type":"xsd:string"},"nationality2CountryCode":{"@id":"polygon-vocab:nationality2CountryCode","@type":"xsd:string"},"nationality3CountryCode":{"@id":"polygon-vocab:nationality3CountryCode","@type":"xsd:string"},"nationality1CountryCodeNumber":{"@id":"polygon-vocab:nationality1CountryCodeNumber","@type":"xsd:integer"},"nationality2CountryCodeNumber":{"@id":"polygon-vocab:nationality2CountryCodeNumber","@type":"xsd:integer"},"nationality3CountryCodeNumber":{"@id":"polygon-vocab:nationality3CountryCodeNumber","@type":"xsd:integer"}},"@id":"polygon-vocab:nationalities"},"customFields":{"@context":{"string1":{"@id":"polygon-vocab:string1","@type":"xsd:string"},"string2":{"@id":"polygon-vocab:string2","@type":"xsd:string"},"string3":{"@id":"polygon-vocab:string3","@type":"xsd:string"},"number1":{"@id":"polygon-vocab:number1","@type":"xsd:double"},"number2":{"@id":"polygon-vocab:number2","@type":"xsd:double"},"number3":{"@id":"polygon-vocab:number3","@type":"xsd:double"},"boolean1":{"@id":"polygon-vocab:boolean1","@type":"xsd:boolean"},"boolean2":{"@id":"polygon-vocab:boolean2","@type":"xsd:boolean"},"boolean3":{"@id":"polygon-vocab:boolean3","@type":"xsd:boolean"}},"@id":"polygon-vocab:customFields"}},"@id":"urn:uuid:0a9897de-0ec5-42df-9e19-dbbe410b2924"}}]}` } ]; diff --git a/tests/proofs/mtp-onchain.test.ts b/tests/proofs/mtp-onchain.test.ts index de1c71844..33afd2481 100644 --- a/tests/proofs/mtp-onchain.test.ts +++ b/tests/proofs/mtp-onchain.test.ts @@ -237,7 +237,7 @@ describe.sequential('mtp onchain proofs', () => { challenge: BigInt(2), skipRevocation: false }); - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof({ proof, pub_signals }, circuitId); expect(isValid).to.be.true; diff --git a/tests/proofs/mtp.test.ts b/tests/proofs/mtp.test.ts index aac1accde..3349e8944 100644 --- a/tests/proofs/mtp.test.ts +++ b/tests/proofs/mtp.test.ts @@ -217,7 +217,7 @@ describe.sequential('mtp proofs', () => { expect(creds.length).to.not.equal(0); const { proof, pub_signals, vp } = await proofService.generateProof(proofReq, userDID); - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof({ proof, pub_signals }, circuitId); expect(isValid).to.be.true; @@ -311,7 +311,7 @@ describe.sequential('mtp proofs', () => { credential: credsForMyUserDID[0], skipRevocation: false }); - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof({ proof, pub_signals }, circuitId); expect(isValid).to.be.true; diff --git a/tests/proofs/sig-onchain.test.ts b/tests/proofs/sig-onchain.test.ts index 73bbb0be5..5bbc9d4eb 100644 --- a/tests/proofs/sig-onchain.test.ts +++ b/tests/proofs/sig-onchain.test.ts @@ -195,7 +195,7 @@ describe.sequential('sig onchain proofs', () => { skipRevocation: false }); - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; if (circuitId === CircuitId.AtomicQueryV3OnChain) { expect(pub_signals[2]).to.be.equal( diff --git a/tests/proofs/sig.test.ts b/tests/proofs/sig.test.ts index 94a1e1506..8407ec890 100644 --- a/tests/proofs/sig.test.ts +++ b/tests/proofs/sig.test.ts @@ -189,7 +189,7 @@ describe.sequential('sig proofs', () => { const { proof, vp } = await proofService.generateProof(proofReq, userDID); expect(proof).not.to.be.undefined; - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; }; it('sigv3-non-merklized', async () => { @@ -252,7 +252,7 @@ describe.sequential('sig proofs', () => { skipRevocation: false }); - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; expect(proof).not.to.be.undefined; }; @@ -304,7 +304,7 @@ describe.sequential('sig proofs', () => { skipRevocation: true }); expect(proof).not.to.be.undefined; - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; }); it('sigv2-ipfs-string-eq', async () => { @@ -361,7 +361,7 @@ describe.sequential('sig proofs', () => { userDID ); expect(proof).not.to.be.undefined; - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof( { @@ -493,12 +493,17 @@ describe.sequential('sig proofs', () => { verifiableCredential: { '@context': [ 'https://www.w3.org/2018/credentials/v1', + 'https://schema.iden3.io/core/jsonld/iden3proofs.jsonld', 'ipfs://QmQXQ5gBNfJuc9QXy5pGbaVfLxzFjCDAvPs4Fa43BaU1U4' ], type: ['VerifiableCredential', 'DeliveryAddress'], credentialSubject: { type: 'DeliveryAddress', postalProviderInformation: { name: 'postal provider' } + }, + credentialStatus: { + id: 'https://rhs-staging.polygonid.me/node?state=ed17a07e8b78ab979507829fa4d37e663ca5906714d506dec8a174d949c5eb09', + type: 'Iden3ReverseSparseMerkleTreeProof' } } }); @@ -522,12 +527,17 @@ describe.sequential('sig proofs', () => { verifiableCredential: { '@context': [ 'https://www.w3.org/2018/credentials/v1', + 'https://schema.iden3.io/core/jsonld/iden3proofs.jsonld', 'ipfs://QmZreEq1z5tMAuNBNTXjfpYMQbQ8KL7YkkVBt5nG1bUqJT' ], type: ['VerifiableCredential', 'DeliverAddressMultiTest'], credentialSubject: { type: 'DeliverAddressMultiTest', postalProviderInformation: { insured: false } + }, + credentialStatus: { + id: 'https://rhs-staging.polygonid.me/node?state=ed17a07e8b78ab979507829fa4d37e663ca5906714d506dec8a174d949c5eb09', + type: 'Iden3ReverseSparseMerkleTreeProof' } } }); @@ -590,7 +600,7 @@ describe.sequential('sig proofs', () => { userDID ); expect(proof).not.to.be.undefined; - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof( { @@ -661,7 +671,7 @@ describe.sequential('sig proofs', () => { userDID ); expect(proof).not.to.be.undefined; - expect(vp).to.be.undefined; + expect(vp).not.to.be.undefined; const isValid = await proofService.verifyProof( {