From a2931c3e026a0df7ec830a00500b37a6f9f62db1 Mon Sep 17 00:00:00 2001 From: Berend Sliedrecht Date: Wed, 3 Jan 2024 15:15:33 +0100 Subject: [PATCH] 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)