From ead1020b60219300d994eb80f1c1337374a76f0f Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 13 Dec 2023 16:20:15 +0100 Subject: [PATCH 01/17] feat(proofs): added PresentationExchangeService Signed-off-by: Berend Sliedrecht --- packages/core/package.json | 3 + packages/core/src/modules/proofs/index.ts | 3 + .../proofs/models/PresentationSubmission.ts | 119 +++++ .../core/src/modules/proofs/models/index.ts | 1 + .../services/PresentationExchangeService.ts | 467 ++++++++++++++++++ .../core/src/modules/proofs/services/index.ts | 1 + .../proofs/utils/credentialSelection.ts | 301 +++++++++++ .../src/modules/proofs/utils/transform.ts | 78 +++ 8 files changed, 973 insertions(+) create mode 100644 packages/core/src/modules/proofs/models/PresentationSubmission.ts create mode 100644 packages/core/src/modules/proofs/services/PresentationExchangeService.ts create mode 100644 packages/core/src/modules/proofs/services/index.ts create mode 100644 packages/core/src/modules/proofs/utils/credentialSelection.ts create mode 100644 packages/core/src/modules/proofs/utils/transform.ts diff --git a/packages/core/package.json b/packages/core/package.json index 4a0306f9b8..04813c5fdd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,6 +33,9 @@ "@stablelib/ed25519": "^1.0.2", "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", + "@sphereon/pex": "^2.2.2", + "@sphereon/pex-models": "^2.1.2", + "@sphereon/ssi-types": "^0.17.5", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts index 30eb44ba0f..a6c8a7eeab 100644 --- a/packages/core/src/modules/proofs/index.ts +++ b/packages/core/src/modules/proofs/index.ts @@ -12,3 +12,6 @@ export * from './ProofsApiOptions' // Module export * from './ProofsModule' export * from './ProofsModuleConfig' + +// Services +export * from './services' diff --git a/packages/core/src/modules/proofs/models/PresentationSubmission.ts b/packages/core/src/modules/proofs/models/PresentationSubmission.ts new file mode 100644 index 0000000000..309cb93c62 --- /dev/null +++ b/packages/core/src/modules/proofs/models/PresentationSubmission.ts @@ -0,0 +1,119 @@ +import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' + +export interface PresentationSubmission { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: PresentationSubmissionRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface PresentationSubmissionRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains a credential that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries where the verifiable credential list is empty. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submissionEntry: SubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' +} + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface SubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credentials that satisfy the input descriptor. + * + * If the value is an empty list, it means the input descriptor could + * not be satisfied. + */ + verifiableCredentials: W3cCredentialRecord[] +} + +/** + * Mapping of selected credentials for an input descriptor + */ +export type InputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/proofs/models/index.ts b/packages/core/src/modules/proofs/models/index.ts index 9dec0e697a..660b1db211 100644 --- a/packages/core/src/modules/proofs/models/index.ts +++ b/packages/core/src/modules/proofs/models/index.ts @@ -1,3 +1,4 @@ export * from './ProofAutoAcceptType' export * from './ProofState' export * from './ProofFormatSpec' +export * from './PresentationSubmission' diff --git a/packages/core/src/modules/proofs/services/PresentationExchangeService.ts b/packages/core/src/modules/proofs/services/PresentationExchangeService.ts new file mode 100644 index 0000000000..63cb0b5060 --- /dev/null +++ b/packages/core/src/modules/proofs/services/PresentationExchangeService.ts @@ -0,0 +1,467 @@ +import type { AgentContext } from '../../../agent' +import type { Query } from '../../../storage/StorageService' +import type { VerificationMethod } from '../../dids' +import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { InputDescriptorToCredentials, PresentationSubmission } from '../models' +import type { + IPresentationDefinition, + PresentationSignCallBackParams, + VerifiablePresentationResult, +} from '@sphereon/pex' +import type { + InputDescriptorV2, + PresentationSubmission as PexPresentationSubmission, + PresentationDefinitionV1, +} from '@sphereon/pex-models' +import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' + +import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { injectable } from 'tsyringe' + +import { getJwkFromKey } from '../../../crypto' +import { AriesFrameworkError } from '../../../error' +import { JsonTransformer } from '../../../utils' +import { getKeyFromVerificationMethod, DidsApi } from '../../dids' +import { SignatureSuiteRegistry, W3cPresentation, W3cCredentialService, ClaimFormat } from '../../vc' +import { W3cCredentialRepository } from '../../vc/repository' +import { selectCredentialsForRequest } from '../utils/credentialSelection' +import { + getSphereonOriginalVerifiableCredential, + getSphereonW3cVerifiablePresentation, + getW3cVerifiablePresentationInstance, +} from '../utils/transform' + +export type ProofStructure = Record>> +export type PresentationDefinition = IPresentationDefinition + +@injectable() +export class PresentationExchangeService { + private pex = new PEX() + + public async selectCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didRecords = await didsApi.getCreatedDids() + const holderDids = didRecords.map((didRecord) => didRecord.did) + + return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const query: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new AriesFrameworkError( + `Unable to determine the Presentation Exchange version from the presentation definition. ${ + presentationDefinitionVersion.error ?? 'Unknown error' + }` + ) + } + + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new AriesFrameworkError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + + return credentialRecords + } + + private addCredentialToSubjectInputDescriptor( + subjectsToInputDescriptors: ProofStructure, + subjectId: string, + inputDescriptorId: string, + credential: W3cVerifiableCredential + ) { + const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} + const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] + + credentials.push(credential) + inputDescriptorsToCredentials[inputDescriptorId] = credentials + subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials + } + + private getPresentationFormat( + presentationDefinition: PresentationDefinition, + credentials: Array + ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + + const inputDescriptorsNotSupportingJwtVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.jwt_vc === undefined) + + const inputDescriptorsNotSupportingLdpVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.ldp_vc === undefined) + + if ( + allCredentialsAreJwtVc && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && + inputDescriptorsNotSupportingJwtVc.length === 0 + ) { + return ClaimFormat.JwtVp + } else if ( + allCredentialsAreLdpVc && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && + inputDescriptorsNotSupportingLdpVc.length === 0 + ) { + return ClaimFormat.LdpVp + } else { + throw new AriesFrameworkError( + 'No suitable presentation format found for the given presentation definition, and credentials' + ) + } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + credentialsForInputDescriptor: InputDescriptorToCredentials + presentationDefinition: PresentationDefinition + challenge?: string + domain?: string + nonce?: string + } + ) { + const { presentationDefinition, challenge, nonce, domain } = options + + const proofStructure: ProofStructure = {} + + Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + credentials.forEach((credential) => { + const subjectId = credential.credentialSubjectIds[0] + if (!subjectId) { + throw new AriesFrameworkError('Missing required credential subject for creating the presentation.') + } + + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) + }) + }) + + const verifiablePresentationResultsWithFormat: Array<{ + verifiablePresentationResult: VerifiablePresentationResult + format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + }> = [] + + const subjectToInputDescriptors = Object.entries(proofStructure) + for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) + + if (!verificationMethod) { + throw new AriesFrameworkError(`No verification method found for subject id '${subjectId}'.`) + } + + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials + ) + + // Get all the credentials associated with the input descriptors + const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) + .flatMap((credentials) => credentials) + .map(getSphereonOriginalVerifiableCredential) + + const presentationDefinitionForSubject: PresentationDefinition = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForSubject, + + // We remove the submission requirements, as it will otherwise fail to create the VP + submission_requirements: undefined, + } + + const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) + + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForSubject, + this.getPresentationSignCallback(agentContext, verificationMethod, format), + { + holderDID: subjectId, + proofOptions: { challenge, domain, nonce }, + signatureOptions: { verificationMethod: verificationMethod?.id }, + presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, + } + ) + + verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new AriesFrameworkError('No verifiable presentations created.') + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new AriesFrameworkError('No verifiable presentations created.') + } + + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { + throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') + } + + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission + const presentationSubmission: PexPresentationSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } + + for (const vpf of verifiablePresentationResultsWithFormat) { + const { verifiablePresentationResult } = vpf + presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) + } + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } + } + + private getSigningAlgorithmFromVerificationMethod( + verificationMethod: VerificationMethod, + suitableAlgorithms?: Array + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new AriesFrameworkError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: Array, + inputDescriptorAlgorithms: Array> + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new AriesFrameworkError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new AriesFrameworkError( + `No signature algorithm found for satisfying restrictions of the input descriptors.` + ) + } + + let suitableAlgorithms: Array | undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: PresentationDefinition, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getProofTypeForLdpVc( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) + .filter((alg) => alg.length > 0) + + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new AriesFrameworkError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + ) + } + + if (suitableSignatureSuites) { + if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new AriesFrameworkError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuite.proofType + } + + return supportedSignatureSuite.proofType + } + + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp + ) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { presentation: presentationJson, options, presentationDefinition } = callBackParams + const { challenge, domain, nonce } = options.proofOptions ?? {} + const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + + if (verificationMethodId && verificationMethodId !== verificationMethod.id) { + throw new AriesFrameworkError( + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` + ) + } + + // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. + const presentationToSign = { ...presentationJson, presentation_submission: undefined } + + let signedPresentation: W3cVerifiablePresentation + if (vpFormat === 'jwt_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else if (vpFormat === 'ldp_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofPurpose: 'authentication', + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else { + throw new AriesFrameworkError( + `Only JWT credentials or JSONLD credentials are supported for a single presentation.` + ) + } + + return getSphereonW3cVerifiablePresentation(signedPresentation) + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new AriesFrameworkError(`Only dids are supported as credentialSubject id. ${subjectId} is not a valid did`) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new AriesFrameworkError(`No authentication verificationMethods found for did ${subjectId} in did document`) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } +} diff --git a/packages/core/src/modules/proofs/services/index.ts b/packages/core/src/modules/proofs/services/index.ts new file mode 100644 index 0000000000..25f2454018 --- /dev/null +++ b/packages/core/src/modules/proofs/services/index.ts @@ -0,0 +1 @@ +export * from './PresentationExchangeService' diff --git a/packages/core/src/modules/proofs/utils/credentialSelection.ts b/packages/core/src/modules/proofs/utils/credentialSelection.ts new file mode 100644 index 0000000000..fa045c205a --- /dev/null +++ b/packages/core/src/modules/proofs/utils/credentialSelection.ts @@ -0,0 +1,301 @@ +import type { W3cCredentialRecord } from '../../vc' +import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' + +import { PEX } from '@sphereon/pex' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { AriesFrameworkError } from '../../../error' + +import { getSphereonOriginalVerifiableCredential } from './transform' + +export async function selectCredentialsForRequest( + presentationDefinition: IPresentationDefinition, + credentialRecords: Array, + holderDIDs: Array +): Promise { + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) + + if (!presentationDefinition) { + throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') + } + + const pex = new PEX() + + // FIXME: there is a function for this in the VP library, but it is not usable atm + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + holderDIDs, + // limitDisclosureSignatureSuites: [], + // restrictToDIDMethods, + // restrictToFormats + }) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { + const credentialIndex = encodedCredentials.indexOf(encoded) + const credentialRecord = credentialRecords[credentialIndex] + if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') + + return credentialRecord + }), + } + + const presentationSubmission: PresentationSubmission = { + requirements: [], + areRequirementsSatisfied: false, + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new AriesFrameworkError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new AriesFrameworkError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new AriesFrameworkError("Missing 'from' in submission requirement match") + } + + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + submissionRequirements.push(selectedSubmission) + } else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + submissionRequirements.push(selectedSubmission) + } + } + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials +} + +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: Array | Array, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + rule: Rules.Pick, + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + + const selectedSubmission: PresentationSubmissionRequirement = { + rule: Rules.All, + needsCount: 0, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + submissionEntry: [], + isRequirementSatisfied: false, + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submissionEntry.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submissionEntry.every( + (submission) => submission.verifiableCredentials.length >= 1 + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + + const selectedSubmission: PresentationSubmissionRequirement = { + rule: 'pick', + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], + isRequirementSatisfied: false, + } + + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredentials.length >= 1) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break + } + + return { + ...selectedSubmission, + + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length >= selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, + selectResults: W3cCredentialRecordSelectResults +): SubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: SubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + verifiableCredentials: [], + } + + // return early if no matches. + if (!matchesForInputDescriptor?.length) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials + + return submissionEntry +} + +function extractCredentialsFromMatch( + match: SubmissionRequirementMatch, + availableCredentials?: Array +) { + const verifiableCredentials: Array = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ + W3cCredentialRecord + ] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + */ +export type W3cCredentialRecordSelectResults = Omit & { + verifiableCredential?: Array +} diff --git a/packages/core/src/modules/proofs/utils/transform.ts b/packages/core/src/modules/proofs/utils/transform.ts new file mode 100644 index 0000000000..b56609774d --- /dev/null +++ b/packages/core/src/modules/proofs/utils/transform.ts @@ -0,0 +1,78 @@ +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { AriesFrameworkError } from '../../../error' +import { JsonTransformer } from '../../../utils' +import { + W3cJsonLdVerifiableCredential, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJwtVerifiablePresentation, + ClaimFormat, +} from '../../vc' + +export function getSphereonOriginalVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonOriginalVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiablePresentation( + w3cVerifiablePresentation: W3cVerifiablePresentation +): SphereonW3cVerifiablePresentation { + if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return w3cVerifiablePresentation.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getW3cVerifiablePresentationInstance( + w3cVerifiablePresentation: SphereonW3cVerifiablePresentation +): W3cVerifiablePresentation { + if (typeof w3cVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) + } else { + return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } +} + +export function getW3cVerifiableCredentialInstance( + w3cVerifiableCredential: SphereonW3cVerifiableCredential +): W3cVerifiableCredential { + if (typeof w3cVerifiableCredential === 'string') { + return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) + } else { + return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + } +} From 3dd4e61021cb910d0ecaf9f489e1913188c97e52 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 13 Dec 2023 16:52:18 +0100 Subject: [PATCH 02/17] feat(proofs): initial boiler plate for ppv2 Signed-off-by: Berend Sliedrecht --- .../core/src/modules/proofs/formats/index.ts | 2 + .../PresentationExchangeProofFormat.ts | 30 +++++ .../PresentationExchangeProofFormatService.ts | 111 ++++++++++++++++++ .../formats/presentation-exchange/index.ts | 2 + 4 files changed, 145 insertions(+) create mode 100644 packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts create mode 100644 packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts create mode 100644 packages/core/src/modules/proofs/formats/presentation-exchange/index.ts diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts index a28e77d623..ff34793737 100644 --- a/packages/core/src/modules/proofs/formats/index.ts +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -2,6 +2,8 @@ export * from './ProofFormat' export * from './ProofFormatService' export * from './ProofFormatServiceOptions' +export * from './presentation-exchange' + import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' export { ProofFormatServiceOptions } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts new file mode 100644 index 0000000000..9ed98d98e0 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -0,0 +1,30 @@ +import type { ProofFormat } from '../ProofFormat' + +export interface PresentationExchangeProofFormat extends ProofFormat { + formatKey: 'presentationExchange' + + proofFormats: { + createProposal: unknown + acceptProposal: { + name?: string + version?: string + } + createRequest: unknown + acceptRequest: unknown + + getCredentialsForRequest: { + input: unknown + output: unknown + } + selectCredentialsForRequest: { + input: unknown + output: unknown + } + } + + formatData: { + proposal: unknown + request: unknown + presentation: unknown + } +} diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts new file mode 100644 index 0000000000..de88c4b385 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -0,0 +1,111 @@ +import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' +import type { AgentContext } from '../../../../agent' +import type { ProofFormatService } from '../ProofFormatService' +import type { + ProofFormatCreateProposalOptions, + ProofFormatCreateReturn, + ProofFormatProcessOptions, + ProofFormatAcceptProposalOptions, + FormatCreateRequestOptions, + ProofFormatAcceptRequestOptions, + ProofFormatProcessPresentationOptions, + ProofFormatGetCredentialsForRequestOptions, + ProofFormatSelectCredentialsForRequestOptions, + ProofFormatAutoRespondProposalOptions, + ProofFormatAutoRespondRequestOptions, + ProofFormatAutoRespondPresentationOptions, +} from '../ProofFormatServiceOptions' + +const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' +const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission@v1.0' + +export class PresentationExchangeProofFormatService implements ProofFormatService { + public readonly formatKey = 'presentationExchange' as const + + public supportsFormat(formatIdentifier: string): boolean { + return [ + PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, + PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + PRESENTATION_EXCHANGE_PRESENTATION, + ].includes(formatIdentifier) + } + + public createProposal( + agentContext: AgentContext, + options: ProofFormatCreateProposalOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public processProposal(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise { + throw new Error('Method not implemented.') + } + + public acceptProposal( + agentContext: AgentContext, + options: ProofFormatAcceptProposalOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public createRequest( + agentContext: AgentContext, + options: FormatCreateRequestOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public processRequest(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise { + throw new Error('Method not implemented.') + } + + public acceptRequest( + agentContext: AgentContext, + options: ProofFormatAcceptRequestOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public processPresentation( + agentContext: AgentContext, + options: ProofFormatProcessPresentationOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public getCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatGetCredentialsForRequestOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public selectCredentialsForRequest( + agentContext: AgentContext, + options: ProofFormatSelectCredentialsForRequestOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public shouldAutoRespondToProposal( + agentContext: AgentContext, + options: ProofFormatAutoRespondProposalOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public shouldAutoRespondToRequest( + agentContext: AgentContext, + options: ProofFormatAutoRespondRequestOptions + ): Promise { + throw new Error('Method not implemented.') + } + + public shouldAutoRespondToPresentation( + agentContext: AgentContext, + options: ProofFormatAutoRespondPresentationOptions + ): Promise { + throw new Error('Method not implemented.') + } +} diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts new file mode 100644 index 0000000000..d2ab2c554d --- /dev/null +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts @@ -0,0 +1,2 @@ +export * from './PresentationExchangeProofFormat' +export * from './PresentationExchangeProofFormatService' From 8ef8f2d9245b27d78fb9c8524d78dde2e80a5361 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 13 Dec 2023 16:20:15 +0100 Subject: [PATCH 03/17] feat(proofs): added PresentationExchangeModule Signed-off-by: Berend Sliedrecht --- packages/presentation-exchange/jest.config.ts | 15 + packages/presentation-exchange/package.json | 37 ++ .../src/PresentationExchangeError.ts | 3 + .../src/PresentationExchangeModule.ts | 25 + .../src/PresentationExchangeService.ts | 481 ++++++++++++++++++ packages/presentation-exchange/src/index.ts | 4 + .../src/models/PresentationSubmission.ts | 119 +++++ .../presentation-exchange/src/models/index.ts | 1 + .../src/utils/credentialSelection.ts | 300 +++++++++++ .../presentation-exchange/src/utils/index.ts | 2 + .../src/utils/transform.ts | 78 +++ .../presentation-exchange/tsconfig.build.json | 9 + packages/presentation-exchange/tsconfig.json | 8 + 13 files changed, 1082 insertions(+) create mode 100644 packages/presentation-exchange/jest.config.ts create mode 100644 packages/presentation-exchange/package.json create mode 100644 packages/presentation-exchange/src/PresentationExchangeError.ts create mode 100644 packages/presentation-exchange/src/PresentationExchangeModule.ts create mode 100644 packages/presentation-exchange/src/PresentationExchangeService.ts create mode 100644 packages/presentation-exchange/src/index.ts create mode 100644 packages/presentation-exchange/src/models/PresentationSubmission.ts create mode 100644 packages/presentation-exchange/src/models/index.ts create mode 100644 packages/presentation-exchange/src/utils/credentialSelection.ts create mode 100644 packages/presentation-exchange/src/utils/index.ts create mode 100644 packages/presentation-exchange/src/utils/transform.ts create mode 100644 packages/presentation-exchange/tsconfig.build.json create mode 100644 packages/presentation-exchange/tsconfig.json diff --git a/packages/presentation-exchange/jest.config.ts b/packages/presentation-exchange/jest.config.ts new file mode 100644 index 0000000000..7b6ec7f1c5 --- /dev/null +++ b/packages/presentation-exchange/jest.config.ts @@ -0,0 +1,15 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +process.env.TZ = 'GMT' + +const config: Config.InitialOptions = { + ...base, + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/presentation-exchange/package.json b/packages/presentation-exchange/package.json new file mode 100644 index 0000000000..f31f73b572 --- /dev/null +++ b/packages/presentation-exchange/package.json @@ -0,0 +1,37 @@ +{ + "name": "@aries-framework/presentation-exchange", + "main": "build/index", + "types": "build/index", + "version": "0.4.2", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/openwallet-foundation/agent-framework-javascript/tree/main/packages/presentation-exchange", + "repository": { + "type": "git", + "url": "https://github.com/openwallet-foundation/agent-framework-javascript", + "directory": "packages/presentation-exchange" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build" + }, + "dependencies": { + "@aries-framework/core": "^0.4.2", + "@sphereon/pex": "^2.2.2", + "@sphereon/pex-models": "^2.1.2", + "@sphereon/ssi-types": "^0.17.5", + "jsonpath": "^1.1.1", + "tsyringe": "^4.8.0" + }, + "devDependencies": { + "@types/jsonpath": "^0.2.4", + "typescript": "~4.9.5" + } +} diff --git a/packages/presentation-exchange/src/PresentationExchangeError.ts b/packages/presentation-exchange/src/PresentationExchangeError.ts new file mode 100644 index 0000000000..5c52b67752 --- /dev/null +++ b/packages/presentation-exchange/src/PresentationExchangeError.ts @@ -0,0 +1,3 @@ +import { AriesFrameworkError } from '@aries-framework/core' + +export class PresentationExchangeError extends AriesFrameworkError {} diff --git a/packages/presentation-exchange/src/PresentationExchangeModule.ts b/packages/presentation-exchange/src/PresentationExchangeModule.ts new file mode 100644 index 0000000000..efeb199aaa --- /dev/null +++ b/packages/presentation-exchange/src/PresentationExchangeModule.ts @@ -0,0 +1,25 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { AgentConfig } from '@aries-framework/core' + +import { PresentationExchangeService } from './PresentationExchangeService' + +/** + * @public + */ +export class PresentationExchangeModule implements Module { + /** + * Registers the dependencies of the presentation-exchange module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@aries-framework/presentation-exchange' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // Services + dependencyManager.registerSingleton(PresentationExchangeService) + } +} diff --git a/packages/presentation-exchange/src/PresentationExchangeService.ts b/packages/presentation-exchange/src/PresentationExchangeService.ts new file mode 100644 index 0000000000..8b9ba5dbcd --- /dev/null +++ b/packages/presentation-exchange/src/PresentationExchangeService.ts @@ -0,0 +1,481 @@ +import type { InputDescriptorToCredentials, PresentationSubmission } from './models' +import type { + AgentContext, + Query, + VerificationMethod, + W3cCredentialRecord, + W3cVerifiableCredential, + W3cVerifiablePresentation, +} from '@aries-framework/core' +import type { + IPresentationDefinition, + PresentationSignCallBackParams, + VerifiablePresentationResult, +} from '@sphereon/pex' +import type { + InputDescriptorV2, + PresentationSubmission as PexPresentationSubmission, + PresentationDefinitionV1, +} from '@sphereon/pex-models' +import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' + +import { + getJwkFromKey, + JsonTransformer, + SignatureSuiteRegistry, + W3cPresentation, + W3cCredentialService, + ClaimFormat, + getKeyFromVerificationMethod, + DidsApi, + W3cCredentialRepository, +} from '@aries-framework/core' +import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { injectable } from 'tsyringe' + +import { PresentationExchangeError } from './PresentationExchangeError' +import { + selectCredentialsForRequest, + getSphereonOriginalVerifiableCredential, + getSphereonW3cVerifiablePresentation, + getW3cVerifiablePresentationInstance, +} from './utils' + +export type ProofStructure = Record>> +export type PresentationDefinition = IPresentationDefinition + +@injectable() +export class PresentationExchangeService { + private pex = new PEX() + + public async selectCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didRecords = await didsApi.getCreatedDids() + const holderDids = didRecords.map((didRecord) => didRecord.did) + + return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const query: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new PresentationExchangeError( + `Unable to determine the Presentation Exchange version from the presentation definition. ${ + presentationDefinitionVersion.error ?? 'Unknown error' + }` + ) + } + + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new PresentationExchangeError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + + return credentialRecords + } + + private addCredentialToSubjectInputDescriptor( + subjectsToInputDescriptors: ProofStructure, + subjectId: string, + inputDescriptorId: string, + credential: W3cVerifiableCredential + ) { + const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} + const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] + + credentials.push(credential) + inputDescriptorsToCredentials[inputDescriptorId] = credentials + subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials + } + + private getPresentationFormat( + presentationDefinition: PresentationDefinition, + credentials: Array + ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + + const inputDescriptorsNotSupportingJwtVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.jwt_vc === undefined) + + const inputDescriptorsNotSupportingLdpVc = ( + presentationDefinition.input_descriptors as Array + ).filter((d) => d.format && d.format.ldp_vc === undefined) + + if ( + allCredentialsAreJwtVc && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && + inputDescriptorsNotSupportingJwtVc.length === 0 + ) { + return ClaimFormat.JwtVp + } else if ( + allCredentialsAreLdpVc && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && + inputDescriptorsNotSupportingLdpVc.length === 0 + ) { + return ClaimFormat.LdpVp + } else { + throw new PresentationExchangeError( + 'No suitable presentation format found for the given presentation definition, and credentials' + ) + } + } + + public async createPresentation( + agentContext: AgentContext, + options: { + credentialsForInputDescriptor: InputDescriptorToCredentials + presentationDefinition: PresentationDefinition + challenge?: string + domain?: string + nonce?: string + } + ) { + const { presentationDefinition, challenge, nonce, domain } = options + + const proofStructure: ProofStructure = {} + + Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + credentials.forEach((credential) => { + const subjectId = credential.credentialSubjectIds[0] + if (!subjectId) { + throw new PresentationExchangeError('Missing required credential subject for creating the presentation.') + } + + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) + }) + }) + + const verifiablePresentationResultsWithFormat: Array<{ + verifiablePresentationResult: VerifiablePresentationResult + format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + }> = [] + + const subjectToInputDescriptors = Object.entries(proofStructure) + for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) + + if (!verificationMethod) { + throw new PresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) + } + + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials + ) + + // Get all the credentials associated with the input descriptors + const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) + .flatMap((credentials) => credentials) + .map(getSphereonOriginalVerifiableCredential) + + const presentationDefinitionForSubject: PresentationDefinition = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForSubject, + + // We remove the submission requirements, as it will otherwise fail to create the VP + submission_requirements: undefined, + } + + const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) + + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForSubject, + this.getPresentationSignCallback(agentContext, verificationMethod, format), + { + holderDID: subjectId, + proofOptions: { challenge, domain, nonce }, + signatureOptions: { verificationMethod: verificationMethod?.id }, + presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, + } + ) + + verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new PresentationExchangeError('No verifiable presentations created.') + } + + if (!verifiablePresentationResultsWithFormat[0]) { + throw new PresentationExchangeError('No verifiable presentations created.') + } + + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { + throw new PresentationExchangeError('Invalid amount of verifiable presentations created.') + } + + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission + const presentationSubmission: PexPresentationSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } + + for (const vpf of verifiablePresentationResultsWithFormat) { + const { verifiablePresentationResult } = vpf + presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) + } + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } + } + + private getSigningAlgorithmFromVerificationMethod( + verificationMethod: VerificationMethod, + suitableAlgorithms?: Array + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new PresentationExchangeError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new PresentationExchangeError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: Array, + inputDescriptorAlgorithms: Array> + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new PresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new PresentationExchangeError( + `No signature algorithm found for satisfying restrictions of the input descriptors.` + ) + } + + let suitableAlgorithms: Array | undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: PresentationDefinition, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getProofTypeForLdpVc( + agentContext: AgentContext, + presentationDefinition: PresentationDefinition, + verificationMethod: VerificationMethod + ) { + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] + + const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) + .filter((alg) => alg.length > 0) + + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new PresentationExchangeError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + ) + } + + if (suitableSignatureSuites) { + if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new PresentationExchangeError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuite.proofType + } + + return supportedSignatureSuite.proofType + } + + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp + ) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { presentation: presentationJson, options, presentationDefinition } = callBackParams + const { challenge, domain, nonce } = options.proofOptions ?? {} + const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + + if (verificationMethodId && verificationMethodId !== verificationMethod.id) { + throw new PresentationExchangeError( + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` + ) + } + + // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. + const presentationToSign = { ...presentationJson, presentation_submission: undefined } + + let signedPresentation: W3cVerifiablePresentation + if (vpFormat === 'jwt_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else if (vpFormat === 'ldp_vp') { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofPurpose: 'authentication', + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else { + throw new PresentationExchangeError( + `Only JWT credentials or JSONLD credentials are supported for a single presentation.` + ) + } + + return getSphereonW3cVerifiablePresentation(signedPresentation) + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new PresentationExchangeError( + `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` + ) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new PresentationExchangeError( + `No authentication verificationMethods found for did ${subjectId} in did document` + ) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } +} diff --git a/packages/presentation-exchange/src/index.ts b/packages/presentation-exchange/src/index.ts new file mode 100644 index 0000000000..0bb3c76aae --- /dev/null +++ b/packages/presentation-exchange/src/index.ts @@ -0,0 +1,4 @@ +export * from './PresentationExchangeError' +export * from './PresentationExchangeModule' +export * from './PresentationExchangeService' +export * from './models' diff --git a/packages/presentation-exchange/src/models/PresentationSubmission.ts b/packages/presentation-exchange/src/models/PresentationSubmission.ts new file mode 100644 index 0000000000..28f9209e5a --- /dev/null +++ b/packages/presentation-exchange/src/models/PresentationSubmission.ts @@ -0,0 +1,119 @@ +import type { W3cCredentialRecord, W3cVerifiableCredential } from '@aries-framework/core' + +export interface PresentationSubmission { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: PresentationSubmissionRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface PresentationSubmissionRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains a credential that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries where the verifiable credential list is empty. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submissionEntry: SubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' +} + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface SubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credentials that satisfy the input descriptor. + * + * If the value is an empty list, it means the input descriptor could + * not be satisfied. + */ + verifiableCredentials: W3cCredentialRecord[] +} + +/** + * Mapping of selected credentials for an input descriptor + */ +export type InputDescriptorToCredentials = Record> diff --git a/packages/presentation-exchange/src/models/index.ts b/packages/presentation-exchange/src/models/index.ts new file mode 100644 index 0000000000..47247cbbc9 --- /dev/null +++ b/packages/presentation-exchange/src/models/index.ts @@ -0,0 +1 @@ +export * from './PresentationSubmission' diff --git a/packages/presentation-exchange/src/utils/credentialSelection.ts b/packages/presentation-exchange/src/utils/credentialSelection.ts new file mode 100644 index 0000000000..966af823c3 --- /dev/null +++ b/packages/presentation-exchange/src/utils/credentialSelection.ts @@ -0,0 +1,300 @@ +import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models' +import type { W3cCredentialRecord } from '@aries-framework/core' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' + +import { AriesFrameworkError } from '@aries-framework/core' +import { PEX } from '@sphereon/pex' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { getSphereonOriginalVerifiableCredential } from './transform' + +export async function selectCredentialsForRequest( + presentationDefinition: IPresentationDefinition, + credentialRecords: Array, + holderDIDs: Array +): Promise { + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) + + if (!presentationDefinition) { + throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') + } + + const pex = new PEX() + + // FIXME: there is a function for this in the VP library, but it is not usable atm + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + holderDIDs, + // limitDisclosureSignatureSuites: [], + // restrictToDIDMethods, + // restrictToFormats + }) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { + const credentialIndex = encodedCredentials.indexOf(encoded) + const credentialRecord = credentialRecords[credentialIndex] + if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') + + return credentialRecord + }), + } + + const presentationSubmission: PresentationSubmission = { + requirements: [], + areRequirementsSatisfied: false, + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new AriesFrameworkError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new AriesFrameworkError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new AriesFrameworkError("Missing 'from' in submission requirement match") + } + + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + submissionRequirements.push(selectedSubmission) + } else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + submissionRequirements.push(selectedSubmission) + } + } + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials +} + +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: Array | Array, + selectResults: W3cCredentialRecordSelectResults +): Array { + const submissionRequirements: Array = [] + + for (const inputDescriptor of inputDescriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + rule: Rules.Pick, + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + + const selectedSubmission: PresentationSubmissionRequirement = { + rule: Rules.All, + needsCount: 0, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + submissionEntry: [], + isRequirementSatisfied: false, + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submissionEntry.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submissionEntry.every( + (submission) => submission.verifiableCredentials.length >= 1 + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: IPresentationDefinition, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + + const selectedSubmission: PresentationSubmissionRequirement = { + rule: 'pick', + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], + isRequirementSatisfied: false, + } + + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredentials.length >= 1) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break + } + + return { + ...selectedSubmission, + + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length >= selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, + selectResults: W3cCredentialRecordSelectResults +): SubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: SubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + verifiableCredentials: [], + } + + // return early if no matches. + if (!matchesForInputDescriptor?.length) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials + + return submissionEntry +} + +function extractCredentialsFromMatch( + match: SubmissionRequirementMatch, + availableCredentials?: Array +) { + const verifiableCredentials: Array = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ + W3cCredentialRecord + ] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + */ +export type W3cCredentialRecordSelectResults = Omit & { + verifiableCredential?: Array +} diff --git a/packages/presentation-exchange/src/utils/index.ts b/packages/presentation-exchange/src/utils/index.ts new file mode 100644 index 0000000000..aaf44fa1b6 --- /dev/null +++ b/packages/presentation-exchange/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './transform' +export * from './credentialSelection' diff --git a/packages/presentation-exchange/src/utils/transform.ts b/packages/presentation-exchange/src/utils/transform.ts new file mode 100644 index 0000000000..a97513b9be --- /dev/null +++ b/packages/presentation-exchange/src/utils/transform.ts @@ -0,0 +1,78 @@ +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' +import type { + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { + AriesFrameworkError, + JsonTransformer, + W3cJsonLdVerifiableCredential, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJwtVerifiablePresentation, + ClaimFormat, +} from '@aries-framework/core' + +export function getSphereonOriginalVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonOriginalVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiablePresentation( + w3cVerifiablePresentation: W3cVerifiablePresentation +): SphereonW3cVerifiablePresentation { + if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return w3cVerifiablePresentation.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getW3cVerifiablePresentationInstance( + w3cVerifiablePresentation: SphereonW3cVerifiablePresentation +): W3cVerifiablePresentation { + if (typeof w3cVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) + } else { + return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } +} + +export function getW3cVerifiableCredentialInstance( + w3cVerifiableCredential: SphereonW3cVerifiableCredential +): W3cVerifiableCredential { + if (typeof w3cVerifiableCredential === 'string') { + return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) + } else { + return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + } +} diff --git a/packages/presentation-exchange/tsconfig.build.json b/packages/presentation-exchange/tsconfig.build.json new file mode 100644 index 0000000000..0a015be666 --- /dev/null +++ b/packages/presentation-exchange/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + + "compilerOptions": { + "outDir": "./build" + }, + + "include": ["src/**/*", "types"] +} diff --git a/packages/presentation-exchange/tsconfig.json b/packages/presentation-exchange/tsconfig.json new file mode 100644 index 0000000000..93d9dd32b5 --- /dev/null +++ b/packages/presentation-exchange/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "allowJs": false, + "typeRoots": ["../../node_modules/@types", "src/types"], + "types": ["jest"] + } +} From 528d31b26a62653de4b8d9a94d0c1621a414a16f Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Fri, 15 Dec 2023 13:52:35 +0100 Subject: [PATCH 04/17] feat(present-proof): initial implementation without tests Signed-off-by: Berend Sliedrecht --- packages/core/package.json | 5 +- .../PresentationExchangeProofFormat.ts | 24 +- .../PresentationExchangeProofFormatService.ts | 260 +++++++++++++++--- ...entationExchangeProofFormatService.test.ts | 144 ++++++++++ packages/presentation-exchange/jest.config.ts | 1 - .../src/PresentationExchangeService.ts | 69 ++++- .../src/models/PresentationSubmission.ts | 6 +- .../src/utils/credentialSelection.ts | 31 ++- 8 files changed, 469 insertions(+), 71 deletions(-) create mode 100644 packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts diff --git a/packages/core/package.json b/packages/core/package.json index 04813c5fdd..73380a3bc0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,9 +3,7 @@ "main": "build/index", "types": "build/index", "version": "0.4.2", - "files": [ - "build" - ], + "files": ["build"], "license": "Apache-2.0", "publishConfig": { "access": "public" @@ -23,6 +21,7 @@ "prepublishOnly": "yarn run build" }, "dependencies": { + "@aries-framework/presentation-exchange": "^0.4.2", "@digitalcredentials/jsonld": "^5.2.1", "@digitalcredentials/jsonld-signatures": "^9.3.1", "@digitalcredentials/vc": "^1.1.2", diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index 9ed98d98e0..ad773ae877 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,24 +1,34 @@ +import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../../vc' import type { ProofFormat } from '../ProofFormat' +import type { PresentationDefinition } from '@aries-framework/presentation-exchange' export interface PresentationExchangeProofFormat extends ProofFormat { formatKey: 'presentationExchange' proofFormats: { - createProposal: unknown + createProposal: { + presentationDefinition: PresentationDefinition + } + acceptProposal: { name?: string version?: string } - createRequest: unknown - acceptRequest: unknown + + createRequest: { presentationDefinition: PresentationDefinition } + + acceptRequest: { + credentials: Record> + } getCredentialsForRequest: { - input: unknown - output: unknown + input: never + output: Array } + selectCredentialsForRequest: { - input: unknown - output: unknown + input: never + output: Array } } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index de88c4b385..a40dbdc53c 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -1,5 +1,10 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/no-cycle */ +/* eslint-disable workspaces/require-dependency */ + import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' +import type { W3cCredentialRecord } from '../../../vc' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -15,6 +20,19 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' +import type { + InputDescriptorToCredentials, + PexPresentationSubmission, + PresentationDefinition, + VerifiablePresentation, +} from '@aries-framework/presentation-exchange' + +import { PresentationExchangeService } from '@aries-framework/presentation-exchange' + +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../error' +import { deepEquality } from '../../../../utils' +import { ProofFormatSpec } from '../../models' const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' @@ -23,6 +41,16 @@ const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission export class PresentationExchangeProofFormatService implements ProofFormatService { public readonly formatKey = 'presentationExchange' as const + private presentationExchangeService(agentContext: AgentContext) { + if (!agentContext.dependencyManager.isRegistered(PresentationExchangeService)) { + throw new AriesFrameworkError( + 'PresentationExchangeService is not registered on the Agent. Please provide the PresentationExchangeModule as a module on the agent' + ) + } + + return agentContext.dependencyManager.resolve(PresentationExchangeService) + } + public supportsFormat(formatIdentifier: string): boolean { return [ PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, @@ -31,81 +59,233 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic ].includes(formatIdentifier) } - public createProposal( + public async createProposal( agentContext: AgentContext, - options: ProofFormatCreateProposalOptions + { proofFormats, attachmentId }: ProofFormatCreateProposalOptions ): Promise { - throw new Error('Method not implemented.') + const ps = this.presentationExchangeService(agentContext) + + const pexFormat = proofFormats.presentationExchange + if (!pexFormat) { + throw new AriesFrameworkError('Missing Presentation Exchange format in create proposal attachment format') + } + + const { presentationDefinition } = pexFormat + + ps?.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, attachmentId }) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { format, attachment } } - public processProposal(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise { - throw new Error('Method not implemented.') + public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const proposal = attachment.getDataAsJson() + ps.validatePresentationDefinition(proposal) } - public acceptProposal( + public async acceptProposal( agentContext: AgentContext, - options: ProofFormatAcceptProposalOptions + { attachmentId, proposalAttachment }: ProofFormatAcceptProposalOptions ): Promise { - throw new Error('Method not implemented.') + const ps = this.presentationExchangeService(agentContext) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const presentationDefinition = proposalAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { format, attachment } } - public createRequest( + public async createRequest( agentContext: AgentContext, - options: FormatCreateRequestOptions + { attachmentId, proofFormats }: FormatCreateRequestOptions ): Promise { - throw new Error('Method not implemented.') + const ps = this.presentationExchangeService(agentContext) + + const presentationExchangeFormat = proofFormats.presentationExchange + + if (!presentationExchangeFormat) { + throw Error('Missing presentation exchange format in create request attachment format') + } + + const { presentationDefinition } = presentationExchangeFormat + + ps.validatePresentationDefinition(presentationDefinition) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, + attachmentId, + }) + + const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + + return { attachment, format } } - public processRequest(agentContext: AgentContext, options: ProofFormatProcessOptions): Promise { - throw new Error('Method not implemented.') + public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { + const ps = this.presentationExchangeService(agentContext) + const proposal = attachment.getDataAsJson() + ps.validatePresentationDefinition(proposal) } - public acceptRequest( + public async acceptRequest( agentContext: AgentContext, - options: ProofFormatAcceptRequestOptions + { attachmentId, requestAttachment, proofFormats }: ProofFormatAcceptRequestOptions ): Promise { - throw new Error('Method not implemented.') + const presentationExchangeFormat = proofFormats?.presentationExchange + + if (!presentationExchangeFormat) { + throw Error('Missing presentation exchange format in create request attachment format') + } + + const ps = this.presentationExchangeService(agentContext) + + const format = new ProofFormatSpec({ + format: PRESENTATION_EXCHANGE_PRESENTATION, + attachmentId, + }) + + const presentationDefinition = requestAttachment.getDataAsJson() + + const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest( + agentContext, + presentationDefinition + ) + + if (!areRequirementsSatisfied) { + throw new AriesFrameworkError('Requirements of the presentation definition could not be satifsied') + } + + const credentials: InputDescriptorToCredentials = {} + + requirements.forEach((r) => { + r.submissionEntry.forEach((r) => { + credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + }) + }) + + const presentation = await ps.createPresentation(agentContext, { + presentationDefinition, + credentialsForInputDescriptor: credentials, + }) + + const attachment = this.getFormatData(presentation, format.attachmentId) + + return { attachment, format } } - public processPresentation( + public async processPresentation( agentContext: AgentContext, - options: ProofFormatProcessPresentationOptions + { requestAttachment, attachment }: ProofFormatProcessPresentationOptions ): Promise { - throw new Error('Method not implemented.') + const ps = this.presentationExchangeService(agentContext) + const presentationDefinition = requestAttachment.getDataAsJson() + const presentation = attachment.getDataAsJson() + + try { + ps.validatePresentationDefinition(presentationDefinition) + if (presentation.presentation_submission) { + ps.validatePresentationSubmission(presentation.presentation_submission as unknown as PexPresentationSubmission) + } + + ps.validatePresentation(presentationDefinition, presentation) + return true + } catch (e) { + agentContext.config.logger.error(e) + return false + } } - public getCredentialsForRequest( + public async getCredentialsForRequest( agentContext: AgentContext, - options: ProofFormatGetCredentialsForRequestOptions - ): Promise { - throw new Error('Method not implemented.') + { requestAttachment }: ProofFormatGetCredentialsForRequestOptions + ): Promise> { + const ps = this.presentationExchangeService(agentContext) + const presentationDefinition = requestAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) + + const credentials = presentationSubmission.requirements.flatMap((r) => + r.submissionEntry.flatMap((e) => e.verifiableCredentials) + ) + + return credentials } - public selectCredentialsForRequest( + public async selectCredentialsForRequest( agentContext: AgentContext, - options: ProofFormatSelectCredentialsForRequestOptions - ): Promise { - throw new Error('Method not implemented.') + { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions + ): Promise> { + const ps = this.presentationExchangeService(agentContext) + const presentationDefinition = requestAttachment.getDataAsJson() + + ps.validatePresentationDefinition(presentationDefinition) + + const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) + + const credentials = presentationSubmission.requirements.flatMap((r) => + r.submissionEntry.flatMap((e) => e.verifiableCredentials) + ) + + return credentials } - public shouldAutoRespondToProposal( - agentContext: AgentContext, - options: ProofFormatAutoRespondProposalOptions + public async shouldAutoRespondToProposal( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions ): Promise { - throw new Error('Method not implemented.') + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() + + return deepEquality(requestData, proposalData) } - public shouldAutoRespondToRequest( - agentContext: AgentContext, - options: ProofFormatAutoRespondRequestOptions + public async shouldAutoRespondToRequest( + _agentContext: AgentContext, + { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions ): Promise { - throw new Error('Method not implemented.') - } + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() - public shouldAutoRespondToPresentation( - agentContext: AgentContext, - options: ProofFormatAutoRespondPresentationOptions + return deepEquality(requestData, proposalData) + } + /** + * + * The presentation is already verified in processPresentation, so we can just return true here. + * It's only an ack, so it's just that we received the presentation. + * + */ + public async shouldAutoRespondToPresentation( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _agentContext: AgentContext, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: ProofFormatAutoRespondPresentationOptions ): Promise { - throw new Error('Method not implemented.') + return true + } + + private getFormatData>(data: T, id: string): Attachment { + const attachment = new Attachment({ + id, + mimeType: 'application/json', + data: new AttachmentData({ + json: data, + }), + }) + + return attachment } } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts new file mode 100644 index 0000000000..61f0903292 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -0,0 +1,144 @@ +import type { AgentContext } from '../../../../../agent' +import type { ProofFormatService } from '../../ProofFormatService' +import type { PresentationExchangeProofFormat } from '../PresentationExchangeProofFormat' +import type { PresentationDefinition } from '@aries-framework/presentation-exchange' + +import { PresentationExchangeModule } from '@aries-framework/presentation-exchange' + +import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' +import { getAgentOptions } from '../../../../../../tests' +import { Agent } from '../../../../../agent/Agent' +import { ProofsModule } from '../../../ProofsModule' +import { ProofState } from '../../../models' +import { V2ProofProtocol } from '../../../protocol' +import { ProofExchangeRecord } from '../../../repository' +import { PresentationExchangeProofFormatService } from '../PresentationExchangeProofFormatService' + +const mockProofRecord = () => + new ProofExchangeRecord({ + state: ProofState.ProposalSent, + threadId: 'add7e1a0-109e-4f37-9caa-cfd0fcdfe540', + protocolVersion: 'v2', + }) + +const mockPresentationDefinition = (): PresentationDefinition => ({ + id: '32f54163-7166-48f1-93d8-ff217bdb0653', + input_descriptors: [ + { + id: 'wa_driver_license', + name: 'Washington State Business License', + purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference', + constraints: { + fields: [ + { + path: [ + '$.credentialSubject.dateOfBirth', + '$.credentialSubject.dob', + '$.vc.credentialSubject.dateOfBirth', + '$.vc.credentialSubject.dob', + ], + }, + ], + }, + }, + ], +}) + +describe('Presentation Exchange ProofFormatService', () => { + let pexFormatService: ProofFormatService + let agentContext: AgentContext + + beforeEach(async () => { + const agent = new Agent( + getAgentOptions( + 'PresentationExchangeProofFormatService', + {}, + { + pex: new PresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], + }), + ...getIndySdkModules(), + } + ) + ) + + await agent.initialize() + + agentContext = agent.context + pexFormatService = agent.dependencyManager.resolve(PresentationExchangeProofFormatService) + }) + + describe('Create Presentation Exchange Proof Proposal / Request', () => { + test('Creates Presentation Exchange Proposal', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createProposal(agentContext, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: presentationDefinition, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + + test('Creates Presentation Exchange Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { format, attachment } = await pexFormatService.createRequest(agentContext, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: presentationDefinition, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + }) + + describe('Accept Proof Request', () => { + test('Accept a Presentation Exchange Proof Request', async () => { + const presentationDefinition = mockPresentationDefinition() + const { attachment: requestAttachment } = await pexFormatService.createRequest(agentContext, { + proofRecord: mockProofRecord(), + proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + }) + + const { attachment, format } = await pexFormatService.acceptRequest(agentContext, { + proofRecord: mockProofRecord(), + requestAttachment, + proofFormats: { presentationExchange: { credentials: { none: [] } } }, + }) + + expect(attachment).toMatchObject({ + id: expect.any(String), + mimeType: 'application/json', + data: { + json: {}, + }, + }) + + expect(format).toMatchObject({ + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }) + }) + }) +}) diff --git a/packages/presentation-exchange/jest.config.ts b/packages/presentation-exchange/jest.config.ts index 7b6ec7f1c5..34d28fd160 100644 --- a/packages/presentation-exchange/jest.config.ts +++ b/packages/presentation-exchange/jest.config.ts @@ -9,7 +9,6 @@ process.env.TZ = 'GMT' const config: Config.InitialOptions = { ...base, displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], } export default config diff --git a/packages/presentation-exchange/src/PresentationExchangeService.ts b/packages/presentation-exchange/src/PresentationExchangeService.ts index 8b9ba5dbcd..3f76c5923a 100644 --- a/packages/presentation-exchange/src/PresentationExchangeService.ts +++ b/packages/presentation-exchange/src/PresentationExchangeService.ts @@ -1,3 +1,5 @@ +/* eslint-disable import/no-cycle */ + import type { InputDescriptorToCredentials, PresentationSubmission } from './models' import type { AgentContext, @@ -10,14 +12,15 @@ import type { import type { IPresentationDefinition, PresentationSignCallBackParams, + Validated, VerifiablePresentationResult, } from '@sphereon/pex' import type { InputDescriptorV2, - PresentationSubmission as PexPresentationSubmission, + PresentationSubmission as SphereonPexPresentationSubmission, PresentationDefinitionV1, } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' +import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' import { getJwkFromKey, @@ -30,7 +33,7 @@ import { DidsApi, W3cCredentialRepository, } from '@aries-framework/core' -import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { injectable } from 'tsyringe' import { PresentationExchangeError } from './PresentationExchangeError' @@ -42,7 +45,13 @@ import { } from './utils' export type ProofStructure = Record>> -export type PresentationDefinition = IPresentationDefinition + +// Record is extended here so that we fulfill the requirements of generics that require it to be an object. +export type PresentationDefinition = IPresentationDefinition & Record + +export type PexPresentationSubmission = SphereonPexPresentationSubmission & Record + +export type VerifiablePresentation = IVerifiablePresentation & Record @injectable() export class PresentationExchangeService { @@ -61,6 +70,50 @@ export class PresentationExchangeService { return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) } + public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { + const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + } + + private formatValidated(v: Validated) { + return Array.isArray(v) + ? (v + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((m) => Boolean(m)) as Array) + : v.tag === Status.ERROR && typeof v.message === 'string' + ? [v.message] + : [] + } + /** * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. @@ -426,7 +479,7 @@ export class PresentationExchangeService { if (vpFormat === 'jwt_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition as PresentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), @@ -435,7 +488,11 @@ export class PresentationExchangeService { } else if (vpFormat === 'ldp_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofType: this.getProofTypeForLdpVc( + agentContext, + presentationDefinition as PresentationDefinition, + verificationMethod + ), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), diff --git a/packages/presentation-exchange/src/models/PresentationSubmission.ts b/packages/presentation-exchange/src/models/PresentationSubmission.ts index 28f9209e5a..aee409fe5a 100644 --- a/packages/presentation-exchange/src/models/PresentationSubmission.ts +++ b/packages/presentation-exchange/src/models/PresentationSubmission.ts @@ -1,6 +1,6 @@ import type { W3cCredentialRecord, W3cVerifiableCredential } from '@aries-framework/core' -export interface PresentationSubmission { +export type PresentationSubmission = { /** * Whether all requirements have been satisfied by the credentials in the wallet. */ @@ -36,7 +36,7 @@ export interface PresentationSubmission { * * Each submission represents a input descriptor. */ -export interface PresentationSubmissionRequirement { +export type PresentationSubmissionRequirement = { /** * Whether the requirement is satisfied. * @@ -88,7 +88,7 @@ export interface PresentationSubmissionRequirement { * A submission entry that satisfies a specific input descriptor from the * presentation definition. */ -export interface SubmissionEntry { +export type SubmissionEntry = { /** * The id of the input descriptor */ diff --git a/packages/presentation-exchange/src/utils/credentialSelection.ts b/packages/presentation-exchange/src/utils/credentialSelection.ts index 966af823c3..a6de97d2b8 100644 --- a/packages/presentation-exchange/src/utils/credentialSelection.ts +++ b/packages/presentation-exchange/src/utils/credentialSelection.ts @@ -3,11 +3,12 @@ import type { W3cCredentialRecord } from '@aries-framework/core' import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' -import { AriesFrameworkError } from '@aries-framework/core' import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' +import { PresentationExchangeError } from '../PresentationExchangeError' + import { getSphereonOriginalVerifiableCredential } from './transform' export async function selectCredentialsForRequest( @@ -18,7 +19,7 @@ export async function selectCredentialsForRequest( const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) if (!presentationDefinition) { - throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') + throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission') } const pex = new PEX() @@ -37,7 +38,9 @@ export async function selectCredentialsForRequest( verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { const credentialIndex = encodedCredentials.indexOf(encoded) const credentialRecord = credentialRecords[credentialIndex] - if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') + if (!credentialRecord) { + throw new PresentationExchangeError('Unable to find credential in credential records') + } return credentialRecord }), @@ -64,7 +67,7 @@ export async function selectCredentialsForRequest( // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) // I see this more as the fault of the presentation definition, as it should have at least some requirements. if (presentationSubmission.requirements.length === 0) { - throw new AriesFrameworkError( + throw new PresentationExchangeError( 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' ) } @@ -93,14 +96,14 @@ function getSubmissionRequirements( for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet if (submissionRequirement.from_nested) { - throw new AriesFrameworkError( + throw new PresentationExchangeError( "Presentation definition contains requirement using 'from_nested', which is not supported yet." ) } // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) { - throw new AriesFrameworkError("Missing 'from' in submission requirement match") + throw new PresentationExchangeError("Missing 'from' in submission requirement match") } if (submissionRequirement.rule === Rules.All) { @@ -154,7 +157,9 @@ function getSubmissionRequirementRuleAll( selectResults: W3cCredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + if (!submissionRequirement.from) { + throw new PresentationExchangeError("Missing 'from' in submission requirement match.") + } const selectedSubmission: PresentationSubmissionRequirement = { rule: Rules.All, @@ -192,7 +197,9 @@ function getSubmissionRequirementRulePick( selectResults: W3cCredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") + if (!submissionRequirement.from) { + throw new PresentationExchangeError("Missing 'from' in submission requirement match") + } const selectedSubmission: PresentationSubmissionRequirement = { rule: 'pick', @@ -283,9 +290,11 @@ function extractCredentialsFromMatch( const verifiableCredentials: Array = [] for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ - W3cCredentialRecord - ] + const [verifiableCredential] = jp.query( + { verifiableCredential: availableCredentials }, + vcPath + ) as Array + verifiableCredentials.push(verifiableCredential) } From e4a41c2d2082e5867a1bbcec398bd506551c034b Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 13 Dec 2023 16:20:15 +0100 Subject: [PATCH 05/17] feat(pex): created core PEX module Signed-off-by: Berend Sliedrecht --- .../modules/presentation-exchange/PresentationExchangeModule.ts | 1 - yarn.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts index dba83cd306..483fb1bc69 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts @@ -19,7 +19,6 @@ export class PresentationExchangeModule implements Module { "The 'PresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) - // Services dependencyManager.registerSingleton(PresentationExchangeService) } } diff --git a/yarn.lock b/yarn.lock index 7f2ecb49e3..44f7e18103 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,7 +2999,7 @@ dependencies: "@types/express" "*" -"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== From 12ca53b5e290fcb51717c05ebd6cea48a2ff40f2 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Mon, 18 Dec 2023 11:36:52 +0100 Subject: [PATCH 06/17] chore(proofs): use core pex implementation Signed-off-by: Berend Sliedrecht --- packages/core/package.json | 1 - .../PresentationExchangeProofFormat.ts | 6 +- .../PresentationExchangeProofFormatService.ts | 16 +- ...entationExchangeProofFormatService.test.ts | 5 +- .../services/PresentationExchangeService.ts | 54 +- packages/presentation-exchange/jest.config.ts | 14 - packages/presentation-exchange/package.json | 37 -- .../src/PresentationExchangeError.ts | 3 - .../src/PresentationExchangeModule.ts | 25 - .../src/PresentationExchangeService.ts | 538 ------------------ packages/presentation-exchange/src/index.ts | 4 - .../src/models/PresentationSubmission.ts | 119 ---- .../presentation-exchange/src/models/index.ts | 1 - .../src/utils/credentialSelection.ts | 309 ---------- .../presentation-exchange/src/utils/index.ts | 2 - .../src/utils/transform.ts | 78 --- .../presentation-exchange/tsconfig.build.json | 9 - packages/presentation-exchange/tsconfig.json | 8 - yarn.lock | 2 +- 19 files changed, 62 insertions(+), 1169 deletions(-) delete mode 100644 packages/presentation-exchange/jest.config.ts delete mode 100644 packages/presentation-exchange/package.json delete mode 100644 packages/presentation-exchange/src/PresentationExchangeError.ts delete mode 100644 packages/presentation-exchange/src/PresentationExchangeModule.ts delete mode 100644 packages/presentation-exchange/src/PresentationExchangeService.ts delete mode 100644 packages/presentation-exchange/src/index.ts delete mode 100644 packages/presentation-exchange/src/models/PresentationSubmission.ts delete mode 100644 packages/presentation-exchange/src/models/index.ts delete mode 100644 packages/presentation-exchange/src/utils/credentialSelection.ts delete mode 100644 packages/presentation-exchange/src/utils/index.ts delete mode 100644 packages/presentation-exchange/src/utils/transform.ts delete mode 100644 packages/presentation-exchange/tsconfig.build.json delete mode 100644 packages/presentation-exchange/tsconfig.json diff --git a/packages/core/package.json b/packages/core/package.json index 73380a3bc0..bbaada80e9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,7 +21,6 @@ "prepublishOnly": "yarn run build" }, "dependencies": { - "@aries-framework/presentation-exchange": "^0.4.2", "@digitalcredentials/jsonld": "^5.2.1", "@digitalcredentials/jsonld-signatures": "^9.3.1", "@digitalcredentials/vc": "^1.1.2", diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index ad773ae877..2de94ef170 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,6 +1,6 @@ import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../../vc' +import type { PresentationDefinition } from '../../services' import type { ProofFormat } from '../ProofFormat' -import type { PresentationDefinition } from '@aries-framework/presentation-exchange' export interface PresentationExchangeProofFormat extends ProofFormat { formatKey: 'presentationExchange' @@ -15,7 +15,9 @@ export interface PresentationExchangeProofFormat extends ProofFormat { version?: string } - createRequest: { presentationDefinition: PresentationDefinition } + createRequest: { + presentationDefinition: PresentationDefinition + } acceptRequest: { credentials: Record> diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index a40dbdc53c..7665c86682 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -1,10 +1,8 @@ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable import/no-cycle */ -/* eslint-disable workspaces/require-dependency */ - import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { W3cCredentialRecord } from '../../../vc' +import type { InputDescriptorToCredentials } from '../../models' +import type { PresentationDefinition, VerifiablePresentation } from '../../services' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -20,19 +18,13 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' -import type { - InputDescriptorToCredentials, - PexPresentationSubmission, - PresentationDefinition, - VerifiablePresentation, -} from '@aries-framework/presentation-exchange' - -import { PresentationExchangeService } from '@aries-framework/presentation-exchange' +import type { PresentationSubmission as PexPresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' import { deepEquality } from '../../../../utils' import { ProofFormatSpec } from '../../models' +import { PresentationExchangeService } from '../../services' const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 61f0903292..004f740d2f 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -1,13 +1,12 @@ import type { AgentContext } from '../../../../../agent' +import type { PresentationDefinition } from '../../../services' import type { ProofFormatService } from '../../ProofFormatService' import type { PresentationExchangeProofFormat } from '../PresentationExchangeProofFormat' -import type { PresentationDefinition } from '@aries-framework/presentation-exchange' - -import { PresentationExchangeModule } from '@aries-framework/presentation-exchange' import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' import { getAgentOptions } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' +import { PresentationExchangeModule } from '../../../../presentation-exchange' import { ProofsModule } from '../../../ProofsModule' import { ProofState } from '../../../models' import { V2ProofProtocol } from '../../../protocol' diff --git a/packages/core/src/modules/proofs/services/PresentationExchangeService.ts b/packages/core/src/modules/proofs/services/PresentationExchangeService.ts index 63cb0b5060..ed05534b91 100644 --- a/packages/core/src/modules/proofs/services/PresentationExchangeService.ts +++ b/packages/core/src/modules/proofs/services/PresentationExchangeService.ts @@ -6,6 +6,7 @@ import type { InputDescriptorToCredentials, PresentationSubmission } from '../mo import type { IPresentationDefinition, PresentationSignCallBackParams, + Validated, VerifiablePresentationResult, } from '@sphereon/pex' import type { @@ -13,15 +14,16 @@ import type { PresentationSubmission as PexPresentationSubmission, PresentationDefinitionV1, } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' +import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' -import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../../crypto' import { AriesFrameworkError } from '../../../error' import { JsonTransformer } from '../../../utils' import { getKeyFromVerificationMethod, DidsApi } from '../../dids' +import { PresentationExchangeError } from '../../presentation-exchange' import { SignatureSuiteRegistry, W3cPresentation, W3cCredentialService, ClaimFormat } from '../../vc' import { W3cCredentialRepository } from '../../vc/repository' import { selectCredentialsForRequest } from '../utils/credentialSelection' @@ -32,7 +34,9 @@ import { } from '../utils/transform' export type ProofStructure = Record>> -export type PresentationDefinition = IPresentationDefinition +export type PresentationDefinition = IPresentationDefinition & Record + +export type VerifiablePresentation = IVerifiablePresentation & Record @injectable() export class PresentationExchangeService { @@ -51,6 +55,50 @@ export class PresentationExchangeService { return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) } + public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { + const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + } + + private formatValidated(v: Validated) { + return Array.isArray(v) + ? (v + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((m) => Boolean(m)) as Array) + : v.tag === Status.ERROR && typeof v.message === 'string' + ? [v.message] + : [] + } + /** * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. diff --git a/packages/presentation-exchange/jest.config.ts b/packages/presentation-exchange/jest.config.ts deleted file mode 100644 index 34d28fd160..0000000000 --- a/packages/presentation-exchange/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Config } from '@jest/types' - -import base from '../../jest.config.base' - -import packageJson from './package.json' - -process.env.TZ = 'GMT' - -const config: Config.InitialOptions = { - ...base, - displayName: packageJson.name, -} - -export default config diff --git a/packages/presentation-exchange/package.json b/packages/presentation-exchange/package.json deleted file mode 100644 index f31f73b572..0000000000 --- a/packages/presentation-exchange/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@aries-framework/presentation-exchange", - "main": "build/index", - "types": "build/index", - "version": "0.4.2", - "files": [ - "build" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "homepage": "https://github.com/openwallet-foundation/agent-framework-javascript/tree/main/packages/presentation-exchange", - "repository": { - "type": "git", - "url": "https://github.com/openwallet-foundation/agent-framework-javascript", - "directory": "packages/presentation-exchange" - }, - "scripts": { - "build": "yarn run clean && yarn run compile", - "clean": "rimraf ./build", - "compile": "tsc -p tsconfig.build.json", - "prepublishOnly": "yarn run build" - }, - "dependencies": { - "@aries-framework/core": "^0.4.2", - "@sphereon/pex": "^2.2.2", - "@sphereon/pex-models": "^2.1.2", - "@sphereon/ssi-types": "^0.17.5", - "jsonpath": "^1.1.1", - "tsyringe": "^4.8.0" - }, - "devDependencies": { - "@types/jsonpath": "^0.2.4", - "typescript": "~4.9.5" - } -} diff --git a/packages/presentation-exchange/src/PresentationExchangeError.ts b/packages/presentation-exchange/src/PresentationExchangeError.ts deleted file mode 100644 index 5c52b67752..0000000000 --- a/packages/presentation-exchange/src/PresentationExchangeError.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AriesFrameworkError } from '@aries-framework/core' - -export class PresentationExchangeError extends AriesFrameworkError {} diff --git a/packages/presentation-exchange/src/PresentationExchangeModule.ts b/packages/presentation-exchange/src/PresentationExchangeModule.ts deleted file mode 100644 index efeb199aaa..0000000000 --- a/packages/presentation-exchange/src/PresentationExchangeModule.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { DependencyManager, Module } from '@aries-framework/core' - -import { AgentConfig } from '@aries-framework/core' - -import { PresentationExchangeService } from './PresentationExchangeService' - -/** - * @public - */ -export class PresentationExchangeModule implements Module { - /** - * Registers the dependencies of the presentation-exchange module on the dependency manager. - */ - public register(dependencyManager: DependencyManager) { - // Warn about experimental module - dependencyManager - .resolve(AgentConfig) - .logger.warn( - "The '@aries-framework/presentation-exchange' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." - ) - - // Services - dependencyManager.registerSingleton(PresentationExchangeService) - } -} diff --git a/packages/presentation-exchange/src/PresentationExchangeService.ts b/packages/presentation-exchange/src/PresentationExchangeService.ts deleted file mode 100644 index 3f76c5923a..0000000000 --- a/packages/presentation-exchange/src/PresentationExchangeService.ts +++ /dev/null @@ -1,538 +0,0 @@ -/* eslint-disable import/no-cycle */ - -import type { InputDescriptorToCredentials, PresentationSubmission } from './models' -import type { - AgentContext, - Query, - VerificationMethod, - W3cCredentialRecord, - W3cVerifiableCredential, - W3cVerifiablePresentation, -} from '@aries-framework/core' -import type { - IPresentationDefinition, - PresentationSignCallBackParams, - Validated, - VerifiablePresentationResult, -} from '@sphereon/pex' -import type { - InputDescriptorV2, - PresentationSubmission as SphereonPexPresentationSubmission, - PresentationDefinitionV1, -} from '@sphereon/pex-models' -import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' - -import { - getJwkFromKey, - JsonTransformer, - SignatureSuiteRegistry, - W3cPresentation, - W3cCredentialService, - ClaimFormat, - getKeyFromVerificationMethod, - DidsApi, - W3cCredentialRepository, -} from '@aries-framework/core' -import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' -import { injectable } from 'tsyringe' - -import { PresentationExchangeError } from './PresentationExchangeError' -import { - selectCredentialsForRequest, - getSphereonOriginalVerifiableCredential, - getSphereonW3cVerifiablePresentation, - getW3cVerifiablePresentationInstance, -} from './utils' - -export type ProofStructure = Record>> - -// Record is extended here so that we fulfill the requirements of generics that require it to be an object. -export type PresentationDefinition = IPresentationDefinition & Record - -export type PexPresentationSubmission = SphereonPexPresentationSubmission & Record - -export type VerifiablePresentation = IVerifiablePresentation & Record - -@injectable() -export class PresentationExchangeService { - private pex = new PEX() - - public async selectCredentialsForRequest( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition - ): Promise { - const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didRecords = await didsApi.getCreatedDids() - const holderDids = didRecords.map((didRecord) => didRecord.did) - - return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) - } - - public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { - const validation = PEX.validateDefinition(presentationDefinition) - const errorMessages = this.formatValidated(validation) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - - public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { - const validation = PEX.validateSubmission(presentationSubmission) - const errorMessages = this.formatValidated(validation) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - - public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { - const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) - - if (errors) { - const errorMessages = this.formatValidated(errors as Validated) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - } - - private formatValidated(v: Validated) { - return Array.isArray(v) - ? (v - .filter((r) => r.tag === Status.ERROR) - .map((r) => r.message) - .filter((m) => Boolean(m)) as Array) - : v.tag === Status.ERROR && typeof v.message === 'string' - ? [v.message] - : [] - } - - /** - * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the - * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. - */ - private async queryCredentialForPresentationDefinition( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition - ): Promise> { - const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const query: Array> = [] - const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) - - if (!presentationDefinitionVersion.version) { - throw new PresentationExchangeError( - `Unable to determine the Presentation Exchange version from the presentation definition. ${ - presentationDefinitionVersion.error ?? 'Unknown error' - }` - ) - } - - if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 - - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of pd.input_descriptors) { - for (const schema of inputDescriptor.schema) { - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], - }) - } - } - } else if (presentationDefinitionVersion.version === PEVersion.v2) { - // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. - // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need - // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. - } else { - throw new PresentationExchangeError( - `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` - ) - } - - // query the wallet ourselves first to avoid the need to query the pex library for all - // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) - - return credentialRecords - } - - private addCredentialToSubjectInputDescriptor( - subjectsToInputDescriptors: ProofStructure, - subjectId: string, - inputDescriptorId: string, - credential: W3cVerifiableCredential - ) { - const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} - const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - - credentials.push(credential) - inputDescriptorsToCredentials[inputDescriptorId] = credentials - subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials - } - - private getPresentationFormat( - presentationDefinition: PresentationDefinition, - credentials: Array - ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { - const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') - const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') - - const inputDescriptorsNotSupportingJwtVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.jwt_vc === undefined) - - const inputDescriptorsNotSupportingLdpVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.ldp_vc === undefined) - - if ( - allCredentialsAreJwtVc && - (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && - inputDescriptorsNotSupportingJwtVc.length === 0 - ) { - return ClaimFormat.JwtVp - } else if ( - allCredentialsAreLdpVc && - (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && - inputDescriptorsNotSupportingLdpVc.length === 0 - ) { - return ClaimFormat.LdpVp - } else { - throw new PresentationExchangeError( - 'No suitable presentation format found for the given presentation definition, and credentials' - ) - } - } - - public async createPresentation( - agentContext: AgentContext, - options: { - credentialsForInputDescriptor: InputDescriptorToCredentials - presentationDefinition: PresentationDefinition - challenge?: string - domain?: string - nonce?: string - } - ) { - const { presentationDefinition, challenge, nonce, domain } = options - - const proofStructure: ProofStructure = {} - - Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { - credentials.forEach((credential) => { - const subjectId = credential.credentialSubjectIds[0] - if (!subjectId) { - throw new PresentationExchangeError('Missing required credential subject for creating the presentation.') - } - - this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) - }) - }) - - const verifiablePresentationResultsWithFormat: Array<{ - verifiablePresentationResult: VerifiablePresentationResult - format: ClaimFormat.LdpVp | ClaimFormat.JwtVp - }> = [] - - const subjectToInputDescriptors = Object.entries(proofStructure) - for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { - // Determine a suitable verification method for the presentation - const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) - - if (!verificationMethod) { - throw new PresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) - } - - // We create a presentation for each subject - // Thus for each subject we need to filter all the related input descriptors and credentials - // FIXME: cast to V1, as tsc errors for strange reasons if not - const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( - (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials - ) - - // Get all the credentials associated with the input descriptors - const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flatMap((credentials) => credentials) - .map(getSphereonOriginalVerifiableCredential) - - const presentationDefinitionForSubject: PresentationDefinition = { - ...presentationDefinition, - input_descriptors: inputDescriptorsForSubject, - - // We remove the submission requirements, as it will otherwise fail to create the VP - submission_requirements: undefined, - } - - const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) - - // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? - // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? - const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinitionForSubject, - credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod, format), - { - holderDID: subjectId, - proofOptions: { challenge, domain, nonce }, - signatureOptions: { verificationMethod: verificationMethod?.id }, - presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, - } - ) - - verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new PresentationExchangeError('No verifiable presentations created.') - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new PresentationExchangeError('No verifiable presentations created.') - } - - if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { - throw new PresentationExchangeError('Invalid amount of verifiable presentations created.') - } - - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission - const presentationSubmission: PexPresentationSubmission = { - id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, - definition_id: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, - descriptor_map: [], - } - - for (const vpf of verifiablePresentationResultsWithFormat) { - const { verifiablePresentationResult } = vpf - presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) - } - - return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => - getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) - ), - presentationSubmission, - presentationSubmissionLocation: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, - } - } - - private getSigningAlgorithmFromVerificationMethod( - verificationMethod: VerificationMethod, - suitableAlgorithms?: Array - ) { - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - if (suitableAlgorithms) { - const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) - if (!possibleAlgorithms || possibleAlgorithms.length === 0) { - throw new PresentationExchangeError( - [ - `Found no suitable signing algorithm.`, - `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, - `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, - ].join('\n') - ) - } - } - - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) throw new PresentationExchangeError(`No supported algs for key type: ${key.keyType}`) - return alg - } - - private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition: Array, - inputDescriptorAlgorithms: Array> - ) { - const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() - const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => - inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) - ) - - const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => - algorithmsSatisfyingDescriptors.includes(alg) - ) - - if ( - algorithmsSatisfyingDefinition.length > 0 && - algorithmsSatisfyingDescriptors.length > 0 && - algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 - ) { - throw new PresentationExchangeError( - `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` - ) - } - - if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { - throw new PresentationExchangeError( - `No signature algorithm found for satisfying restrictions of the input descriptors.` - ) - } - - let suitableAlgorithms: Array | undefined - if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { - suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions - } else if (algorithmsSatisfyingDescriptors.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDescriptors - } else if (algorithmsSatisfyingDefinition.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDefinition - } - - return suitableAlgorithms - } - - private getSigningAlgorithmForJwtVc( - presentationDefinition: PresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] - - const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) - .filter((alg) => alg.length > 0) - - const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) - } - - private getProofTypeForLdpVc( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] - - const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) - .filter((alg) => alg.length > 0) - - const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - // For each of the supported algs, find the key types, then find the proof types - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { - throw new PresentationExchangeError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` - ) - } - - if (suitableSignatureSuites) { - if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { - throw new PresentationExchangeError( - [ - 'No possible signature suite found for the given verification method.', - `Verification method type: ${verificationMethod.type}`, - `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, - `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, - ].join('\n') - ) - } - - return supportedSignatureSuite.proofType - } - - return supportedSignatureSuite.proofType - } - - public getPresentationSignCallback( - agentContext: AgentContext, - verificationMethod: VerificationMethod, - vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp - ) { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - - return async (callBackParams: PresentationSignCallBackParams) => { - // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options, presentationDefinition } = callBackParams - const { challenge, domain, nonce } = options.proofOptions ?? {} - const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} - - if (verificationMethodId && verificationMethodId !== verificationMethod.id) { - throw new PresentationExchangeError( - `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` - ) - } - - // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - const presentationToSign = { ...presentationJson, presentation_submission: undefined } - - let signedPresentation: W3cVerifiablePresentation - if (vpFormat === 'jwt_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition as PresentationDefinition, verificationMethod), - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else if (vpFormat === 'ldp_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc( - agentContext, - presentationDefinition as PresentationDefinition, - verificationMethod - ), - proofPurpose: 'authentication', - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else { - throw new PresentationExchangeError( - `Only JWT credentials or JSONLD credentials are supported for a single presentation.` - ) - } - - return getSphereonW3cVerifiablePresentation(signedPresentation) - } - } - - private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - - if (!subjectId.startsWith('did:')) { - throw new PresentationExchangeError( - `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` - ) - } - - const didDocument = await didsApi.resolveDidDocument(subjectId) - - if (!didDocument.authentication || didDocument.authentication.length === 0) { - throw new PresentationExchangeError( - `No authentication verificationMethods found for did ${subjectId} in did document` - ) - } - - // the signature suite to use for the presentation is dependant on the credentials we share. - // 1. Get the verification method for this given proof purpose in this DID document - let [verificationMethod] = didDocument.authentication - if (typeof verificationMethod === 'string') { - verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) - } - - return verificationMethod - } -} diff --git a/packages/presentation-exchange/src/index.ts b/packages/presentation-exchange/src/index.ts deleted file mode 100644 index 0bb3c76aae..0000000000 --- a/packages/presentation-exchange/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './PresentationExchangeError' -export * from './PresentationExchangeModule' -export * from './PresentationExchangeService' -export * from './models' diff --git a/packages/presentation-exchange/src/models/PresentationSubmission.ts b/packages/presentation-exchange/src/models/PresentationSubmission.ts deleted file mode 100644 index aee409fe5a..0000000000 --- a/packages/presentation-exchange/src/models/PresentationSubmission.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { W3cCredentialRecord, W3cVerifiableCredential } from '@aries-framework/core' - -export type PresentationSubmission = { - /** - * Whether all requirements have been satisfied by the credentials in the wallet. - */ - areRequirementsSatisfied: boolean - - /** - * The requirements for the presentation definition. If the `areRequirementsSatisfied` value - * is `false`, this list will still be populated with requirements, but won't contain credentials - * for all requirements. This can be useful to display the missing credentials for a presentation - * definition to be satisfied. - * - * NOTE: Presentation definition requirements can be really complex as there's a lot of different - * combinations that are possible. The structure doesn't include all possible combinations yet that - * could satisfy a presentation definition. - */ - requirements: PresentationSubmissionRequirement[] - - /** - * Name of the presentation definition - */ - name?: string - - /** - * Purpose of the presentation definition. - */ - purpose?: string -} - -/** - * A requirement for the presentation submission. A requirement - * is a group of input descriptors that together fulfill a requirement - * from the presentation definition. - * - * Each submission represents a input descriptor. - */ -export type PresentationSubmissionRequirement = { - /** - * Whether the requirement is satisfied. - * - * If the requirement is not satisfied, the submission will still contain - * entries, but the `verifiableCredentials` list will be empty. - */ - isRequirementSatisfied: boolean - - /** - * Name of the requirement - */ - name?: string - - /** - * Purpose of the requirement - */ - purpose?: string - - /** - * Array of objects, where each entry contains a credential that will be part - * of the submission. - * - * NOTE: if the `isRequirementSatisfied` is `false` the submission list will - * contain entries where the verifiable credential list is empty. In this case it could also - * contain more entries than are actually needed (as you sometimes can choose from - * e.g. 4 types of credentials and need to submit at least two). If - * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value - * to see how many of those submissions needed. - */ - submissionEntry: SubmissionEntry[] - - /** - * The number of submission entries that are needed to fulfill the requirement. - * If `isRequirementSatisfied` is `true`, the submission list will always be equal - * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of - * submissions could be longer. - */ - needsCount: number - - /** - * The rule that is used to select the credentials for the submission. - * If the rule is `pick`, the user can select which credentials to use for the submission. - * If the rule is `all`, all credentials that satisfy the input descriptor will be used. - */ - rule: 'pick' | 'all' -} - -/** - * A submission entry that satisfies a specific input descriptor from the - * presentation definition. - */ -export type SubmissionEntry = { - /** - * The id of the input descriptor - */ - inputDescriptorId: string - - /** - * Name of the input descriptor - */ - name?: string - - /** - * Purpose of the input descriptor - */ - purpose?: string - - /** - * The verifiable credentials that satisfy the input descriptor. - * - * If the value is an empty list, it means the input descriptor could - * not be satisfied. - */ - verifiableCredentials: W3cCredentialRecord[] -} - -/** - * Mapping of selected credentials for an input descriptor - */ -export type InputDescriptorToCredentials = Record> diff --git a/packages/presentation-exchange/src/models/index.ts b/packages/presentation-exchange/src/models/index.ts deleted file mode 100644 index 47247cbbc9..0000000000 --- a/packages/presentation-exchange/src/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './PresentationSubmission' diff --git a/packages/presentation-exchange/src/utils/credentialSelection.ts b/packages/presentation-exchange/src/utils/credentialSelection.ts deleted file mode 100644 index a6de97d2b8..0000000000 --- a/packages/presentation-exchange/src/utils/credentialSelection.ts +++ /dev/null @@ -1,309 +0,0 @@ -import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models' -import type { W3cCredentialRecord } from '@aries-framework/core' -import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' -import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' - -import { PEX } from '@sphereon/pex' -import { Rules } from '@sphereon/pex-models' -import { default as jp } from 'jsonpath' - -import { PresentationExchangeError } from '../PresentationExchangeError' - -import { getSphereonOriginalVerifiableCredential } from './transform' - -export async function selectCredentialsForRequest( - presentationDefinition: IPresentationDefinition, - credentialRecords: Array, - holderDIDs: Array -): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - - if (!presentationDefinition) { - throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission') - } - - const pex = new PEX() - - // FIXME: there is a function for this in the VP library, but it is not usable atm - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { - holderDIDs, - // limitDisclosureSignatureSuites: [], - // restrictToDIDMethods, - // restrictToFormats - }) - - const selectResults = { - ...selectResultsRaw, - // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialIndex = encodedCredentials.indexOf(encoded) - const credentialRecord = credentialRecords[credentialIndex] - if (!credentialRecord) { - throw new PresentationExchangeError('Unable to find credential in credential records') - } - - return credentialRecord - }), - } - - const presentationSubmission: PresentationSubmission = { - requirements: [], - areRequirementsSatisfied: false, - name: presentationDefinition.name, - purpose: presentationDefinition.purpose, - } - - // If there's no submission requirements, ALL input descriptors MUST be satisfied - if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { - presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( - presentationDefinition.input_descriptors, - selectResults - ) - } else { - presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) - } - - // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error - // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) - // I see this more as the fault of the presentation definition, as it should have at least some requirements. - if (presentationSubmission.requirements.length === 0) { - throw new PresentationExchangeError( - 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' - ) - } - if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { - return presentationSubmission - } - - return { - ...presentationSubmission, - - // If all requirements are satisfied, the presentation submission is satisfied - areRequirementsSatisfied: presentationSubmission.requirements.every( - (requirement) => requirement.isRequirementSatisfied - ), - } -} - -function getSubmissionRequirements( - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] - - // There are submission requirements, so we need to select the input_descriptors - // based on the submission requirements - for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { - // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet - if (submissionRequirement.from_nested) { - throw new PresentationExchangeError( - "Presentation definition contains requirement using 'from_nested', which is not supported yet." - ) - } - - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new PresentationExchangeError("Missing 'from' in submission requirement match") - } - - if (submissionRequirement.rule === Rules.All) { - const selectedSubmission = getSubmissionRequirementRuleAll( - submissionRequirement, - presentationDefinition, - selectResults - ) - submissionRequirements.push(selectedSubmission) - } else { - const selectedSubmission = getSubmissionRequirementRulePick( - submissionRequirement, - presentationDefinition, - selectResults - ) - - submissionRequirements.push(selectedSubmission) - } - } - - // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) - // We use minimization strategy, and thus only disclose the minimum amount of information - const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) - - return requirementsWithCredentials -} - -function getSubmissionRequirementsForAllInputDescriptors( - inputDescriptors: Array | Array, - selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] - - for (const inputDescriptor of inputDescriptors) { - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - submissionRequirements.push({ - rule: Rules.Pick, - needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, - submissionEntry: [submission], - isRequirementSatisfied: submission.verifiableCredentials.length >= 1, - }) - } - - return submissionRequirements -} - -function getSubmissionRequirementRuleAll( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new PresentationExchangeError("Missing 'from' in submission requirement match.") - } - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: Rules.All, - needsCount: 0, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - submissionEntry: [], - isRequirementSatisfied: false, - } - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - // Rule ALL, so for every input descriptor that matches in this group, we need to add it - selectedSubmission.needsCount += 1 - selectedSubmission.submissionEntry.push(submission) - } - - return { - ...selectedSubmission, - - // If all submissions have a credential, the requirement is satisfied - isRequirementSatisfied: selectedSubmission.submissionEntry.every( - (submission) => submission.verifiableCredentials.length >= 1 - ), - } -} - -function getSubmissionRequirementRulePick( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new PresentationExchangeError("Missing 'from' in submission requirement match") - } - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: 'pick', - needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - // If there's no count, min, or max we assume one credential is required for submission - // however, the exact behavior is not specified in the spec - submissionEntry: [], - isRequirementSatisfied: false, - } - - const satisfiedSubmissions: Array = [] - const unsatisfiedSubmissions: Array = [] - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - if (submission.verifiableCredentials.length >= 1) { - satisfiedSubmissions.push(submission) - } else { - unsatisfiedSubmissions.push(submission) - } - - // If we have found enough credentials to satisfy the requirement, we could stop - // but the user may not want the first x that match, so we continue and return all matches - // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break - } - - return { - ...selectedSubmission, - - // If there are enough satisfied submissions, the requirement is satisfied - isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, - - // if the requirement is satisfied, we only need to return the satisfied submissions - // however if the requirement is not satisfied, we include all entries so the wallet could - // render which credentials are missing. - submission: - satisfiedSubmissions.length >= selectedSubmission.needsCount - ? satisfiedSubmissions - : [...satisfiedSubmissions, ...unsatisfiedSubmissions], - } -} - -function getSubmissionForInputDescriptor( - inputDescriptor: InputDescriptorV1 | InputDescriptorV2, - selectResults: W3cCredentialRecordSelectResults -): SubmissionEntry { - // https://github.com/Sphereon-Opensource/PEX/issues/116 - // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it - const matchesForInputDescriptor = selectResults.matches?.filter( - (m) => - m.name === inputDescriptor.id || - // FIXME: this is not collision proof as the name doesn't have to be unique - m.name === inputDescriptor.name - ) - - const submissionEntry: SubmissionEntry = { - inputDescriptorId: inputDescriptor.id, - name: inputDescriptor.name, - purpose: inputDescriptor.purpose, - verifiableCredentials: [], - } - - // return early if no matches. - if (!matchesForInputDescriptor?.length) return submissionEntry - - // FIXME: This can return multiple credentials for multiple input_descriptors, - // which I think is a bug in the PEX library - // Extract all credentials from the match - const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => - extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) - ) - - submissionEntry.verifiableCredentials = verifiableCredentials - - return submissionEntry -} - -function extractCredentialsFromMatch( - match: SubmissionRequirementMatch, - availableCredentials?: Array -) { - const verifiableCredentials: Array = [] - - for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query( - { verifiableCredential: availableCredentials }, - vcPath - ) as Array - - verifiableCredentials.push(verifiableCredential) - } - - return verifiableCredentials -} - -/** - * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential - */ -export type W3cCredentialRecordSelectResults = Omit & { - verifiableCredential?: Array -} diff --git a/packages/presentation-exchange/src/utils/index.ts b/packages/presentation-exchange/src/utils/index.ts deleted file mode 100644 index aaf44fa1b6..0000000000 --- a/packages/presentation-exchange/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './transform' -export * from './credentialSelection' diff --git a/packages/presentation-exchange/src/utils/transform.ts b/packages/presentation-exchange/src/utils/transform.ts deleted file mode 100644 index a97513b9be..0000000000 --- a/packages/presentation-exchange/src/utils/transform.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' -import type { - OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, - W3CVerifiableCredential as SphereonW3cVerifiableCredential, - W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, -} from '@sphereon/ssi-types' - -import { - AriesFrameworkError, - JsonTransformer, - W3cJsonLdVerifiableCredential, - W3cJsonLdVerifiablePresentation, - W3cJwtVerifiableCredential, - W3cJwtVerifiablePresentation, - ClaimFormat, -} from '@aries-framework/core' - -export function getSphereonOriginalVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonOriginalVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getSphereonW3cVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonW3cVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getSphereonW3cVerifiablePresentation( - w3cVerifiablePresentation: W3cVerifiablePresentation -): SphereonW3cVerifiablePresentation { - if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { - return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation - } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { - return w3cVerifiablePresentation.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getW3cVerifiablePresentationInstance( - w3cVerifiablePresentation: SphereonW3cVerifiablePresentation -): W3cVerifiablePresentation { - if (typeof w3cVerifiablePresentation === 'string') { - return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) - } else { - return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) - } -} - -export function getW3cVerifiableCredentialInstance( - w3cVerifiableCredential: SphereonW3cVerifiableCredential -): W3cVerifiableCredential { - if (typeof w3cVerifiableCredential === 'string') { - return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) - } else { - return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) - } -} diff --git a/packages/presentation-exchange/tsconfig.build.json b/packages/presentation-exchange/tsconfig.build.json deleted file mode 100644 index 0a015be666..0000000000 --- a/packages/presentation-exchange/tsconfig.build.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - - "compilerOptions": { - "outDir": "./build" - }, - - "include": ["src/**/*", "types"] -} diff --git a/packages/presentation-exchange/tsconfig.json b/packages/presentation-exchange/tsconfig.json deleted file mode 100644 index 93d9dd32b5..0000000000 --- a/packages/presentation-exchange/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "allowJs": false, - "typeRoots": ["../../node_modules/@types", "src/types"], - "types": ["jest"] - } -} diff --git a/yarn.lock b/yarn.lock index 44f7e18103..7f2ecb49e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,7 +2999,7 @@ dependencies: "@types/express" "*" -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.18.8": +"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== From 802a0162333f356a5854944a0f50e4f502b7c804 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Tue, 19 Dec 2023 13:50:02 +0100 Subject: [PATCH 07/17] feat(present-proof): working initial full flow Signed-off-by: Berend Sliedrecht --- packages/core/package.json | 4 +- .../PresentationExchangeService.ts | 78 ++- .../utils/credentialSelection.ts | 12 +- .../PresentationExchangeProofFormat.ts | 6 +- .../PresentationExchangeProofFormatService.ts | 62 ++- ...entationExchangeProofFormatService.test.ts | 55 +- packages/core/src/modules/proofs/index.ts | 3 - .../proofs/protocol/v2/__tests__/fixtures.ts | 10 + ...entation-exchange-presentation.e2e.test.ts | 451 +++++++++++++++ .../services/PresentationExchangeService.ts | 515 ------------------ .../core/src/modules/proofs/services/index.ts | 1 - packages/core/tests/jsonld.ts | 5 + 12 files changed, 616 insertions(+), 586 deletions(-) create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts create mode 100644 packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts delete mode 100644 packages/core/src/modules/proofs/services/PresentationExchangeService.ts delete mode 100644 packages/core/src/modules/proofs/services/index.ts diff --git a/packages/core/package.json b/packages/core/package.json index bbaada80e9..04813c5fdd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,7 +3,9 @@ "main": "build/index", "types": "build/index", "version": "0.4.2", - "files": ["build"], + "files": [ + "build" + ], "license": "Apache-2.0", "publishConfig": { "access": "public" diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts index 5b3f1d54f6..b99c2963ad 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts @@ -6,6 +6,7 @@ import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresent import type { IPresentationDefinition, PresentationSignCallBackParams, + Validated, VerifiablePresentationResult, } from '@sphereon/pex' import type { @@ -13,9 +14,9 @@ import type { PresentationSubmission as PexPresentationSubmission, PresentationDefinitionV1, } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' +import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' -import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' @@ -38,7 +39,9 @@ import { } from './utils' export type ProofStructure = Record>> -export type PresentationDefinition = IPresentationDefinition +export type PresentationDefinition = IPresentationDefinition & Record + +export type VerifiablePresentation = IVerifiablePresentation & Record @injectable() export class PresentationExchangeService { @@ -57,6 +60,50 @@ export class PresentationExchangeService { return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) } + public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { + const validation = PEX.validateDefinition(presentationDefinition) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { + const validation = PEX.validateSubmission(presentationSubmission) + const errorMessages = this.formatValidated(validation) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + + public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { + const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) + + if (errors) { + const errorMessages = this.formatValidated(errors as Validated) + if (errorMessages.length > 0) { + throw new PresentationExchangeError( + `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` + ) + } + } + } + + private formatValidated(v: Validated) { + return Array.isArray(v) + ? (v + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((m) => Boolean(m)) as Array) + : v.tag === Status.ERROR && typeof v.message === 'string' + ? [v.message] + : [] + } + /** * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. @@ -100,9 +147,12 @@ export class PresentationExchangeService { // query the wallet ourselves first to avoid the need to query the pex library for all // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) + const credentialRecords = + query.length > 0 + ? await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + : await w3cCredentialRepository.getAll(agentContext) return credentialRecords } @@ -237,15 +287,10 @@ export class PresentationExchangeService { throw new PresentationExchangeError('No verifiable presentations created.') } - if (!verifiablePresentationResultsWithFormat[0]) { - throw new PresentationExchangeError('No verifiable presentations created.') - } - if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { throw new PresentationExchangeError('Invalid amount of verifiable presentations created.') } - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission const presentationSubmission: PexPresentationSubmission = { id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, definition_id: @@ -416,13 +461,14 @@ export class PresentationExchangeService { } // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - const presentationToSign = { ...presentationJson, presentation_submission: undefined } + const presentationToSign = { ...presentationJson } + delete presentationToSign['presentation_submission'] let signedPresentation: W3cVerifiablePresentation if (vpFormat === 'jwt_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition as PresentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), @@ -431,7 +477,11 @@ export class PresentationExchangeService { } else if (vpFormat === 'ldp_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), + proofType: this.getProofTypeForLdpVc( + agentContext, + presentationDefinition as PresentationDefinition, + verificationMethod + ), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), diff --git a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts index fdec050b9e..5339c2d261 100644 --- a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts @@ -7,6 +7,7 @@ import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' +import { deepEquality } from '../../../utils' import { PresentationExchangeError } from '../PresentationExchangeError' import { getSphereonOriginalVerifiableCredential } from './transform' @@ -16,14 +17,14 @@ export async function selectCredentialsForRequest( credentialRecords: Array, holderDIDs: Array ): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - if (!presentationDefinition) { throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission.') } const pex = new PEX() + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) + // FIXME: there is a function for this in the VP library, but it is not usable atm const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { holderDIDs, @@ -36,8 +37,11 @@ export async function selectCredentialsForRequest( ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialIndex = encodedCredentials.indexOf(encoded) - const credentialRecord = credentialRecords[credentialIndex] + const credentialRecord = credentialRecords.find((record) => { + const originalVc = getSphereonOriginalVerifiableCredential(record.credential) + return deepEquality(originalVc, encoded) + }) + if (!credentialRecord) { throw new PresentationExchangeError('Unable to find credential in credential records.') } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index 2de94ef170..81c64f6655 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,5 +1,5 @@ -import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../../vc' -import type { PresentationDefinition } from '../../services' +import type { PresentationDefinition } from '../../../presentation-exchange' +import type { W3cCredentialRecord } from '../../../vc' import type { ProofFormat } from '../ProofFormat' export interface PresentationExchangeProofFormat extends ProofFormat { @@ -20,7 +20,7 @@ export interface PresentationExchangeProofFormat extends ProofFormat { } acceptRequest: { - credentials: Record> + credentials: Array } getCredentialsForRequest: { diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index 7665c86682..baf67a8192 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -1,8 +1,11 @@ import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' -import type { W3cCredentialRecord } from '../../../vc' +import type { + PresentationDefinition, + VerifiablePresentation, +} from '../../../presentation-exchange/PresentationExchangeService' +import type { W3cCredentialRecord, W3cVerifiablePresentation } from '../../../vc' import type { InputDescriptorToCredentials } from '../../models' -import type { PresentationDefinition, VerifiablePresentation } from '../../services' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -18,13 +21,13 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' -import type { PresentationSubmission as PexPresentationSubmission } from '@sphereon/pex-models' +import type { PresentationSubmission as PexPresentationSubmission, PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' import { deepEquality } from '../../../../utils' +import { PresentationExchangeService } from '../../../presentation-exchange/PresentationExchangeService' import { ProofFormatSpec } from '../../models' -import { PresentationExchangeService } from '../../services' const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' @@ -94,7 +97,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic ps.validatePresentationDefinition(presentationDefinition) - const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + const attachment = this.getFormatData({ presentation_definition: presentationDefinition }, format.attachmentId) return { format, attachment } } @@ -120,15 +123,22 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const attachment = this.getFormatData(presentationDefinition, format.attachmentId) + const options = { challenge: 'TODO' } + + const attachment = this.getFormatData( + { options, presentation_definition: presentationDefinition }, + format.attachmentId + ) return { attachment, format } } public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { const ps = this.presentationExchangeService(agentContext) - const proposal = attachment.getDataAsJson() - ps.validatePresentationDefinition(proposal) + const { presentation_definition: presentationDefinition } = attachment.getDataAsJson<{ + presentation_definition: PresentationDefinition + }>() + ps.validatePresentationDefinition(presentationDefinition) } public async acceptRequest( @@ -148,7 +158,10 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const presentationDefinition = requestAttachment.getDataAsJson() + const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ + presentation_definition: PresentationDefinition + options?: { challenge?: string; domain?: string } + }>() const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest( agentContext, @@ -172,7 +185,16 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic credentialsForInputDescriptor: credentials, }) - const attachment = this.getFormatData(presentation, format.attachmentId) + if (presentation.verifiablePresentations.length > 1) { + throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') + } + + const data = { + presentation_submission: presentation.presentationSubmission, + ...presentation.verifiablePresentations[0], + } + + const attachment = this.getFormatData(data, format.attachmentId) return { attachment, format } } @@ -182,16 +204,20 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic { requestAttachment, attachment }: ProofFormatProcessPresentationOptions ): Promise { const ps = this.presentationExchangeService(agentContext) - const presentationDefinition = requestAttachment.getDataAsJson() - const presentation = attachment.getDataAsJson() + const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ + presentation_definition: PresentationDefinition + }>() + const presentation = attachment.getDataAsJson< + W3cVerifiablePresentation & { presentation_submission: PresentationSubmission } + >() try { ps.validatePresentationDefinition(presentationDefinition) if (presentation.presentation_submission) { - ps.validatePresentationSubmission(presentation.presentation_submission as unknown as PexPresentationSubmission) + ps.validatePresentationSubmission(presentation.presentation_submission) } - ps.validatePresentation(presentationDefinition, presentation) + ps.validatePresentation(presentationDefinition, presentation as unknown as VerifiablePresentation) return true } catch (e) { agentContext.config.logger.error(e) @@ -204,7 +230,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic { requestAttachment }: ProofFormatGetCredentialsForRequestOptions ): Promise> { const ps = this.presentationExchangeService(agentContext) - const presentationDefinition = requestAttachment.getDataAsJson() + const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ + presentation_definition: PresentationDefinition + }>() ps.validatePresentationDefinition(presentationDefinition) @@ -222,7 +250,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions ): Promise> { const ps = this.presentationExchangeService(agentContext) - const presentationDefinition = requestAttachment.getDataAsJson() + const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ + presentation_definition: PresentationDefinition + }>() ps.validatePresentationDefinition(presentationDefinition) diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 004f740d2f..0c0bfeb46f 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -1,12 +1,11 @@ -import type { AgentContext } from '../../../../../agent' -import type { PresentationDefinition } from '../../../services' +import type { PresentationDefinition } from '../../../../presentation-exchange' import type { ProofFormatService } from '../../ProofFormatService' import type { PresentationExchangeProofFormat } from '../PresentationExchangeProofFormat' import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' -import { getAgentOptions } from '../../../../../../tests' +import { agentDependencies, getAgentConfig } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' -import { PresentationExchangeModule } from '../../../../presentation-exchange' +import { PresentationExchangeModule, PresentationExchangeService } from '../../../../presentation-exchange' import { ProofsModule } from '../../../ProofsModule' import { ProofState } from '../../../models' import { V2ProofProtocol } from '../../../protocol' @@ -45,35 +44,33 @@ const mockPresentationDefinition = (): PresentationDefinition => ({ describe('Presentation Exchange ProofFormatService', () => { let pexFormatService: ProofFormatService - let agentContext: AgentContext - - beforeEach(async () => { - const agent = new Agent( - getAgentOptions( - 'PresentationExchangeProofFormatService', - {}, - { - pex: new PresentationExchangeModule(), - proofs: new ProofsModule({ - proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], - }), - ...getIndySdkModules(), - } - ) - ) + let agent: Agent + + beforeAll(async () => { + agent = new Agent({ + config: getAgentConfig('PresentationExchangeProofFormatService'), + modules: { + someModule: new PresentationExchangeModule(), + proofs: new ProofsModule({ + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], + }), + ...getIndySdkModules(), + }, + dependencies: agentDependencies, + }) await agent.initialize() - agentContext = agent.context + agent.dependencyManager.resolve(PresentationExchangeService) pexFormatService = agent.dependencyManager.resolve(PresentationExchangeProofFormatService) }) describe('Create Presentation Exchange Proof Proposal / Request', () => { test('Creates Presentation Exchange Proposal', async () => { const presentationDefinition = mockPresentationDefinition() - const { format, attachment } = await pexFormatService.createProposal(agentContext, { + const { format, attachment } = await pexFormatService.createProposal(agent.context, { proofRecord: mockProofRecord(), - proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + proofFormats: { presentationExchange: { presentationDefinition } }, }) expect(attachment).toMatchObject({ @@ -92,9 +89,9 @@ describe('Presentation Exchange ProofFormatService', () => { test('Creates Presentation Exchange Request', async () => { const presentationDefinition = mockPresentationDefinition() - const { format, attachment } = await pexFormatService.createRequest(agentContext, { + const { format, attachment } = await pexFormatService.createRequest(agent.context, { proofRecord: mockProofRecord(), - proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + proofFormats: { presentationExchange: { presentationDefinition } }, }) expect(attachment).toMatchObject({ @@ -115,15 +112,15 @@ describe('Presentation Exchange ProofFormatService', () => { describe('Accept Proof Request', () => { test('Accept a Presentation Exchange Proof Request', async () => { const presentationDefinition = mockPresentationDefinition() - const { attachment: requestAttachment } = await pexFormatService.createRequest(agentContext, { + const { attachment: requestAttachment } = await pexFormatService.createRequest(agent.context, { proofRecord: mockProofRecord(), - proofFormats: { presentationExchange: { presentationDefinition: presentationDefinition } }, + proofFormats: { presentationExchange: { presentationDefinition } }, }) - const { attachment, format } = await pexFormatService.acceptRequest(agentContext, { + const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { proofRecord: mockProofRecord(), requestAttachment, - proofFormats: { presentationExchange: { credentials: { none: [] } } }, + proofFormats: { presentationExchange: { credentials: [] } }, }) expect(attachment).toMatchObject({ diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts index a6c8a7eeab..30eb44ba0f 100644 --- a/packages/core/src/modules/proofs/index.ts +++ b/packages/core/src/modules/proofs/index.ts @@ -12,6 +12,3 @@ export * from './ProofsApiOptions' // Module export * from './ProofsModule' export * from './ProofsModuleConfig' - -// Services -export * from './services' diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts new file mode 100644 index 0000000000..4c776b09da --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts @@ -0,0 +1,10 @@ +export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { + constraints: { + fields: [ + { + path: ['$.credentialSubject.degree.type'], + }, + ], + }, + id: 'citizenship_input_1', +} diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts new file mode 100644 index 0000000000..92fe9d8f22 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -0,0 +1,451 @@ +import type { getJsonLdModules } from '../../../../../../tests' +import type { Agent } from '../../../../../agent/Agent' +import type { ProofExchangeRecord } from '../../../repository/ProofExchangeRecord' + +import { waitForCredentialRecord, setupJsonLdTests, waitForProofExchangeRecord } from '../../../../../../tests' +import testLogger from '../../../../../../tests/logger' +import { KeyType } from '../../../../../crypto' +import { DidCommMessageRepository } from '../../../../../storage' +import { TypedArrayEncoder } from '../../../../../utils' +import { AutoAcceptCredential, CredentialState } from '../../../../credentials' +import { CREDENTIALS_CONTEXT_V1_URL } from '../../../../vc' +import { ProofState } from '../../../models/ProofState' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposePresentationMessage } from '../messages/V2ProposePresentationMessage' + +import { TEST_INPUT_DESCRIPTORS_CITIZENSHIP } from './fixtures' + +const jsonld = { + credential: { + '@context': [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + }, + options: { + proofType: 'Ed25519Signature2018', + proofPurpose: 'assertionMethod', + }, +} + +describe('Present Proof', () => { + let proverAgent: Agent> + let issuerAgent: Agent> + let verifierAgent: Agent> + + let verifierProverConnectionId: string + let issuerProverConnectionId: string + let proverVerifierConnectionId: string + + let verifierProofExchangeRecord: ProofExchangeRecord + let proverProofExchangeRecord: ProofExchangeRecord + + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ + holderAgent: proverAgent, + issuerAgent, + verifierAgent, + verifierHolderConnectionId: verifierProverConnectionId, + issuerHolderConnectionId: issuerProverConnectionId, + holderVerifierConnectionId: proverVerifierConnectionId, + } = await setupJsonLdTests({ + holderName: 'presentation exchange prover agent', + issuerName: 'presentation exchange issuer agent', + verifierName: 'presentation exchange verifier agent', + createConnections: true, + autoAcceptCredentials: AutoAcceptCredential.Always, + })) + + await issuerAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await proverAgent.wallet.createKey({ + privateKey: TypedArrayEncoder.fromString('testseed000000000000000000000001'), + keyType: KeyType.Ed25519, + }) + + await issuerAgent.credentials.offerCredential({ + connectionId: issuerProverConnectionId, + protocolVersion: 'v2', + credentialFormats: { jsonld }, + }) + + await waitForCredentialRecord(proverAgent, { state: CredentialState.Done }) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await proverAgent.shutdown() + await proverAgent.wallet.delete() + await verifierAgent.shutdown() + await verifierAgent.wallet.delete() + }) + + test(`Prover Creates and sends Proof Proposal to a Verifier`, async () => { + testLogger.test('Prover sends proof proposal to a Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + state: ProofState.ProposalReceived, + }) + + proverProofExchangeRecord = await proverAgent.proofs.proposeProof({ + connectionId: proverVerifierConnectionId, + protocolVersion: 'v2', + proofFormats: { + presentationExchange: { + presentationDefinition: { + id: 'e950bfe5-d7ec-4303-ad61-6983fb976ac9', + input_descriptors: [TEST_INPUT_DESCRIPTORS_CITIZENSHIP], + }, + }, + }, + comment: 'V2 Presentation Exchange propose proof test', + }) + + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + didCommMessageRepository = proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + proposalAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + input_descriptors: expect.any(Array), + }, + }, + }, + ], + id: expect.any(String), + comment: 'V2 Presentation Exchange propose proof test', + }) + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the Proposal send by the Prover`, async () => { + const proverPresentationRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Verifier accepts presentation proposal from the Prover') + verifierProofExchangeRecord = await verifierAgent.proofs.acceptProposal({ + proofRecordId: verifierProofExchangeRecord.id, + }) + + testLogger.test('Prover waits for proof request from the Verifier') + proverProofExchangeRecord = await proverPresentationRecordPromise + + didCommMessageRepository = proverAgent.dependencyManager.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage(proverAgent.context, { + associatedRecordId: proverProofExchangeRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + id: expect.any(String), + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/definitions@v1.0', + }, + ], + requestAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + presentation_definition: { + id: expect.any(String), + input_descriptors: [ + { + id: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.id, + constraints: { + fields: TEST_INPUT_DESCRIPTORS_CITIZENSHIP.constraints.fields, + }, + }, + ], + }, + }, + }, + }, + ], + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: 'v2', + }) + }) + + test(`Prover accepts presentation request from the Verifier`, async () => { + // Prover retrieves the requested credentials and accepts the presentation request + testLogger.test('Prover accepts presentation request from Verifier') + + const verifierPresentationRecordPromise = waitForProofExchangeRecord(verifierAgent, { + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const { + proofFormats: { presentationExchange }, + } = await proverAgent.proofs.selectCredentialsForRequest({ + proofRecordId: proverProofExchangeRecord.id, + }) + + await proverAgent.proofs.acceptRequest({ + proofRecordId: proverProofExchangeRecord.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + proofFormats: { presentationExchange: { credentials: presentationExchange! } }, + }) + + // Verifier waits for the presentation from the Prover + testLogger.test('Verifier waits for presentation from the Prover') + verifierProofExchangeRecord = await verifierPresentationRecordPromise + + const presentation = await didCommMessageRepository.findAgentMessage(verifierAgent.context, { + associatedRecordId: verifierProofExchangeRecord.id, + messageClass: V2PresentationMessage, + }) + + // { + // "@type":"https://didcomm.org/present-proof/2.0/presentation", + // "last_presentation":true, + // "formats":[ + // { + // "attach_id":"97cf1dbf-2ce0-4641-9083-00f4aec99478", + // "format":"dif/presentation-exchange/submission@v1.0" + // } + // ], + // "presentations~attach":[ + // { + // "@id":"97cf1dbf-2ce0-4641-9083-00f4aec99478", + // "mime-type":"application/json", + // "data":{ + // "json":{ + // "presentation_submission":{ + // "id":"dHOs_n7UF7QAbJvEovHeW", + // "definition_id":"e950bfe5-d7ec-4303-ad61-6983fb976ac9", + // "descriptor_map":[ + // { + // "id":"citizenship_input_1", + // "format":"ldp_vp", + // "path":"$", + // "path_nested":{ + // "id":"citizenship_input_1", + // "format":"ldp_vc ", + // "path":"$.verifiableCredential[0]" + // } + // } + // ] + // }, + // "context":[ + // "https://www.w3.org/2018/credentials/v1" + // ], + // "type":[ + // "VerifiableP resentation" + // ], + // "holder":"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + // "verifiableCredential":[ + // { + // "@context":[ + // "https://www.w3.org/2018/credentials/v1", + // "https://www.w3.org/2018/credentials/examples/v1" + // ], + // "type":[ + // "Verifiab leCredential", + // "UniversityDegreeCredential" + // ], + // "issuer":"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + // "issuanceDate":"2017-10-22T12:23:48Z", + // "credentialSubject":{ + // "id":"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38Eef XmgDL", + // "degree":{ + // "type":"BachelorDegree", + // "name":"Bachelor of Science and Arts" + // } + // }, + // "proof":{ + // "verificationMethod":"di d:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + // "type":"E d25519Signature2018", + // "created":"2023-12-19T12:38:36Z", + // "proofPurpose":"assertionMethod", + // "jws":"eyJhbGciOiJFZERTQSIs ImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..U3oPjRgz-fTd_kkUtNgWKh-FRWWkKdy0iSgOiGA1d7IyImuL1URQwJjd3UlJAkFf1kl7NeakiCtZ cFfxkPpECQ" + // } + // } + // ], + // "proof":{ + // "verificationMethod":"did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Yc puk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL", + // "type":"Ed25519Signature2018", + // "created":"2023-12-19T12:38:37Z", + // "proofPurpos e":"authentication", + // "challenge":"273899451763000636595367", + // "jws":"eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsi YjY0Il19..X_pR5Evhj-byuMkhJfXfoj9HO03iLKtltq64A4cueuLAH-Ix5D-G9g7r4xec9ysyga8GS2tZQl0OK4W9LJcOAQ" + // } + // } + // } + // } + // ], + // "@id":"2cdf aa16-d132-4778-9d6f-622fc0e0fa84", + // "~thread":{ + // "thid":"e03cfab3-7ab1-477f-9df7-dc7ede70b952" + // }, + // "~please_ack":{ + // "on":[ + // " RECEIPT" + // ] + // } + // } + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: 'dif/presentation-exchange/submission@v1.0', + }, + ], + presentationAttachments: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + json: { + context: expect.any(Array), + type: expect.any(Array), + presentation_submission: { + id: expect.any(String), + definition_id: expect.any(String), + descriptor_map: [ + { + id: 'citizenship_input_1', + format: 'ldp_vp', + path: '$', + path_nested: { + id: 'citizenship_input_1', + format: 'ldp_vc', + path: '$.verifiableCredential[0]', + }, + }, + ], + }, + verifiableCredential: [ + { + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://www.w3.org/2018/credentials/examples/v1', + ], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + degree: { + type: 'BachelorDegree', + name: 'Bachelor of Science and Arts', + }, + }, + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'assertionMethod', + jws: expect.any(String), + }, + }, + ], + proof: { + verificationMethod: expect.any(String), + type: 'Ed25519Signature2018', + created: expect.any(String), + proofPurpose: 'authentication', + challenge: expect.any(String), + jws: expect.any(String), + }, + }, + }, + }, + ], + id: expect.any(String), + thread: { + threadId: verifierProofExchangeRecord.threadId, + }, + }) + + expect(verifierProofExchangeRecord.id).not.toBeNull() + expect(verifierProofExchangeRecord).toMatchObject({ + threadId: verifierProofExchangeRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: 'v2', + }) + }) + + test(`Verifier accepts the presentation provided by the Prover`, async () => { + const proverProofExchangeRecordPromise = waitForProofExchangeRecord(proverAgent, { + threadId: proverProofExchangeRecord.threadId, + state: ProofState.Done, + }) + + // Verifier accepts the presentation provided by by the Prover + testLogger.test('Verifier accepts the presentation provided by the Prover') + await verifierAgent.proofs.acceptPresentation({ proofRecordId: verifierProofExchangeRecord.id }) + + // Prover waits until she received a presentation acknowledgement + testLogger.test('Prover waits until she receives a presentation acknowledgement') + proverProofExchangeRecord = await proverProofExchangeRecordPromise + + expect(verifierProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: proverProofExchangeRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(proverProofExchangeRecord).toMatchObject({ + id: expect.any(String), + createdAt: expect.any(Date), + threadId: verifierProofExchangeRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/services/PresentationExchangeService.ts b/packages/core/src/modules/proofs/services/PresentationExchangeService.ts deleted file mode 100644 index ed05534b91..0000000000 --- a/packages/core/src/modules/proofs/services/PresentationExchangeService.ts +++ /dev/null @@ -1,515 +0,0 @@ -import type { AgentContext } from '../../../agent' -import type { Query } from '../../../storage/StorageService' -import type { VerificationMethod } from '../../dids' -import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' -import type { InputDescriptorToCredentials, PresentationSubmission } from '../models' -import type { - IPresentationDefinition, - PresentationSignCallBackParams, - Validated, - VerifiablePresentationResult, -} from '@sphereon/pex' -import type { - InputDescriptorV2, - PresentationSubmission as PexPresentationSubmission, - PresentationDefinitionV1, -} from '@sphereon/pex-models' -import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' - -import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' -import { injectable } from 'tsyringe' - -import { getJwkFromKey } from '../../../crypto' -import { AriesFrameworkError } from '../../../error' -import { JsonTransformer } from '../../../utils' -import { getKeyFromVerificationMethod, DidsApi } from '../../dids' -import { PresentationExchangeError } from '../../presentation-exchange' -import { SignatureSuiteRegistry, W3cPresentation, W3cCredentialService, ClaimFormat } from '../../vc' -import { W3cCredentialRepository } from '../../vc/repository' -import { selectCredentialsForRequest } from '../utils/credentialSelection' -import { - getSphereonOriginalVerifiableCredential, - getSphereonW3cVerifiablePresentation, - getW3cVerifiablePresentationInstance, -} from '../utils/transform' - -export type ProofStructure = Record>> -export type PresentationDefinition = IPresentationDefinition & Record - -export type VerifiablePresentation = IVerifiablePresentation & Record - -@injectable() -export class PresentationExchangeService { - private pex = new PEX() - - public async selectCredentialsForRequest( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition - ): Promise { - const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didRecords = await didsApi.getCreatedDids() - const holderDids = didRecords.map((didRecord) => didRecord.did) - - return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) - } - - public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { - const validation = PEX.validateDefinition(presentationDefinition) - const errorMessages = this.formatValidated(validation) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - - public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { - const validation = PEX.validateSubmission(presentationSubmission) - const errorMessages = this.formatValidated(validation) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - - public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { - const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) - - if (errors) { - const errorMessages = this.formatValidated(errors as Validated) - if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` - ) - } - } - } - - private formatValidated(v: Validated) { - return Array.isArray(v) - ? (v - .filter((r) => r.tag === Status.ERROR) - .map((r) => r.message) - .filter((m) => Boolean(m)) as Array) - : v.tag === Status.ERROR && typeof v.message === 'string' - ? [v.message] - : [] - } - - /** - * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the - * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. - */ - private async queryCredentialForPresentationDefinition( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition - ): Promise> { - const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const query: Array> = [] - const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) - - if (!presentationDefinitionVersion.version) { - throw new AriesFrameworkError( - `Unable to determine the Presentation Exchange version from the presentation definition. ${ - presentationDefinitionVersion.error ?? 'Unknown error' - }` - ) - } - - if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 - - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of pd.input_descriptors) { - for (const schema of inputDescriptor.schema) { - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], - }) - } - } - } else if (presentationDefinitionVersion.version === PEVersion.v2) { - // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. - // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need - // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. - } else { - throw new AriesFrameworkError( - `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` - ) - } - - // query the wallet ourselves first to avoid the need to query the pex library for all - // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) - - return credentialRecords - } - - private addCredentialToSubjectInputDescriptor( - subjectsToInputDescriptors: ProofStructure, - subjectId: string, - inputDescriptorId: string, - credential: W3cVerifiableCredential - ) { - const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} - const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - - credentials.push(credential) - inputDescriptorsToCredentials[inputDescriptorId] = credentials - subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials - } - - private getPresentationFormat( - presentationDefinition: PresentationDefinition, - credentials: Array - ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { - const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') - const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') - - const inputDescriptorsNotSupportingJwtVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.jwt_vc === undefined) - - const inputDescriptorsNotSupportingLdpVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.ldp_vc === undefined) - - if ( - allCredentialsAreJwtVc && - (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && - inputDescriptorsNotSupportingJwtVc.length === 0 - ) { - return ClaimFormat.JwtVp - } else if ( - allCredentialsAreLdpVc && - (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && - inputDescriptorsNotSupportingLdpVc.length === 0 - ) { - return ClaimFormat.LdpVp - } else { - throw new AriesFrameworkError( - 'No suitable presentation format found for the given presentation definition, and credentials' - ) - } - } - - public async createPresentation( - agentContext: AgentContext, - options: { - credentialsForInputDescriptor: InputDescriptorToCredentials - presentationDefinition: PresentationDefinition - challenge?: string - domain?: string - nonce?: string - } - ) { - const { presentationDefinition, challenge, nonce, domain } = options - - const proofStructure: ProofStructure = {} - - Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { - credentials.forEach((credential) => { - const subjectId = credential.credentialSubjectIds[0] - if (!subjectId) { - throw new AriesFrameworkError('Missing required credential subject for creating the presentation.') - } - - this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) - }) - }) - - const verifiablePresentationResultsWithFormat: Array<{ - verifiablePresentationResult: VerifiablePresentationResult - format: ClaimFormat.LdpVp | ClaimFormat.JwtVp - }> = [] - - const subjectToInputDescriptors = Object.entries(proofStructure) - for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { - // Determine a suitable verification method for the presentation - const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) - - if (!verificationMethod) { - throw new AriesFrameworkError(`No verification method found for subject id '${subjectId}'.`) - } - - // We create a presentation for each subject - // Thus for each subject we need to filter all the related input descriptors and credentials - // FIXME: cast to V1, as tsc errors for strange reasons if not - const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( - (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials - ) - - // Get all the credentials associated with the input descriptors - const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flatMap((credentials) => credentials) - .map(getSphereonOriginalVerifiableCredential) - - const presentationDefinitionForSubject: PresentationDefinition = { - ...presentationDefinition, - input_descriptors: inputDescriptorsForSubject, - - // We remove the submission requirements, as it will otherwise fail to create the VP - submission_requirements: undefined, - } - - const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) - - // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? - // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? - const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinitionForSubject, - credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod, format), - { - holderDID: subjectId, - proofOptions: { challenge, domain, nonce }, - signatureOptions: { verificationMethod: verificationMethod?.id }, - presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, - } - ) - - verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new AriesFrameworkError('No verifiable presentations created.') - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new AriesFrameworkError('No verifiable presentations created.') - } - - if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { - throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') - } - - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission - const presentationSubmission: PexPresentationSubmission = { - id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, - definition_id: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, - descriptor_map: [], - } - - for (const vpf of verifiablePresentationResultsWithFormat) { - const { verifiablePresentationResult } = vpf - presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) - } - - return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => - getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) - ), - presentationSubmission, - presentationSubmissionLocation: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, - } - } - - private getSigningAlgorithmFromVerificationMethod( - verificationMethod: VerificationMethod, - suitableAlgorithms?: Array - ) { - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - if (suitableAlgorithms) { - const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) - if (!possibleAlgorithms || possibleAlgorithms.length === 0) { - throw new AriesFrameworkError( - [ - `Found no suitable signing algorithm.`, - `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, - `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, - ].join('\n') - ) - } - } - - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) - return alg - } - - private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition: Array, - inputDescriptorAlgorithms: Array> - ) { - const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() - const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => - inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) - ) - - const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => - algorithmsSatisfyingDescriptors.includes(alg) - ) - - if ( - algorithmsSatisfyingDefinition.length > 0 && - algorithmsSatisfyingDescriptors.length > 0 && - algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 - ) { - throw new AriesFrameworkError( - `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` - ) - } - - if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { - throw new AriesFrameworkError( - `No signature algorithm found for satisfying restrictions of the input descriptors.` - ) - } - - let suitableAlgorithms: Array | undefined - if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { - suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions - } else if (algorithmsSatisfyingDescriptors.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDescriptors - } else if (algorithmsSatisfyingDefinition.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDefinition - } - - return suitableAlgorithms - } - - private getSigningAlgorithmForJwtVc( - presentationDefinition: PresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] - - const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg ?? []) - .filter((alg) => alg.length > 0) - - const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) - } - - private getProofTypeForLdpVc( - agentContext: AgentContext, - presentationDefinition: PresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] - - const inputDescriptorAlgorithms: Array> = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type ?? []) - .filter((alg) => alg.length > 0) - - const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - // For each of the supported algs, find the key types, then find the proof types - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { - throw new AriesFrameworkError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` - ) - } - - if (suitableSignatureSuites) { - if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { - throw new AriesFrameworkError( - [ - 'No possible signature suite found for the given verification method.', - `Verification method type: ${verificationMethod.type}`, - `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, - `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, - ].join('\n') - ) - } - - return supportedSignatureSuite.proofType - } - - return supportedSignatureSuite.proofType - } - - public getPresentationSignCallback( - agentContext: AgentContext, - verificationMethod: VerificationMethod, - vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp - ) { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - - return async (callBackParams: PresentationSignCallBackParams) => { - // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options, presentationDefinition } = callBackParams - const { challenge, domain, nonce } = options.proofOptions ?? {} - const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} - - if (verificationMethodId && verificationMethodId !== verificationMethod.id) { - throw new AriesFrameworkError( - `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` - ) - } - - // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - const presentationToSign = { ...presentationJson, presentation_submission: undefined } - - let signedPresentation: W3cVerifiablePresentation - if (vpFormat === 'jwt_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else if (vpFormat === 'ldp_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), - proofPurpose: 'authentication', - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else { - throw new AriesFrameworkError( - `Only JWT credentials or JSONLD credentials are supported for a single presentation.` - ) - } - - return getSphereonW3cVerifiablePresentation(signedPresentation) - } - } - - private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - - if (!subjectId.startsWith('did:')) { - throw new AriesFrameworkError(`Only dids are supported as credentialSubject id. ${subjectId} is not a valid did`) - } - - const didDocument = await didsApi.resolveDidDocument(subjectId) - - if (!didDocument.authentication || didDocument.authentication.length === 0) { - throw new AriesFrameworkError(`No authentication verificationMethods found for did ${subjectId} in did document`) - } - - // the signature suite to use for the presentation is dependant on the credentials we share. - // 1. Get the verification method for this given proof purpose in this DID document - let [verificationMethod] = didDocument.authentication - if (typeof verificationMethod === 'string') { - verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) - } - - return verificationMethod - } -} diff --git a/packages/core/src/modules/proofs/services/index.ts b/packages/core/src/modules/proofs/services/index.ts deleted file mode 100644 index 25f2454018..0000000000 --- a/packages/core/src/modules/proofs/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './PresentationExchangeService' diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 9b0f211097..35d601e9cb 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -5,6 +5,8 @@ import { BbsModule } from '../../bbs-signatures/src/BbsModule' import { IndySdkModule } from '../../indy-sdk/src' import { indySdk } from '../../indy-sdk/tests/setupIndySdkModule' import { + PresentationExchangeProofFormatService, + V2ProofProtocol, CacheModule, CredentialEventTypes, InMemoryLruCache, @@ -16,6 +18,7 @@ import { V2CredentialProtocol, W3cCredentialsModule, } from '../src' +import { PresentationExchangeModule } from '../src/modules/presentation-exchange' import { customDocumentLoader } from '../src/modules/vc/data-integrity/__tests__/documentLoader' import { setupEventReplaySubjects } from './events' @@ -38,6 +41,7 @@ export const getJsonLdModules = ({ }), proofs: new ProofsModule({ autoAcceptProofs, + proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], }), cache: new CacheModule({ cache: new InMemoryLruCache({ limit: 100 }), @@ -45,6 +49,7 @@ export const getJsonLdModules = ({ indySdk: new IndySdkModule({ indySdk, }), + pex: new PresentationExchangeModule(), bbs: new BbsModule(), } as const) From 2df672f6d604ccdee30b16db4e3d9f3dd8526b4a Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Tue, 19 Dec 2023 15:59:21 +0100 Subject: [PATCH 08/17] fix(present-proof): fixed the pex tests Signed-off-by: Berend Sliedrecht --- .../PresentationExchangeService.ts | 2 +- .../utils/credentialSelection.ts | 1 + .../PresentationExchangeProofFormatService.ts | 6 +- ...entationExchangeProofFormatService.test.ts | 87 ++++- ...entation-exchange-presentation.e2e.test.ts | 2 - .../proofs/utils/credentialSelection.ts | 301 ------------------ .../src/modules/proofs/utils/transform.ts | 78 ----- packages/core/src/utils/objectEquality.ts | 14 +- 8 files changed, 91 insertions(+), 400 deletions(-) delete mode 100644 packages/core/src/modules/proofs/utils/credentialSelection.ts delete mode 100644 packages/core/src/modules/proofs/utils/transform.ts diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts index b99c2963ad..d5429579e3 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts @@ -253,7 +253,7 @@ export class PresentationExchangeService { // Get all the credentials associated with the input descriptors const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flatMap((credentials) => credentials) + .flat() .map(getSphereonOriginalVerifiableCredential) const presentationDefinitionForSubject: PresentationDefinition = { diff --git a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts index 5339c2d261..7f9f23729f 100644 --- a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts @@ -39,6 +39,7 @@ export async function selectCredentialsForRequest( verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { const credentialRecord = credentialRecords.find((record) => { const originalVc = getSphereonOriginalVerifiableCredential(record.credential) + return deepEquality(originalVc, encoded) }) diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index baf67a8192..3f3f6b1196 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -21,7 +21,7 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' -import type { PresentationSubmission as PexPresentationSubmission, PresentationSubmission } from '@sphereon/pex-models' +import type { PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' @@ -158,7 +158,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ + const { presentation_definition: presentationDefinition, options } = requestAttachment.getDataAsJson<{ presentation_definition: PresentationDefinition options?: { challenge?: string; domain?: string } }>() @@ -183,6 +183,8 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const presentation = await ps.createPresentation(agentContext, { presentationDefinition, credentialsForInputDescriptor: credentials, + challenge: options?.challenge, + domain: options?.domain, }) if (presentation.verifiablePresentations.length > 1) { diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 0c0bfeb46f..03b89325f2 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -6,6 +6,13 @@ import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndy import { agentDependencies, getAgentConfig } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' import { PresentationExchangeModule, PresentationExchangeService } from '../../../../presentation-exchange' +import { + W3cJsonLdVerifiableCredential, + W3cCredentialRecord, + W3cCredentialRepository, + CREDENTIALS_CONTEXT_V1_URL, + W3cJsonLdVerifiablePresentation, +} from '../../../../vc' import { ProofsModule } from '../../../ProofsModule' import { ProofState } from '../../../models' import { V2ProofProtocol } from '../../../protocol' @@ -29,12 +36,7 @@ const mockPresentationDefinition = (): PresentationDefinition => ({ constraints: { fields: [ { - path: [ - '$.credentialSubject.dateOfBirth', - '$.credentialSubject.dob', - '$.vc.credentialSubject.dateOfBirth', - '$.vc.credentialSubject.dob', - ], + path: ['$.credentialSubject.id'], }, ], }, @@ -42,6 +44,45 @@ const mockPresentationDefinition = (): PresentationDefinition => ({ ], }) +const mockCredentialRecord = new W3cCredentialRecord({ + tags: {}, + credential: new W3cJsonLdVerifiableCredential({ + id: 'did:some:id', + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + issuanceDate: '2017-10-22T12:23:48Z', + credentialSubject: { + id: 'did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL', + }, + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), +}) + +jest.spyOn(W3cCredentialRepository.prototype, 'getAll').mockResolvedValue([mockCredentialRecord]) +jest.spyOn(PresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ + presentationSubmission: { id: 'did:id', definition_id: 'my-id', descriptor_map: [] }, + verifiablePresentations: [ + new W3cJsonLdVerifiablePresentation({ + verifiableCredential: [mockCredentialRecord.credential], + proof: { + type: 'Ed25519Signature2020', + created: '2021-11-13T18:19:39Z', + verificationMethod: 'https://example.edu/issuers/14#key-1', + proofPurpose: 'assertionMethod', + proofValue: 'z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz', + }, + }), + ], + presentationSubmissionLocation: 0, +}) + describe('Presentation Exchange ProofFormatService', () => { let pexFormatService: ProofFormatService let agent: Agent @@ -98,7 +139,12 @@ describe('Presentation Exchange ProofFormatService', () => { id: expect.any(String), mimeType: 'application/json', data: { - json: presentationDefinition, + json: { + options: { + challenge: 'TODO', + }, + presentation_definition: presentationDefinition, + }, }, }) @@ -120,20 +166,41 @@ describe('Presentation Exchange ProofFormatService', () => { const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { proofRecord: mockProofRecord(), requestAttachment, - proofFormats: { presentationExchange: { credentials: [] } }, + proofFormats: { presentationExchange: { credentials: [mockCredentialRecord] } }, }) expect(attachment).toMatchObject({ id: expect.any(String), mimeType: 'application/json', data: { - json: {}, + json: { + presentation_submission: { + id: expect.any(String), + definition_id: expect.any(String), + descriptor_map: [], + }, + context: expect.any(Array), + type: expect.any(Array), + verifiableCredential: [ + { + context: expect.any(Array), + id: expect.any(String), + type: expect.any(Array), + issuer: expect.any(String), + issuanceDate: expect.any(String), + credentialSubject: { + id: expect.any(String), + }, + proof: expect.any(Object), + }, + ], + }, }, }) expect(format).toMatchObject({ attachmentId: expect.any(String), - format: 'dif/presentation-exchange/definitions@v1.0', + format: 'dif/presentation-exchange/submission@v1.0', }) }) }) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts index 92fe9d8f22..8c524f7cfc 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -40,7 +40,6 @@ describe('Present Proof', () => { let issuerAgent: Agent> let verifierAgent: Agent> - let verifierProverConnectionId: string let issuerProverConnectionId: string let proverVerifierConnectionId: string @@ -55,7 +54,6 @@ describe('Present Proof', () => { holderAgent: proverAgent, issuerAgent, verifierAgent, - verifierHolderConnectionId: verifierProverConnectionId, issuerHolderConnectionId: issuerProverConnectionId, holderVerifierConnectionId: proverVerifierConnectionId, } = await setupJsonLdTests({ diff --git a/packages/core/src/modules/proofs/utils/credentialSelection.ts b/packages/core/src/modules/proofs/utils/credentialSelection.ts deleted file mode 100644 index fa045c205a..0000000000 --- a/packages/core/src/modules/proofs/utils/credentialSelection.ts +++ /dev/null @@ -1,301 +0,0 @@ -import type { W3cCredentialRecord } from '../../vc' -import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models' -import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' -import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' - -import { PEX } from '@sphereon/pex' -import { Rules } from '@sphereon/pex-models' -import { default as jp } from 'jsonpath' - -import { AriesFrameworkError } from '../../../error' - -import { getSphereonOriginalVerifiableCredential } from './transform' - -export async function selectCredentialsForRequest( - presentationDefinition: IPresentationDefinition, - credentialRecords: Array, - holderDIDs: Array -): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - - if (!presentationDefinition) { - throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') - } - - const pex = new PEX() - - // FIXME: there is a function for this in the VP library, but it is not usable atm - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { - holderDIDs, - // limitDisclosureSignatureSuites: [], - // restrictToDIDMethods, - // restrictToFormats - }) - - const selectResults = { - ...selectResultsRaw, - // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialIndex = encodedCredentials.indexOf(encoded) - const credentialRecord = credentialRecords[credentialIndex] - if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') - - return credentialRecord - }), - } - - const presentationSubmission: PresentationSubmission = { - requirements: [], - areRequirementsSatisfied: false, - name: presentationDefinition.name, - purpose: presentationDefinition.purpose, - } - - // If there's no submission requirements, ALL input descriptors MUST be satisfied - if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { - presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( - presentationDefinition.input_descriptors, - selectResults - ) - } else { - presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) - } - - // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error - // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) - // I see this more as the fault of the presentation definition, as it should have at least some requirements. - if (presentationSubmission.requirements.length === 0) { - throw new AriesFrameworkError( - 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' - ) - } - if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { - return presentationSubmission - } - - return { - ...presentationSubmission, - - // If all requirements are satisfied, the presentation submission is satisfied - areRequirementsSatisfied: presentationSubmission.requirements.every( - (requirement) => requirement.isRequirementSatisfied - ), - } -} - -function getSubmissionRequirements( - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] - - // There are submission requirements, so we need to select the input_descriptors - // based on the submission requirements - for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { - // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet - if (submissionRequirement.from_nested) { - throw new AriesFrameworkError( - "Presentation definition contains requirement using 'from_nested', which is not supported yet." - ) - } - - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new AriesFrameworkError("Missing 'from' in submission requirement match") - } - - if (submissionRequirement.rule === Rules.All) { - const selectedSubmission = getSubmissionRequirementRuleAll( - submissionRequirement, - presentationDefinition, - selectResults - ) - submissionRequirements.push(selectedSubmission) - } else { - const selectedSubmission = getSubmissionRequirementRulePick( - submissionRequirement, - presentationDefinition, - selectResults - ) - - submissionRequirements.push(selectedSubmission) - } - } - - // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) - // We use minimization strategy, and thus only disclose the minimum amount of information - const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) - - return requirementsWithCredentials -} - -function getSubmissionRequirementsForAllInputDescriptors( - inputDescriptors: Array | Array, - selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] - - for (const inputDescriptor of inputDescriptors) { - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - submissionRequirements.push({ - rule: Rules.Pick, - needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, - submissionEntry: [submission], - isRequirementSatisfied: submission.verifiableCredentials.length >= 1, - }) - } - - return submissionRequirements -} - -function getSubmissionRequirementRuleAll( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: Rules.All, - needsCount: 0, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - submissionEntry: [], - isRequirementSatisfied: false, - } - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - // Rule ALL, so for every input descriptor that matches in this group, we need to add it - selectedSubmission.needsCount += 1 - selectedSubmission.submissionEntry.push(submission) - } - - return { - ...selectedSubmission, - - // If all submissions have a credential, the requirement is satisfied - isRequirementSatisfied: selectedSubmission.submissionEntry.every( - (submission) => submission.verifiableCredentials.length >= 1 - ), - } -} - -function getSubmissionRequirementRulePick( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: 'pick', - needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - // If there's no count, min, or max we assume one credential is required for submission - // however, the exact behavior is not specified in the spec - submissionEntry: [], - isRequirementSatisfied: false, - } - - const satisfiedSubmissions: Array = [] - const unsatisfiedSubmissions: Array = [] - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - if (submission.verifiableCredentials.length >= 1) { - satisfiedSubmissions.push(submission) - } else { - unsatisfiedSubmissions.push(submission) - } - - // If we have found enough credentials to satisfy the requirement, we could stop - // but the user may not want the first x that match, so we continue and return all matches - // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break - } - - return { - ...selectedSubmission, - - // If there are enough satisfied submissions, the requirement is satisfied - isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, - - // if the requirement is satisfied, we only need to return the satisfied submissions - // however if the requirement is not satisfied, we include all entries so the wallet could - // render which credentials are missing. - submission: - satisfiedSubmissions.length >= selectedSubmission.needsCount - ? satisfiedSubmissions - : [...satisfiedSubmissions, ...unsatisfiedSubmissions], - } -} - -function getSubmissionForInputDescriptor( - inputDescriptor: InputDescriptorV1 | InputDescriptorV2, - selectResults: W3cCredentialRecordSelectResults -): SubmissionEntry { - // https://github.com/Sphereon-Opensource/PEX/issues/116 - // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it - const matchesForInputDescriptor = selectResults.matches?.filter( - (m) => - m.name === inputDescriptor.id || - // FIXME: this is not collision proof as the name doesn't have to be unique - m.name === inputDescriptor.name - ) - - const submissionEntry: SubmissionEntry = { - inputDescriptorId: inputDescriptor.id, - name: inputDescriptor.name, - purpose: inputDescriptor.purpose, - verifiableCredentials: [], - } - - // return early if no matches. - if (!matchesForInputDescriptor?.length) return submissionEntry - - // FIXME: This can return multiple credentials for multiple input_descriptors, - // which I think is a bug in the PEX library - // Extract all credentials from the match - const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => - extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) - ) - - submissionEntry.verifiableCredentials = verifiableCredentials - - return submissionEntry -} - -function extractCredentialsFromMatch( - match: SubmissionRequirementMatch, - availableCredentials?: Array -) { - const verifiableCredentials: Array = [] - - for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ - W3cCredentialRecord - ] - verifiableCredentials.push(verifiableCredential) - } - - return verifiableCredentials -} - -/** - * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential - */ -export type W3cCredentialRecordSelectResults = Omit & { - verifiableCredential?: Array -} diff --git a/packages/core/src/modules/proofs/utils/transform.ts b/packages/core/src/modules/proofs/utils/transform.ts deleted file mode 100644 index b56609774d..0000000000 --- a/packages/core/src/modules/proofs/utils/transform.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' -import type { - OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, - W3CVerifiableCredential as SphereonW3cVerifiableCredential, - W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, -} from '@sphereon/ssi-types' - -import { AriesFrameworkError } from '../../../error' -import { JsonTransformer } from '../../../utils' -import { - W3cJsonLdVerifiableCredential, - W3cJsonLdVerifiablePresentation, - W3cJwtVerifiableCredential, - W3cJwtVerifiablePresentation, - ClaimFormat, -} from '../../vc' - -export function getSphereonOriginalVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonOriginalVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getSphereonW3cVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonW3cVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getSphereonW3cVerifiablePresentation( - w3cVerifiablePresentation: W3cVerifiablePresentation -): SphereonW3cVerifiablePresentation { - if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { - return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation - } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { - return w3cVerifiablePresentation.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getW3cVerifiablePresentationInstance( - w3cVerifiablePresentation: SphereonW3cVerifiablePresentation -): W3cVerifiablePresentation { - if (typeof w3cVerifiablePresentation === 'string') { - return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) - } else { - return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) - } -} - -export function getW3cVerifiableCredentialInstance( - w3cVerifiableCredential: SphereonW3cVerifiableCredential -): W3cVerifiableCredential { - if (typeof w3cVerifiableCredential === 'string') { - return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) - } else { - return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) - } -} diff --git a/packages/core/src/utils/objectEquality.ts b/packages/core/src/utils/objectEquality.ts index 5288d1a52d..33db64084a 100644 --- a/packages/core/src/utils/objectEquality.ts +++ b/packages/core/src/utils/objectEquality.ts @@ -1,14 +1,16 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function areObjectsEqual(a: any, b: any): boolean { +export function areObjectsEqual(a: A, b: B): boolean { if (typeof a == 'object' && a != null && typeof b == 'object' && b != null) { - if (Object.keys(a).length !== Object.keys(b).length) return false - for (const key in a) { - if (!(key in b) || !areObjectsEqual(a[key], b[key])) { + const definedA = Object.fromEntries(Object.entries(a).filter(([, value]) => value !== undefined)) + const definedB = Object.fromEntries(Object.entries(b).filter(([, value]) => value !== undefined)) + if (Object.keys(definedA).length !== Object.keys(definedB).length) return false + for (const key in definedA) { + if (!(key in definedB) || !areObjectsEqual(definedA[key], definedB[key])) { return false } } - for (const key in b) { - if (!(key in a) || !areObjectsEqual(b[key], a[key])) { + for (const key in definedB) { + if (!(key in definedA) || !areObjectsEqual(definedB[key], definedA[key])) { return false } } From 985b19bb11b896ded04a2157fc71492cc73e5074 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Thu, 21 Dec 2023 13:50:17 +0100 Subject: [PATCH 09/17] fix(present-proof): do not take credentials as an input Signed-off-by: Berend Sliedrecht --- .../PresentationExchangeProofFormat.ts | 4 +--- .../PresentationExchangeProofFormatService.ts | 8 +------- .../PresentationExchangeProofFormatService.test.ts | 1 - .../v2-presentation-exchange-presentation.e2e.test.ts | 8 -------- 4 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index 81c64f6655..81845eece3 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -19,9 +19,7 @@ export interface PresentationExchangeProofFormat extends ProofFormat { presentationDefinition: PresentationDefinition } - acceptRequest: { - credentials: Array - } + acceptRequest: never getCredentialsForRequest: { input: never diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index 3f3f6b1196..9f1796951b 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -143,14 +143,8 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async acceptRequest( agentContext: AgentContext, - { attachmentId, requestAttachment, proofFormats }: ProofFormatAcceptRequestOptions + { attachmentId, requestAttachment }: ProofFormatAcceptRequestOptions ): Promise { - const presentationExchangeFormat = proofFormats?.presentationExchange - - if (!presentationExchangeFormat) { - throw Error('Missing presentation exchange format in create request attachment format') - } - const ps = this.presentationExchangeService(agentContext) const format = new ProofFormatSpec({ diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 03b89325f2..5df82cc53f 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -166,7 +166,6 @@ describe('Presentation Exchange ProofFormatService', () => { const { attachment, format } = await pexFormatService.acceptRequest(agent.context, { proofRecord: mockProofRecord(), requestAttachment, - proofFormats: { presentationExchange: { credentials: [mockCredentialRecord] } }, }) expect(attachment).toMatchObject({ diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts index 8c524f7cfc..a60af9fe4a 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -222,16 +222,8 @@ describe('Present Proof', () => { state: ProofState.PresentationReceived, }) - const { - proofFormats: { presentationExchange }, - } = await proverAgent.proofs.selectCredentialsForRequest({ - proofRecordId: proverProofExchangeRecord.id, - }) - await proverAgent.proofs.acceptRequest({ proofRecordId: proverProofExchangeRecord.id, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - proofFormats: { presentationExchange: { credentials: presentationExchange! } }, }) // Verifier waits for the presentation from the Prover From f3e1534e0eecc0dec58b5492e8154d6c2c2b9e6e Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 3 Jan 2024 15:15:33 +0100 Subject: [PATCH 10/17] fix(present-proof): resolved feedback but proof is not included Signed-off-by: Berend Sliedrecht --- packages/core/src/agent/AgentModules.ts | 2 + .../PresentationExchangeError.ts | 12 +- .../PresentationExchangeModule.ts | 1 + .../PresentationExchangeService.ts | 49 +++---- .../utils/credentialSelection.ts | 1 - .../PresentationExchangeProofFormat.ts | 30 +++- .../PresentationExchangeProofFormatService.ts | 138 ++++++++++-------- .../src/modules/proofs/models/v2/exchange.ts | 16 ++ .../src/modules/proofs/models/v2/index.ts | 1 + packages/core/tests/jsonld.ts | 2 - 10 files changed, 154 insertions(+), 98 deletions(-) create mode 100644 packages/core/src/modules/proofs/models/v2/exchange.ts create mode 100644 packages/core/src/modules/proofs/models/v2/index.ts diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index ed0afcede9..a1d4388605 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -11,6 +11,7 @@ import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' +import { PresentationExchangeModule } from '../modules/presentation-exchange' import { ProofsModule } from '../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../modules/routing' import { W3cCredentialsModule } from '../modules/vc' @@ -131,6 +132,7 @@ function getDefaultAgentModules() { oob: () => new OutOfBandModule(), w3cCredentials: () => new W3cCredentialsModule(), cache: () => new CacheModule(), + pex: () => new PresentationExchangeModule(), } as const } diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts index e9be720603..2cf10e1a4b 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts @@ -1,3 +1,13 @@ import { AriesFrameworkError } from '../../error' -export class PresentationExchangeError extends AriesFrameworkError {} +export class PresentationExchangeError extends AriesFrameworkError { + public additionalMessages?: Array + + public constructor( + message: string, + { cause, additionalMessages }: { cause?: Error; additionalMessages?: Array } = {} + ) { + super(message, { cause }) + this.additionalMessages = additionalMessages + } +} diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts index 483fb1bc69..a9673509e9 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts @@ -19,6 +19,7 @@ export class PresentationExchangeModule implements Module { "The 'PresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) + // service dependencyManager.registerSingleton(PresentationExchangeService) } } diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts index d5429579e3..cb3234a945 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts @@ -11,7 +11,7 @@ import type { } from '@sphereon/pex' import type { InputDescriptorV2, - PresentationSubmission as PexPresentationSubmission, + PresentationSubmission as PePresentationSubmission, PresentationDefinitionV1, } from '@sphereon/pex-models' import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' @@ -40,8 +40,8 @@ import { export type ProofStructure = Record>> export type PresentationDefinition = IPresentationDefinition & Record - export type VerifiablePresentation = IVerifiablePresentation & Record +export type PexPresentationSubmission = PePresentationSubmission & Record @injectable() export class PresentationExchangeService { @@ -64,9 +64,7 @@ export class PresentationExchangeService { const validation = PEX.validateDefinition(presentationDefinition) const errorMessages = this.formatValidated(validation) if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation definition. The following errors were found: ${errorMessages.join(', ')}` - ) + throw new PresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) } } @@ -74,9 +72,7 @@ export class PresentationExchangeService { const validation = PEX.validateSubmission(presentationSubmission) const errorMessages = this.formatValidated(validation) if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation submission. The following errors were found: ${errorMessages.join(', ')}` - ) + throw new PresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) } } @@ -86,22 +82,17 @@ export class PresentationExchangeService { if (errors) { const errorMessages = this.formatValidated(errors as Validated) if (errorMessages.length > 0) { - throw new PresentationExchangeError( - `Invalid presentation. The following errors were found: ${errorMessages.join(', ')}` - ) + throw new PresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) } } } private formatValidated(v: Validated) { - return Array.isArray(v) - ? (v - .filter((r) => r.tag === Status.ERROR) - .map((r) => r.message) - .filter((m) => Boolean(m)) as Array) - : v.tag === Status.ERROR && typeof v.message === 'string' - ? [v.message] - : [] + const validated = Array.isArray(v) ? v : [v] + return validated + .filter((r) => r.tag === Status.ERROR) + .map((r) => r.message) + .filter((r): r is string => Boolean(r)) } /** @@ -118,9 +109,9 @@ export class PresentationExchangeService { if (!presentationDefinitionVersion.version) { throw new PresentationExchangeError( - `Unable to determine the Presentation Exchange version from the presentation definition. ${ - presentationDefinitionVersion.error ?? 'Unknown error' - }` + `Unable to determine the Presentation Exchange version from the presentation definition + `, + presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} ) } @@ -284,11 +275,11 @@ export class PresentationExchangeService { } if (!verifiablePresentationResultsWithFormat[0]) { - throw new PresentationExchangeError('No verifiable presentations created.') + throw new PresentationExchangeError('No verifiable presentations created') } if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { - throw new PresentationExchangeError('Invalid amount of verifiable presentations created.') + throw new PresentationExchangeError('Invalid amount of verifiable presentations created') } const presentationSubmission: PexPresentationSubmission = { @@ -357,13 +348,13 @@ export class PresentationExchangeService { algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 ) { throw new PresentationExchangeError( - `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors` ) } if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { throw new PresentationExchangeError( - `No signature algorithm found for satisfying restrictions of the input descriptors.` + `No signature algorithm found for satisfying restrictions of the input descriptors` ) } @@ -419,7 +410,7 @@ export class PresentationExchangeService { const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) if (!supportedSignatureSuite) { throw new PresentationExchangeError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'` ) } @@ -456,7 +447,7 @@ export class PresentationExchangeService { if (verificationMethodId && verificationMethodId !== verificationMethod.id) { throw new PresentationExchangeError( - `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}` ) } @@ -490,7 +481,7 @@ export class PresentationExchangeService { }) } else { throw new PresentationExchangeError( - `Only JWT credentials or JSONLD credentials are supported for a single presentation.` + `Only JWT credentials or JSONLD credentials are supported for a single presentation` ) } diff --git a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts index 7f9f23729f..5339c2d261 100644 --- a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts @@ -39,7 +39,6 @@ export async function selectCredentialsForRequest( verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { const credentialRecord = credentialRecords.find((record) => { const originalVc = getSphereonOriginalVerifiableCredential(record.credential) - return deepEquality(originalVc, encoded) }) diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index 81845eece3..e0db1d5840 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,5 +1,9 @@ -import type { PresentationDefinition } from '../../../presentation-exchange' -import type { W3cCredentialRecord } from '../../../vc' +import type { InputDescriptorToCredentials, PresentationDefinition } from '../../../presentation-exchange' +import type { + PresentationExchangePresentation, + PresentationExchangeProposal, + PresentationExchangeRequest, +} from '../../models/v2' import type { ProofFormat } from '../ProofFormat' export interface PresentationExchangeProofFormat extends ProofFormat { @@ -17,24 +21,34 @@ export interface PresentationExchangeProofFormat extends ProofFormat { createRequest: { presentationDefinition: PresentationDefinition + options?: { + challenge?: string + domain?: string + } } - acceptRequest: never + acceptRequest: { + credentials?: InputDescriptorToCredentials + } getCredentialsForRequest: { input: never - output: Array + output: { + credentials: InputDescriptorToCredentials + } } selectCredentialsForRequest: { input: never - output: Array + output: { + credentials: InputDescriptorToCredentials + } } } formatData: { - proposal: unknown - request: unknown - presentation: unknown + proposal: PresentationExchangeProposal + request: PresentationExchangeRequest + presentation: PresentationExchangePresentation } } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index 9f1796951b..44bd3dba5d 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -4,8 +4,12 @@ import type { PresentationDefinition, VerifiablePresentation, } from '../../../presentation-exchange/PresentationExchangeService' -import type { W3cCredentialRecord, W3cVerifiablePresentation } from '../../../vc' import type { InputDescriptorToCredentials } from '../../models' +import type { + PresentationExchangePresentation, + PresentationExchangeProposal, + PresentationExchangeRequest, +} from '../../models/v2' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -21,7 +25,6 @@ import type { ProofFormatAutoRespondRequestOptions, ProofFormatAutoRespondPresentationOptions, } from '../ProofFormatServiceOptions' -import type { PresentationSubmission } from '@sphereon/pex-models' import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' @@ -67,7 +70,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const { presentationDefinition } = pexFormat - ps?.validatePresentationDefinition(presentationDefinition) + ps.validatePresentationDefinition(presentationDefinition) const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL, attachmentId }) @@ -78,7 +81,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { const ps = this.presentationExchangeService(agentContext) - const proposal = attachment.getDataAsJson() + const proposal = attachment.getDataAsJson() ps.validatePresentationDefinition(proposal) } @@ -93,7 +96,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const presentationDefinition = proposalAttachment.getDataAsJson() + const presentationDefinition = proposalAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) @@ -114,7 +117,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw Error('Missing presentation exchange format in create request attachment format') } - const { presentationDefinition } = presentationExchangeFormat + const { presentationDefinition, options } = presentationExchangeFormat ps.validatePresentationDefinition(presentationDefinition) @@ -123,10 +126,18 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const options = { challenge: 'TODO' } + const challenge = options?.challenge ?? (await agentContext.wallet.generateNonce()) + + const optionsWithChallenge: PresentationExchangeRequest['options'] = { + challenge, + domain: options?.domain, + } const attachment = this.getFormatData( - { options, presentation_definition: presentationDefinition }, + { + options: optionsWithChallenge, + presentation_definition: presentationDefinition, + } satisfies PresentationExchangeRequest, format.attachmentId ) @@ -135,15 +146,13 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { const ps = this.presentationExchangeService(agentContext) - const { presentation_definition: presentationDefinition } = attachment.getDataAsJson<{ - presentation_definition: PresentationDefinition - }>() + const { presentation_definition: presentationDefinition } = attachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) } public async acceptRequest( agentContext: AgentContext, - { attachmentId, requestAttachment }: ProofFormatAcceptRequestOptions + { attachmentId, requestAttachment, proofFormats }: ProofFormatAcceptRequestOptions ): Promise { const ps = this.presentationExchangeService(agentContext) @@ -152,27 +161,27 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const { presentation_definition: presentationDefinition, options } = requestAttachment.getDataAsJson<{ - presentation_definition: PresentationDefinition - options?: { challenge?: string; domain?: string } - }>() + const { presentation_definition: presentationDefinition, options } = + requestAttachment.getDataAsJson() - const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest( - agentContext, - presentationDefinition - ) + const credentials: InputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} - if (!areRequirementsSatisfied) { - throw new AriesFrameworkError('Requirements of the presentation definition could not be satifsied') - } + if (Object.keys(credentials).length === 0) { + const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest( + agentContext, + presentationDefinition + ) - const credentials: InputDescriptorToCredentials = {} + if (!areRequirementsSatisfied) { + throw new AriesFrameworkError('Requirements of the presentation definition could not be satifsied') + } - requirements.forEach((r) => { - r.submissionEntry.forEach((r) => { - credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + requirements.forEach((r) => { + r.submissionEntry.forEach((r) => { + credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) + }) }) - }) + } const presentation = await ps.createPresentation(agentContext, { presentationDefinition, @@ -185,9 +194,14 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') } - const data = { + // TODO: how do we get the `proof` from this? It does not seem to be available on the JWT class + const { type, context, verifiableCredential } = presentation.verifiablePresentations[0] + + const data: PresentationExchangePresentation = { presentation_submission: presentation.presentationSubmission, - ...presentation.verifiablePresentations[0], + type, + context, + verifiableCredential, } const attachment = this.getFormatData(data, format.attachmentId) @@ -203,20 +217,18 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ presentation_definition: PresentationDefinition }>() - const presentation = attachment.getDataAsJson< - W3cVerifiablePresentation & { presentation_submission: PresentationSubmission } - >() + const presentation = attachment.getDataAsJson() try { ps.validatePresentationDefinition(presentationDefinition) - if (presentation.presentation_submission) { - ps.validatePresentationSubmission(presentation.presentation_submission) - } + ps.validatePresentationSubmission(presentation.presentation_submission) ps.validatePresentation(presentationDefinition, presentation as unknown as VerifiablePresentation) return true } catch (e) { - agentContext.config.logger.error(e) + agentContext.config.logger.error(`Failed to verify presentation in PEX proof format service: ${e.message}`, { + cause: e, + }) return false } } @@ -224,49 +236,61 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async getCredentialsForRequest( agentContext: AgentContext, { requestAttachment }: ProofFormatGetCredentialsForRequestOptions - ): Promise> { + ): Promise<{ credentials: InputDescriptorToCredentials }> { const ps = this.presentationExchangeService(agentContext) - const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ - presentation_definition: PresentationDefinition - }>() + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) - const credentials = presentationSubmission.requirements.flatMap((r) => - r.submissionEntry.flatMap((e) => e.verifiableCredentials) + if (!presentationSubmission.areRequirementsSatisfied) { + throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') + } + + const credentials: InputDescriptorToCredentials = {} + + presentationSubmission.requirements.forEach((r) => + r.submissionEntry.forEach((s) => { + credentials[s.inputDescriptorId] = s.verifiableCredentials.map((v) => v.credential) + }) ) - return credentials + return { credentials } } public async selectCredentialsForRequest( agentContext: AgentContext, { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions - ): Promise> { + ): Promise<{ credentials: InputDescriptorToCredentials }> { const ps = this.presentationExchangeService(agentContext) - const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ - presentation_definition: PresentationDefinition - }>() - - ps.validatePresentationDefinition(presentationDefinition) + const { presentation_definition: presentationDefinition } = + requestAttachment.getDataAsJson() const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) - const credentials = presentationSubmission.requirements.flatMap((r) => - r.submissionEntry.flatMap((e) => e.verifiableCredentials) + if (!presentationSubmission.areRequirementsSatisfied) { + throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') + } + + const credentials: InputDescriptorToCredentials = {} + + presentationSubmission.requirements.forEach((r) => + r.submissionEntry.forEach((s) => { + credentials[s.inputDescriptorId] = s.verifiableCredentials.map((v) => v.credential) + }) ) - return credentials + return { credentials } } public async shouldAutoRespondToProposal( _agentContext: AgentContext, { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions ): Promise { - const proposalData = proposalAttachment.getDataAsJson() - const requestData = requestAttachment.getDataAsJson() + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() return deepEquality(requestData, proposalData) } @@ -275,8 +299,8 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic _agentContext: AgentContext, { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions ): Promise { - const proposalData = proposalAttachment.getDataAsJson() - const requestData = requestAttachment.getDataAsJson() + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() return deepEquality(requestData, proposalData) } diff --git a/packages/core/src/modules/proofs/models/v2/exchange.ts b/packages/core/src/modules/proofs/models/v2/exchange.ts new file mode 100644 index 0000000000..c5fddab467 --- /dev/null +++ b/packages/core/src/modules/proofs/models/v2/exchange.ts @@ -0,0 +1,16 @@ +import type { PexPresentationSubmission, PresentationDefinition } from '../../../presentation-exchange' +import type { W3cPresentation } from '../../../vc' + +export type PresentationExchangeProposal = PresentationDefinition + +export type PresentationExchangeRequest = { + options: { + challenge?: string + domain?: string + } + presentation_definition: PresentationDefinition +} + +export type PresentationExchangePresentation = W3cPresentation & { + presentation_submission: PexPresentationSubmission +} & Record diff --git a/packages/core/src/modules/proofs/models/v2/index.ts b/packages/core/src/modules/proofs/models/v2/index.ts new file mode 100644 index 0000000000..7c802c4137 --- /dev/null +++ b/packages/core/src/modules/proofs/models/v2/index.ts @@ -0,0 +1 @@ +export * from './exchange' diff --git a/packages/core/tests/jsonld.ts b/packages/core/tests/jsonld.ts index 35d601e9cb..1cfb263ab0 100644 --- a/packages/core/tests/jsonld.ts +++ b/packages/core/tests/jsonld.ts @@ -18,7 +18,6 @@ import { V2CredentialProtocol, W3cCredentialsModule, } from '../src' -import { PresentationExchangeModule } from '../src/modules/presentation-exchange' import { customDocumentLoader } from '../src/modules/vc/data-integrity/__tests__/documentLoader' import { setupEventReplaySubjects } from './events' @@ -49,7 +48,6 @@ export const getJsonLdModules = ({ indySdk: new IndySdkModule({ indySdk, }), - pex: new PresentationExchangeModule(), bbs: new BbsModule(), } as const) From 6757add93eb606f46e26e788a7e8855dddece64f Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 11:40:11 +0700 Subject: [PATCH 11/17] updates to PEX flow Signed-off-by: Timo Glastra --- .../src/decorators/attachment/Attachment.ts | 5 +- .../PresentationExchangeService.ts | 86 ++++++--- .../models/PresentationSubmission.ts | 12 +- .../utils/credentialSelection.ts | 32 ++-- .../PresentationExchangeProofFormat.ts | 19 +- .../PresentationExchangeProofFormatService.ts | 172 ++++++++++-------- ...entationExchangeProofFormatService.test.ts | 15 +- .../src/modules/proofs/models/v2/exchange.ts | 14 +- ...entation-exchange-presentation.e2e.test.ts | 11 +- .../vc/jwt-vc/W3cJwtVerifiablePresentation.ts | 4 + .../vc/models/presentation/W3cPresentation.ts | 10 + 11 files changed, 236 insertions(+), 144 deletions(-) diff --git a/packages/core/src/decorators/attachment/Attachment.ts b/packages/core/src/decorators/attachment/Attachment.ts index 85996ff039..3a91065b56 100644 --- a/packages/core/src/decorators/attachment/Attachment.ts +++ b/packages/core/src/decorators/attachment/Attachment.ts @@ -4,6 +4,7 @@ import { Expose, Type } from 'class-transformer' import { IsDate, IsHash, IsInstance, IsInt, IsMimeType, IsOptional, IsString, ValidateNested } from 'class-validator' import { AriesFrameworkError } from '../../error' +import { JsonValue } from '../../types' import { JsonEncoder } from '../../utils/JsonEncoder' import { uuid } from '../../utils/uuid' @@ -19,7 +20,7 @@ export interface AttachmentOptions { export interface AttachmentDataOptions { base64?: string - json?: Record + json?: JsonValue links?: string[] jws?: JwsDetachedFormat | JwsFlattenedDetachedFormat sha256?: string @@ -40,7 +41,7 @@ export class AttachmentData { * Directly embedded JSON data, when representing content inline instead of via links, and when the content is natively conveyable as JSON. Optional. */ @IsOptional() - public json?: Record + public json?: JsonValue /** * A list of zero or more locations at which the content may be fetched. Optional. diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts index cb3234a945..62d01b8c94 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts @@ -1,4 +1,4 @@ -import type { InputDescriptorToCredentials, PresentationSubmission } from './models' +import type { InputDescriptorToCredentials, PexCredentialsForRequest } from './models' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { VerificationMethod } from '../dids' @@ -11,15 +11,21 @@ import type { } from '@sphereon/pex' import type { InputDescriptorV2, - PresentationSubmission as PePresentationSubmission, + PresentationSubmission, PresentationDefinitionV1, + PresentationDefinitionV2, } from '@sphereon/pex-models' -import type { IVerifiablePresentation, OriginalVerifiableCredential } from '@sphereon/ssi-types' +import type { + IVerifiablePresentation, + OriginalVerifiableCredential, + OriginalVerifiablePresentation, +} from '@sphereon/ssi-types' import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' +import { AriesFrameworkError } from '../../error' import { JsonTransformer } from '../../utils' import { DidsApi, getKeyFromVerificationMethod } from '../dids' import { @@ -32,32 +38,61 @@ import { import { PresentationExchangeError } from './PresentationExchangeError' import { - selectCredentialsForRequest, + getCredentialsForRequest, getSphereonOriginalVerifiableCredential, getSphereonW3cVerifiablePresentation, getW3cVerifiablePresentationInstance, } from './utils' +// FIXME: Why are these Record types used? export type ProofStructure = Record>> export type PresentationDefinition = IPresentationDefinition & Record export type VerifiablePresentation = IVerifiablePresentation & Record -export type PexPresentationSubmission = PePresentationSubmission & Record +export type PexPresentationSubmission = PresentationSubmission @injectable() export class PresentationExchangeService { private pex = new PEX() - public async selectCredentialsForRequest( + public async getCredentialsForRequest( agentContext: AgentContext, presentationDefinition: PresentationDefinition - ): Promise { + ): Promise { const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + // FIXME: why are we resolving all created dids here? + // If we want to do this we should extract all dids from the credential records and only + // fetch the dids for the queried credential records const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didRecords = await didsApi.getCreatedDids() const holderDids = didRecords.map((didRecord) => didRecord.did) - return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + return getCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + } + + /** + * Selects the credentials to use based on the output from `getCredentialsForRequest` + * Use this method if you don't want to manually select the credentials yourself. + */ + public selectCredentialsForRequest(credentialsForRequest: PexCredentialsForRequest): InputDescriptorToCredentials { + if (!credentialsForRequest.areRequirementsSatisfied) { + throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') + } + + const credentials: InputDescriptorToCredentials = {} + + for (const requirement of credentialsForRequest.requirements) { + for (const submission of requirement.submissionEntry) { + if (!credentials[submission.inputDescriptorId]) { + credentials[submission.inputDescriptorId] = [] + } + + // We pick the first matching VC if we are auto-selecting + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential) + } + } + + return credentials } public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { @@ -76,8 +111,11 @@ export class PresentationExchangeService { } } - public validatePresentation(presentationDefinition: PresentationDefinition, presentation: VerifiablePresentation) { - const { errors } = this.pex.evaluatePresentation(presentationDefinition, presentation) + public validatePresentation(presentationDefinition: PresentationDefinition, presentation: W3cVerifiablePresentation) { + const { errors } = this.pex.evaluatePresentation( + presentationDefinition, + presentation.encoded as OriginalVerifiablePresentation + ) if (errors) { const errorMessages = this.formatValidated(errors as Validated) @@ -201,12 +239,16 @@ export class PresentationExchangeService { options: { credentialsForInputDescriptor: InputDescriptorToCredentials presentationDefinition: PresentationDefinition + /** + * Defaults to {@link PresentationSubmissionLocation.PRESENTATION} + */ + presentationSubmissionLocation?: PresentationSubmissionLocation challenge?: string domain?: string nonce?: string } ) { - const { presentationDefinition, challenge, nonce, domain } = options + const { presentationDefinition, challenge, nonce, domain, presentationSubmissionLocation } = options const proofStructure: ProofStructure = {} @@ -267,7 +309,7 @@ export class PresentationExchangeService { holderDID: subjectId, proofOptions: { challenge, domain, nonce }, signatureOptions: { verificationMethod: verificationMethod?.id }, - presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, + presentationSubmissionLocation: presentationSubmissionLocation ?? PresentationSubmissionLocation.PRESENTATION, } ) @@ -371,7 +413,7 @@ export class PresentationExchangeService { } private getSigningAlgorithmForJwtVc( - presentationDefinition: PresentationDefinition, + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, verificationMethod: VerificationMethod ) { const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] @@ -390,7 +432,7 @@ export class PresentationExchangeService { private getProofTypeForLdpVc( agentContext: AgentContext, - presentationDefinition: PresentationDefinition, + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, verificationMethod: VerificationMethod ) { const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] @@ -451,31 +493,23 @@ export class PresentationExchangeService { ) } - // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - const presentationToSign = { ...presentationJson } - delete presentationToSign['presentation_submission'] - let signedPresentation: W3cVerifiablePresentation if (vpFormat === 'jwt_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition as PresentationDefinition, verificationMethod), + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) } else if (vpFormat === 'ldp_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc( - agentContext, - presentationDefinition as PresentationDefinition, - verificationMethod - ), + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) diff --git a/packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts b/packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts index 309cb93c62..4b88d58681 100644 --- a/packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts +++ b/packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts @@ -1,6 +1,6 @@ import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' -export interface PresentationSubmission { +export interface PexCredentialsForRequest { /** * Whether all requirements have been satisfied by the credentials in the wallet. */ @@ -16,7 +16,7 @@ export interface PresentationSubmission { * combinations that are possible. The structure doesn't include all possible combinations yet that * could satisfy a presentation definition. */ - requirements: PresentationSubmissionRequirement[] + requirements: PexCredentialsForRequestRequirement[] /** * Name of the presentation definition @@ -36,7 +36,7 @@ export interface PresentationSubmission { * * Each submission represents a input descriptor. */ -export interface PresentationSubmissionRequirement { +export interface PexCredentialsForRequestRequirement { /** * Whether the requirement is satisfied. * @@ -56,7 +56,7 @@ export interface PresentationSubmissionRequirement { purpose?: string /** - * Array of objects, where each entry contains a credential that will be part + * Array of objects, where each entry contains one or more credentials that will be part * of the submission. * * NOTE: if the `isRequirementSatisfied` is `false` the submission list will @@ -66,7 +66,7 @@ export interface PresentationSubmissionRequirement { * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value * to see how many of those submissions needed. */ - submissionEntry: SubmissionEntry[] + submissionEntry: PexCredentialsForRequestSubmissionEntry[] /** * The number of submission entries that are needed to fulfill the requirement. @@ -88,7 +88,7 @@ export interface PresentationSubmissionRequirement { * A submission entry that satisfies a specific input descriptor from the * presentation definition. */ -export interface SubmissionEntry { +export interface PexCredentialsForRequestSubmissionEntry { /** * The id of the input descriptor */ diff --git a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts index 5339c2d261..299e5844cd 100644 --- a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts @@ -1,5 +1,9 @@ import type { W3cCredentialRecord } from '../../vc' -import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from '../models' +import type { + PexCredentialsForRequest, + PexCredentialsForRequestRequirement, + PexCredentialsForRequestSubmissionEntry, +} from '../models' import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' @@ -12,11 +16,11 @@ import { PresentationExchangeError } from '../PresentationExchangeError' import { getSphereonOriginalVerifiableCredential } from './transform' -export async function selectCredentialsForRequest( +export async function getCredentialsForRequest( presentationDefinition: IPresentationDefinition, credentialRecords: Array, holderDIDs: Array -): Promise { +): Promise { if (!presentationDefinition) { throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission.') } @@ -50,7 +54,7 @@ export async function selectCredentialsForRequest( }), } - const presentationSubmission: PresentationSubmission = { + const presentationSubmission: PexCredentialsForRequest = { requirements: [], areRequirementsSatisfied: false, name: presentationDefinition.name, @@ -92,8 +96,8 @@ export async function selectCredentialsForRequest( function getSubmissionRequirements( presentationDefinition: IPresentationDefinition, selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] +): Array { + const submissionRequirements: Array = [] // There are submission requirements, so we need to select the input_descriptors // based on the submission requirements @@ -138,8 +142,8 @@ function getSubmissionRequirements( function getSubmissionRequirementsForAllInputDescriptors( inputDescriptors: Array | Array, selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] +): Array { + const submissionRequirements: Array = [] for (const inputDescriptor of inputDescriptors) { const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) @@ -164,7 +168,7 @@ function getSubmissionRequirementRuleAll( if (!submissionRequirement.from) throw new PresentationExchangeError("Missing 'from' in submission requirement match.") - const selectedSubmission: PresentationSubmissionRequirement = { + const selectedSubmission: PexCredentialsForRequestRequirement = { rule: Rules.All, needsCount: 0, name: submissionRequirement.name, @@ -204,7 +208,7 @@ function getSubmissionRequirementRulePick( throw new PresentationExchangeError("Missing 'from' in submission requirement match.") } - const selectedSubmission: PresentationSubmissionRequirement = { + const selectedSubmission: PexCredentialsForRequestRequirement = { rule: Rules.Pick, needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, name: submissionRequirement.name, @@ -215,8 +219,8 @@ function getSubmissionRequirementRulePick( isRequirementSatisfied: false, } - const satisfiedSubmissions: Array = [] - const unsatisfiedSubmissions: Array = [] + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] for (const inputDescriptor of presentationDefinition.input_descriptors) { // We only want to get the submission if the input descriptor belongs to the group @@ -254,7 +258,7 @@ function getSubmissionRequirementRulePick( function getSubmissionForInputDescriptor( inputDescriptor: InputDescriptorV1 | InputDescriptorV2, selectResults: W3cCredentialRecordSelectResults -): SubmissionEntry { +): PexCredentialsForRequestSubmissionEntry { // https://github.com/Sphereon-Opensource/PEX/issues/116 // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it const matchesForInputDescriptor = selectResults.matches?.filter( @@ -264,7 +268,7 @@ function getSubmissionForInputDescriptor( m.name === inputDescriptor.name ) - const submissionEntry: SubmissionEntry = { + const submissionEntry: PexCredentialsForRequestSubmissionEntry = { inputDescriptorId: inputDescriptor.id, name: inputDescriptor.name, purpose: inputDescriptor.purpose, diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index e0db1d5840..487a1a3235 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,4 +1,8 @@ -import type { InputDescriptorToCredentials, PresentationDefinition } from '../../../presentation-exchange' +import type { + InputDescriptorToCredentials, + PresentationDefinition, + PexCredentialsForRequest, +} from '../../../presentation-exchange' import type { PresentationExchangePresentation, PresentationExchangeProposal, @@ -15,8 +19,10 @@ export interface PresentationExchangeProofFormat extends ProofFormat { } acceptProposal: { - name?: string - version?: string + options?: { + challenge?: string + domain?: string + } } createRequest: { @@ -33,13 +39,14 @@ export interface PresentationExchangeProofFormat extends ProofFormat { getCredentialsForRequest: { input: never - output: { - credentials: InputDescriptorToCredentials - } + // Presentation submission details which the options that are available + output: PexCredentialsForRequest } selectCredentialsForRequest: { input: never + // Input descriptor to credentials specifically details which credentials + // should be used for which input descriptor output: { credentials: InputDescriptorToCredentials } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index 44bd3dba5d..c52f76f3c3 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -1,9 +1,7 @@ import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' -import type { - PresentationDefinition, - VerifiablePresentation, -} from '../../../presentation-exchange/PresentationExchangeService' +import type { JsonValue } from '../../../../types' +import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' import type { InputDescriptorToCredentials } from '../../models' import type { PresentationExchangePresentation, @@ -28,8 +26,14 @@ import type { import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' -import { deepEquality } from '../../../../utils' +import { deepEquality, JsonTransformer } from '../../../../utils' import { PresentationExchangeService } from '../../../presentation-exchange/PresentationExchangeService' +import { + W3cCredentialService, + ClaimFormat, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, +} from '../../../vc' import { ProofFormatSpec } from '../../models' const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/definitions@v1.0' @@ -87,20 +91,35 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async acceptProposal( agentContext: AgentContext, - { attachmentId, proposalAttachment }: ProofFormatAcceptProposalOptions + { + attachmentId, + proposalAttachment, + proofFormats, + }: ProofFormatAcceptProposalOptions ): Promise { const ps = this.presentationExchangeService(agentContext) + const presentationExchangeFormat = proofFormats?.presentationExchange + const format = new ProofFormatSpec({ format: PRESENTATION_EXCHANGE_PRESENTATION_REQUEST, attachmentId, }) const presentationDefinition = proposalAttachment.getDataAsJson() - ps.validatePresentationDefinition(presentationDefinition) - const attachment = this.getFormatData({ presentation_definition: presentationDefinition }, format.attachmentId) + const attachment = this.getFormatData( + { + presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: presentationExchangeFormat?.options?.domain, + }, + } satisfies PresentationExchangeRequest, + format.attachmentId + ) return { format, attachment } } @@ -112,7 +131,6 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const ps = this.presentationExchangeService(agentContext) const presentationExchangeFormat = proofFormats.presentationExchange - if (!presentationExchangeFormat) { throw Error('Missing presentation exchange format in create request attachment format') } @@ -126,17 +144,14 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const challenge = options?.challenge ?? (await agentContext.wallet.generateNonce()) - - const optionsWithChallenge: PresentationExchangeRequest['options'] = { - challenge, - domain: options?.domain, - } - const attachment = this.getFormatData( { - options: optionsWithChallenge, presentation_definition: presentationDefinition, + options: { + // NOTE: we always want to include a challenge to prevent replay attacks + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), + domain: options?.domain, + }, } satisfies PresentationExchangeRequest, format.attachmentId ) @@ -165,15 +180,14 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic requestAttachment.getDataAsJson() const credentials: InputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} - if (Object.keys(credentials).length === 0) { - const { areRequirementsSatisfied, requirements } = await ps.selectCredentialsForRequest( + const { areRequirementsSatisfied, requirements } = await ps.getCredentialsForRequest( agentContext, presentationDefinition ) if (!areRequirementsSatisfied) { - throw new AriesFrameworkError('Requirements of the presentation definition could not be satifsied') + throw new AriesFrameworkError('Requirements of the presentation definition could not be satisfied') } requirements.forEach((r) => { @@ -194,17 +208,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') } - // TODO: how do we get the `proof` from this? It does not seem to be available on the JWT class - const { type, context, verifiableCredential } = presentation.verifiablePresentations[0] - - const data: PresentationExchangePresentation = { - presentation_submission: presentation.presentationSubmission, - type, - context, - verifiableCredential, - } - - const attachment = this.getFormatData(data, format.attachmentId) + const firstPresentation = presentation.verifiablePresentations[0] + const attachmentData = firstPresentation.encoded as PresentationExchangePresentation + const attachment = this.getFormatData(attachmentData, format.attachmentId) return { attachment, format } } @@ -214,16 +220,66 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic { requestAttachment, attachment }: ProofFormatProcessPresentationOptions ): Promise { const ps = this.presentationExchangeService(agentContext) - const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson<{ - presentation_definition: PresentationDefinition - }>() + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + const request = requestAttachment.getDataAsJson() const presentation = attachment.getDataAsJson() + let parsedPresentation: W3cVerifiablePresentation + + // TODO: we should probably move this transformation logic into the VC module, so it + // can be reused in AFJ when we need to go from encoded -> parsed + if (typeof presentation === 'string') { + parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) + } else { + parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) + } + + if (!parsedPresentation.presentationSubmission) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without presentation submission. This should not happen.' + ) + return false + } + + if (!request.options?.challenge) { + agentContext.config.logger.error( + 'Received presentation in PEX proof format without challenge. This should not happen.' + ) + return false + } try { - ps.validatePresentationDefinition(presentationDefinition) - ps.validatePresentationSubmission(presentation.presentation_submission) + ps.validatePresentationDefinition(request.presentation_definition) + ps.validatePresentationSubmission(parsedPresentation.presentationSubmission) + ps.validatePresentation(request.presentation_definition, parsedPresentation) + + let verificationResult: W3cVerifyPresentationResult + + // FIXME: for some reason it won't accept the input if it doesn't know + // whether it's a JWT or JSON-LD VP even though the input is the same. + // Not sure how to fix + if (parsedPresentation.claimFormat === ClaimFormat.JwtVp) { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } else { + verificationResult = await w3cCredentialService.verifyPresentation(agentContext, { + presentation: parsedPresentation, + challenge: request.options.challenge, + domain: request.options.domain, + }) + } + + if (!verificationResult.isValid) { + agentContext.config.logger.error( + `Received presentation in PEX proof format that could not be verified: ${verificationResult.error}`, + { verificationResult } + ) + return false + } - ps.validatePresentation(presentationDefinition, presentation as unknown as VerifiablePresentation) return true } catch (e) { agentContext.config.logger.error(`Failed to verify presentation in PEX proof format service: ${e.message}`, { @@ -236,53 +292,27 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async getCredentialsForRequest( agentContext: AgentContext, { requestAttachment }: ProofFormatGetCredentialsForRequestOptions - ): Promise<{ credentials: InputDescriptorToCredentials }> { + ) { const ps = this.presentationExchangeService(agentContext) const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) - const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) - - if (!presentationSubmission.areRequirementsSatisfied) { - throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') - } - - const credentials: InputDescriptorToCredentials = {} - - presentationSubmission.requirements.forEach((r) => - r.submissionEntry.forEach((s) => { - credentials[s.inputDescriptorId] = s.verifiableCredentials.map((v) => v.credential) - }) - ) - - return { credentials } + const presentationSubmission = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return presentationSubmission } public async selectCredentialsForRequest( agentContext: AgentContext, { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions - ): Promise<{ credentials: InputDescriptorToCredentials }> { + ) { const ps = this.presentationExchangeService(agentContext) const { presentation_definition: presentationDefinition } = requestAttachment.getDataAsJson() - const presentationSubmission = await ps.selectCredentialsForRequest(agentContext, presentationDefinition) - - if (!presentationSubmission.areRequirementsSatisfied) { - throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') - } - - const credentials: InputDescriptorToCredentials = {} - - presentationSubmission.requirements.forEach((r) => - r.submissionEntry.forEach((s) => { - credentials[s.inputDescriptorId] = s.verifiableCredentials.map((v) => v.credential) - }) - ) - - return { credentials } + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + return { credentials: ps.selectCredentialsForRequest(credentialsForRequest) } } public async shouldAutoRespondToProposal( @@ -319,12 +349,12 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic return true } - private getFormatData>(data: T, id: string): Attachment { + private getFormatData | string>(data: T, id: string): Attachment { const attachment = new Attachment({ id, mimeType: 'application/json', data: new AttachmentData({ - json: data, + json: data as JsonValue, }), }) diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index 5df82cc53f..ff9de982a8 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -2,6 +2,8 @@ import type { PresentationDefinition } from '../../../../presentation-exchange' import type { ProofFormatService } from '../../ProofFormatService' import type { PresentationExchangeProofFormat } from '../PresentationExchangeProofFormat' +import { PresentationSubmissionLocation } from '@sphereon/pex' + import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' import { agentDependencies, getAgentConfig } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' @@ -65,11 +67,13 @@ const mockCredentialRecord = new W3cCredentialRecord({ }), }) +const presentationSubmission = { id: 'did:id', definition_id: 'my-id', descriptor_map: [] } jest.spyOn(W3cCredentialRepository.prototype, 'getAll').mockResolvedValue([mockCredentialRecord]) jest.spyOn(PresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ - presentationSubmission: { id: 'did:id', definition_id: 'my-id', descriptor_map: [] }, + presentationSubmission, verifiablePresentations: [ new W3cJsonLdVerifiablePresentation({ + presentationSubmission, verifiableCredential: [mockCredentialRecord.credential], proof: { type: 'Ed25519Signature2020', @@ -80,7 +84,7 @@ jest.spyOn(PresentationExchangeService.prototype, 'createPresentation').mockReso }, }), ], - presentationSubmissionLocation: 0, + presentationSubmissionLocation: PresentationSubmissionLocation.PRESENTATION, }) describe('Presentation Exchange ProofFormatService', () => { @@ -102,7 +106,6 @@ describe('Presentation Exchange ProofFormatService', () => { await agent.initialize() - agent.dependencyManager.resolve(PresentationExchangeService) pexFormatService = agent.dependencyManager.resolve(PresentationExchangeProofFormatService) }) @@ -141,7 +144,7 @@ describe('Presentation Exchange ProofFormatService', () => { data: { json: { options: { - challenge: 'TODO', + challenge: expect.any(String), }, presentation_definition: presentationDefinition, }, @@ -178,11 +181,11 @@ describe('Presentation Exchange ProofFormatService', () => { definition_id: expect.any(String), descriptor_map: [], }, - context: expect.any(Array), + '@context': expect.any(Array), type: expect.any(Array), verifiableCredential: [ { - context: expect.any(Array), + '@context': expect.any(Array), id: expect.any(String), type: expect.any(Array), issuer: expect.any(String), diff --git a/packages/core/src/modules/proofs/models/v2/exchange.ts b/packages/core/src/modules/proofs/models/v2/exchange.ts index c5fddab467..41175c423b 100644 --- a/packages/core/src/modules/proofs/models/v2/exchange.ts +++ b/packages/core/src/modules/proofs/models/v2/exchange.ts @@ -1,16 +1,20 @@ import type { PexPresentationSubmission, PresentationDefinition } from '../../../presentation-exchange' -import type { W3cPresentation } from '../../../vc' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' export type PresentationExchangeProposal = PresentationDefinition export type PresentationExchangeRequest = { - options: { + options?: { challenge?: string domain?: string } presentation_definition: PresentationDefinition } -export type PresentationExchangePresentation = W3cPresentation & { - presentation_submission: PexPresentationSubmission -} & Record +export type PresentationExchangePresentation = + | (W3cJsonPresentation & { + presentation_submission: PexPresentationSubmission + }) + // NOTE: this is not spec compliant, as it doesn't describe how to submit + // JWT VPs but to support JWT VPs we also allow the value to be a string + | string diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts index a60af9fe4a..264dd8d660 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/v2-presentation-exchange-presentation.e2e.test.ts @@ -338,7 +338,7 @@ describe('Present Proof', () => { mimeType: 'application/json', data: { json: { - context: expect.any(Array), + '@context': expect.any(Array), type: expect.any(Array), presentation_submission: { id: expect.any(String), @@ -346,13 +346,8 @@ describe('Present Proof', () => { descriptor_map: [ { id: 'citizenship_input_1', - format: 'ldp_vp', - path: '$', - path_nested: { - id: 'citizenship_input_1', - format: 'ldp_vc', - path: '$.verifiableCredential[0]', - }, + format: 'ldp_vc', + path: '$.verifiableCredential[0]', }, ], }, diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts index e2869c5333..99034ef06d 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts @@ -80,6 +80,10 @@ export class W3cJwtVerifiablePresentation { return this.presentation.holderId } + public get presentationSubmission() { + return this.presentation.presentationSubmission + } + /** * The {@link ClaimFormat} of the presentation. For JWT presentations this is always `jwt_vp`. */ diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index 299d0fbbc4..c69ef66ade 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -1,8 +1,10 @@ import type { W3cHolderOptions } from './W3cHolder' import type { JsonObject } from '../../../../types' +import type { PexPresentationSubmission } from '../../../presentation-exchange' import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' import type { ValidationOptions } from 'class-validator' +import { PresentationSubmission } from '@sphereon/ssi-types' import { Expose } from 'class-transformer' import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' @@ -22,6 +24,7 @@ export interface W3cPresentationOptions { type?: Array verifiableCredential: SingleOrArray holder?: string | W3cHolderOptions + presentationSubmission?: PexPresentationSubmission } export class W3cPresentation { @@ -31,6 +34,7 @@ export class W3cPresentation { this.context = options.context ?? [CREDENTIALS_CONTEXT_V1_URL] this.type = options.type ?? [VERIFIABLE_PRESENTATION_TYPE] this.verifiableCredential = options.verifiableCredential + this.presentationSubmission = options.presentationSubmission if (options.holder) { this.holder = typeof options.holder === 'string' ? options.holder : new W3cHolder(options.holder) @@ -42,6 +46,12 @@ export class W3cPresentation { @IsCredentialJsonLdContext() public context!: Array + /** + * NOTE: not validated + */ + @Expose({ name: 'presentation_submission' }) + public presentationSubmission?: PresentationSubmission + @IsOptional() @IsUri() public id?: string From 59ed3d3a2a34c3260b3386f1eab27037fe456cc3 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 11:58:50 +0700 Subject: [PATCH 12/17] cleanup and ppv2 only supports pex v1 Signed-off-by: Timo Glastra --- .../PresentationExchangeService.ts | 23 +--- ...mission.ts => PexCredentialsForRequest.ts} | 0 .../presentation-exchange/models/index.ts | 8 +- .../PresentationExchangeProofFormat.ts | 31 +++-- .../PresentationExchangeProofFormatService.ts | 14 +-- .../proofs/models/PresentationSubmission.ts | 119 ------------------ .../core/src/modules/proofs/models/index.ts | 1 - .../src/modules/proofs/models/v2/exchange.ts | 20 --- .../src/modules/proofs/models/v2/index.ts | 1 - .../proofs/protocol/v2/__tests__/fixtures.ts | 5 +- .../vc/models/presentation/W3cPresentation.ts | 5 +- 11 files changed, 48 insertions(+), 179 deletions(-) rename packages/core/src/modules/presentation-exchange/models/{PresentationSubmission.ts => PexCredentialsForRequest.ts} (100%) delete mode 100644 packages/core/src/modules/proofs/models/PresentationSubmission.ts delete mode 100644 packages/core/src/modules/proofs/models/v2/exchange.ts delete mode 100644 packages/core/src/modules/proofs/models/v2/index.ts diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts index 62d01b8c94..85deb2964b 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts @@ -1,25 +1,16 @@ -import type { InputDescriptorToCredentials, PexCredentialsForRequest } from './models' +import type { InputDescriptorToCredentials, PexCredentialsForRequest, PresentationDefinition } from './models' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { VerificationMethod } from '../dids' import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' -import type { - IPresentationDefinition, - PresentationSignCallBackParams, - Validated, - VerifiablePresentationResult, -} from '@sphereon/pex' +import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' import type { InputDescriptorV2, PresentationSubmission, PresentationDefinitionV1, PresentationDefinitionV2, } from '@sphereon/pex-models' -import type { - IVerifiablePresentation, - OriginalVerifiableCredential, - OriginalVerifiablePresentation, -} from '@sphereon/ssi-types' +import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { injectable } from 'tsyringe' @@ -44,11 +35,7 @@ import { getW3cVerifiablePresentationInstance, } from './utils' -// FIXME: Why are these Record types used? export type ProofStructure = Record>> -export type PresentationDefinition = IPresentationDefinition & Record -export type VerifiablePresentation = IVerifiablePresentation & Record -export type PexPresentationSubmission = PresentationSubmission @injectable() export class PresentationExchangeService { @@ -103,7 +90,7 @@ export class PresentationExchangeService { } } - public validatePresentationSubmission(presentationSubmission: PexPresentationSubmission) { + public validatePresentationSubmission(presentationSubmission: PresentationSubmission) { const validation = PEX.validateSubmission(presentationSubmission) const errorMessages = this.formatValidated(validation) if (errorMessages.length > 0) { @@ -324,7 +311,7 @@ export class PresentationExchangeService { throw new PresentationExchangeError('Invalid amount of verifiable presentations created') } - const presentationSubmission: PexPresentationSubmission = { + const presentationSubmission: PresentationSubmission = { id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, definition_id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, diff --git a/packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts b/packages/core/src/modules/presentation-exchange/models/PexCredentialsForRequest.ts similarity index 100% rename from packages/core/src/modules/presentation-exchange/models/PresentationSubmission.ts rename to packages/core/src/modules/presentation-exchange/models/PexCredentialsForRequest.ts diff --git a/packages/core/src/modules/presentation-exchange/models/index.ts b/packages/core/src/modules/presentation-exchange/models/index.ts index 47247cbbc9..e468787e6c 100644 --- a/packages/core/src/modules/presentation-exchange/models/index.ts +++ b/packages/core/src/modules/presentation-exchange/models/index.ts @@ -1 +1,7 @@ -export * from './PresentationSubmission' +export * from './PexCredentialsForRequest' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +type PresentationDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 + +// Re-export some types from sphereon library +export type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission, PresentationDefinition } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts index 487a1a3235..654efee8d8 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts @@ -1,21 +1,36 @@ import type { InputDescriptorToCredentials, - PresentationDefinition, PexCredentialsForRequest, + PresentationDefinitionV1, + PresentationSubmission, } from '../../../presentation-exchange' -import type { - PresentationExchangePresentation, - PresentationExchangeProposal, - PresentationExchangeRequest, -} from '../../models/v2' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' import type { ProofFormat } from '../ProofFormat' +export type PresentationExchangeProposal = PresentationDefinitionV1 + +export type PresentationExchangeRequest = { + options?: { + challenge?: string + domain?: string + } + presentation_definition: PresentationDefinitionV1 +} + +export type PresentationExchangePresentation = + | (W3cJsonPresentation & { + presentation_submission: PresentationSubmission + }) + // NOTE: this is not spec compliant, as it doesn't describe how to submit + // JWT VPs but to support JWT VPs we also allow the value to be a string + | string + export interface PresentationExchangeProofFormat extends ProofFormat { formatKey: 'presentationExchange' proofFormats: { createProposal: { - presentationDefinition: PresentationDefinition + presentationDefinition: PresentationDefinitionV1 } acceptProposal: { @@ -26,7 +41,7 @@ export interface PresentationExchangeProofFormat extends ProofFormat { } createRequest: { - presentationDefinition: PresentationDefinition + presentationDefinition: PresentationDefinitionV1 options?: { challenge?: string domain?: string diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts index c52f76f3c3..0f1c31b19b 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts @@ -1,13 +1,13 @@ -import type { PresentationExchangeProofFormat } from './PresentationExchangeProofFormat' -import type { AgentContext } from '../../../../agent' -import type { JsonValue } from '../../../../types' -import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' -import type { InputDescriptorToCredentials } from '../../models' import type { PresentationExchangePresentation, + PresentationExchangeProofFormat, PresentationExchangeProposal, PresentationExchangeRequest, -} from '../../models/v2' +} from './PresentationExchangeProofFormat' +import type { AgentContext } from '../../../../agent' +import type { JsonValue } from '../../../../types' +import type { InputDescriptorToCredentials } from '../../../presentation-exchange' +import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -349,7 +349,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic return true } - private getFormatData | string>(data: T, id: string): Attachment { + private getFormatData(data: unknown, id: string): Attachment { const attachment = new Attachment({ id, mimeType: 'application/json', diff --git a/packages/core/src/modules/proofs/models/PresentationSubmission.ts b/packages/core/src/modules/proofs/models/PresentationSubmission.ts deleted file mode 100644 index 309cb93c62..0000000000 --- a/packages/core/src/modules/proofs/models/PresentationSubmission.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' - -export interface PresentationSubmission { - /** - * Whether all requirements have been satisfied by the credentials in the wallet. - */ - areRequirementsSatisfied: boolean - - /** - * The requirements for the presentation definition. If the `areRequirementsSatisfied` value - * is `false`, this list will still be populated with requirements, but won't contain credentials - * for all requirements. This can be useful to display the missing credentials for a presentation - * definition to be satisfied. - * - * NOTE: Presentation definition requirements can be really complex as there's a lot of different - * combinations that are possible. The structure doesn't include all possible combinations yet that - * could satisfy a presentation definition. - */ - requirements: PresentationSubmissionRequirement[] - - /** - * Name of the presentation definition - */ - name?: string - - /** - * Purpose of the presentation definition. - */ - purpose?: string -} - -/** - * A requirement for the presentation submission. A requirement - * is a group of input descriptors that together fulfill a requirement - * from the presentation definition. - * - * Each submission represents a input descriptor. - */ -export interface PresentationSubmissionRequirement { - /** - * Whether the requirement is satisfied. - * - * If the requirement is not satisfied, the submission will still contain - * entries, but the `verifiableCredentials` list will be empty. - */ - isRequirementSatisfied: boolean - - /** - * Name of the requirement - */ - name?: string - - /** - * Purpose of the requirement - */ - purpose?: string - - /** - * Array of objects, where each entry contains a credential that will be part - * of the submission. - * - * NOTE: if the `isRequirementSatisfied` is `false` the submission list will - * contain entries where the verifiable credential list is empty. In this case it could also - * contain more entries than are actually needed (as you sometimes can choose from - * e.g. 4 types of credentials and need to submit at least two). If - * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value - * to see how many of those submissions needed. - */ - submissionEntry: SubmissionEntry[] - - /** - * The number of submission entries that are needed to fulfill the requirement. - * If `isRequirementSatisfied` is `true`, the submission list will always be equal - * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of - * submissions could be longer. - */ - needsCount: number - - /** - * The rule that is used to select the credentials for the submission. - * If the rule is `pick`, the user can select which credentials to use for the submission. - * If the rule is `all`, all credentials that satisfy the input descriptor will be used. - */ - rule: 'pick' | 'all' -} - -/** - * A submission entry that satisfies a specific input descriptor from the - * presentation definition. - */ -export interface SubmissionEntry { - /** - * The id of the input descriptor - */ - inputDescriptorId: string - - /** - * Name of the input descriptor - */ - name?: string - - /** - * Purpose of the input descriptor - */ - purpose?: string - - /** - * The verifiable credentials that satisfy the input descriptor. - * - * If the value is an empty list, it means the input descriptor could - * not be satisfied. - */ - verifiableCredentials: W3cCredentialRecord[] -} - -/** - * Mapping of selected credentials for an input descriptor - */ -export type InputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/proofs/models/index.ts b/packages/core/src/modules/proofs/models/index.ts index 660b1db211..9dec0e697a 100644 --- a/packages/core/src/modules/proofs/models/index.ts +++ b/packages/core/src/modules/proofs/models/index.ts @@ -1,4 +1,3 @@ export * from './ProofAutoAcceptType' export * from './ProofState' export * from './ProofFormatSpec' -export * from './PresentationSubmission' diff --git a/packages/core/src/modules/proofs/models/v2/exchange.ts b/packages/core/src/modules/proofs/models/v2/exchange.ts deleted file mode 100644 index 41175c423b..0000000000 --- a/packages/core/src/modules/proofs/models/v2/exchange.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PexPresentationSubmission, PresentationDefinition } from '../../../presentation-exchange' -import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' - -export type PresentationExchangeProposal = PresentationDefinition - -export type PresentationExchangeRequest = { - options?: { - challenge?: string - domain?: string - } - presentation_definition: PresentationDefinition -} - -export type PresentationExchangePresentation = - | (W3cJsonPresentation & { - presentation_submission: PexPresentationSubmission - }) - // NOTE: this is not spec compliant, as it doesn't describe how to submit - // JWT VPs but to support JWT VPs we also allow the value to be a string - | string diff --git a/packages/core/src/modules/proofs/models/v2/index.ts b/packages/core/src/modules/proofs/models/v2/index.ts deleted file mode 100644 index 7c802c4137..0000000000 --- a/packages/core/src/modules/proofs/models/v2/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './exchange' diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts index 4c776b09da..0b3d8c39b9 100644 --- a/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/fixtures.ts @@ -1,3 +1,5 @@ +import type { InputDescriptorV1 } from '@sphereon/pex-models' + export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { constraints: { fields: [ @@ -7,4 +9,5 @@ export const TEST_INPUT_DESCRIPTORS_CITIZENSHIP = { ], }, id: 'citizenship_input_1', -} + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], +} satisfies InputDescriptorV1 diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index c69ef66ade..28cc6ba1c4 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -1,15 +1,14 @@ import type { W3cHolderOptions } from './W3cHolder' import type { JsonObject } from '../../../../types' -import type { PexPresentationSubmission } from '../../../presentation-exchange' import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' import type { ValidationOptions } from 'class-validator' -import { PresentationSubmission } from '@sphereon/ssi-types' import { Expose } from 'class-transformer' import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' import { SingleOrArray } from '../../../../utils/type' import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' +import { PresentationSubmission } from '../../../presentation-exchange/models' import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models/W3cJsonLdVerifiableCredential' import { W3cJwtVerifiableCredential } from '../../jwt-vc/W3cJwtVerifiableCredential' @@ -24,7 +23,7 @@ export interface W3cPresentationOptions { type?: Array verifiableCredential: SingleOrArray holder?: string | W3cHolderOptions - presentationSubmission?: PexPresentationSubmission + presentationSubmission?: PresentationSubmission } export class W3cPresentation { From 04b17b657602c10b4e7689927006479e1a78940e Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 12:27:23 +0700 Subject: [PATCH 13/17] refactor: add dif prefix Signed-off-by: Timo Glastra --- packages/core/src/agent/AgentModules.ts | 4 +- .../DifPresentationExchangeError.ts} | 2 +- .../DifPresentationExchangeModule.ts} | 8 +- .../DifPresentationExchangeService.ts} | 105 ++++++++++-------- .../dif-presentation-exchange/index.ts | 4 + .../models/DifPexCredentialsForRequest.ts} | 12 +- .../dif-presentation-exchange/models/index.ts | 11 ++ .../utils/credentialSelection.ts | 46 ++++---- .../utils/index.ts | 0 .../utils/transform.ts | 8 +- .../modules/presentation-exchange/index.ts | 4 - .../presentation-exchange/models/index.ts | 7 -- .../DifPresentationExchangeProofFormat.ts} | 38 +++---- ...PresentationExchangeProofFormatService.ts} | 76 +++++++------ ...entationExchangeProofFormatService.test.ts | 19 ++-- .../dif-presentation-exchange/index.ts | 2 + .../core/src/modules/proofs/formats/index.ts | 2 +- .../formats/presentation-exchange/index.ts | 2 - .../vc/models/presentation/W3cPresentation.ts | 6 +- 19 files changed, 188 insertions(+), 168 deletions(-) rename packages/core/src/modules/{presentation-exchange/PresentationExchangeError.ts => dif-presentation-exchange/DifPresentationExchangeError.ts} (81%) rename packages/core/src/modules/{presentation-exchange/PresentationExchangeModule.ts => dif-presentation-exchange/DifPresentationExchangeModule.ts} (51%) rename packages/core/src/modules/{presentation-exchange/PresentationExchangeService.ts => dif-presentation-exchange/DifPresentationExchangeService.ts} (85%) create mode 100644 packages/core/src/modules/dif-presentation-exchange/index.ts rename packages/core/src/modules/{presentation-exchange/models/PexCredentialsForRequest.ts => dif-presentation-exchange/models/DifPexCredentialsForRequest.ts} (89%) create mode 100644 packages/core/src/modules/dif-presentation-exchange/models/index.ts rename packages/core/src/modules/{presentation-exchange => dif-presentation-exchange}/utils/credentialSelection.ts (87%) rename packages/core/src/modules/{presentation-exchange => dif-presentation-exchange}/utils/index.ts (100%) rename packages/core/src/modules/{presentation-exchange => dif-presentation-exchange}/utils/transform.ts (93%) delete mode 100644 packages/core/src/modules/presentation-exchange/index.ts delete mode 100644 packages/core/src/modules/presentation-exchange/models/index.ts rename packages/core/src/modules/proofs/formats/{presentation-exchange/PresentationExchangeProofFormat.ts => dif-presentation-exchange/DifPresentationExchangeProofFormat.ts} (52%) rename packages/core/src/modules/proofs/formats/{presentation-exchange/PresentationExchangeProofFormatService.ts => dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts} (81%) rename packages/core/src/modules/proofs/formats/{presentation-exchange => dif-presentation-exchange}/__tests__/PresentationExchangeProofFormatService.test.ts (88%) create mode 100644 packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts delete mode 100644 packages/core/src/modules/proofs/formats/presentation-exchange/index.ts diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index a1d4388605..a93e8e2f98 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -11,7 +11,7 @@ import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' -import { PresentationExchangeModule } from '../modules/presentation-exchange' +import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { ProofsModule } from '../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../modules/routing' import { W3cCredentialsModule } from '../modules/vc' @@ -132,7 +132,7 @@ function getDefaultAgentModules() { oob: () => new OutOfBandModule(), w3cCredentials: () => new W3cCredentialsModule(), cache: () => new CacheModule(), - pex: () => new PresentationExchangeModule(), + pex: () => new DifPresentationExchangeModule(), } as const } diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts similarity index 81% rename from packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts rename to packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts index 2cf10e1a4b..5c06ec420a 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeError.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeError.ts @@ -1,6 +1,6 @@ import { AriesFrameworkError } from '../../error' -export class PresentationExchangeError extends AriesFrameworkError { +export class DifPresentationExchangeError extends AriesFrameworkError { public additionalMessages?: Array public constructor( diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts similarity index 51% rename from packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts rename to packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts index a9673509e9..7cb2c86c5a 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeModule.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeModule.ts @@ -2,12 +2,12 @@ import type { DependencyManager, Module } from '../../plugins' import { AgentConfig } from '../../agent/AgentConfig' -import { PresentationExchangeService } from './PresentationExchangeService' +import { DifPresentationExchangeService } from './DifPresentationExchangeService' /** * @public */ -export class PresentationExchangeModule implements Module { +export class DifPresentationExchangeModule implements Module { /** * Registers the dependencies of the presentation-exchange module on the dependency manager. */ @@ -16,10 +16,10 @@ export class PresentationExchangeModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The 'PresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The 'DifPresentationExchangeModule' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) // service - dependencyManager.registerSingleton(PresentationExchangeService) + dependencyManager.registerSingleton(DifPresentationExchangeService) } } diff --git a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts similarity index 85% rename from packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts rename to packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 85deb2964b..eab8642230 100644 --- a/packages/core/src/modules/presentation-exchange/PresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -1,18 +1,20 @@ -import type { InputDescriptorToCredentials, PexCredentialsForRequest, PresentationDefinition } from './models' +import type { + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinition, + DifPresentationExchangeDefinitionV1, + DifPresentationExchangeSubmission, + DifPresentationExchangeDefinitionV2, +} from './models' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { VerificationMethod } from '../dids' import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' -import type { - InputDescriptorV2, - PresentationSubmission, - PresentationDefinitionV1, - PresentationDefinitionV2, -} from '@sphereon/pex-models' +import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' -import { Status, PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' +import { Status, PEVersion, PEX } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' @@ -27,7 +29,8 @@ import { W3cPresentation, } from '../vc' -import { PresentationExchangeError } from './PresentationExchangeError' +import { DifPresentationExchangeError } from './DifPresentationExchangeError' +import { DifPresentationExchangeSubmissionLocation } from './models' import { getCredentialsForRequest, getSphereonOriginalVerifiableCredential, @@ -38,13 +41,13 @@ import { export type ProofStructure = Record>> @injectable() -export class PresentationExchangeService { +export class DifPresentationExchangeService { private pex = new PEX() public async getCredentialsForRequest( agentContext: AgentContext, - presentationDefinition: PresentationDefinition - ): Promise { + presentationDefinition: DifPresentationExchangeDefinition + ): Promise { const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) // FIXME: why are we resolving all created dids here? @@ -61,12 +64,14 @@ export class PresentationExchangeService { * Selects the credentials to use based on the output from `getCredentialsForRequest` * Use this method if you don't want to manually select the credentials yourself. */ - public selectCredentialsForRequest(credentialsForRequest: PexCredentialsForRequest): InputDescriptorToCredentials { + public selectCredentialsForRequest( + credentialsForRequest: DifPexCredentialsForRequest + ): DifPexInputDescriptorToCredentials { if (!credentialsForRequest.areRequirementsSatisfied) { throw new AriesFrameworkError('Could not find the required credentials for the presentation submission') } - const credentials: InputDescriptorToCredentials = {} + const credentials: DifPexInputDescriptorToCredentials = {} for (const requirement of credentialsForRequest.requirements) { for (const submission of requirement.submissionEntry) { @@ -82,23 +87,26 @@ export class PresentationExchangeService { return credentials } - public validatePresentationDefinition(presentationDefinition: PresentationDefinition) { + public validatePresentationDefinition(presentationDefinition: DifPresentationExchangeDefinition) { const validation = PEX.validateDefinition(presentationDefinition) const errorMessages = this.formatValidated(validation) if (errorMessages.length > 0) { - throw new PresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) + throw new DifPresentationExchangeError(`Invalid presentation definition`, { additionalMessages: errorMessages }) } } - public validatePresentationSubmission(presentationSubmission: PresentationSubmission) { + public validatePresentationSubmission(presentationSubmission: DifPresentationExchangeSubmission) { const validation = PEX.validateSubmission(presentationSubmission) const errorMessages = this.formatValidated(validation) if (errorMessages.length > 0) { - throw new PresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) + throw new DifPresentationExchangeError(`Invalid presentation submission`, { additionalMessages: errorMessages }) } } - public validatePresentation(presentationDefinition: PresentationDefinition, presentation: W3cVerifiablePresentation) { + public validatePresentation( + presentationDefinition: DifPresentationExchangeDefinition, + presentation: W3cVerifiablePresentation + ) { const { errors } = this.pex.evaluatePresentation( presentationDefinition, presentation.encoded as OriginalVerifiablePresentation @@ -107,7 +115,7 @@ export class PresentationExchangeService { if (errors) { const errorMessages = this.formatValidated(errors as Validated) if (errorMessages.length > 0) { - throw new PresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) + throw new DifPresentationExchangeError(`Invalid presentation`, { additionalMessages: errorMessages }) } } } @@ -126,14 +134,14 @@ export class PresentationExchangeService { */ private async queryCredentialForPresentationDefinition( agentContext: AgentContext, - presentationDefinition: PresentationDefinition + presentationDefinition: DifPresentationExchangeDefinition ): Promise> { const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) const query: Array> = [] const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) if (!presentationDefinitionVersion.version) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Unable to determine the Presentation Exchange version from the presentation definition `, presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} @@ -156,7 +164,7 @@ export class PresentationExchangeService { // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` ) } @@ -188,7 +196,7 @@ export class PresentationExchangeService { } private getPresentationFormat( - presentationDefinition: PresentationDefinition, + presentationDefinition: DifPresentationExchangeDefinition, credentials: Array ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') @@ -215,7 +223,7 @@ export class PresentationExchangeService { ) { return ClaimFormat.LdpVp } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( 'No suitable presentation format found for the given presentation definition, and credentials' ) } @@ -224,12 +232,12 @@ export class PresentationExchangeService { public async createPresentation( agentContext: AgentContext, options: { - credentialsForInputDescriptor: InputDescriptorToCredentials - presentationDefinition: PresentationDefinition + credentialsForInputDescriptor: DifPexInputDescriptorToCredentials + presentationDefinition: DifPresentationExchangeDefinition /** - * Defaults to {@link PresentationSubmissionLocation.PRESENTATION} + * Defaults to {@link DifPresentationExchangeSubmissionLocation.PRESENTATION} */ - presentationSubmissionLocation?: PresentationSubmissionLocation + presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation challenge?: string domain?: string nonce?: string @@ -243,7 +251,7 @@ export class PresentationExchangeService { credentials.forEach((credential) => { const subjectId = credential.credentialSubjectIds[0] if (!subjectId) { - throw new PresentationExchangeError('Missing required credential subject for creating the presentation.') + throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') } this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) @@ -261,7 +269,7 @@ export class PresentationExchangeService { const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) if (!verificationMethod) { - throw new PresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) + throw new DifPresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) } // We create a presentation for each subject @@ -276,7 +284,7 @@ export class PresentationExchangeService { .flat() .map(getSphereonOriginalVerifiableCredential) - const presentationDefinitionForSubject: PresentationDefinition = { + const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { ...presentationDefinition, input_descriptors: inputDescriptorsForSubject, @@ -296,7 +304,8 @@ export class PresentationExchangeService { holderDID: subjectId, proofOptions: { challenge, domain, nonce }, signatureOptions: { verificationMethod: verificationMethod?.id }, - presentationSubmissionLocation: presentationSubmissionLocation ?? PresentationSubmissionLocation.PRESENTATION, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, } ) @@ -304,14 +313,14 @@ export class PresentationExchangeService { } if (!verifiablePresentationResultsWithFormat[0]) { - throw new PresentationExchangeError('No verifiable presentations created') + throw new DifPresentationExchangeError('No verifiable presentations created') } if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { - throw new PresentationExchangeError('Invalid amount of verifiable presentations created') + throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') } - const presentationSubmission: PresentationSubmission = { + const presentationSubmission: DifPresentationExchangeSubmission = { id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, definition_id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, @@ -343,7 +352,7 @@ export class PresentationExchangeService { if (suitableAlgorithms) { const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) if (!possibleAlgorithms || possibleAlgorithms.length === 0) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( [ `Found no suitable signing algorithm.`, `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, @@ -354,7 +363,7 @@ export class PresentationExchangeService { } const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) throw new PresentationExchangeError(`No supported algs for key type: ${key.keyType}`) + if (!alg) throw new DifPresentationExchangeError(`No supported algs for key type: ${key.keyType}`) return alg } @@ -376,13 +385,13 @@ export class PresentationExchangeService { algorithmsSatisfyingDescriptors.length > 0 && algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 ) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors` ) } if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `No signature algorithm found for satisfying restrictions of the input descriptors` ) } @@ -400,7 +409,7 @@ export class PresentationExchangeService { } private getSigningAlgorithmForJwtVc( - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, verificationMethod: VerificationMethod ) { const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg ?? [] @@ -419,7 +428,7 @@ export class PresentationExchangeService { private getProofTypeForLdpVc( agentContext: AgentContext, - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, + presentationDefinition: DifPresentationExchangeDefinitionV1 | DifPresentationExchangeDefinitionV2, verificationMethod: VerificationMethod ) { const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type ?? [] @@ -438,14 +447,14 @@ export class PresentationExchangeService { const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) if (!supportedSignatureSuite) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'` ) } if (suitableSignatureSuites) { if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( [ 'No possible signature suite found for the given verification method.', `Verification method type: ${verificationMethod.type}`, @@ -475,7 +484,7 @@ export class PresentationExchangeService { const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} if (verificationMethodId && verificationMethodId !== verificationMethod.id) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}` ) } @@ -501,7 +510,7 @@ export class PresentationExchangeService { domain, }) } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Only JWT credentials or JSONLD credentials are supported for a single presentation` ) } @@ -514,7 +523,7 @@ export class PresentationExchangeService { const didsApi = agentContext.dependencyManager.resolve(DidsApi) if (!subjectId.startsWith('did:')) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Only dids are supported as credentialSubject id. ${subjectId} is not a valid did` ) } @@ -522,7 +531,7 @@ export class PresentationExchangeService { const didDocument = await didsApi.resolveDidDocument(subjectId) if (!didDocument.authentication || didDocument.authentication.length === 0) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `No authentication verificationMethods found for did ${subjectId} in did document` ) } diff --git a/packages/core/src/modules/dif-presentation-exchange/index.ts b/packages/core/src/modules/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..4f4e4b3923 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/index.ts @@ -0,0 +1,4 @@ +export * from './DifPresentationExchangeError' +export * from './DifPresentationExchangeModule' +export * from './DifPresentationExchangeService' +export * from './models' diff --git a/packages/core/src/modules/presentation-exchange/models/PexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts similarity index 89% rename from packages/core/src/modules/presentation-exchange/models/PexCredentialsForRequest.ts rename to packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts index 4b88d58681..ec2e83d17e 100644 --- a/packages/core/src/modules/presentation-exchange/models/PexCredentialsForRequest.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -1,6 +1,6 @@ import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' -export interface PexCredentialsForRequest { +export interface DifPexCredentialsForRequest { /** * Whether all requirements have been satisfied by the credentials in the wallet. */ @@ -16,7 +16,7 @@ export interface PexCredentialsForRequest { * combinations that are possible. The structure doesn't include all possible combinations yet that * could satisfy a presentation definition. */ - requirements: PexCredentialsForRequestRequirement[] + requirements: DifPexCredentialsForRequestRequirement[] /** * Name of the presentation definition @@ -36,7 +36,7 @@ export interface PexCredentialsForRequest { * * Each submission represents a input descriptor. */ -export interface PexCredentialsForRequestRequirement { +export interface DifPexCredentialsForRequestRequirement { /** * Whether the requirement is satisfied. * @@ -66,7 +66,7 @@ export interface PexCredentialsForRequestRequirement { * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value * to see how many of those submissions needed. */ - submissionEntry: PexCredentialsForRequestSubmissionEntry[] + submissionEntry: DifPexCredentialsForRequestSubmissionEntry[] /** * The number of submission entries that are needed to fulfill the requirement. @@ -88,7 +88,7 @@ export interface PexCredentialsForRequestRequirement { * A submission entry that satisfies a specific input descriptor from the * presentation definition. */ -export interface PexCredentialsForRequestSubmissionEntry { +export interface DifPexCredentialsForRequestSubmissionEntry { /** * The id of the input descriptor */ @@ -116,4 +116,4 @@ export interface PexCredentialsForRequestSubmissionEntry { /** * Mapping of selected credentials for an input descriptor */ -export type InputDescriptorToCredentials = Record> +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts new file mode 100644 index 0000000000..01ce9d6767 --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -0,0 +1,11 @@ +export * from './DifPexCredentialsForRequest' +import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' + +import { PresentationSubmissionLocation } from '@sphereon/pex' + +// Re-export some types from sphereon library, but under more explicit names +export type DifPresentationExchangeDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 +export type DifPresentationExchangeDefinitionV1 = PresentationDefinitionV1 +export type DifPresentationExchangeDefinitionV2 = PresentationDefinitionV2 +export type DifPresentationExchangeSubmission = PresentationSubmission +export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLocation } diff --git a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts similarity index 87% rename from packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts rename to packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 299e5844cd..1fca34b943 100644 --- a/packages/core/src/modules/presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -1,8 +1,8 @@ import type { W3cCredentialRecord } from '../../vc' import type { - PexCredentialsForRequest, - PexCredentialsForRequestRequirement, - PexCredentialsForRequestSubmissionEntry, + DifPexCredentialsForRequest, + DifPexCredentialsForRequestRequirement, + DifPexCredentialsForRequestSubmissionEntry, } from '../models' import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' @@ -12,7 +12,7 @@ import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' import { deepEquality } from '../../../utils' -import { PresentationExchangeError } from '../PresentationExchangeError' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' import { getSphereonOriginalVerifiableCredential } from './transform' @@ -20,9 +20,9 @@ export async function getCredentialsForRequest( presentationDefinition: IPresentationDefinition, credentialRecords: Array, holderDIDs: Array -): Promise { +): Promise { if (!presentationDefinition) { - throw new PresentationExchangeError('Presentation Definition is required to select credentials for submission.') + throw new DifPresentationExchangeError('Presentation Definition is required to select credentials for submission.') } const pex = new PEX() @@ -47,14 +47,14 @@ export async function getCredentialsForRequest( }) if (!credentialRecord) { - throw new PresentationExchangeError('Unable to find credential in credential records.') + throw new DifPresentationExchangeError('Unable to find credential in credential records.') } return credentialRecord }), } - const presentationSubmission: PexCredentialsForRequest = { + const presentationSubmission: DifPexCredentialsForRequest = { requirements: [], areRequirementsSatisfied: false, name: presentationDefinition.name, @@ -75,7 +75,7 @@ export async function getCredentialsForRequest( // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) // I see this more as the fault of the presentation definition, as it should have at least some requirements. if (presentationSubmission.requirements.length === 0) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' ) } @@ -96,22 +96,22 @@ export async function getCredentialsForRequest( function getSubmissionRequirements( presentationDefinition: IPresentationDefinition, selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] +): Array { + const submissionRequirements: Array = [] // There are submission requirements, so we need to select the input_descriptors // based on the submission requirements for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet if (submissionRequirement.from_nested) { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( "Presentation definition contains requirement using 'from_nested', which is not supported yet." ) } // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) { - throw new PresentationExchangeError("Missing 'from' in submission requirement match") + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match") } if (submissionRequirement.rule === Rules.All) { @@ -142,8 +142,8 @@ function getSubmissionRequirements( function getSubmissionRequirementsForAllInputDescriptors( inputDescriptors: Array | Array, selectResults: W3cCredentialRecordSelectResults -): Array { - const submissionRequirements: Array = [] +): Array { + const submissionRequirements: Array = [] for (const inputDescriptor of inputDescriptors) { const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) @@ -166,9 +166,9 @@ function getSubmissionRequirementRuleAll( ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) - throw new PresentationExchangeError("Missing 'from' in submission requirement match.") + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") - const selectedSubmission: PexCredentialsForRequestRequirement = { + const selectedSubmission: DifPexCredentialsForRequestRequirement = { rule: Rules.All, needsCount: 0, name: submissionRequirement.name, @@ -205,10 +205,10 @@ function getSubmissionRequirementRulePick( ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) { - throw new PresentationExchangeError("Missing 'from' in submission requirement match.") + throw new DifPresentationExchangeError("Missing 'from' in submission requirement match.") } - const selectedSubmission: PexCredentialsForRequestRequirement = { + const selectedSubmission: DifPexCredentialsForRequestRequirement = { rule: Rules.Pick, needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, name: submissionRequirement.name, @@ -219,8 +219,8 @@ function getSubmissionRequirementRulePick( isRequirementSatisfied: false, } - const satisfiedSubmissions: Array = [] - const unsatisfiedSubmissions: Array = [] + const satisfiedSubmissions: Array = [] + const unsatisfiedSubmissions: Array = [] for (const inputDescriptor of presentationDefinition.input_descriptors) { // We only want to get the submission if the input descriptor belongs to the group @@ -258,7 +258,7 @@ function getSubmissionRequirementRulePick( function getSubmissionForInputDescriptor( inputDescriptor: InputDescriptorV1 | InputDescriptorV2, selectResults: W3cCredentialRecordSelectResults -): PexCredentialsForRequestSubmissionEntry { +): DifPexCredentialsForRequestSubmissionEntry { // https://github.com/Sphereon-Opensource/PEX/issues/116 // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it const matchesForInputDescriptor = selectResults.matches?.filter( @@ -268,7 +268,7 @@ function getSubmissionForInputDescriptor( m.name === inputDescriptor.name ) - const submissionEntry: PexCredentialsForRequestSubmissionEntry = { + const submissionEntry: DifPexCredentialsForRequestSubmissionEntry = { inputDescriptorId: inputDescriptor.id, name: inputDescriptor.name, purpose: inputDescriptor.purpose, diff --git a/packages/core/src/modules/presentation-exchange/utils/index.ts b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts similarity index 100% rename from packages/core/src/modules/presentation-exchange/utils/index.ts rename to packages/core/src/modules/dif-presentation-exchange/utils/index.ts diff --git a/packages/core/src/modules/presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts similarity index 93% rename from packages/core/src/modules/presentation-exchange/utils/transform.ts rename to packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index c857c362db..e4d5f694c9 100644 --- a/packages/core/src/modules/presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -13,7 +13,7 @@ import { W3cJwtVerifiablePresentation, ClaimFormat, } from '../../vc' -import { PresentationExchangeError } from '../PresentationExchangeError' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' export function getSphereonOriginalVerifiableCredential( w3cVerifiableCredential: W3cVerifiableCredential @@ -23,7 +23,7 @@ export function getSphereonOriginalVerifiableCredential( } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { return w3cVerifiableCredential.serializedJwt } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` ) } @@ -37,7 +37,7 @@ export function getSphereonW3cVerifiableCredential( } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { return w3cVerifiableCredential.serializedJwt } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` ) } @@ -51,7 +51,7 @@ export function getSphereonW3cVerifiablePresentation( } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { return w3cVerifiablePresentation.serializedJwt } else { - throw new PresentationExchangeError( + throw new DifPresentationExchangeError( `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` ) } diff --git a/packages/core/src/modules/presentation-exchange/index.ts b/packages/core/src/modules/presentation-exchange/index.ts deleted file mode 100644 index 0bb3c76aae..0000000000 --- a/packages/core/src/modules/presentation-exchange/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './PresentationExchangeError' -export * from './PresentationExchangeModule' -export * from './PresentationExchangeService' -export * from './models' diff --git a/packages/core/src/modules/presentation-exchange/models/index.ts b/packages/core/src/modules/presentation-exchange/models/index.ts deleted file mode 100644 index e468787e6c..0000000000 --- a/packages/core/src/modules/presentation-exchange/models/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './PexCredentialsForRequest' -import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' - -type PresentationDefinition = PresentationDefinitionV1 | PresentationDefinitionV2 - -// Re-export some types from sphereon library -export type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission, PresentationDefinition } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts similarity index 52% rename from packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts rename to packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts index 654efee8d8..7ebef33c46 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts @@ -1,36 +1,36 @@ import type { - InputDescriptorToCredentials, - PexCredentialsForRequest, - PresentationDefinitionV1, - PresentationSubmission, -} from '../../../presentation-exchange' + DifPexInputDescriptorToCredentials, + DifPexCredentialsForRequest, + DifPresentationExchangeDefinitionV1, + DifPresentationExchangeSubmission, +} from '../../../dif-presentation-exchange' import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' import type { ProofFormat } from '../ProofFormat' -export type PresentationExchangeProposal = PresentationDefinitionV1 +export type DifPresentationExchangeProposal = DifPresentationExchangeDefinitionV1 -export type PresentationExchangeRequest = { +export type DifPresentationExchangeRequest = { options?: { challenge?: string domain?: string } - presentation_definition: PresentationDefinitionV1 + presentation_definition: DifPresentationExchangeDefinitionV1 } -export type PresentationExchangePresentation = +export type DifPresentationExchangePresentation = | (W3cJsonPresentation & { - presentation_submission: PresentationSubmission + presentation_submission: DifPresentationExchangeSubmission }) // NOTE: this is not spec compliant, as it doesn't describe how to submit // JWT VPs but to support JWT VPs we also allow the value to be a string | string -export interface PresentationExchangeProofFormat extends ProofFormat { +export interface DifPresentationExchangeProofFormat extends ProofFormat { formatKey: 'presentationExchange' proofFormats: { createProposal: { - presentationDefinition: PresentationDefinitionV1 + presentationDefinition: DifPresentationExchangeDefinitionV1 } acceptProposal: { @@ -41,7 +41,7 @@ export interface PresentationExchangeProofFormat extends ProofFormat { } createRequest: { - presentationDefinition: PresentationDefinitionV1 + presentationDefinition: DifPresentationExchangeDefinitionV1 options?: { challenge?: string domain?: string @@ -49,13 +49,13 @@ export interface PresentationExchangeProofFormat extends ProofFormat { } acceptRequest: { - credentials?: InputDescriptorToCredentials + credentials?: DifPexInputDescriptorToCredentials } getCredentialsForRequest: { input: never // Presentation submission details which the options that are available - output: PexCredentialsForRequest + output: DifPexCredentialsForRequest } selectCredentialsForRequest: { @@ -63,14 +63,14 @@ export interface PresentationExchangeProofFormat extends ProofFormat { // Input descriptor to credentials specifically details which credentials // should be used for which input descriptor output: { - credentials: InputDescriptorToCredentials + credentials: DifPexInputDescriptorToCredentials } } } formatData: { - proposal: PresentationExchangeProposal - request: PresentationExchangeRequest - presentation: PresentationExchangePresentation + proposal: DifPresentationExchangeProposal + request: DifPresentationExchangeRequest + presentation: DifPresentationExchangePresentation } } diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts similarity index 81% rename from packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts rename to packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 0f1c31b19b..60c5d5807d 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -1,12 +1,12 @@ import type { - PresentationExchangePresentation, - PresentationExchangeProofFormat, - PresentationExchangeProposal, - PresentationExchangeRequest, -} from './PresentationExchangeProofFormat' + DifPresentationExchangePresentation, + DifPresentationExchangeProofFormat, + DifPresentationExchangeProposal, + DifPresentationExchangeRequest, +} from './DifPresentationExchangeProofFormat' import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' -import type { InputDescriptorToCredentials } from '../../../presentation-exchange' +import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' import type { ProofFormatService } from '../ProofFormatService' import type { @@ -27,7 +27,7 @@ import type { import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' import { deepEquality, JsonTransformer } from '../../../../utils' -import { PresentationExchangeService } from '../../../presentation-exchange/PresentationExchangeService' +import { DifPresentationExchangeService } from '../../../dif-presentation-exchange' import { W3cCredentialService, ClaimFormat, @@ -40,17 +40,17 @@ const PRESENTATION_EXCHANGE_PRESENTATION_PROPOSAL = 'dif/presentation-exchange/d const PRESENTATION_EXCHANGE_PRESENTATION_REQUEST = 'dif/presentation-exchange/definitions@v1.0' const PRESENTATION_EXCHANGE_PRESENTATION = 'dif/presentation-exchange/submission@v1.0' -export class PresentationExchangeProofFormatService implements ProofFormatService { +export class PresentationExchangeProofFormatService implements ProofFormatService { public readonly formatKey = 'presentationExchange' as const private presentationExchangeService(agentContext: AgentContext) { - if (!agentContext.dependencyManager.isRegistered(PresentationExchangeService)) { + if (!agentContext.dependencyManager.isRegistered(DifPresentationExchangeService)) { throw new AriesFrameworkError( - 'PresentationExchangeService is not registered on the Agent. Please provide the PresentationExchangeModule as a module on the agent' + 'DifPresentationExchangeService is not registered on the Agent. Please provide the PresentationExchangeModule as a module on the agent' ) } - return agentContext.dependencyManager.resolve(PresentationExchangeService) + return agentContext.dependencyManager.resolve(DifPresentationExchangeService) } public supportsFormat(formatIdentifier: string): boolean { @@ -63,7 +63,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async createProposal( agentContext: AgentContext, - { proofFormats, attachmentId }: ProofFormatCreateProposalOptions + { proofFormats, attachmentId }: ProofFormatCreateProposalOptions ): Promise { const ps = this.presentationExchangeService(agentContext) @@ -85,7 +85,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async processProposal(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { const ps = this.presentationExchangeService(agentContext) - const proposal = attachment.getDataAsJson() + const proposal = attachment.getDataAsJson() ps.validatePresentationDefinition(proposal) } @@ -95,7 +95,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, proposalAttachment, proofFormats, - }: ProofFormatAcceptProposalOptions + }: ProofFormatAcceptProposalOptions ): Promise { const ps = this.presentationExchangeService(agentContext) @@ -106,7 +106,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic attachmentId, }) - const presentationDefinition = proposalAttachment.getDataAsJson() + const presentationDefinition = proposalAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) const attachment = this.getFormatData( @@ -117,7 +117,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic challenge: presentationExchangeFormat?.options?.challenge ?? (await agentContext.wallet.generateNonce()), domain: presentationExchangeFormat?.options?.domain, }, - } satisfies PresentationExchangeRequest, + } satisfies DifPresentationExchangeRequest, format.attachmentId ) @@ -126,7 +126,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async createRequest( agentContext: AgentContext, - { attachmentId, proofFormats }: FormatCreateRequestOptions + { attachmentId, proofFormats }: FormatCreateRequestOptions ): Promise { const ps = this.presentationExchangeService(agentContext) @@ -152,7 +152,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), domain: options?.domain, }, - } satisfies PresentationExchangeRequest, + } satisfies DifPresentationExchangeRequest, format.attachmentId ) @@ -161,13 +161,18 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async processRequest(agentContext: AgentContext, { attachment }: ProofFormatProcessOptions): Promise { const ps = this.presentationExchangeService(agentContext) - const { presentation_definition: presentationDefinition } = attachment.getDataAsJson() + const { presentation_definition: presentationDefinition } = + attachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) } public async acceptRequest( agentContext: AgentContext, - { attachmentId, requestAttachment, proofFormats }: ProofFormatAcceptRequestOptions + { + attachmentId, + requestAttachment, + proofFormats, + }: ProofFormatAcceptRequestOptions ): Promise { const ps = this.presentationExchangeService(agentContext) @@ -177,9 +182,9 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic }) const { presentation_definition: presentationDefinition, options } = - requestAttachment.getDataAsJson() + requestAttachment.getDataAsJson() - const credentials: InputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} + const credentials: DifPexInputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} if (Object.keys(credentials).length === 0) { const { areRequirementsSatisfied, requirements } = await ps.getCredentialsForRequest( agentContext, @@ -209,7 +214,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic } const firstPresentation = presentation.verifiablePresentations[0] - const attachmentData = firstPresentation.encoded as PresentationExchangePresentation + const attachmentData = firstPresentation.encoded as DifPresentationExchangePresentation const attachment = this.getFormatData(attachmentData, format.attachmentId) return { attachment, format } @@ -222,8 +227,8 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const ps = this.presentationExchangeService(agentContext) const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - const request = requestAttachment.getDataAsJson() - const presentation = attachment.getDataAsJson() + const request = requestAttachment.getDataAsJson() + const presentation = attachment.getDataAsJson() let parsedPresentation: W3cVerifiablePresentation // TODO: we should probably move this transformation logic into the VC module, so it @@ -291,11 +296,11 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async getCredentialsForRequest( agentContext: AgentContext, - { requestAttachment }: ProofFormatGetCredentialsForRequestOptions + { requestAttachment }: ProofFormatGetCredentialsForRequestOptions ) { const ps = this.presentationExchangeService(agentContext) const { presentation_definition: presentationDefinition } = - requestAttachment.getDataAsJson() + requestAttachment.getDataAsJson() ps.validatePresentationDefinition(presentationDefinition) @@ -305,11 +310,11 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic public async selectCredentialsForRequest( agentContext: AgentContext, - { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions + { requestAttachment }: ProofFormatSelectCredentialsForRequestOptions ) { const ps = this.presentationExchangeService(agentContext) const { presentation_definition: presentationDefinition } = - requestAttachment.getDataAsJson() + requestAttachment.getDataAsJson() const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) return { credentials: ps.selectCredentialsForRequest(credentialsForRequest) } @@ -319,21 +324,22 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic _agentContext: AgentContext, { requestAttachment, proposalAttachment }: ProofFormatAutoRespondProposalOptions ): Promise { - const proposalData = proposalAttachment.getDataAsJson() - const requestData = requestAttachment.getDataAsJson() + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() - return deepEquality(requestData, proposalData) + return deepEquality(requestData.presentation_definition, proposalData) } public async shouldAutoRespondToRequest( _agentContext: AgentContext, { requestAttachment, proposalAttachment }: ProofFormatAutoRespondRequestOptions ): Promise { - const proposalData = proposalAttachment.getDataAsJson() - const requestData = requestAttachment.getDataAsJson() + const proposalData = proposalAttachment.getDataAsJson() + const requestData = requestAttachment.getDataAsJson() - return deepEquality(requestData, proposalData) + return deepEquality(requestData.presentation_definition, proposalData) } + /** * * The presentation is already verified in processPresentation, so we can just return true here. diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts similarity index 88% rename from packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts rename to packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index ff9de982a8..b27362b714 100644 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -1,13 +1,13 @@ -import type { PresentationDefinition } from '../../../../presentation-exchange' +import type { DifPresentationExchangeDefinitionV1 } from '../../../../dif-presentation-exchange' import type { ProofFormatService } from '../../ProofFormatService' -import type { PresentationExchangeProofFormat } from '../PresentationExchangeProofFormat' +import type { DifPresentationExchangeProofFormat } from '../DifPresentationExchangeProofFormat' import { PresentationSubmissionLocation } from '@sphereon/pex' import { getIndySdkModules } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' import { agentDependencies, getAgentConfig } from '../../../../../../tests' import { Agent } from '../../../../../agent/Agent' -import { PresentationExchangeModule, PresentationExchangeService } from '../../../../presentation-exchange' +import { DifPresentationExchangeModule, DifPresentationExchangeService } from '../../../../dif-presentation-exchange' import { W3cJsonLdVerifiableCredential, W3cCredentialRecord, @@ -19,7 +19,7 @@ import { ProofsModule } from '../../../ProofsModule' import { ProofState } from '../../../models' import { V2ProofProtocol } from '../../../protocol' import { ProofExchangeRecord } from '../../../repository' -import { PresentationExchangeProofFormatService } from '../PresentationExchangeProofFormatService' +import { PresentationExchangeProofFormatService } from '../DifPresentationExchangeProofFormatService' const mockProofRecord = () => new ProofExchangeRecord({ @@ -28,10 +28,11 @@ const mockProofRecord = () => protocolVersion: 'v2', }) -const mockPresentationDefinition = (): PresentationDefinition => ({ +const mockPresentationDefinition = (): DifPresentationExchangeDefinitionV1 => ({ id: '32f54163-7166-48f1-93d8-ff217bdb0653', input_descriptors: [ { + schema: [{ uri: 'https://www.w3.org/2018/credentials/examples/v1' }], id: 'wa_driver_license', name: 'Washington State Business License', purpose: 'We can only allow licensed Washington State business representatives into the WA Business Conference', @@ -68,8 +69,8 @@ const mockCredentialRecord = new W3cCredentialRecord({ }) const presentationSubmission = { id: 'did:id', definition_id: 'my-id', descriptor_map: [] } -jest.spyOn(W3cCredentialRepository.prototype, 'getAll').mockResolvedValue([mockCredentialRecord]) -jest.spyOn(PresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ +jest.spyOn(W3cCredentialRepository.prototype, 'findByQuery').mockResolvedValue([mockCredentialRecord]) +jest.spyOn(DifPresentationExchangeService.prototype, 'createPresentation').mockResolvedValue({ presentationSubmission, verifiablePresentations: [ new W3cJsonLdVerifiablePresentation({ @@ -88,14 +89,14 @@ jest.spyOn(PresentationExchangeService.prototype, 'createPresentation').mockReso }) describe('Presentation Exchange ProofFormatService', () => { - let pexFormatService: ProofFormatService + let pexFormatService: ProofFormatService let agent: Agent beforeAll(async () => { agent = new Agent({ config: getAgentConfig('PresentationExchangeProofFormatService'), modules: { - someModule: new PresentationExchangeModule(), + someModule: new DifPresentationExchangeModule(), proofs: new ProofsModule({ proofProtocols: [new V2ProofProtocol({ proofFormats: [new PresentationExchangeProofFormatService()] })], }), diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts new file mode 100644 index 0000000000..b8a8c35e4e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/index.ts @@ -0,0 +1,2 @@ +export * from './DifPresentationExchangeProofFormat' +export * from './DifPresentationExchangeProofFormatService' diff --git a/packages/core/src/modules/proofs/formats/index.ts b/packages/core/src/modules/proofs/formats/index.ts index ff34793737..a2cc952c57 100644 --- a/packages/core/src/modules/proofs/formats/index.ts +++ b/packages/core/src/modules/proofs/formats/index.ts @@ -2,7 +2,7 @@ export * from './ProofFormat' export * from './ProofFormatService' export * from './ProofFormatServiceOptions' -export * from './presentation-exchange' +export * from './dif-presentation-exchange' import * as ProofFormatServiceOptions from './ProofFormatServiceOptions' diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts deleted file mode 100644 index d2ab2c554d..0000000000 --- a/packages/core/src/modules/proofs/formats/presentation-exchange/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './PresentationExchangeProofFormat' -export * from './PresentationExchangeProofFormatService' diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index 28cc6ba1c4..efe6dbb1df 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -8,7 +8,7 @@ import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-vali import { SingleOrArray } from '../../../../utils/type' import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' -import { PresentationSubmission } from '../../../presentation-exchange/models' +import { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange/models' import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models/W3cJsonLdVerifiableCredential' import { W3cJwtVerifiableCredential } from '../../jwt-vc/W3cJwtVerifiableCredential' @@ -23,7 +23,7 @@ export interface W3cPresentationOptions { type?: Array verifiableCredential: SingleOrArray holder?: string | W3cHolderOptions - presentationSubmission?: PresentationSubmission + presentationSubmission?: DifPresentationExchangeSubmission } export class W3cPresentation { @@ -49,7 +49,7 @@ export class W3cPresentation { * NOTE: not validated */ @Expose({ name: 'presentation_submission' }) - public presentationSubmission?: PresentationSubmission + public presentationSubmission?: DifPresentationExchangeSubmission @IsOptional() @IsUri() From d19ccb5e1bebc32a9a7532ba3ebef0c4cda87222 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:22:19 +0700 Subject: [PATCH 14/17] fix: json-ld issues ... :( Signed-off-by: Timo Glastra --- .../DifPresentationExchangeProofFormat.ts | 5 +---- .../DifPresentationExchangeProofFormatService.ts | 8 ++++++-- ...PresentationExchangeProofFormatService.test.ts | 6 ------ .../data-integrity/W3cJsonLdCredentialService.ts | 2 ++ .../vc/jwt-vc/W3cJwtVerifiablePresentation.ts | 4 ---- .../vc/models/presentation/W3cJsonPresentation.ts | 2 ++ .../vc/models/presentation/W3cPresentation.ts | 15 ++++++--------- 7 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts index 7ebef33c46..ca7e908a76 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormat.ts @@ -2,7 +2,6 @@ import type { DifPexInputDescriptorToCredentials, DifPexCredentialsForRequest, DifPresentationExchangeDefinitionV1, - DifPresentationExchangeSubmission, } from '../../../dif-presentation-exchange' import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' import type { ProofFormat } from '../ProofFormat' @@ -18,9 +17,7 @@ export type DifPresentationExchangeRequest = { } export type DifPresentationExchangePresentation = - | (W3cJsonPresentation & { - presentation_submission: DifPresentationExchangeSubmission - }) + | W3cJsonPresentation // NOTE: this is not spec compliant, as it doesn't describe how to submit // JWT VPs but to support JWT VPs we also allow the value to be a string | string diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 60c5d5807d..3260e806d2 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -8,6 +8,7 @@ import type { AgentContext } from '../../../../agent' import type { JsonValue } from '../../../../types' import type { DifPexInputDescriptorToCredentials } from '../../../dif-presentation-exchange' import type { W3cVerifiablePresentation, W3cVerifyPresentationResult } from '../../../vc' +import type { W3cJsonPresentation } from '../../../vc/models/presentation/W3cJsonPresentation' import type { ProofFormatService } from '../ProofFormatService' import type { ProofFormatCreateProposalOptions, @@ -230,16 +231,19 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const request = requestAttachment.getDataAsJson() const presentation = attachment.getDataAsJson() let parsedPresentation: W3cVerifiablePresentation + let jsonPresentation: W3cJsonPresentation // TODO: we should probably move this transformation logic into the VC module, so it // can be reused in AFJ when we need to go from encoded -> parsed if (typeof presentation === 'string') { parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) + jsonPresentation = parsedPresentation.presentation.toJSON() } else { parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) + jsonPresentation = parsedPresentation.toJSON() } - if (!parsedPresentation.presentationSubmission) { + if (!jsonPresentation.presentation_submission) { agentContext.config.logger.error( 'Received presentation in PEX proof format without presentation submission. This should not happen.' ) @@ -255,7 +259,7 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic try { ps.validatePresentationDefinition(request.presentation_definition) - ps.validatePresentationSubmission(parsedPresentation.presentationSubmission) + ps.validatePresentationSubmission(jsonPresentation.presentation_submission) ps.validatePresentation(request.presentation_definition, parsedPresentation) let verificationResult: W3cVerifyPresentationResult diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts index b27362b714..316927aaf8 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/__tests__/PresentationExchangeProofFormatService.test.ts @@ -74,7 +74,6 @@ jest.spyOn(DifPresentationExchangeService.prototype, 'createPresentation').mockR presentationSubmission, verifiablePresentations: [ new W3cJsonLdVerifiablePresentation({ - presentationSubmission, verifiableCredential: [mockCredentialRecord.credential], proof: { type: 'Ed25519Signature2020', @@ -177,11 +176,6 @@ describe('Presentation Exchange ProofFormatService', () => { mimeType: 'application/json', data: { json: { - presentation_submission: { - id: expect.any(String), - definition_id: expect.any(String), - descriptor_map: [], - }, '@context': expect.any(Array), type: expect.any(Array), verifiableCredential: [ diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 841271fa39..c0761c4375 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -199,6 +199,8 @@ export class W3cJsonLdCredentialService { useNativeCanonize: false, }) + console.log(JsonTransformer.toJSON(options.presentation), options.presentation) + const result = await vc.signPresentation({ presentation: JsonTransformer.toJSON(options.presentation), suite: suite, diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts index 99034ef06d..e2869c5333 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiablePresentation.ts @@ -80,10 +80,6 @@ export class W3cJwtVerifiablePresentation { return this.presentation.holderId } - public get presentationSubmission() { - return this.presentation.presentationSubmission - } - /** * The {@link ClaimFormat} of the presentation. For JWT presentations this is always `jwt_vp`. */ diff --git a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts index 5625627ef8..a47b3e90dc 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cJsonPresentation.ts @@ -1,4 +1,5 @@ import type { JsonObject } from '../../../../types' +import type { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange' import type { W3cJsonCredential } from '../credential/W3cJsonCredential' export interface W3cJsonPresentation { @@ -7,5 +8,6 @@ export interface W3cJsonPresentation { type: Array holder: string | { id?: string } verifiableCredential: Array + presentation_submission?: DifPresentationExchangeSubmission [key: string]: unknown } diff --git a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts index efe6dbb1df..cf37fc434b 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cPresentation.ts @@ -1,4 +1,5 @@ import type { W3cHolderOptions } from './W3cHolder' +import type { W3cJsonPresentation } from './W3cJsonPresentation' import type { JsonObject } from '../../../../types' import type { W3cVerifiableCredential } from '../credential/W3cVerifiableCredential' import type { ValidationOptions } from 'class-validator' @@ -6,9 +7,9 @@ import type { ValidationOptions } from 'class-validator' import { Expose } from 'class-transformer' import { ValidateNested, buildMessage, IsOptional, ValidateBy } from 'class-validator' +import { JsonTransformer } from '../../../../utils' import { SingleOrArray } from '../../../../utils/type' import { IsUri, IsInstanceOrArrayOfInstances } from '../../../../utils/validators' -import { DifPresentationExchangeSubmission } from '../../../dif-presentation-exchange/models' import { CREDENTIALS_CONTEXT_V1_URL, VERIFIABLE_PRESENTATION_TYPE } from '../../constants' import { W3cJsonLdVerifiableCredential } from '../../data-integrity/models/W3cJsonLdVerifiableCredential' import { W3cJwtVerifiableCredential } from '../../jwt-vc/W3cJwtVerifiableCredential' @@ -23,7 +24,6 @@ export interface W3cPresentationOptions { type?: Array verifiableCredential: SingleOrArray holder?: string | W3cHolderOptions - presentationSubmission?: DifPresentationExchangeSubmission } export class W3cPresentation { @@ -33,7 +33,6 @@ export class W3cPresentation { this.context = options.context ?? [CREDENTIALS_CONTEXT_V1_URL] this.type = options.type ?? [VERIFIABLE_PRESENTATION_TYPE] this.verifiableCredential = options.verifiableCredential - this.presentationSubmission = options.presentationSubmission if (options.holder) { this.holder = typeof options.holder === 'string' ? options.holder : new W3cHolder(options.holder) @@ -45,12 +44,6 @@ export class W3cPresentation { @IsCredentialJsonLdContext() public context!: Array - /** - * NOTE: not validated - */ - @Expose({ name: 'presentation_submission' }) - public presentationSubmission?: DifPresentationExchangeSubmission - @IsOptional() @IsUri() public id?: string @@ -73,6 +66,10 @@ export class W3cPresentation { return this.holder instanceof W3cHolder ? this.holder.id : this.holder } + + public toJSON() { + return JsonTransformer.toJSON(this) as W3cJsonPresentation + } } // Custom validators From e2a67b81b2b506c09ea88d6a19f753797eda83ac Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:23:24 +0700 Subject: [PATCH 15/17] style: formatting Signed-off-by: Timo Glastra --- packages/core/src/agent/AgentModules.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index a93e8e2f98..faf87ecec7 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -7,11 +7,11 @@ import { CacheModule } from '../modules/cache' import { ConnectionsModule } from '../modules/connections' import { CredentialsModule } from '../modules/credentials' import { DidsModule } from '../modules/dids' +import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' -import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { ProofsModule } from '../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../modules/routing' import { W3cCredentialsModule } from '../modules/vc' From 1e5fe20c7b092611be0e579fecf6583011fe9a15 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:39:41 +0700 Subject: [PATCH 16/17] remove console log Signed-off-by: Timo Glastra --- .../src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index c0761c4375..841271fa39 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -199,8 +199,6 @@ export class W3cJsonLdCredentialService { useNativeCanonize: false, }) - console.log(JsonTransformer.toJSON(options.presentation), options.presentation) - const result = await vc.signPresentation({ presentation: JsonTransformer.toJSON(options.presentation), suite: suite, From 80bbc8b8062e630c33f22dc9499ad415eb26f2b0 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:41:31 +0700 Subject: [PATCH 17/17] fix test Signed-off-by: Timo Glastra --- packages/core/src/agent/__tests__/AgentModules.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/agent/__tests__/AgentModules.test.ts b/packages/core/src/agent/__tests__/AgentModules.test.ts index 7717608581..a4c3be88a3 100644 --- a/packages/core/src/agent/__tests__/AgentModules.test.ts +++ b/packages/core/src/agent/__tests__/AgentModules.test.ts @@ -5,6 +5,7 @@ import { CacheModule } from '../../modules/cache' import { ConnectionsModule } from '../../modules/connections' import { CredentialsModule } from '../../modules/credentials' import { DidsModule } from '../../modules/dids' +import { DifPresentationExchangeModule } from '../../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../../modules/discover-features' import { GenericRecordsModule } from '../../modules/generic-records' import { MessagePickupModule } from '../../modules/message-pickup' @@ -62,6 +63,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -86,6 +88,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule), @@ -113,6 +116,7 @@ describe('AgentModules', () => { mediationRecipient: expect.any(MediationRecipientModule), messagePickup: expect.any(MessagePickupModule), basicMessages: expect.any(BasicMessagesModule), + pex: expect.any(DifPresentationExchangeModule), genericRecords: expect.any(GenericRecordsModule), discovery: expect.any(DiscoverFeaturesModule), dids: expect.any(DidsModule),