diff --git a/demo/src/Alice.ts b/demo/src/Alice.ts index c46db6988b..38e320bfca 100644 --- a/demo/src/Alice.ts +++ b/demo/src/Alice.ts @@ -69,11 +69,17 @@ export class Alice extends BaseAgent { } public async acceptProofRequest(proofRecord: ProofRecord) { - const retrievedCredentials = await this.agent.proofs.getRequestedCredentialsForProofRequest(proofRecord.id, { - filterByPresentationPreview: true, + const requestedCredentials = await this.agent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: proofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + await this.agent.proofs.acceptRequest({ + proofRecordId: proofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, }) - const requestedCredentials = this.agent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await this.agent.proofs.acceptRequest(proofRecord.id, requestedCredentials) console.log(greenText('\nProof request accepted!\n')) } diff --git a/demo/src/Faber.ts b/demo/src/Faber.ts index a99b02cac8..acd77bf36b 100644 --- a/demo/src/Faber.ts +++ b/demo/src/Faber.ts @@ -8,6 +8,7 @@ import { AttributeFilter, ProofAttributeInfo, utils, + ProofProtocolVersion, } from '@aries-framework/core' import { ui } from 'inquirer' @@ -143,8 +144,18 @@ export class Faber extends BaseAgent { const connectionRecord = await this.getConnectionRecord() const proofAttribute = await this.newProofAttribute() await this.printProofFlow(greenText('\nRequesting proof...\n', false)) - await this.agent.proofs.requestProof(connectionRecord.id, { - requestedAttributes: proofAttribute, + + await this.agent.proofs.requestProof({ + protocolVersion: ProofProtocolVersion.V1, + connectionId: connectionRecord.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: proofAttribute, + }, + }, }) this.ui.updateBottomBar( `\nProof request sent!\n\nGo to the Alice agent to accept the proof request\n\n${Color.Reset}` diff --git a/packages/core/src/agent/AgentConfig.ts b/packages/core/src/agent/AgentConfig.ts index 682e6a9685..1a5b4c25f0 100644 --- a/packages/core/src/agent/AgentConfig.ts +++ b/packages/core/src/agent/AgentConfig.ts @@ -9,7 +9,7 @@ import { DID_COMM_TRANSPORT_QUEUE } from '../constants' import { AriesFrameworkError } from '../error' import { ConsoleLogger, LogLevel } from '../logger' import { AutoAcceptCredential } from '../modules/credentials/CredentialAutoAcceptType' -import { AutoAcceptProof } from '../modules/proofs/ProofAutoAcceptType' +import { AutoAcceptProof } from '../modules/proofs/models/ProofAutoAcceptType' import { DidCommMimeType } from '../types' export class AgentConfig { diff --git a/packages/core/src/agent/__tests__/Agent.test.ts b/packages/core/src/agent/__tests__/Agent.test.ts index b56eb476fe..c042b514f5 100644 --- a/packages/core/src/agent/__tests__/Agent.test.ts +++ b/packages/core/src/agent/__tests__/Agent.test.ts @@ -12,8 +12,10 @@ import { CredentialRepository } from '../../modules/credentials' import { CredentialsModule } from '../../modules/credentials/CredentialsModule' import { IndyLedgerService } from '../../modules/ledger' import { LedgerModule } from '../../modules/ledger/LedgerModule' -import { ProofRepository, ProofService } from '../../modules/proofs' +import { ProofRepository } from '../../modules/proofs' import { ProofsModule } from '../../modules/proofs/ProofsModule' +import { V1ProofService } from '../../modules/proofs/protocol/v1' +import { V2ProofService } from '../../modules/proofs/protocol/v2' import { MediatorModule, RecipientModule, @@ -119,7 +121,8 @@ describe('Agent', () => { expect(container.resolve(TrustPingService)).toBeInstanceOf(TrustPingService) expect(container.resolve(ProofsModule)).toBeInstanceOf(ProofsModule) - expect(container.resolve(ProofService)).toBeInstanceOf(ProofService) + expect(container.resolve(V1ProofService)).toBeInstanceOf(V1ProofService) + expect(container.resolve(V2ProofService)).toBeInstanceOf(V2ProofService) expect(container.resolve(ProofRepository)).toBeInstanceOf(ProofRepository) expect(container.resolve(CredentialsModule)).toBeInstanceOf(CredentialsModule) @@ -162,7 +165,8 @@ describe('Agent', () => { expect(container.resolve(TrustPingService)).toBe(container.resolve(TrustPingService)) expect(container.resolve(ProofsModule)).toBe(container.resolve(ProofsModule)) - expect(container.resolve(ProofService)).toBe(container.resolve(ProofService)) + expect(container.resolve(V1ProofService)).toBe(container.resolve(V1ProofService)) + expect(container.resolve(V2ProofService)).toBe(container.resolve(V2ProofService)) expect(container.resolve(ProofRepository)).toBe(container.resolve(ProofRepository)) expect(container.resolve(CredentialsModule)).toBe(container.resolve(CredentialsModule)) diff --git a/packages/core/src/decorators/ack/AckDecorator.test.ts b/packages/core/src/decorators/ack/AckDecorator.test.ts index fe0ccba759..879c98f8ec 100644 --- a/packages/core/src/decorators/ack/AckDecorator.test.ts +++ b/packages/core/src/decorators/ack/AckDecorator.test.ts @@ -2,6 +2,7 @@ import { BaseMessage } from '../../agent/BaseMessage' import { JsonTransformer } from '../../utils/JsonTransformer' import { Compose } from '../../utils/mixins' +import { AckValues } from './AckDecorator' import { AckDecorated } from './AckDecoratorExtension' describe('Decorators | AckDecoratorExtension', () => { @@ -13,7 +14,7 @@ describe('Decorators | AckDecoratorExtension', () => { test('transforms AckDecorator class to JSON', () => { const message = new TestMessage() - message.setPleaseAck() + message.setPleaseAck([AckValues.Receipt]) expect(message.toJSON()).toEqual({ '@id': undefined, '@type': undefined, diff --git a/packages/core/src/modules/credentials/services/CredentialService.ts b/packages/core/src/modules/credentials/services/CredentialService.ts index 1bb7df046c..f7c2486100 100644 --- a/packages/core/src/modules/credentials/services/CredentialService.ts +++ b/packages/core/src/modules/credentials/services/CredentialService.ts @@ -7,6 +7,7 @@ import type { InboundMessageContext } from '../../../agent/models/InboundMessage import type { Logger } from '../../../logger' import type { DidCommMessageRepository } from '../../../storage' import type { MediationRecipientService } from '../../routing' +import type { CredentialExchangeRecord } from '../repository/CredentialExchangeRecord' import type { CredentialStateChangedEvent } from './../CredentialEvents' import type { CredentialProtocolVersion } from './../CredentialProtocolVersion' import type { @@ -37,7 +38,7 @@ import type { V2IssueCredentialMessage } from './../protocol/v2/messages/V2Issue import type { V2OfferCredentialMessage } from './../protocol/v2/messages/V2OfferCredentialMessage' import type { V2ProposeCredentialMessage } from './../protocol/v2/messages/V2ProposeCredentialMessage' import type { V2RequestCredentialMessage } from './../protocol/v2/messages/V2RequestCredentialMessage' -import type { CredentialExchangeRecord, CredentialRepository } from './../repository' +import type { CredentialRepository } from './../repository' import type { RevocationService } from './RevocationService' import { CredentialEventTypes } from './../CredentialEvents' diff --git a/packages/core/src/modules/indy/services/IndyHolderService.ts b/packages/core/src/modules/indy/services/IndyHolderService.ts index c8f0ca2f8b..6b1a2e9047 100644 --- a/packages/core/src/modules/indy/services/IndyHolderService.ts +++ b/packages/core/src/modules/indy/services/IndyHolderService.ts @@ -1,5 +1,5 @@ import type { Logger } from '../../../logger' -import type { RequestedCredentials } from '../../proofs' +import type { RequestedCredentials } from '../../proofs/formats/indy/models/RequestedCredentials' import type * as Indy from 'indy-sdk' import { Lifecycle, scoped } from 'tsyringe' diff --git a/packages/core/src/modules/indy/services/IndyRevocationService.ts b/packages/core/src/modules/indy/services/IndyRevocationService.ts index 4b88e88413..dc18e38d02 100644 --- a/packages/core/src/modules/indy/services/IndyRevocationService.ts +++ b/packages/core/src/modules/indy/services/IndyRevocationService.ts @@ -1,7 +1,7 @@ import type { Logger } from '../../../logger' import type { FileSystem } from '../../../storage/FileSystem' -import type { RevocationInterval } from '../../credentials' -import type { RequestedCredentials } from '../../proofs' +import type { RevocationInterval } from '../../credentials/protocol/v1/models/RevocationInterval' +import type { RequestedCredentials } from '../../proofs/formats/indy/models/RequestedCredentials' import type { default as Indy } from 'indy-sdk' import { scoped, Lifecycle } from 'tsyringe' diff --git a/packages/core/src/modules/proofs/ProofEvents.ts b/packages/core/src/modules/proofs/ProofEvents.ts index b3612dc9c6..ba1c7b047b 100644 --- a/packages/core/src/modules/proofs/ProofEvents.ts +++ b/packages/core/src/modules/proofs/ProofEvents.ts @@ -1,5 +1,5 @@ import type { BaseEvent } from '../../agent/Events' -import type { ProofState } from './ProofState' +import type { ProofState } from './models/ProofState' import type { ProofRecord } from './repository' export enum ProofEventTypes { diff --git a/packages/core/src/modules/proofs/ProofResponseCoordinator.ts b/packages/core/src/modules/proofs/ProofResponseCoordinator.ts index 859e5c4ae9..94d73d3d39 100644 --- a/packages/core/src/modules/proofs/ProofResponseCoordinator.ts +++ b/packages/core/src/modules/proofs/ProofResponseCoordinator.ts @@ -4,7 +4,8 @@ import { scoped, Lifecycle } from 'tsyringe' import { AgentConfig } from '../../agent/AgentConfig' -import { AutoAcceptProof } from './ProofAutoAcceptType' +import { ProofService } from './ProofService' +import { AutoAcceptProof } from './models/ProofAutoAcceptType' /** * This class handles all the automation with all the messages in the present proof protocol @@ -13,9 +14,11 @@ import { AutoAcceptProof } from './ProofAutoAcceptType' @scoped(Lifecycle.ContainerScoped) export class ProofResponseCoordinator { private agentConfig: AgentConfig + private proofService: ProofService - public constructor(agentConfig: AgentConfig) { + public constructor(agentConfig: AgentConfig, proofService: ProofService) { this.agentConfig = agentConfig + this.proofService = proofService } /** @@ -35,52 +38,48 @@ export class ProofResponseCoordinator { * Checks whether it should automatically respond to a proposal */ public shouldAutoRespondToProposal(proofRecord: ProofRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - this.agentConfig.autoAcceptProofs - ) - - if (autoAccept === AutoAcceptProof.Always) { - return true - } - return false + return this.isAutoAcceptProofAlways(proofRecord) } /** * Checks whether it should automatically respond to a request */ public shouldAutoRespondToRequest(proofRecord: ProofRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - this.agentConfig.autoAcceptProofs - ) - - if ( - autoAccept === AutoAcceptProof.Always || - (autoAccept === AutoAcceptProof.ContentApproved && proofRecord.proposalMessage) - ) { - return true - } - - return false + return this.isAutoAcceptProofAlways(proofRecord) + ? this.isAutoAcceptProofAlways(proofRecord) + : this.isAutoAcceptProofContentApproved(proofRecord) } /** * Checks whether it should automatically respond to a presentation of proof */ public shouldAutoRespondToPresentation(proofRecord: ProofRecord) { - const autoAccept = ProofResponseCoordinator.composeAutoAccept( - proofRecord.autoAcceptProof, - this.agentConfig.autoAcceptProofs - ) - - if ( - autoAccept === AutoAcceptProof.Always || - (autoAccept === AutoAcceptProof.ContentApproved && proofRecord.requestMessage) - ) { + return this.isAutoAcceptProofAlways(proofRecord) + ? this.isAutoAcceptProofAlways(proofRecord) + : this.isAutoAcceptProofContentApproved(proofRecord) + } + + private checkAutoRespond(proofRecord: ProofRecord) { + return ProofResponseCoordinator.composeAutoAccept(proofRecord.autoAcceptProof, this.agentConfig.autoAcceptProofs) + } + + private isAutoAcceptProofAlways(proofRecord: ProofRecord) { + const autoAccept = this.checkAutoRespond(proofRecord) + + if (autoAccept === AutoAcceptProof.Always) { return true } return false } + + private isAutoAcceptProofContentApproved(proofRecord: ProofRecord) { + const autoAccept = this.checkAutoRespond(proofRecord) + + if (autoAccept === AutoAcceptProof.ContentApproved) { + return this.proofService.shouldAutoRespondToRequest(proofRecord) + } + + return false + } } diff --git a/packages/core/src/modules/proofs/ProofService.ts b/packages/core/src/modules/proofs/ProofService.ts new file mode 100644 index 0000000000..0aa2d0b854 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofService.ts @@ -0,0 +1,240 @@ +import type { AgentConfig } from '../../agent/AgentConfig' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { Dispatcher } from '../../agent/Dispatcher' +import type { EventEmitter } from '../../agent/EventEmitter' +import type { InboundMessageContext } from '../../agent/models/InboundMessageContext' +import type { Logger } from '../../logger' +import type { DidCommMessageRepository, DidCommMessageRole } from '../../storage' +import type { Wallet } from '../../wallet/Wallet' +import type { ConnectionService } from '../connections/services' +import type { MediationRecipientService } from '../routing' +import type { ProofStateChangedEvent } from './ProofEvents' +import type { ProofResponseCoordinator } from './ProofResponseCoordinator' +import type { CreateProblemReportOptions } from './formats/models/ProofFormatServiceOptions' +import type { ProofProtocolVersion } from './models/ProofProtocolVersion' +import type { + CreateAckOptions, + CreatePresentationOptions, + CreateProposalAsResponseOptions, + CreateProposalOptions, + CreateRequestAsResponseOptions, + CreateRequestOptions, + GetRequestedCredentialsForProofRequestOptions, + ProofRequestFromProposalOptions, +} from './models/ProofServiceOptions' +import type { ProofState } from './models/ProofState' +import type { + RetrievedCredentialOptions, + ProofRequestFormats, + RequestedCredentialsFormats, +} from './models/SharedOptions' +import type { ProofRecord, ProofRepository } from './repository' + +import { ProofEventTypes } from './ProofEvents' + +export abstract class ProofService { + protected proofRepository: ProofRepository + protected didCommMessageRepository: DidCommMessageRepository + protected eventEmitter: EventEmitter + protected connectionService: ConnectionService + protected wallet: Wallet + protected logger: Logger + + public constructor( + agentConfig: AgentConfig, + proofRepository: ProofRepository, + connectionService: ConnectionService, + didCommMessageRepository: DidCommMessageRepository, + wallet: Wallet, + eventEmitter: EventEmitter + ) { + this.proofRepository = proofRepository + this.connectionService = connectionService + this.didCommMessageRepository = didCommMessageRepository + this.eventEmitter = eventEmitter + this.wallet = wallet + this.logger = agentConfig.logger + } + + public async generateProofRequestNonce() { + return await this.wallet.generateNonce() + } + + /** + * Update the record to a new state and emit an state changed event. Also updates the record + * in storage. + * + * @param proofRecord The proof record to update the state for + * @param newState The state to update to + * + */ + public async updateState(proofRecord: ProofRecord, newState: ProofState) { + const previousState = proofRecord.state + proofRecord.state = newState + await this.proofRepository.update(proofRecord) + + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: previousState }, + }) + } + + abstract getVersion(): ProofProtocolVersion + + /** + * 1. Assert (connection ready, record state) + * 2. Create proposal message + * 3. loop through all formats from ProposeProofOptions and call format service + * 4. Create and store proof record + * 5. Store proposal message + * 6. Return proposal message + proof record + */ + abstract createProposal(options: CreateProposalOptions): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + /** + * Create a proposal message in response to a received proof request message + * + * 1. assert record state + * 2. Create proposal message + * 3. loop through all formats from ProposeProofOptions and call format service + * 4. Update proof record + * 5. Create or update proposal message + * 6. Return proposal message + proof record + */ + abstract createProposalAsResponse( + options: CreateProposalAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + /** + * Process a received proposal message (does not accept yet) + * + * 1. Find proof record by thread and connection id + * + * Two flows possible: + * - Proof record already exist + * 2. Assert state + * 3. Save or update proposal message in storage (didcomm message record) + * 4. Loop through all format services to process proposal message + * 5. Update & return record + * + * - Proof record does not exist yet + * 2. Create record + * 3. Save proposal message + * 4. Loop through all format services to process proposal message + * 5. Save & return record + */ + abstract processProposal(messageContext: InboundMessageContext): Promise + + abstract createRequest(options: CreateRequestOptions): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + abstract createRequestAsResponse( + options: CreateRequestAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + abstract processRequest(messageContext: InboundMessageContext): Promise + + abstract createPresentation( + options: CreatePresentationOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + abstract processPresentation(messageContext: InboundMessageContext): Promise + + abstract createAck(options: CreateAckOptions): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + + abstract processAck(messageContext: InboundMessageContext): Promise + + abstract createProblemReport( + options: CreateProblemReportOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> + abstract processProblemReport(messageContext: InboundMessageContext): Promise + + public abstract shouldAutoRespondToRequest(proofRecord: ProofRecord): Promise + + public abstract shouldAutoRespondToPresentation(proofRecord: ProofRecord): Promise + + public abstract registerHandlers( + dispatcher: Dispatcher, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + mediationRecipientService: MediationRecipientService + ): Promise + + public abstract findRequestMessage(proofRecordId: string): Promise + + public abstract findPresentationMessage(proofRecordId: string): Promise + + public abstract findProposalMessage(proofRecordId: string): Promise + + public async saveOrUpdatePresentationMessage(options: { + proofRecord: ProofRecord + message: AgentMessage + role: DidCommMessageRole + }): Promise { + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + associatedRecordId: options.proofRecord.id, + agentMessage: options.message, + role: options.role, + }) + } + + public abstract getRequestedCredentialsForProofRequest( + options: GetRequestedCredentialsForProofRequestOptions + ): Promise + + public abstract autoSelectCredentialsForProofRequest( + options: RetrievedCredentialOptions + ): Promise + + public abstract createProofRequestFromProposal(options: ProofRequestFromProposalOptions): Promise + + /** + * Retrieve all proof records + * + * @returns List containing all proof records + */ + public async getAll(): Promise { + return this.proofRepository.getAll() + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @throws {RecordNotFoundError} If no record is found + * @return The proof record + * + */ + public async getById(proofRecordId: string): Promise { + return this.proofRepository.getById(proofRecordId) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @return The proof record or null if not found + * + */ + public async findById(proofRecordId: string): Promise { + return this.proofRepository.findById(proofRecordId) + } + + /** + * Delete a proof record by id + * + * @param proofId the proof record id + */ + public async deleteById(proofId: string) { + const proofRecord = await this.getById(proofId) + return this.proofRepository.delete(proofRecord) + } + + /** + * Update a proof record by + * + * @param proofRecord the proof record + */ + public update(proofRecord: ProofRecord) { + return this.proofRepository.update(proofRecord) + } +} diff --git a/packages/core/src/modules/proofs/ProofsModule.ts b/packages/core/src/modules/proofs/ProofsModule.ts index d0dbcd1a96..e13f926f17 100644 --- a/packages/core/src/modules/proofs/ProofsModule.ts +++ b/packages/core/src/modules/proofs/ProofsModule.ts @@ -1,7 +1,22 @@ -import type { AutoAcceptProof } from './ProofAutoAcceptType' -import type { PresentationPreview, RequestPresentationMessage } from './messages' -import type { RequestedCredentials, RetrievedCredentials } from './models' -import type { ProofRequestOptions } from './models/ProofRequest' +import type { AgentMessage } from '../../agent/AgentMessage' +import type { ProofService } from './ProofService' +import type { + AcceptPresentationOptions, + AcceptProposalOptions, + AutoSelectCredentialsForProofRequestOptions, + OutOfBandRequestOptions, + ProposeProofOptions, + RequestProofOptions, +} from './models/ModuleOptions' +import type { AutoAcceptProof } from './models/ProofAutoAcceptType' +import type { + CreateOutOfBandRequestOptions, + CreatePresentationOptions, + CreateProposalOptions, + CreateRequestOptions, + ProofRequestFromProposalOptions, +} from './models/ProofServiceOptions' +import type { RequestedCredentialsFormats } from './models/SharedOptions' import type { ProofRecord } from './repository/ProofRecord' import { Lifecycle, scoped } from 'tsyringe' @@ -12,70 +27,84 @@ import { MessageSender } from '../../agent/MessageSender' import { createOutboundMessage } from '../../agent/helpers' import { ServiceDecorator } from '../../decorators/service/ServiceDecorator' import { AriesFrameworkError } from '../../error' +import { DidCommMessageRole } from '../../storage' import { ConnectionService } from '../connections/services/ConnectionService' import { MediationRecipientService } from '../routing/services/MediationRecipientService' import { ProofResponseCoordinator } from './ProofResponseCoordinator' -import { PresentationProblemReportReason } from './errors' -import { - ProposePresentationHandler, - RequestPresentationHandler, - PresentationAckHandler, - PresentationHandler, - PresentationProblemReportHandler, -} from './handlers' -import { PresentationProblemReportMessage } from './messages/PresentationProblemReportMessage' -import { ProofRequest } from './models/ProofRequest' -import { ProofService } from './services' +import { ProofProtocolVersion } from './models/ProofProtocolVersion' +import { ProofState } from './models/ProofState' +import { V1ProofService } from './protocol/v1/V1ProofService' +import { V2ProofService } from './protocol/v2/V2ProofService' +import { ProofRepository } from './repository/ProofRepository' @scoped(Lifecycle.ContainerScoped) export class ProofsModule { - private proofService: ProofService private connectionService: ConnectionService private messageSender: MessageSender - private mediationRecipientService: MediationRecipientService private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator + private mediationRecipientService: MediationRecipientService + private serviceMap: { [key in ProofProtocolVersion]: ProofService } + private proofRepository: ProofRepository public constructor( dispatcher: Dispatcher, - proofService: ProofService, connectionService: ConnectionService, - mediationRecipientService: MediationRecipientService, - agentConfig: AgentConfig, messageSender: MessageSender, - proofResponseCoordinator: ProofResponseCoordinator + agentConfig: AgentConfig, + mediationRecipientService: MediationRecipientService, + v1ProofService: V1ProofService, + v2ProofService: V2ProofService, + proofRepository: ProofRepository ) { - this.proofService = proofService this.connectionService = connectionService this.messageSender = messageSender - this.mediationRecipientService = mediationRecipientService this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.registerHandlers(dispatcher) + this.mediationRecipientService = mediationRecipientService + this.proofRepository = proofRepository + + this.serviceMap = { + [ProofProtocolVersion.V1]: v1ProofService, + [ProofProtocolVersion.V2]: v2ProofService, + } + + void this.registerHandlers(dispatcher, mediationRecipientService) + } + + private getService(protocolVersion: ProofProtocolVersion) { + return this.serviceMap[protocolVersion] } /** * Initiate a new presentation exchange as prover by sending a presentation proposal message * to the connection with the specified connection id. * - * @param connectionId The connection to send the proof proposal to - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal + * @param options multiple properties like protocol version, connection id, proof format (indy/ presentation exchange) + * to include in the message * @returns Proof record associated with the sent proposal message - * */ - public async proposeProof( - connectionId: string, - presentationProposal: PresentationPreview, - config?: { - comment?: string - autoAcceptProof?: AutoAcceptProof - } - ): Promise { + public async proposeProof(options: ProposeProofOptions): Promise { + const version: ProofProtocolVersion = options.protocolVersion + + const service = this.getService(version) + + const { connectionId } = options + const connection = await this.connectionService.getById(connectionId) - const { message, proofRecord } = await this.proofService.createProposal(connection, presentationProposal, config) + // Assert + connection.assertReady() + + const proposalOptions: CreateProposalOptions = { + connectionRecord: connection, + protocolVersion: version, + proofFormats: options.proofFormats, + autoAcceptProof: options.autoAcceptProof, + goalCode: options.goalCode, + comment: options.comment, + } + + const { message, proofRecord } = await service.createProposal(proposalOptions) const outbound = createOutboundMessage(connection, message) await this.messageSender.sendMessage(outbound) @@ -87,23 +116,14 @@ export class ProofsModule { * Accept a presentation proposal as verifier (by sending a presentation request message) to the connection * associated with the proof record. * - * @param proofRecordId The id of the proof record for which to accept the proposal - * @param config Additional configuration to use for the request + * @param options multiple properties like proof record id, additional configuration for creating the request * @returns Proof record associated with the presentation request - * */ - public async acceptProposal( - proofRecordId: string, - config?: { - request?: { - name?: string - version?: string - nonce?: string - } - comment?: string - } - ): Promise { - const proofRecord = await this.proofService.getById(proofRecordId) + public async acceptProposal(options: AcceptProposalOptions): Promise { + const { proofRecordId } = options + const proofRecord = await this.getById(proofRecordId) + + const service = this.getService(proofRecord.protocolVersion) if (!proofRecord.connectionId) { throw new AriesFrameworkError( @@ -113,19 +133,24 @@ export class ProofsModule { const connection = await this.connectionService.getById(proofRecord.connectionId) - const presentationProposal = proofRecord.proposalMessage?.presentationProposal - if (!presentationProposal) { - throw new AriesFrameworkError(`Proof record with id ${proofRecordId} is missing required presentation proposal`) + // Assert + connection.assertReady() + + const proofRequestFromProposalOptions: ProofRequestFromProposalOptions = { + name: options.config?.name ? options.config.name : 'proof-request', + version: options.config?.version ?? '1.0', + nonce: await service.generateProofRequestNonce(), + proofRecord, } - const proofRequest = await this.proofService.createProofRequestFromProposal(presentationProposal, { - name: config?.request?.name ?? 'proof-request', - version: config?.request?.version ?? '1.0', - nonce: config?.request?.nonce, - }) + const proofRequest = await service.createProofRequestFromProposal(proofRequestFromProposalOptions) - const { message } = await this.proofService.createRequestAsResponse(proofRecord, proofRequest, { - comment: config?.comment, + const { message } = await service.createRequestAsResponse({ + proofRecord: proofRecord, + proofFormats: proofRequest, + goalCode: options.goalCode, + willConfirm: options.willConfirm ?? true, + comment: options.comment, }) const outboundMessage = createOutboundMessage(connection, message) @@ -138,29 +163,26 @@ export class ProofsModule { * Initiate a new presentation exchange as verifier by sending a presentation request message * to the connection with the specified connection id * - * @param connectionId The connection to send the proof request to - * @param proofRequestOptions Options to build the proof request + * @param options multiple properties like connection id, protocol version, proof Formats to build the proof request * @returns Proof record associated with the sent request message - * */ - public async requestProof( - connectionId: string, - proofRequestOptions: CreateProofRequestOptions, - config?: ProofRequestConfig - ): Promise { - const connection = await this.connectionService.getById(connectionId) + public async requestProof(options: RequestProofOptions): Promise { + const version: ProofProtocolVersion = options.protocolVersion + const service = this.getService(options.protocolVersion) - const nonce = proofRequestOptions.nonce ?? (await this.proofService.generateProofRequestNonce()) + const connection = await this.connectionService.getById(options.connectionId) - const proofRequest = new ProofRequest({ - name: proofRequestOptions.name ?? 'proof-request', - version: proofRequestOptions.name ?? '1.0', - nonce, - requestedAttributes: proofRequestOptions.requestedAttributes, - requestedPredicates: proofRequestOptions.requestedPredicates, - }) + // Assert + connection.assertReady() - const { message, proofRecord } = await this.proofService.createRequest(proofRequest, connection, config) + const createProofRequest: CreateRequestOptions = { + connectionRecord: connection, + proofFormats: options.proofFormats, + protocolVersion: version, + autoAcceptProof: options.autoAcceptProof, + comment: options.comment, + } + const { message, proofRecord } = await service.createRequest(createProofRequest) const outboundMessage = createOutboundMessage(connection, message) await this.messageSender.sendMessage(outboundMessage) @@ -172,28 +194,25 @@ export class ProofsModule { * Initiate a new presentation exchange as verifier by creating a presentation request * not bound to any connection. The request must be delivered out-of-band to the holder * - * @param proofRequestOptions Options to build the proof request + * @param options multiple properties like protocol version and proof formats to build the proof request * @returns The proof record and proof request message - * */ - public async createOutOfBandRequest( - proofRequestOptions: CreateProofRequestOptions, - config?: ProofRequestConfig - ): Promise<{ - requestMessage: RequestPresentationMessage + public async createOutOfBandRequest(options: OutOfBandRequestOptions): Promise<{ + message: AgentMessage proofRecord: ProofRecord }> { - const nonce = proofRequestOptions.nonce ?? (await this.proofService.generateProofRequestNonce()) - - const proofRequest = new ProofRequest({ - name: proofRequestOptions.name ?? 'proof-request', - version: proofRequestOptions.name ?? '1.0', - nonce, - requestedAttributes: proofRequestOptions.requestedAttributes, - requestedPredicates: proofRequestOptions.requestedPredicates, - }) + const version: ProofProtocolVersion = options.protocolVersion + + const service = this.getService(version) + + const createProofRequest: CreateOutOfBandRequestOptions = { + proofFormats: options.proofFormats, + protocolVersion: version, + autoAcceptProof: options.autoAcceptProof, + comment: options.comment, + } - const { message, proofRecord } = await this.proofService.createRequest(proofRequest, undefined, config) + const { message, proofRecord } = await service.createRequest(createProofRequest) // Create and set ~service decorator const routing = await this.mediationRecipientService.getRouting() @@ -204,43 +223,59 @@ export class ProofsModule { }) // Save ~service decorator to record (to remember our verkey) - proofRecord.requestMessage = message - await this.proofService.update(proofRecord) - return { proofRecord, requestMessage: message } + await service.saveOrUpdatePresentationMessage({ + message, + proofRecord: proofRecord, + role: DidCommMessageRole.Sender, + }) + + await service.update(proofRecord) + + return { proofRecord, message } } /** * Accept a presentation request as prover (by sending a presentation message) to the connection * associated with the proof record. * - * @param proofRecordId The id of the proof record for which to accept the request - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @param config Additional configuration to use for the presentation + * @param options multiple properties like proof record id, proof formats to accept requested credentials object + * specifying which credentials to use for the proof * @returns Proof record associated with the sent presentation message - * */ - public async acceptRequest( - proofRecordId: string, - requestedCredentials: RequestedCredentials, - config?: { - comment?: string + public async acceptRequest(options: AcceptPresentationOptions): Promise { + const { proofRecordId, proofFormats, comment } = options + + const record = await this.getById(proofRecordId) + + const version: ProofProtocolVersion = record.protocolVersion + const service = this.getService(version) + + const presentationOptions: CreatePresentationOptions = { + proofFormats, + proofRecord: record, + protocolVersion: version, + comment, } - ): Promise { - const record = await this.proofService.getById(proofRecordId) - const { message, proofRecord } = await this.proofService.createPresentation(record, requestedCredentials, config) + const { message, proofRecord } = await service.createPresentation(presentationOptions) + + const requestMessage = await service.findRequestMessage(proofRecord.id) // Use connection if present if (proofRecord.connectionId) { const connection = await this.connectionService.getById(proofRecord.connectionId) + // Assert + connection.assertReady() + const outboundMessage = createOutboundMessage(connection, message) await this.messageSender.sendMessage(outboundMessage) return proofRecord } + // Use ~service decorator otherwise - else if (proofRecord.requestMessage?.service) { + else if (requestMessage?.service) { // Create ~service decorator const routing = await this.mediationRecipientService.getRouting() const ourService = new ServiceDecorator({ @@ -249,12 +284,16 @@ export class ProofsModule { routingKeys: routing.routingKeys, }) - const recipientService = proofRecord.requestMessage.service + const recipientService = requestMessage.service // Set and save ~service decorator to record (to remember our verkey) message.service = ourService - proofRecord.presentationMessage = message - await this.proofService.update(proofRecord) + + await service.saveOrUpdatePresentationMessage({ + proofRecord: proofRecord, + message: message, + role: DidCommMessageRole.Sender, + }) await this.messageSender.sendMessageToService({ message, @@ -278,9 +317,14 @@ export class ProofsModule { * @param proofRecordId the id of the proof request to be declined * @returns proof record that was declined */ - public async declineRequest(proofRecordId: string) { - const proofRecord = await this.proofService.getById(proofRecordId) - await this.proofService.declineRequest(proofRecord) + public async declineRequest(proofRecordId: string): Promise { + const proofRecord = await this.getById(proofRecordId) + const service = this.getService(proofRecord.protocolVersion) + + proofRecord.assertState(ProofState.RequestReceived) + + await service.updateState(proofRecord, ProofState.Declined) + return proofRecord } @@ -293,19 +337,31 @@ export class ProofsModule { * */ public async acceptPresentation(proofRecordId: string): Promise { - const record = await this.proofService.getById(proofRecordId) - const { message, proofRecord } = await this.proofService.createAck(record) + const record = await this.getById(proofRecordId) + const service = this.getService(record.protocolVersion) + + const { message, proofRecord } = await service.createAck({ + proofRecord: record, + }) + + const requestMessage = await service.findRequestMessage(record.id) + + const presentationMessage = await service.findPresentationMessage(record.id) // Use connection if present if (proofRecord.connectionId) { const connection = await this.connectionService.getById(proofRecord.connectionId) + + // Assert + connection.assertReady() + const outboundMessage = createOutboundMessage(connection, message) await this.messageSender.sendMessage(outboundMessage) } // Use ~service decorator otherwise - else if (proofRecord.requestMessage?.service && proofRecord.presentationMessage?.service) { - const recipientService = proofRecord.presentationMessage?.service - const ourService = proofRecord.requestMessage.service + else if (requestMessage?.service && presentationMessage?.service) { + const recipientService = presentationMessage?.service + const ourService = requestMessage.service await this.messageSender.sendMessageToService({ message, @@ -314,7 +370,6 @@ export class ProofsModule { returnRoute: true, }) } - // Cannot send message without credentialId or ~service decorator else { throw new AriesFrameworkError( @@ -322,7 +377,7 @@ export class ProofsModule { ) } - return proofRecord + return record } /** @@ -330,72 +385,48 @@ export class ProofsModule { * use credentials in the wallet to build indy requested credentials object for input to proof creation. * If restrictions allow, self attested attributes will be used. * - * - * @param proofRecordId the id of the proof request to get the matching credentials for - * @param config optional configuration for credential selection process. Use `filterByPresentationPreview` (default `true`) to only include - * credentials that match the presentation preview from the presentation proposal (if available). - - * @returns RetrievedCredentials object + * @param options multiple properties like proof record id and optional configuration + * @returns RequestedCredentials */ - public async getRequestedCredentialsForProofRequest( - proofRecordId: string, - config?: GetRequestedCredentialsConfig - ): Promise { - const proofRecord = await this.proofService.getById(proofRecordId) - - const indyProofRequest = proofRecord.requestMessage?.indyProofRequest - const presentationPreview = config?.filterByPresentationPreview - ? proofRecord.proposalMessage?.presentationProposal - : undefined - - if (!indyProofRequest) { - throw new AriesFrameworkError( - 'Unable to get requested credentials for proof request. No proof request message was found or the proof request message does not contain an indy proof request.' - ) - } + public async autoSelectCredentialsForProofRequest( + options: AutoSelectCredentialsForProofRequestOptions + ): Promise { + const proofRecord = await this.getById(options.proofRecordId) - return this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { - presentationProposal: presentationPreview, - filterByNonRevocationRequirements: config?.filterByNonRevocationRequirements ?? true, + const service = this.getService(proofRecord.protocolVersion) + + const retrievedCredentials = await service.getRequestedCredentialsForProofRequest({ + proofRecord: proofRecord, + config: options.config, }) - } - /** - * Takes a RetrievedCredentials object and auto selects credentials in a RequestedCredentials object - * - * Use the return value of this method as input to {@link ProofService.createPresentation} to - * automatically accept a received presentation request. - * - * @param retrievedCredentials The retrieved credentials object to get credentials from - * - * @returns RequestedCredentials - */ - public autoSelectCredentialsForProofRequest(retrievedCredentials: RetrievedCredentials): RequestedCredentials { - return this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) + return await service.autoSelectCredentialsForProofRequest(retrievedCredentials) } /** * Send problem report message for a proof record + * * @param proofRecordId The id of the proof record for which to send problem report * @param message message to send * @returns proof record associated with the proof problem report message */ public async sendProblemReport(proofRecordId: string, message: string) { - const record = await this.proofService.getById(proofRecordId) + const record = await this.getById(proofRecordId) + const service = this.getService(record.protocolVersion) if (!record.connectionId) { throw new AriesFrameworkError(`No connectionId found for proof record '${record.id}'.`) } const connection = await this.connectionService.getById(record.connectionId) - const presentationProblemReportMessage = new PresentationProblemReportMessage({ - description: { - en: message, - code: PresentationProblemReportReason.Abandoned, - }, - }) - presentationProblemReportMessage.setThread({ - threadId: record.threadId, + + // Assert + connection.assertReady() + + const { message: problemReport } = await service.createProblemReport({ + proofRecord: record, + description: message, }) - const outboundMessage = createOutboundMessage(connection, presentationProblemReportMessage) + + const outboundMessage = createOutboundMessage(connection, problemReport) await this.messageSender.sendMessage(outboundMessage) return record @@ -407,7 +438,7 @@ export class ProofsModule { * @returns List containing all proof records */ public getAll(): Promise { - return this.proofService.getAll() + return this.proofRepository.getAll() } /** @@ -420,7 +451,7 @@ export class ProofsModule { * */ public async getById(proofRecordId: string): Promise { - return this.proofService.getById(proofRecordId) + return this.proofRepository.getById(proofRecordId) } /** @@ -431,7 +462,7 @@ export class ProofsModule { * */ public async findById(proofRecordId: string): Promise { - return this.proofService.findById(proofRecordId) + return this.proofRepository.findById(proofRecordId) } /** @@ -440,54 +471,23 @@ export class ProofsModule { * @param proofId the proof record id */ public async deleteById(proofId: string) { - return this.proofService.deleteById(proofId) + const proofRecord = await this.getById(proofId) + return this.proofRepository.delete(proofRecord) } - private registerHandlers(dispatcher: Dispatcher) { - dispatcher.registerHandler( - new ProposePresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) - ) - dispatcher.registerHandler( - new RequestPresentationHandler( - this.proofService, + private async registerHandlers(dispatcher: Dispatcher, mediationRecipientService: MediationRecipientService) { + for (const service of Object.values(this.serviceMap)) { + await service.registerHandlers( + dispatcher, this.agentConfig, - this.proofResponseCoordinator, - this.mediationRecipientService + new ProofResponseCoordinator(this.agentConfig, service), + mediationRecipientService ) - ) - dispatcher.registerHandler( - new PresentationHandler(this.proofService, this.agentConfig, this.proofResponseCoordinator) - ) - dispatcher.registerHandler(new PresentationAckHandler(this.proofService)) - dispatcher.registerHandler(new PresentationProblemReportHandler(this.proofService)) + } } } -export type CreateProofRequestOptions = Partial< - Pick -> - export interface ProofRequestConfig { comment?: string autoAcceptProof?: AutoAcceptProof } - -export interface GetRequestedCredentialsConfig { - /** - * Whether to filter the retrieved credentials using the presentation preview. - * This configuration will only have effect if a presentation proposal message is available - * containing a presentation preview. - * - * @default false - */ - filterByPresentationPreview?: boolean - - /** - * Whether to filter the retrieved credentials using the non-revocation request in the proof request. - * This configuration will only have effect if the proof request requires proof on non-revocation of any kind. - * Default to true - * - * @default true - */ - filterByNonRevocationRequirements?: boolean -} diff --git a/packages/core/src/modules/proofs/ProofsUtil.ts b/packages/core/src/modules/proofs/ProofsUtil.ts new file mode 100644 index 0000000000..b101ab6705 --- /dev/null +++ b/packages/core/src/modules/proofs/ProofsUtil.ts @@ -0,0 +1,110 @@ +import type { IndyProposeProofFormat } from './formats/IndyProofFormatsServiceOptions' +import type { CreateProposalOptions } from './models/ProofServiceOptions' +import type { ProofRequestFormats } from './models/SharedOptions' +import type { PresentationPreviewAttribute } from './protocol/v1/models/V1PresentationPreview' + +import { AriesFrameworkError } from '../../error/AriesFrameworkError' +import { uuid } from '../../utils/uuid' + +import { ProofRequest } from './formats/indy/models/ProofRequest' +import { AttributeFilter } from './protocol/v1/models/AttributeFilter' +import { ProofAttributeInfo } from './protocol/v1/models/ProofAttributeInfo' +import { ProofPredicateInfo } from './protocol/v1/models/ProofPredicateInfo' +import { PresentationPreview } from './protocol/v1/models/V1PresentationPreview' + +export class ProofsUtils { + public static async createRequestFromPreview(options: CreateProposalOptions): Promise { + const indyFormat = options.proofFormats?.indy + + if (!indyFormat) { + throw new AriesFrameworkError('No Indy format found.') + } + + const preview = new PresentationPreview({ + attributes: indyFormat.attributes, + predicates: indyFormat.predicates, + }) + + if (!preview) { + throw new AriesFrameworkError(`No preview found`) + } + + const proofRequest = ProofsUtils.createReferentForProofRequest(indyFormat, preview) + + return { + indy: proofRequest, + } + } + + public static createReferentForProofRequest(indyFormat: IndyProposeProofFormat, preview: PresentationPreview) { + const proofRequest = new ProofRequest({ + name: indyFormat.name, + version: indyFormat.version, + nonce: indyFormat.nonce, + }) + + /** + * Create mapping of attributes by referent. This required the + * attributes to come from the same credential. + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent + * + * { + * "referent1": [Attribute1, Attribute2], + * "referent2": [Attribute3] + * } + */ + const attributesByReferent: Record = {} + for (const proposedAttributes of preview.attributes) { + if (!proposedAttributes.referent) proposedAttributes.referent = uuid() + + const referentAttributes = attributesByReferent[proposedAttributes.referent] + + // Referent key already exist, add to list + if (referentAttributes) { + referentAttributes.push(proposedAttributes) + } + + // Referent key does not exist yet, create new entry + else { + attributesByReferent[proposedAttributes.referent] = [proposedAttributes] + } + } + + // Transform attributes by referent to requested attributes + for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { + // Either attributeName or attributeNames will be undefined + const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined + const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map((a) => a.name) : undefined + + const requestedAttribute = new ProofAttributeInfo({ + name: attributeName, + names: attributeNames, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: proposedAttributes[0].credentialDefinitionId, + }), + ], + }) + + proofRequest.requestedAttributes.set(referent, requestedAttribute) + } + + // Transform proposed predicates to requested predicates + for (const proposedPredicate of preview.predicates) { + const requestedPredicate = new ProofPredicateInfo({ + name: proposedPredicate.name, + predicateType: proposedPredicate.predicate, + predicateValue: proposedPredicate.threshold, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: proposedPredicate.credentialDefinitionId, + }), + ], + }) + + proofRequest.requestedPredicates.set(uuid(), requestedPredicate) + } + + return proofRequest + } +} diff --git a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts index 0d0b74cde8..f15021f05b 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofRequest.test.ts @@ -1,6 +1,6 @@ import { JsonTransformer } from '../../../utils/JsonTransformer' import { MessageValidator } from '../../../utils/MessageValidator' -import { ProofRequest } from '../models' +import { ProofRequest } from '../formats/indy/models/ProofRequest' describe('ProofRequest', () => { it('should successfully validate if the proof request json contains a valid structure', async () => { @@ -8,7 +8,7 @@ describe('ProofRequest', () => { { name: 'ProofRequest', version: '1.0', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + nonce: '947121108704767252195123', requested_attributes: { First: { name: 'Timo', @@ -43,7 +43,7 @@ describe('ProofRequest', () => { { name: 'ProofRequest', version: '1.0', - nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + nonce: '947121108704767252195123', requested_attributes: { First: { names: [], diff --git a/packages/core/src/modules/proofs/__tests__/ProofState.test.ts b/packages/core/src/modules/proofs/__tests__/ProofState.test.ts index 9cabafd183..4b67ed11d0 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofState.test.ts +++ b/packages/core/src/modules/proofs/__tests__/ProofState.test.ts @@ -1,4 +1,4 @@ -import { ProofState } from '../ProofState' +import { ProofState } from '../models/ProofState' describe('ProofState', () => { test('state matches Present Proof 1.0 (RFC 0037) state value', () => { diff --git a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts b/packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts similarity index 82% rename from packages/core/src/modules/proofs/__tests__/ProofService.test.ts rename to packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts index d654dd924a..a2f6ecadaf 100644 --- a/packages/core/src/modules/proofs/__tests__/ProofService.test.ts +++ b/packages/core/src/modules/proofs/__tests__/V1ProofService.test.ts @@ -1,27 +1,28 @@ import type { Wallet } from '../../../wallet/Wallet' import type { CredentialRepository } from '../../credentials/repository' import type { ProofStateChangedEvent } from '../ProofEvents' +import type { IndyProofFormatService } from '../formats/indy/IndyProofFormatService' import type { CustomProofTags } from './../repository/ProofRecord' import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' import { EventEmitter } from '../../../agent/EventEmitter' import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../storage' import { ConnectionService, DidExchangeState } from '../../connections' import { IndyHolderService } from '../../indy/services/IndyHolderService' import { IndyRevocationService } from '../../indy/services/IndyRevocationService' import { IndyLedgerService } from '../../ledger/services' import { ProofEventTypes } from '../ProofEvents' -import { ProofState } from '../ProofState' import { PresentationProblemReportReason } from '../errors/PresentationProblemReportReason' -import { INDY_PROOF_REQUEST_ATTACHMENT_ID } from '../messages' +import { ProofProtocolVersion } from '../models/ProofProtocolVersion' +import { ProofState } from '../models/ProofState' +import { V1ProofService } from '../protocol/v1' +import { INDY_PROOF_REQUEST_ATTACHMENT_ID, V1RequestPresentationMessage } from '../protocol/v1/messages' +import { V1PresentationProblemReportMessage } from '../protocol/v1/messages/V1PresentationProblemReportMessage' import { ProofRecord } from '../repository/ProofRecord' import { ProofRepository } from '../repository/ProofRepository' -import { ProofService } from '../services' -import { IndyVerifierService } from './../../indy/services/IndyVerifierService' -import { PresentationProblemReportMessage } from './../messages/PresentationProblemReportMessage' -import { RequestPresentationMessage } from './../messages/RequestPresentationMessage' import { credDef } from './fixtures' // Mock classes @@ -32,14 +33,15 @@ jest.mock('../../indy/services/IndyIssuerService') jest.mock('../../indy/services/IndyVerifierService') jest.mock('../../indy/services/IndyRevocationService') jest.mock('../../connections/services/ConnectionService') +jest.mock('../../../storage/Repository') // Mock typed object const ProofRepositoryMock = ProofRepository as jest.Mock const IndyLedgerServiceMock = IndyLedgerService as jest.Mock const IndyHolderServiceMock = IndyHolderService as jest.Mock -const IndyVerifierServiceMock = IndyVerifierService as jest.Mock const IndyRevocationServiceMock = IndyRevocationService as jest.Mock const connectionServiceMock = ConnectionService as jest.Mock +const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock const connection = getMockConnection({ id: '123', @@ -59,26 +61,25 @@ const requestAttachment = new Attachment({ // object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. const mockProofRecord = ({ state, - requestMessage, threadId, connectionId, tags, id, }: { state?: ProofState - requestMessage?: RequestPresentationMessage + requestMessage?: V1RequestPresentationMessage tags?: CustomProofTags threadId?: string connectionId?: string id?: string } = {}) => { - const requestPresentationMessage = new RequestPresentationMessage({ + const requestPresentationMessage = new V1RequestPresentationMessage({ comment: 'some comment', requestPresentationAttachments: [requestAttachment], }) const proofRecord = new ProofRecord({ - requestMessage, + protocolVersion: ProofProtocolVersion.V1, id, state: state || ProofState.RequestSent, threadId: threadId ?? requestPresentationMessage.id, @@ -89,50 +90,52 @@ const mockProofRecord = ({ return proofRecord } -describe('ProofService', () => { +describe('V1ProofService', () => { let proofRepository: ProofRepository - let proofService: ProofService + let proofService: V1ProofService let ledgerService: IndyLedgerService let wallet: Wallet - let indyVerifierService: IndyVerifierService let indyHolderService: IndyHolderService let indyRevocationService: IndyRevocationService let eventEmitter: EventEmitter let credentialRepository: CredentialRepository let connectionService: ConnectionService + let didCommMessageRepository: DidCommMessageRepository + let indyProofFormatService: IndyProofFormatService beforeEach(() => { - const agentConfig = getAgentConfig('ProofServiceTest') + const agentConfig = getAgentConfig('V1ProofServiceTest') proofRepository = new ProofRepositoryMock() - indyVerifierService = new IndyVerifierServiceMock() indyHolderService = new IndyHolderServiceMock() indyRevocationService = new IndyRevocationServiceMock() ledgerService = new IndyLedgerServiceMock() eventEmitter = new EventEmitter(agentConfig) connectionService = new connectionServiceMock() + didCommMessageRepository = new didCommMessageRepositoryMock() - proofService = new ProofService( + proofService = new V1ProofService( proofRepository, + didCommMessageRepository, ledgerService, wallet, agentConfig, - indyHolderService, - indyVerifierService, - indyRevocationService, connectionService, eventEmitter, - credentialRepository + credentialRepository, + indyProofFormatService, + indyHolderService, + indyRevocationService ) mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) }) describe('processProofRequest', () => { - let presentationRequest: RequestPresentationMessage - let messageContext: InboundMessageContext + let presentationRequest: V1RequestPresentationMessage + let messageContext: InboundMessageContext beforeEach(() => { - presentationRequest = new RequestPresentationMessage({ + presentationRequest = new V1RequestPresentationMessage({ comment: 'abcd', requestPresentationAttachments: [requestAttachment], }) @@ -199,7 +202,7 @@ describe('ProofService', () => { mockFunction(proofRepository.getById).mockReturnValue(Promise.resolve(proof)) // when - const presentationProblemReportMessage = await new PresentationProblemReportMessage({ + const presentationProblemReportMessage = await new V1PresentationProblemReportMessage({ description: { en: 'Indy error', code: PresentationProblemReportReason.Abandoned, @@ -220,14 +223,14 @@ describe('ProofService', () => { describe('processProblemReport', () => { let proof: ProofRecord - let messageContext: InboundMessageContext + let messageContext: InboundMessageContext beforeEach(() => { proof = mockProofRecord({ state: ProofState.RequestReceived, }) - const presentationProblemReportMessage = new PresentationProblemReportMessage({ + const presentationProblemReportMessage = new V1PresentationProblemReportMessage({ description: { en: 'Indy error', code: PresentationProblemReportReason.Abandoned, diff --git a/packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts b/packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts new file mode 100644 index 0000000000..1ee30bacdd --- /dev/null +++ b/packages/core/src/modules/proofs/__tests__/V2ProofService.test.ts @@ -0,0 +1,271 @@ +import type { Wallet } from '../../../wallet/Wallet' +import type { ProofStateChangedEvent } from '../ProofEvents' +import type { CustomProofTags } from '../repository/ProofRecord' + +import { getAgentConfig, getMockConnection, mockFunction } from '../../../../tests/helpers' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InboundMessageContext } from '../../../agent/models/InboundMessageContext' +import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' +import { DidCommMessageRepository } from '../../../storage' +import { ConnectionService, DidExchangeState } from '../../connections' +import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' +import { ProofEventTypes } from '../ProofEvents' +import { PresentationProblemReportReason } from '../errors/PresentationProblemReportReason' +import { V2_INDY_PRESENTATION, V2_INDY_PRESENTATION_REQUEST } from '../formats/ProofFormats' +import { IndyProofFormatService } from '../formats/indy/IndyProofFormatService' +import { ProofProtocolVersion } from '../models/ProofProtocolVersion' +import { ProofState } from '../models/ProofState' +import { V2ProofService } from '../protocol/v2/V2ProofService' +import { V2PresentationProblemReportMessage, V2RequestPresentationMessage } from '../protocol/v2/messages' +import { ProofRecord } from '../repository/ProofRecord' +import { ProofRepository } from '../repository/ProofRepository' + +import { credDef } from './fixtures' + +// Mock classes +jest.mock('../repository/ProofRepository') +jest.mock('../../../modules/ledger/services/IndyLedgerService') +jest.mock('../../indy/services/IndyHolderService') +jest.mock('../../indy/services/IndyIssuerService') +jest.mock('../../indy/services/IndyVerifierService') +jest.mock('../../connections/services/ConnectionService') +jest.mock('../../../storage/Repository') + +// Mock typed object +const ProofRepositoryMock = ProofRepository as jest.Mock +const IndyLedgerServiceMock = IndyLedgerService as jest.Mock +const connectionServiceMock = ConnectionService as jest.Mock +const didCommMessageRepositoryMock = DidCommMessageRepository as jest.Mock +const indyProofFormatServiceMock = IndyProofFormatService as jest.Mock + +const connection = getMockConnection({ + id: '123', + state: DidExchangeState.Completed, +}) + +const requestAttachment = new Attachment({ + id: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + mimeType: 'application/json', + data: new AttachmentData({ + base64: + 'eyJuYW1lIjogIlByb29mIHJlcXVlc3QiLCAibm9uX3Jldm9rZWQiOiB7ImZyb20iOiAxNjQwOTk1MTk5LCAidG8iOiAxNjQwOTk1MTk5fSwgIm5vbmNlIjogIjEiLCAicmVxdWVzdGVkX2F0dHJpYnV0ZXMiOiB7ImFkZGl0aW9uYWxQcm9wMSI6IHsibmFtZSI6ICJmYXZvdXJpdGVEcmluayIsICJub25fcmV2b2tlZCI6IHsiZnJvbSI6IDE2NDA5OTUxOTksICJ0byI6IDE2NDA5OTUxOTl9LCAicmVzdHJpY3Rpb25zIjogW3siY3JlZF9kZWZfaWQiOiAiV2dXeHF6dHJOb29HOTJSWHZ4U1RXdjozOkNMOjIwOnRhZyJ9XX19LCAicmVxdWVzdGVkX3ByZWRpY2F0ZXMiOiB7fSwgInZlcnNpb24iOiAiMS4wIn0=', + }), +}) + +// A record is deserialized to JSON when it's stored into the storage. We want to simulate this behaviour for `offer` +// object to test our service would behave correctly. We use type assertion for `offer` attribute to `any`. +const mockProofRecord = ({ + state, + threadId, + connectionId, + tags, + id, +}: { + state?: ProofState + requestMessage?: V2RequestPresentationMessage + tags?: CustomProofTags + threadId?: string + connectionId?: string + id?: string +} = {}) => { + const requestPresentationMessage = new V2RequestPresentationMessage({ + attachmentInfo: [ + { + format: { + attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + format: V2_INDY_PRESENTATION, + }, + attachment: requestAttachment, + }, + ], + comment: 'some comment', + }) + + const proofRecord = new ProofRecord({ + protocolVersion: ProofProtocolVersion.V2, + id, + state: state || ProofState.RequestSent, + threadId: threadId ?? requestPresentationMessage.id, + connectionId: connectionId ?? '123', + tags, + }) + + return proofRecord +} + +describe('V2ProofService', () => { + let proofRepository: ProofRepository + let proofService: V2ProofService + let ledgerService: IndyLedgerService + let wallet: Wallet + let eventEmitter: EventEmitter + let connectionService: ConnectionService + let didCommMessageRepository: DidCommMessageRepository + let indyProofFormatService: IndyProofFormatService + + beforeEach(() => { + const agentConfig = getAgentConfig('V2ProofServiceTest') + proofRepository = new ProofRepositoryMock() + ledgerService = new IndyLedgerServiceMock() + eventEmitter = new EventEmitter(agentConfig) + connectionService = new connectionServiceMock() + didCommMessageRepository = new didCommMessageRepositoryMock() + indyProofFormatService = new indyProofFormatServiceMock() + + proofService = new V2ProofService( + agentConfig, + connectionService, + proofRepository, + didCommMessageRepository, + eventEmitter, + indyProofFormatService, + wallet + ) + + mockFunction(ledgerService.getCredentialDefinition).mockReturnValue(Promise.resolve(credDef)) + }) + + describe('processProofRequest', () => { + let presentationRequest: V2RequestPresentationMessage + let messageContext: InboundMessageContext + + beforeEach(() => { + presentationRequest = new V2RequestPresentationMessage({ + attachmentInfo: [ + { + format: { + attachmentId: 'abdc8b63-29c6-49ad-9e10-98f9d85db9a2', + format: V2_INDY_PRESENTATION_REQUEST, + }, + attachment: requestAttachment, + }, + ], + comment: 'Proof Request', + }) + messageContext = new InboundMessageContext(presentationRequest, { + connection, + }) + }) + + test(`creates and return proof record in ${ProofState.PresentationReceived} state with offer, without thread ID`, async () => { + const repositorySaveSpy = jest.spyOn(proofRepository, 'save') + + // when + const returnedProofRecord = await proofService.processRequest(messageContext) + + // then + const expectedProofRecord = { + type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + state: ProofState.RequestReceived, + threadId: presentationRequest.id, + connectionId: connection.id, + } + expect(repositorySaveSpy).toHaveBeenCalledTimes(1) + const [[createdProofRecord]] = repositorySaveSpy.mock.calls + expect(createdProofRecord).toMatchObject(expectedProofRecord) + expect(returnedProofRecord).toMatchObject(expectedProofRecord) + }) + + test(`emits stateChange event with ${ProofState.RequestReceived}`, async () => { + const eventListenerMock = jest.fn() + eventEmitter.on(ProofEventTypes.ProofStateChanged, eventListenerMock) + + // when + await proofService.processRequest(messageContext) + + // then + expect(eventListenerMock).toHaveBeenCalledWith({ + type: 'ProofStateChanged', + payload: { + previousState: null, + proofRecord: expect.objectContaining({ + state: ProofState.RequestReceived, + }), + }, + }) + }) + }) + + describe('createProblemReport', () => { + const threadId = 'fd9c5ddb-ec11-4acd-bc32-540736249746' + let proof: ProofRecord + + beforeEach(() => { + proof = mockProofRecord({ + state: ProofState.RequestReceived, + threadId, + connectionId: 'b1e2f039-aa39-40be-8643-6ce2797b5190', + }) + }) + + test('returns problem report message base once get error', async () => { + // given + mockFunction(proofRepository.getById).mockReturnValue(Promise.resolve(proof)) + + // when + const presentationProblemReportMessage = await new V2PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + + presentationProblemReportMessage.setThread({ threadId }) + // then + expect(presentationProblemReportMessage.toJSON()).toMatchObject({ + '@id': expect.any(String), + '@type': 'https://didcomm.org/present-proof/2.0/problem-report', + '~thread': { + thid: 'fd9c5ddb-ec11-4acd-bc32-540736249746', + }, + }) + }) + }) + + describe('processProblemReport', () => { + let proof: ProofRecord + let messageContext: InboundMessageContext + + beforeEach(() => { + proof = mockProofRecord({ + state: ProofState.RequestReceived, + }) + + const presentationProblemReportMessage = new V2PresentationProblemReportMessage({ + description: { + en: 'Indy error', + code: PresentationProblemReportReason.Abandoned, + }, + }) + presentationProblemReportMessage.setThread({ threadId: 'somethreadid' }) + messageContext = new InboundMessageContext(presentationProblemReportMessage, { + connection, + }) + }) + + test(`updates problem report error message and returns proof record`, async () => { + const repositoryUpdateSpy = jest.spyOn(proofRepository, 'update') + + // given + mockFunction(proofRepository.getSingleByQuery).mockReturnValue(Promise.resolve(proof)) + + // when + const returnedCredentialRecord = await proofService.processProblemReport(messageContext) + + // then + const expectedCredentialRecord = { + errorMessage: 'abandoned: Indy error', + } + expect(proofRepository.getSingleByQuery).toHaveBeenNthCalledWith(1, { + threadId: 'somethreadid', + connectionId: connection.id, + }) + expect(repositoryUpdateSpy).toHaveBeenCalledTimes(1) + const [[updatedCredentialRecord]] = repositoryUpdateSpy.mock.calls + expect(updatedCredentialRecord).toMatchObject(expectedCredentialRecord) + expect(returnedCredentialRecord).toMatchObject(expectedCredentialRecord) + }) + }) +}) diff --git a/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts b/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts deleted file mode 100644 index 2869a026d5..0000000000 --- a/packages/core/src/modules/proofs/errors/PresentationProblemReportError.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ProblemReportErrorOptions } from '../../problem-reports' -import type { PresentationProblemReportReason } from './PresentationProblemReportReason' - -import { PresentationProblemReportMessage } from '../messages' - -import { ProblemReportError } from './../../problem-reports/errors/ProblemReportError' - -interface PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { - problemCode: PresentationProblemReportReason -} - -export class PresentationProblemReportError extends ProblemReportError { - public problemReport: PresentationProblemReportMessage - - public constructor(public message: string, { problemCode }: PresentationProblemReportErrorOptions) { - super(message, { problemCode }) - this.problemReport = new PresentationProblemReportMessage({ - description: { - en: message, - code: problemCode, - }, - }) - } -} diff --git a/packages/core/src/modules/proofs/errors/index.ts b/packages/core/src/modules/proofs/errors/index.ts index 5e0ca1453b..b14650ff96 100644 --- a/packages/core/src/modules/proofs/errors/index.ts +++ b/packages/core/src/modules/proofs/errors/index.ts @@ -1,2 +1 @@ -export * from './PresentationProblemReportError' export * from './PresentationProblemReportReason' diff --git a/packages/core/src/modules/proofs/formats/IndyProofFormatsServiceOptions.ts b/packages/core/src/modules/proofs/formats/IndyProofFormatsServiceOptions.ts new file mode 100644 index 0000000000..dd42a0774c --- /dev/null +++ b/packages/core/src/modules/proofs/formats/IndyProofFormatsServiceOptions.ts @@ -0,0 +1,54 @@ +import type { Attachment } from '../../../decorators/attachment/Attachment' +import type { RevocationInterval } from '../../credentials/protocol/v1/models/RevocationInterval' +import type { GetRequestedCredentialsConfig } from '../models/GetRequestedCredentialsConfig' +import type { RequestProofFormats } from '../models/SharedOptions' +import type { RequestedAttribute, RequestedPredicate } from '../protocol/v1/models' +import type { ProofAttributeInfo } from '../protocol/v1/models/ProofAttributeInfo' +import type { ProofPredicateInfo } from '../protocol/v1/models/ProofPredicateInfo' +import type { + PresentationPreview, + PresentationPreviewAttribute, + PresentationPreviewPredicate, +} from '../protocol/v1/models/V1PresentationPreview' +import type { ProofRequest } from './indy/models/ProofRequest' + +export interface IndyProposeProofFormat { + attributes?: PresentationPreviewAttribute[] + predicates?: PresentationPreviewPredicate[] + nonce: string + name: string + version: string +} + +export interface IndyRequestProofFormat { + name: string + version: string + nonce: string + nonRevoked?: RevocationInterval + ver?: '1.0' | '2.0' + requestedAttributes?: Record | Map + requestedPredicates?: Record | Map + proofRequest?: ProofRequest +} + +export interface IndyVerifyProofFormat { + proofJson: Attachment + proofRequest: Attachment +} + +export interface IndyPresentationProofFormat { + requestedAttributes?: Record + requestedPredicates?: Record + selfAttestedAttributes?: Record +} + +export interface GetRequestedCredentialsFormat { + attachment: Attachment + presentationProposal?: PresentationPreview + config?: GetRequestedCredentialsConfig +} + +export interface CreateRequestAsResponseOptions { + id?: string + formats: RequestProofFormats +} diff --git a/packages/core/src/modules/proofs/formats/ProofFormatService.ts b/packages/core/src/modules/proofs/formats/ProofFormatService.ts new file mode 100644 index 0000000000..dace91722f --- /dev/null +++ b/packages/core/src/modules/proofs/formats/ProofFormatService.ts @@ -0,0 +1,67 @@ +import type { AgentConfig } from '../../../agent/AgentConfig' +import type { DidCommMessageRepository } from '../../../storage' +import type { + RetrievedCredentialOptions, + ProofRequestFormats, + RequestedCredentialsFormats, +} from '../models/SharedOptions' +import type { CreateRequestAsResponseOptions, GetRequestedCredentialsFormat } from './IndyProofFormatsServiceOptions' +import type { ProofAttachmentFormat } from './models/ProofAttachmentFormat' +import type { + CreatePresentationFormatsOptions, + CreatePresentationOptions, + CreateProposalOptions, + CreateRequestOptions, + ProcessPresentationOptions, + ProcessProposalOptions, + ProcessRequestOptions, +} from './models/ProofFormatServiceOptions' + +/** + * This abstract class is the base class for any proof format + * specific service. + * + * @export + * @abstract + * @class ProofFormatService + */ +export abstract class ProofFormatService { + protected didCommMessageRepository: DidCommMessageRepository + protected agentConfig: AgentConfig + + public constructor(didCommMessageRepository: DidCommMessageRepository, agentConfig: AgentConfig) { + this.didCommMessageRepository = didCommMessageRepository + this.agentConfig = agentConfig + } + + abstract createProposal(options: CreateProposalOptions): Promise + + abstract processProposal(options: ProcessProposalOptions): Promise + + abstract createRequest(options: CreateRequestOptions): Promise + + abstract processRequest(options: ProcessRequestOptions): Promise + + abstract createPresentation(options: CreatePresentationOptions): Promise + + abstract processPresentation(options: ProcessPresentationOptions): Promise + + abstract createProofRequestFromProposal(options: CreatePresentationFormatsOptions): Promise + + public abstract getRequestedCredentialsForProofRequest( + options: GetRequestedCredentialsFormat + ): Promise + + public abstract autoSelectCredentialsForProofRequest( + options: RetrievedCredentialOptions + ): Promise + + abstract proposalAndRequestAreEqual( + proposalAttachments: ProofAttachmentFormat[], + requestAttachments: ProofAttachmentFormat[] + ): boolean + + abstract supportsFormat(formatIdentifier: string): boolean + + abstract createRequestAsResponse(options: CreateRequestAsResponseOptions): Promise +} diff --git a/packages/core/src/modules/proofs/formats/ProofFormats.ts b/packages/core/src/modules/proofs/formats/ProofFormats.ts new file mode 100644 index 0000000000..35e1ce33ab --- /dev/null +++ b/packages/core/src/modules/proofs/formats/ProofFormats.ts @@ -0,0 +1,4 @@ +export const INDY_ATTACH_ID = 'indy' +export const V2_INDY_PRESENTATION_PROPOSAL = 'hlindy/proof-req@v2.0' +export const V2_INDY_PRESENTATION_REQUEST = 'hlindy/proof-req@v2.0' +export const V2_INDY_PRESENTATION = 'hlindy/proof@v2.0' diff --git a/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts b/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts new file mode 100644 index 0000000000..a81e4e5553 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/errors/InvalidEncodedValueError.ts @@ -0,0 +1,3 @@ +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' + +export class InvalidEncodedValueError extends AriesFrameworkError {} diff --git a/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts b/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts new file mode 100644 index 0000000000..a00abc40cb --- /dev/null +++ b/packages/core/src/modules/proofs/formats/errors/MissingIndyProofMessageError.ts @@ -0,0 +1,3 @@ +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' + +export class MissingIndyProofMessageError extends AriesFrameworkError {} diff --git a/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts new file mode 100644 index 0000000000..522b52a335 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/indy/IndyProofFormatService.ts @@ -0,0 +1,642 @@ +import type { Logger } from '../../../../logger' +import type { + RetrievedCredentialOptions, + ProofRequestFormats, + RequestedCredentialsFormats, +} from '../../models/SharedOptions' +import type { ProofAttributeInfo, ProofPredicateInfo } from '../../protocol/v1/models' +import type { + CreateRequestAsResponseOptions, + GetRequestedCredentialsFormat, + IndyProposeProofFormat, +} from '../IndyProofFormatsServiceOptions' +import type { ProofAttachmentFormat } from '../models/ProofAttachmentFormat' +import type { + CreatePresentationFormatsOptions, + CreatePresentationOptions, + CreateProofAttachmentOptions, + CreateProposalOptions, + CreateRequestAttachmentOptions, + CreateRequestOptions, + ProcessPresentationOptions, + ProcessProposalOptions, + ProcessRequestOptions, + VerifyProofOptions, +} from '../models/ProofFormatServiceOptions' +import type { CredDef, IndyProof, Schema } from 'indy-sdk' + +import { validateOrReject } from 'class-validator' +import { Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' +import { ConsoleLogger, LogLevel } from '../../../../logger' +import { DidCommMessageRepository } from '../../../../storage/didcomm/DidCommMessageRepository' +import { checkProofRequestForDuplicates } from '../../../../utils' +import { JsonEncoder } from '../../../../utils/JsonEncoder' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { uuid } from '../../../../utils/uuid' +import { IndyWallet } from '../../../../wallet/IndyWallet' +import { CredentialUtils } from '../../../credentials' +import { Credential, IndyCredentialInfo } from '../../../credentials/protocol/v1/models' +import { IndyHolderService, IndyVerifierService, IndyRevocationService } from '../../../indy' +import { IndyLedgerService } from '../../../ledger' +import { ProofsUtils } from '../../ProofsUtil' +import { PartialProof, RequestedPredicate, RequestedAttribute } from '../../protocol/v1/models' +import { PresentationPreview } from '../../protocol/v1/models/V1PresentationPreview' +import { ProofFormatService } from '../ProofFormatService' +import { V2_INDY_PRESENTATION, V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST } from '../ProofFormats' +import { InvalidEncodedValueError } from '../errors/InvalidEncodedValueError' +import { MissingIndyProofMessageError } from '../errors/MissingIndyProofMessageError' +import { ProofFormatSpec } from '../models/ProofFormatSpec' + +import { ProofRequest } from './models/ProofRequest' +import { RequestedCredentials } from './models/RequestedCredentials' +import { RetrievedCredentials } from './models/RetrievedCredentials' + +@scoped(Lifecycle.ContainerScoped) +export class IndyProofFormatService extends ProofFormatService { + private indyHolderService: IndyHolderService + private indyVerifierService: IndyVerifierService + private indyRevocationService: IndyRevocationService + private ledgerService: IndyLedgerService + private logger: Logger + private wallet: IndyWallet + + public constructor( + agentConfig: AgentConfig, + indyHolderService: IndyHolderService, + indyVerifierService: IndyVerifierService, + indyRevocationService: IndyRevocationService, + ledgerService: IndyLedgerService, + didCommMessageRepository: DidCommMessageRepository, + wallet: IndyWallet + ) { + super(didCommMessageRepository, agentConfig) + this.indyHolderService = indyHolderService + this.indyVerifierService = indyVerifierService + this.indyRevocationService = indyRevocationService + this.ledgerService = ledgerService + this.wallet = wallet + this.logger = new ConsoleLogger(LogLevel.off) + } + + private createRequestAttachment(options: CreateRequestAttachmentOptions): ProofAttachmentFormat { + const format = new ProofFormatSpec({ + attachmentId: options.id, + format: V2_INDY_PRESENTATION_REQUEST, + }) + + const request = new ProofRequest(options.proofRequestOptions) + + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(request) + + const attachment = new Attachment({ + id: options.id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(request), + }), + }) + return { format, attachment } + } + + private createProofAttachment(options: CreateProofAttachmentOptions): ProofAttachmentFormat { + const format = new ProofFormatSpec({ + attachmentId: options.id, + format: V2_INDY_PRESENTATION_PROPOSAL, + }) + + const attachment = new Attachment({ + id: options.id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(options.proofProposalOptions), + }), + }) + return { format, attachment } + } + + public async createProposal(options: CreateProposalOptions): Promise { + if (!options.formats.indy) { + throw Error('Missing indy format to create proposal attachment format') + } + const indyFormat = options.formats.indy + + const preview = new PresentationPreview({ + attributes: indyFormat.attributes, + predicates: indyFormat.predicates, + }) + + if (!preview) { + throw Error('Missing presentation preview to create proposal attachment format') + } + + return this.createProofAttachment({ + id: options.id ?? uuid(), + proofProposalOptions: preview, + }) + } + + public async processProposal(options: ProcessProposalOptions): Promise { + const proofProposalJson = options.proposal.attachment.getDataAsJson() + + let proposalMessage + + // Assert attachment + if (!proofProposalJson) { + throw new AriesFrameworkError( + `Missing required base64 or json encoded attachment data for presentation proposal with thread id ${options.record?.threadId}` + ) + } + + if (typeof proofProposalJson === typeof PresentationPreview) { + proposalMessage = JsonTransformer.fromJSON(proofProposalJson, PresentationPreview) + + await validateOrReject(proposalMessage) + } else { + proposalMessage = JsonTransformer.fromJSON(proofProposalJson, ProofRequest) + + await validateOrReject(proposalMessage) + } + } + + public async createRequestAsResponse(options: CreateRequestAsResponseOptions): Promise { + if (!options.formats.indy) { + throw Error('Missing indy format to create proposal attachment format') + } + + const id = options.id ?? uuid() + + const format = new ProofFormatSpec({ + attachmentId: id, + format: V2_INDY_PRESENTATION_REQUEST, + }) + + const attachment = new Attachment({ + id: id, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(options.formats.indy), + }), + }) + return { format, attachment } + } + + public async createRequest(options: CreateRequestOptions): Promise { + if (!options.formats.indy) { + throw new AriesFrameworkError('Missing indy format to create proof request attachment format.') + } + + return this.createRequestAttachment({ + id: options.id ?? uuid(), + proofRequestOptions: options.formats.indy, + }) + } + + public async processRequest(options: ProcessRequestOptions): Promise { + const proofRequestJson = options.request.attachment.getDataAsJson() + + const proofRequestMessage = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + // Assert attachment + if (!proofRequestMessage) { + throw new AriesFrameworkError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${options.record?.threadId}` + ) + } + await validateOrReject(proofRequestMessage) + } + + public async createPresentation(options: CreatePresentationOptions): Promise { + // Extract proof request from attachment + const proofRequestJson = options.attachment.getDataAsJson() ?? null + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + // verify everything is there + if (!options.formats.indy) { + throw new AriesFrameworkError('Missing indy format to create proof presentation attachment format.') + } + + const requestedCredentials = new RequestedCredentials({ + requestedAttributes: options.formats.indy.requestedAttributes, + requestedPredicates: options.formats.indy.requestedPredicates, + selfAttestedAttributes: options.formats.indy.selfAttestedAttributes, + }) + + const proof = await this.createProof(proofRequest, requestedCredentials) + + const attachmentId = options.id ?? uuid() + + const format = new ProofFormatSpec({ + attachmentId, + format: V2_INDY_PRESENTATION, + }) + + const attachment = new Attachment({ + id: attachmentId, + mimeType: 'application/json', + data: new AttachmentData({ + base64: JsonEncoder.toBase64(proof), + }), + }) + return { format, attachment } + } + + public async processPresentation(options: ProcessPresentationOptions): Promise { + const requestFormat = options.presentation.request.find((x) => x.format.format === V2_INDY_PRESENTATION_REQUEST) + + if (!requestFormat) { + throw new MissingIndyProofMessageError( + 'Missing Indy Proof Request format while trying to process an Indy proof presentation.' + ) + } + + const proofFormat = options.presentation.proof.find((x) => x.format.format === V2_INDY_PRESENTATION) + + if (!proofFormat) { + throw new MissingIndyProofMessageError( + 'Missing Indy Proof Presentation format while trying to process an Indy proof presentation.' + ) + } + + return await this.verifyProof({ request: requestFormat.attachment, proof: proofFormat.attachment }) + } + + public async verifyProof(options: VerifyProofOptions): Promise { + if (!options) { + throw new AriesFrameworkError('No Indy proof was provided.') + } + const proofRequestJson = options.request.getDataAsJson() ?? null + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + const proofJson = options.proof.getDataAsJson() ?? null + + const proof = JsonTransformer.fromJSON(proofJson, PartialProof) + + for (const [referent, attribute] of proof.requestedProof.revealedAttributes.entries()) { + if (!CredentialUtils.checkValidEncoding(attribute.raw, attribute.encoded)) { + throw new InvalidEncodedValueError( + `The encoded value for '${referent}' is invalid. ` + + `Expected '${CredentialUtils.encode(attribute.raw)}'. ` + + `Actual '${attribute.encoded}'` + ) + } + } + + // TODO: pre verify proof json + // I'm not 100% sure how much indy does. Also if it checks whether the proof requests matches the proof + // @see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/indy/sdk/verifier.py#L79-L164 + + const schemas = await this.getSchemas(new Set(proof.identifiers.map((i) => i.schemaId))) + const credentialDefinitions = await this.getCredentialDefinitions( + new Set(proof.identifiers.map((i) => i.credentialDefinitionId)) + ) + + return await this.indyVerifierService.verifyProof({ + proofRequest: proofRequest.toJSON(), + proof: proofJson, + schemas, + credentialDefinitions, + }) + } + + public supportsFormat(formatIdentifier: string): boolean { + const supportedFormats = [V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST, V2_INDY_PRESENTATION] + return supportedFormats.includes(formatIdentifier) + } + + /** + * Compare presentation attrs with request/proposal attrs (auto-accept) + * + * @param proposalAttachments attachment data from the proposal + * @param requestAttachments attachment data from the request + * @returns boolean value + */ + public proposalAndRequestAreEqual( + proposalAttachments: ProofAttachmentFormat[], + requestAttachments: ProofAttachmentFormat[] + ) { + const proposalAttachment = proposalAttachments.find( + (x) => x.format.format === V2_INDY_PRESENTATION_PROPOSAL + )?.attachment + const requestAttachment = requestAttachments.find( + (x) => x.format.format === V2_INDY_PRESENTATION_REQUEST + )?.attachment + + if (!proposalAttachment) { + throw new AriesFrameworkError('Proposal message has no attachment linked to it') + } + + if (!requestAttachment) { + throw new AriesFrameworkError('Request message has no attachment linked to it') + } + + const proposalAttachmentData = proposalAttachment.getDataAsJson() + const requestAttachmentData = requestAttachment.getDataAsJson() + + if ( + proposalAttachmentData.requestedAttributes === requestAttachmentData.requestedAttributes && + proposalAttachmentData.requestedPredicates === requestAttachmentData.requestedPredicates + ) { + return true + } + + return false + } + + /** + * Build credential definitions object needed to create and verify proof objects. + * + * Creates object with `{ credentialDefinitionId: CredentialDefinition }` mapping + * + * @param credentialDefinitionIds List of credential definition ids + * @returns Object containing credential definitions for specified credential definition ids + * + */ + private async getCredentialDefinitions(credentialDefinitionIds: Set) { + const credentialDefinitions: { [key: string]: CredDef } = {} + + for (const credDefId of credentialDefinitionIds) { + const credDef = await this.ledgerService.getCredentialDefinition(credDefId) + credentialDefinitions[credDefId] = credDef + } + + return credentialDefinitions + } + + public async getRequestedCredentialsForProofRequest( + options: GetRequestedCredentialsFormat + ): Promise { + const retrievedCredentials = new RetrievedCredentials({}) + const { attachment, presentationProposal } = options + const filterByNonRevocationRequirements = options.config?.filterByNonRevocationRequirements + + const proofRequestJson = attachment.getDataAsJson() ?? null + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + for (const [referent, requestedAttribute] of proofRequest.requestedAttributes.entries()) { + let credentialMatch: Credential[] = [] + const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) + + // If we have exactly one credential, or no proposal to pick preferences + // on the credentials to use, we will use the first one + if (credentials.length === 1 || !presentationProposal) { + credentialMatch = credentials + } + // If we have a proposal we will use that to determine the credentials to use + else { + const names = requestedAttribute.names ?? [requestedAttribute.name] + + // Find credentials that matches all parameters from the proposal + credentialMatch = credentials.filter((credential) => { + const { attributes, credentialDefinitionId } = credential.credentialInfo + + // Check if credentials matches all parameters from proposal + return names.every((name) => + presentationProposal.attributes.find( + (a) => + a.name === name && + a.credentialDefinitionId === credentialDefinitionId && + (!a.value || a.value === attributes[name]) + ) + ) + }) + } + + retrievedCredentials.requestedAttributes[referent] = await Promise.all( + credentialMatch.map(async (credential: Credential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem: requestedAttribute, + credential, + }) + + return new RequestedAttribute({ + credentialId: credential.credentialInfo.referent, + revealed: true, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) + }) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (filterByNonRevocationRequirements) { + retrievedCredentials.requestedAttributes[referent] = retrievedCredentials.requestedAttributes[referent].filter( + (r) => !r.revoked + ) + } + } + + for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { + const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) + + retrievedCredentials.requestedPredicates[referent] = await Promise.all( + credentials.map(async (credential) => { + const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem: requestedPredicate, + credential, + }) + + return new RequestedPredicate({ + credentialId: credential.credentialInfo.referent, + credentialInfo: credential.credentialInfo, + timestamp: deltaTimestamp, + revoked, + }) + }) + ) + + // We only attach revoked state if non-revocation is requested. So if revoked is true it means + // the credential is not applicable to the proof request + if (filterByNonRevocationRequirements) { + retrievedCredentials.requestedPredicates[referent] = retrievedCredentials.requestedPredicates[referent].filter( + (r) => !r.revoked + ) + } + } + + return { + indy: retrievedCredentials, + } + } + + private async getCredentialsForProofRequest( + proofRequest: ProofRequest, + attributeReferent: string + ): Promise { + const credentialsJson = await this.indyHolderService.getCredentialsForProofRequest({ + proofRequest: proofRequest.toJSON(), + attributeReferent, + }) + + return JsonTransformer.fromJSON(credentialsJson, Credential) as unknown as Credential[] + } + + public async autoSelectCredentialsForProofRequest( + options: RetrievedCredentialOptions + ): Promise { + const indy = options.indy + + if (!indy) { + throw new AriesFrameworkError('No indy options provided') + } + + const requestedCredentials = new RequestedCredentials({}) + + Object.keys(indy.requestedAttributes).forEach((attributeName) => { + const attributeArray = indy.requestedAttributes[attributeName] + + if (attributeArray.length === 0) { + throw new AriesFrameworkError('Unable to automatically select requested attributes.') + } else { + requestedCredentials.requestedAttributes[attributeName] = attributeArray[0] + } + }) + + Object.keys(indy.requestedPredicates).forEach((attributeName) => { + if (indy.requestedPredicates[attributeName].length === 0) { + throw new AriesFrameworkError('Unable to automatically select requested predicates.') + } else { + requestedCredentials.requestedPredicates[attributeName] = indy.requestedPredicates[attributeName][0] + } + }) + + return { + indy: requestedCredentials, + } + } + + /** + * Build schemas object needed to create and verify proof objects. + * + * Creates object with `{ schemaId: Schema }` mapping + * + * @param schemaIds List of schema ids + * @returns Object containing schemas for specified schema ids + * + */ + private async getSchemas(schemaIds: Set) { + const schemas: { [key: string]: Schema } = {} + + for (const schemaId of schemaIds) { + const schema = await this.ledgerService.getSchema(schemaId) + schemas[schemaId] = schema + } + + return schemas + } + + /** + * Create indy proof from a given proof request and requested credential object. + * + * @param proofRequest The proof request to create the proof for + * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof + * @returns indy proof object + */ + private async createProof( + proofRequest: ProofRequest, + requestedCredentials: RequestedCredentials + ): Promise { + const credentialObjects = await Promise.all( + [ + ...Object.values(requestedCredentials.requestedAttributes), + ...Object.values(requestedCredentials.requestedPredicates), + ].map(async (c) => { + if (c.credentialInfo) { + return c.credentialInfo + } + const credentialInfo = await this.indyHolderService.getCredential(c.credentialId) + return JsonTransformer.fromJSON(credentialInfo, IndyCredentialInfo) + }) + ) + + const schemas = await this.getSchemas(new Set(credentialObjects.map((c) => c.schemaId))) + const credentialDefinitions = await this.getCredentialDefinitions( + new Set(credentialObjects.map((c) => c.credentialDefinitionId)) + ) + + return await this.indyHolderService.createProof({ + proofRequest: proofRequest.toJSON(), + requestedCredentials: requestedCredentials, + schemas, + credentialDefinitions, + }) + } + + public async createProofRequestFromProposal(options: CreatePresentationFormatsOptions): Promise { + const indyAttachment = options.presentationAttachment + const indyConfig = options?.config + + if (!indyAttachment) { + throw new AriesFrameworkError('Indy attachment is missing to create proof request from proposal.') + } + + if (!indyConfig) { + throw new AriesFrameworkError('Indy config is missing to create proof request from proposal.') + } + + const proposalJson = indyAttachment.getDataAsJson() ?? null + + if (!proposalJson) { + throw new AriesFrameworkError(`Presentation Preview is missing`) + } + + const nonce = indyConfig?.nonce ?? (await this.wallet.generateNonce()) + + const indyProposeProofFormat: IndyProposeProofFormat = { + name: indyConfig?.name ?? 'Proof Request', + version: indyConfig?.version ?? '1.0', + nonce: nonce, + } + + const proofRequest = ProofsUtils.createReferentForProofRequest(indyProposeProofFormat, proposalJson) + + return { + indy: proofRequest, + } + } + + private async getRevocationStatusForRequestedItem({ + proofRequest, + requestedItem, + credential, + }: { + proofRequest: ProofRequest + requestedItem: ProofAttributeInfo | ProofPredicateInfo + credential: Credential + }) { + const requestNonRevoked = requestedItem.nonRevoked ?? proofRequest.nonRevoked + const credentialRevocationId = credential.credentialInfo.credentialRevocationId + const revocationRegistryId = credential.credentialInfo.revocationRegistryId + + // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display + if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { + this.logger.trace( + `Presentation is requesting proof of non revocation, getting revocation status for credential`, + { + requestNonRevoked, + credentialRevocationId, + revocationRegistryId, + } + ) + + // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals + const status = await this.indyRevocationService.getRevocationStatus( + credentialRevocationId, + revocationRegistryId, + requestNonRevoked + ) + + return status + } + + return { revoked: undefined, deltaTimestamp: undefined } + } +} diff --git a/packages/core/src/modules/proofs/models/ProofRequest.ts b/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts similarity index 87% rename from packages/core/src/modules/proofs/models/ProofRequest.ts rename to packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts index 754ee220d9..61654828a4 100644 --- a/packages/core/src/modules/proofs/models/ProofRequest.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/ProofRequest.ts @@ -3,12 +3,11 @@ import type { IndyProofRequest } from 'indy-sdk' import { Expose, Type } from 'class-transformer' import { IsString, ValidateNested, IsOptional, IsIn, IsInstance } from 'class-validator' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { IsMap } from '../../../utils/transformers' -import { RevocationInterval } from '../../credentials' - -import { ProofAttributeInfo } from './ProofAttributeInfo' -import { ProofPredicateInfo } from './ProofPredicateInfo' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IsMap } from '../../../../../utils/transformers' +import { RevocationInterval } from '../../../../credentials' +import { ProofAttributeInfo } from '../../../protocol/v1/models/ProofAttributeInfo' +import { ProofPredicateInfo } from '../../../protocol/v1/models/ProofPredicateInfo' export interface ProofRequestOptions { name: string diff --git a/packages/core/src/modules/proofs/models/RequestedCredentials.ts b/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts similarity index 80% rename from packages/core/src/modules/proofs/models/RequestedCredentials.ts rename to packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts index 5d15cae028..4d814fb9b4 100644 --- a/packages/core/src/modules/proofs/models/RequestedCredentials.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/RequestedCredentials.ts @@ -3,13 +3,12 @@ import type { IndyRequestedCredentials } from 'indy-sdk' import { Expose } from 'class-transformer' import { ValidateNested } from 'class-validator' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { RecordTransformer } from '../../../utils/transformers' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { RecordTransformer } from '../../../../../utils/transformers' +import { RequestedAttribute } from '../../../protocol/v1/models/RequestedAttribute' +import { RequestedPredicate } from '../../../protocol/v1/models/RequestedPredicate' -import { RequestedAttribute } from './RequestedAttribute' -import { RequestedPredicate } from './RequestedPredicate' - -interface RequestedCredentialsOptions { +export interface IndyRequestedCredentialsOptions { requestedAttributes?: Record requestedPredicates?: Record selfAttestedAttributes?: Record @@ -21,7 +20,7 @@ interface RequestedCredentialsOptions { * @see https://github.com/hyperledger/indy-sdk/blob/57dcdae74164d1c7aa06f2cccecaae121cefac25/libindy/src/api/anoncreds.rs#L1433-L1445 */ export class RequestedCredentials { - public constructor(options: RequestedCredentialsOptions = {}) { + public constructor(options: IndyRequestedCredentialsOptions = {}) { if (options) { this.requestedAttributes = options.requestedAttributes ?? {} this.requestedPredicates = options.requestedPredicates ?? {} diff --git a/packages/core/src/modules/proofs/models/RetrievedCredentials.ts b/packages/core/src/modules/proofs/formats/indy/models/RetrievedCredentials.ts similarity index 77% rename from packages/core/src/modules/proofs/models/RetrievedCredentials.ts rename to packages/core/src/modules/proofs/formats/indy/models/RetrievedCredentials.ts index e529b24065..f2977ceede 100644 --- a/packages/core/src/modules/proofs/models/RetrievedCredentials.ts +++ b/packages/core/src/modules/proofs/formats/indy/models/RetrievedCredentials.ts @@ -1,5 +1,5 @@ -import type { RequestedAttribute } from './RequestedAttribute' -import type { RequestedPredicate } from './RequestedPredicate' +import type { RequestedAttribute } from '../../../protocol/v1/models/RequestedAttribute' +import type { RequestedPredicate } from '../../../protocol/v1/models/RequestedPredicate' export interface RetrievedCredentialsOptions { requestedAttributes?: Record diff --git a/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts b/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts new file mode 100644 index 0000000000..0e13a9bf8c --- /dev/null +++ b/packages/core/src/modules/proofs/formats/models/ProofAttachmentFormat.ts @@ -0,0 +1,7 @@ +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { ProofFormatSpec } from './ProofFormatSpec' + +export interface ProofAttachmentFormat { + format: ProofFormatSpec + attachment: Attachment +} diff --git a/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts b/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts new file mode 100644 index 0000000000..efb52d8be9 --- /dev/null +++ b/packages/core/src/modules/proofs/formats/models/ProofFormatServiceOptions.ts @@ -0,0 +1,71 @@ +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { CreatePresentationFormats, ProposeProofFormats } from '../../models/SharedOptions' +import type { PresentationPreview } from '../../protocol/v1/models/V1PresentationPreview' +import type { ProofRecord } from '../../repository' +import type { ProofRequestOptions } from '../indy/models/ProofRequest' +import type { ProofAttachmentFormat } from './ProofAttachmentFormat' + +export interface CreateRequestAttachmentOptions { + id?: string + proofRequestOptions: ProofRequestOptions +} + +export interface CreateProofAttachmentOptions { + id?: string + proofProposalOptions: PresentationPreview +} + +export interface CreateProposalOptions { + id?: string + formats: ProposeProofFormats +} + +export interface ProcessProposalOptions { + proposal: ProofAttachmentFormat + record?: ProofRecord +} + +export interface CreateRequestOptions { + id?: string + formats: ProposeProofFormats +} + +export interface ProcessRequestOptions { + request: ProofAttachmentFormat + record?: ProofRecord +} + +export interface CreatePresentationOptions { + id?: string + attachment: Attachment + formats: CreatePresentationFormats +} + +export interface ProcessPresentationOptions { + record: ProofRecord + presentation: { + request: ProofAttachmentFormat[] + proof: ProofAttachmentFormat[] + } +} + +export interface VerifyProofOptions { + request: Attachment + proof: Attachment +} + +export interface CreateProblemReportOptions { + proofRecord: ProofRecord + description: string +} + +export interface CreatePresentationFormatsOptions { + presentationAttachment: Attachment + config: IndyProofConfig +} + +interface IndyProofConfig { + name: string + version: string + nonce?: string +} diff --git a/packages/core/src/modules/proofs/formats/models/ProofFormatSpec.ts b/packages/core/src/modules/proofs/formats/models/ProofFormatSpec.ts new file mode 100644 index 0000000000..97d8c21f1e --- /dev/null +++ b/packages/core/src/modules/proofs/formats/models/ProofFormatSpec.ts @@ -0,0 +1,25 @@ +import { Expose } from 'class-transformer' +import { IsString } from 'class-validator' + +import { uuid } from '../../../../utils/uuid' + +export interface ProofFormatSpecOptions { + attachmentId?: string + format: string +} + +export class ProofFormatSpec { + public constructor(options: ProofFormatSpecOptions) { + if (options) { + this.attachmentId = options.attachmentId ?? uuid() + this.format = options.format + } + } + + @Expose({ name: 'attach_id' }) + @IsString() + public attachmentId!: string + + @IsString() + public format!: string +} diff --git a/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeFormatService.ts b/packages/core/src/modules/proofs/formats/presentation-exchange/PresentationExchangeFormatService.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core/src/modules/proofs/handlers/PresentationAckHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationAckHandler.ts deleted file mode 100644 index fa7b194df6..0000000000 --- a/packages/core/src/modules/proofs/handlers/PresentationAckHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { ProofService } from '../services' - -import { PresentationAckMessage } from '../messages' - -export class PresentationAckHandler implements Handler { - private proofService: ProofService - public supportedMessages = [PresentationAckMessage] - - public constructor(proofService: ProofService) { - this.proofService = proofService - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processAck(messageContext) - } -} diff --git a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationHandler.ts deleted file mode 100644 index c00fa139c7..0000000000 --- a/packages/core/src/modules/proofs/handlers/PresentationHandler.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' -import type { ProofRecord } from '../repository' -import type { ProofService } from '../services' - -import { createOutboundMessage, createOutboundServiceMessage } from '../../../agent/helpers' -import { PresentationMessage } from '../messages' - -export class PresentationHandler implements Handler { - private proofService: ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - public supportedMessages = [PresentationMessage] - - public constructor( - proofService: ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - } - - public async handle(messageContext: HandlerInboundMessage) { - const proofRecord = await this.proofService.processPresentation(messageContext) - - if (this.proofResponseCoordinator.shouldAutoRespondToPresentation(proofRecord)) { - return await this.createAck(proofRecord, messageContext) - } - } - - private async createAck(record: ProofRecord, messageContext: HandlerInboundMessage) { - this.agentConfig.logger.info( - `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) - - const { message, proofRecord } = await this.proofService.createAck(record) - - if (messageContext.connection) { - return createOutboundMessage(messageContext.connection, message) - } else if (proofRecord.requestMessage?.service && proofRecord.presentationMessage?.service) { - const recipientService = proofRecord.presentationMessage?.service - const ourService = proofRecord.requestMessage?.service - - return createOutboundServiceMessage({ - payload: message, - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }) - } - - this.agentConfig.logger.error(`Could not automatically create presentation ack`) - } -} diff --git a/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts deleted file mode 100644 index 925941e3a4..0000000000 --- a/packages/core/src/modules/proofs/handlers/PresentationProblemReportHandler.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { ProofService } from '../services' - -import { PresentationProblemReportMessage } from '../messages' - -export class PresentationProblemReportHandler implements Handler { - private proofService: ProofService - public supportedMessages = [PresentationProblemReportMessage] - - public constructor(proofService: ProofService) { - this.proofService = proofService - } - - public async handle(messageContext: HandlerInboundMessage) { - await this.proofService.processProblemReport(messageContext) - } -} diff --git a/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts b/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts deleted file mode 100644 index de29cc2e1d..0000000000 --- a/packages/core/src/modules/proofs/handlers/ProposePresentationHandler.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' -import type { ProofRecord } from '../repository' -import type { ProofService } from '../services' - -import { createOutboundMessage } from '../../../agent/helpers' -import { ProposePresentationMessage } from '../messages' - -export class ProposePresentationHandler implements Handler { - private proofService: ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - public supportedMessages = [ProposePresentationMessage] - - public constructor( - proofService: ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - } - - public async handle(messageContext: HandlerInboundMessage) { - const proofRecord = await this.proofService.processProposal(messageContext) - - if (this.proofResponseCoordinator.shouldAutoRespondToProposal(proofRecord)) { - return await this.createRequest(proofRecord, messageContext) - } - } - - private async createRequest( - proofRecord: ProofRecord, - messageContext: HandlerInboundMessage - ) { - this.agentConfig.logger.info( - `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) - - if (!messageContext.connection) { - this.agentConfig.logger.error('No connection on the messageContext') - return - } - if (!proofRecord.proposalMessage) { - this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) - return - } - const proofRequest = await this.proofService.createProofRequestFromProposal( - proofRecord.proposalMessage.presentationProposal, - { - name: 'proof-request', - version: '1.0', - } - ) - - const { message } = await this.proofService.createRequestAsResponse(proofRecord, proofRequest) - - return createOutboundMessage(messageContext.connection, message) - } -} diff --git a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts b/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts deleted file mode 100644 index b2df52c6d4..0000000000 --- a/packages/core/src/modules/proofs/handlers/RequestPresentationHandler.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { AgentConfig } from '../../../agent/AgentConfig' -import type { Handler, HandlerInboundMessage } from '../../../agent/Handler' -import type { MediationRecipientService } from '../../routing' -import type { ProofResponseCoordinator } from '../ProofResponseCoordinator' -import type { ProofRecord } from '../repository' -import type { ProofService } from '../services' - -import { createOutboundMessage, createOutboundServiceMessage } from '../../../agent/helpers' -import { ServiceDecorator } from '../../../decorators/service/ServiceDecorator' -import { RequestPresentationMessage } from '../messages' - -export class RequestPresentationHandler implements Handler { - private proofService: ProofService - private agentConfig: AgentConfig - private proofResponseCoordinator: ProofResponseCoordinator - private mediationRecipientService: MediationRecipientService - public supportedMessages = [RequestPresentationMessage] - - public constructor( - proofService: ProofService, - agentConfig: AgentConfig, - proofResponseCoordinator: ProofResponseCoordinator, - mediationRecipientService: MediationRecipientService - ) { - this.proofService = proofService - this.agentConfig = agentConfig - this.proofResponseCoordinator = proofResponseCoordinator - this.mediationRecipientService = mediationRecipientService - } - - public async handle(messageContext: HandlerInboundMessage) { - const proofRecord = await this.proofService.processRequest(messageContext) - - if (this.proofResponseCoordinator.shouldAutoRespondToRequest(proofRecord)) { - return await this.createPresentation(proofRecord, messageContext) - } - } - - private async createPresentation( - record: ProofRecord, - messageContext: HandlerInboundMessage - ) { - const indyProofRequest = record.requestMessage?.indyProofRequest - const presentationProposal = record.proposalMessage?.presentationProposal - - this.agentConfig.logger.info( - `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` - ) - - if (!indyProofRequest) { - this.agentConfig.logger.error('Proof request is undefined.') - return - } - - const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest(indyProofRequest, { - presentationProposal, - }) - - const requestedCredentials = this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) - - const { message, proofRecord } = await this.proofService.createPresentation(record, requestedCredentials) - - if (messageContext.connection) { - return createOutboundMessage(messageContext.connection, message) - } else if (proofRecord.requestMessage?.service) { - // Create ~service decorator - const routing = await this.mediationRecipientService.getRouting() - const ourService = new ServiceDecorator({ - serviceEndpoint: routing.endpoints[0], - recipientKeys: [routing.verkey], - routingKeys: routing.routingKeys, - }) - - const recipientService = proofRecord.requestMessage.service - - // Set and save ~service decorator to record (to remember our verkey) - message.service = ourService - proofRecord.presentationMessage = message - await this.proofService.update(proofRecord) - - return createOutboundServiceMessage({ - payload: message, - service: recipientService.resolvedDidCommService, - senderKey: ourService.resolvedDidCommService.recipientKeys[0], - }) - } - - this.agentConfig.logger.error(`Could not automatically create presentation`) - } -} diff --git a/packages/core/src/modules/proofs/handlers/index.ts b/packages/core/src/modules/proofs/handlers/index.ts deleted file mode 100644 index ba30911942..0000000000 --- a/packages/core/src/modules/proofs/handlers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './PresentationAckHandler' -export * from './PresentationHandler' -export * from './ProposePresentationHandler' -export * from './RequestPresentationHandler' -export * from './PresentationProblemReportHandler' diff --git a/packages/core/src/modules/proofs/index.ts b/packages/core/src/modules/proofs/index.ts index a4e5d95714..aec23b9a42 100644 --- a/packages/core/src/modules/proofs/index.ts +++ b/packages/core/src/modules/proofs/index.ts @@ -1,8 +1,12 @@ -export * from './messages' -export * from './models' -export * from './services' -export * from './ProofState' +export * from './protocol/v1/messages' +export * from './protocol/v1/models' +export * from './protocol/v2/messages' +export * from './ProofService' +export * from './models/ProofState' export * from './repository' export * from './ProofEvents' export * from './ProofsModule' -export * from './ProofAutoAcceptType' +export * from './models/ProofAutoAcceptType' +export * from './models/ProofProtocolVersion' +export * from './formats/indy/models/ProofRequest' +export * from './ProofsUtil' diff --git a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts b/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts index 12d405f6dc..64e60f56b2 100644 --- a/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts +++ b/packages/core/src/modules/proofs/messages/PresentationAckMessage.ts @@ -1,19 +1,10 @@ +import type { ProtocolVersion } from '../../../types' import type { AckMessageOptions } from '../../common' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' -import { AckMessage } from '../../common' - export type PresentationAckMessageOptions = AckMessageOptions -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks - */ -export class PresentationAckMessage extends AckMessage { - public constructor(options: PresentationAckMessageOptions) { - super(options) - } +type PresentationAckMessageType = `https://didcomm.org/present-proof/${ProtocolVersion}/ack` - @IsValidMessageType(PresentationAckMessage.type) - public readonly type = PresentationAckMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/ack') +export interface PresentationAckMessage { + type: PresentationAckMessageType } diff --git a/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts deleted file mode 100644 index 2d62a6e2b9..0000000000 --- a/packages/core/src/modules/proofs/messages/PresentationProblemReportMessage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ProblemReportMessageOptions } from '../../problem-reports/messages/ProblemReportMessage' - -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' -import { ProblemReportMessage } from '../../problem-reports/messages/ProblemReportMessage' - -export type PresentationProblemReportMessageOptions = ProblemReportMessageOptions - -/** - * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md - */ -export class PresentationProblemReportMessage extends ProblemReportMessage { - /** - * Create new PresentationProblemReportMessage instance. - * @param options - */ - public constructor(options: PresentationProblemReportMessageOptions) { - super(options) - } - - @IsValidMessageType(PresentationProblemReportMessage.type) - public readonly type = PresentationProblemReportMessage.type.messageTypeUri - public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/problem-report') -} diff --git a/packages/core/src/modules/proofs/messages/index.ts b/packages/core/src/modules/proofs/messages/index.ts deleted file mode 100644 index f2ad906c75..0000000000 --- a/packages/core/src/modules/proofs/messages/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './ProposePresentationMessage' -export * from './RequestPresentationMessage' -export * from './PresentationMessage' -export * from './PresentationPreview' -export * from './PresentationAckMessage' -export * from './PresentationProblemReportMessage' diff --git a/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts b/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts new file mode 100644 index 0000000000..9041bbabe3 --- /dev/null +++ b/packages/core/src/modules/proofs/models/GetRequestedCredentialsConfig.ts @@ -0,0 +1,19 @@ +export interface GetRequestedCredentialsConfig { + /** + * Whether to filter the retrieved credentials using the presentation preview. + * This configuration will only have effect if a presentation proposal message is available + * containing a presentation preview. + * + * @default false + */ + filterByPresentationPreview?: boolean + + /** + * Whether to filter the retrieved credentials using the non-revocation request in the proof request. + * This configuration will only have effect if the proof request requires proof on non-revocation of any kind. + * Default to true + * + * @default true + */ + filterByNonRevocationRequirements?: boolean +} diff --git a/packages/core/src/modules/proofs/models/ModuleOptions.ts b/packages/core/src/modules/proofs/models/ModuleOptions.ts new file mode 100644 index 0000000000..540acd59ae --- /dev/null +++ b/packages/core/src/modules/proofs/models/ModuleOptions.ts @@ -0,0 +1,59 @@ +import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' +import type { AutoAcceptProof } from './ProofAutoAcceptType' +import type { ProofProtocolVersion } from './ProofProtocolVersion' +import type { CreatePresentationFormats, ProposeProofFormats, RequestProofFormats } from './SharedOptions' + +export interface ProofConfig { + name: string + version: string +} + +export interface ProposeProofOptions { + connectionId: string + protocolVersion: ProofProtocolVersion + proofFormats: ProposeProofFormats + comment?: string + goalCode?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface NegotiateRequestOptions { + proofRecordId: string + proofFormats: ProposeProofFormats + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface AcceptProposalOptions { + proofRecordId: string + config?: ProofConfig + goalCode?: string + willConfirm?: boolean + comment?: string +} + +export interface RequestProofOptions { + protocolVersion: ProofProtocolVersion + connectionId: string + proofFormats: RequestProofFormats + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface OutOfBandRequestOptions { + protocolVersion: ProofProtocolVersion + proofFormats: RequestProofFormats + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface AcceptPresentationOptions { + proofRecordId: string + comment?: string + proofFormats: CreatePresentationFormats +} + +export interface AutoSelectCredentialsForProofRequestOptions { + proofRecordId: string + config?: GetRequestedCredentialsConfig +} diff --git a/packages/core/src/modules/proofs/ProofAutoAcceptType.ts b/packages/core/src/modules/proofs/models/ProofAutoAcceptType.ts similarity index 100% rename from packages/core/src/modules/proofs/ProofAutoAcceptType.ts rename to packages/core/src/modules/proofs/models/ProofAutoAcceptType.ts diff --git a/packages/core/src/modules/proofs/models/ProofProtocolVersion.ts b/packages/core/src/modules/proofs/models/ProofProtocolVersion.ts new file mode 100644 index 0000000000..6027d21111 --- /dev/null +++ b/packages/core/src/modules/proofs/models/ProofProtocolVersion.ts @@ -0,0 +1,4 @@ +export enum ProofProtocolVersion { + V1 = 'v1', + V2 = 'v2', +} diff --git a/packages/core/src/modules/proofs/models/ProofServiceOptions.ts b/packages/core/src/modules/proofs/models/ProofServiceOptions.ts new file mode 100644 index 0000000000..54eb76fd2a --- /dev/null +++ b/packages/core/src/modules/proofs/models/ProofServiceOptions.ts @@ -0,0 +1,86 @@ +import type { ConnectionRecord } from '../../connections' +import type { ProofRequest } from '../formats/indy/models/ProofRequest' +import type { PresentationPreview } from '../protocol/v1/models/V1PresentationPreview' +import type { ProofRecord } from '../repository' +import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' +import type { AutoAcceptProof } from './ProofAutoAcceptType' +import type { ProofProtocolVersion } from './ProofProtocolVersion' +import type { CreatePresentationFormats, ProposeProofFormats, RequestProofFormats } from './SharedOptions' + +export interface CreateProposalOptions { + connectionRecord: ConnectionRecord + protocolVersion: ProofProtocolVersion + proofFormats: ProposeProofFormats + willConfirm?: boolean + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface CreateProposalAsResponseOptions { + proofRecord: ProofRecord + proofFormats: ProposeProofFormats + willConfirm?: boolean + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +// ----- Out Of Band Proof ----- // +export interface CreateOutOfBandRequestOptions { + protocolVersion: ProofProtocolVersion + proofFormats: ProposeProofFormats + willConfirm?: boolean + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface CreateRequestOptions { + connectionRecord?: ConnectionRecord + protocolVersion: ProofProtocolVersion + proofFormats: ProposeProofFormats + willConfirm?: boolean + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface CreateRequestAsResponseOptions { + proofRecord: ProofRecord + proofFormats: RequestProofFormats + willConfirm?: boolean + goalCode?: string + comment?: string + autoAcceptProof?: AutoAcceptProof +} + +export interface CreatePresentationOptions { + proofRecord: ProofRecord + proofFormats: CreatePresentationFormats + lastPresentation?: boolean + goalCode?: string + comment?: string + protocolVersion: ProofProtocolVersion + willConfirm?: boolean +} + +export interface CreateAckOptions { + proofRecord: ProofRecord +} + +export interface RequestedCredentialForProofRequestOptions { + proofRequest: ProofRequest + presentationProposal?: PresentationPreview +} +export interface GetRequestedCredentialsForProofRequestOptions { + proofRecord: ProofRecord + config?: GetRequestedCredentialsConfig +} + +export interface ProofRequestFromProposalOptions { + name: string + version: string + nonce: string + proofRecord: ProofRecord +} diff --git a/packages/core/src/modules/proofs/ProofState.ts b/packages/core/src/modules/proofs/models/ProofState.ts similarity index 94% rename from packages/core/src/modules/proofs/ProofState.ts rename to packages/core/src/modules/proofs/models/ProofState.ts index 73869e80aa..e10b5d1ff8 100644 --- a/packages/core/src/modules/proofs/ProofState.ts +++ b/packages/core/src/modules/proofs/models/ProofState.ts @@ -11,5 +11,6 @@ export enum ProofState { PresentationSent = 'presentation-sent', PresentationReceived = 'presentation-received', Declined = 'declined', + Abandoned = 'abandoned', Done = 'done', } diff --git a/packages/core/src/modules/proofs/models/SharedOptions.ts b/packages/core/src/modules/proofs/models/SharedOptions.ts new file mode 100644 index 0000000000..05028c5926 --- /dev/null +++ b/packages/core/src/modules/proofs/models/SharedOptions.ts @@ -0,0 +1,71 @@ +import type { + IndyProposeProofFormat, + IndyRequestProofFormat, + IndyVerifyProofFormat, +} from '../formats/IndyProofFormatsServiceOptions' +import type { ProofRequest } from '../formats/indy/models/ProofRequest' +import type { RequestedCredentials, IndyRequestedCredentialsOptions } from '../formats/indy/models/RequestedCredentials' +import type { RetrievedCredentials } from '../formats/indy/models/RetrievedCredentials' +import type { GetRequestedCredentialsConfig } from './GetRequestedCredentialsConfig' + +export interface ProposeProofFormats { + // If you want to propose an indy proof without attributes or + // any of the other properties you should pass an empty object + indy?: IndyProposeProofFormat + presentationExchange?: never +} + +export interface RequestProofFormats { + // If you want to propose an indy proof without attributes or + // any of the other properties you should pass an empty object + indy?: IndyRequestProofFormat + presentationExchange?: never +} + +export interface CreatePresentationFormats { + // If you want to propose an indy proof without attributes or + // any of the other properties you should pass an empty object + indy?: IndyRequestedCredentialsOptions + presentationExchange?: never +} + +export interface AcceptProposalFormats { + // If you want to propose an indy proof without attributes or + // any of the other properties you should pass an empty object + indy?: IndyAcceptProposalOptions + presentationExchange?: never +} + +export interface VerifyProofFormats { + indy?: IndyVerifyProofFormat + presentationExchange?: never +} + +export interface RequestedCredentialConfigOptions { + indy?: GetRequestedCredentialsConfig + jsonLd?: never +} + +export interface RetrievedCredentialOptions { + indy?: RetrievedCredentials | undefined + presentationExchange?: undefined +} + +export interface ProofRequestFormats { + indy?: ProofRequest | undefined + jsonLd?: undefined +} + +export interface RequestedCredentialsFormats { + indy?: RequestedCredentials | undefined + presentationExchange?: undefined +} + +interface IndyAcceptProposalOptions { + request: ProofRequest +} + +export interface AutoSelectCredentialOptions { + indy?: RetrievedCredentials | undefined + jsonLd?: undefined +} diff --git a/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts b/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts new file mode 100644 index 0000000000..01ff0349f9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/V1ProofService.ts @@ -0,0 +1,933 @@ +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { Dispatcher } from '../../../../agent/Dispatcher' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { Attachment } from '../../../../decorators/attachment/Attachment' +import type { MediationRecipientService } from '../../../routing/services/MediationRecipientService' +import type { ProofStateChangedEvent } from '../../ProofEvents' +import type { ProofResponseCoordinator } from '../../ProofResponseCoordinator' +import type { IndyProposeProofFormat } from '../../formats/IndyProofFormatsServiceOptions' +import type { CreateProblemReportOptions } from '../../formats/models/ProofFormatServiceOptions' +import type { + CreateAckOptions, + CreatePresentationOptions, + CreateProposalAsResponseOptions, + CreateProposalOptions, + CreateRequestAsResponseOptions, + CreateRequestOptions, + GetRequestedCredentialsForProofRequestOptions, + ProofRequestFromProposalOptions, +} from '../../models/ProofServiceOptions' +import type { + RetrievedCredentialOptions, + ProofRequestFormats, + RequestedCredentialsFormats, +} from '../../models/SharedOptions' +import type { ProofAttributeInfo } from './models' + +import { validateOrReject } from 'class-validator' +import { inject, Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../../constants' +import { AriesFrameworkError } from '../../../../error/AriesFrameworkError' +import { DidCommMessageRole } from '../../../../storage' +import { DidCommMessageRepository } from '../../../../storage/didcomm/DidCommMessageRepository' +import { checkProofRequestForDuplicates } from '../../../../utils' +import { JsonTransformer } from '../../../../utils/JsonTransformer' +import { Wallet } from '../../../../wallet' +import { AckStatus } from '../../../common/messages/AckMessage' +import { ConnectionService } from '../../../connections' +import { CredentialRepository } from '../../../credentials' +import { IndyCredentialInfo } from '../../../credentials/protocol/v1/models' +import { IndyHolderService, IndyRevocationService } from '../../../indy' +import { IndyLedgerService } from '../../../ledger/services/IndyLedgerService' +import { ProofEventTypes } from '../../ProofEvents' +import { ProofService } from '../../ProofService' +import { ProofsUtils } from '../../ProofsUtil' +import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' +import { IndyProofFormatService } from '../../formats/indy/IndyProofFormatService' +import { ProofRequest } from '../../formats/indy/models/ProofRequest' +import { RequestedCredentials } from '../../formats/indy/models/RequestedCredentials' +import { ProofProtocolVersion } from '../../models/ProofProtocolVersion' +import { ProofState } from '../../models/ProofState' +import { ProofRecord } from '../../repository/ProofRecord' +import { ProofRepository } from '../../repository/ProofRepository' + +import { V1PresentationProblemReportError } from './errors' +import { + V1PresentationAckHandler, + V1PresentationHandler, + V1PresentationProblemReportHandler, + V1ProposePresentationHandler, + V1RequestPresentationHandler, +} from './handlers' +import { + INDY_PROOF_ATTACHMENT_ID, + INDY_PROOF_REQUEST_ATTACHMENT_ID, + V1PresentationAckMessage, + V1PresentationMessage, + V1ProposePresentationMessage, + V1RequestPresentationMessage, +} from './messages' +import { V1PresentationProblemReportMessage } from './messages/V1PresentationProblemReportMessage' +import { PresentationPreview } from './models/V1PresentationPreview' + +/** + * @todo add method to check if request matches proposal. Useful to see if a request I received is the same as the proposal I sent. + * @todo add method to reject / revoke messages + * @todo validate attachments / messages + */ +@scoped(Lifecycle.ContainerScoped) +export class V1ProofService extends ProofService { + private credentialRepository: CredentialRepository + private ledgerService: IndyLedgerService + private indyProofFormatService: IndyProofFormatService + private indyHolderService: IndyHolderService + private indyRevocationService: IndyRevocationService + + public constructor( + proofRepository: ProofRepository, + didCommMessageRepository: DidCommMessageRepository, + ledgerService: IndyLedgerService, + @inject(InjectionSymbols.Wallet) wallet: Wallet, + agentConfig: AgentConfig, + connectionService: ConnectionService, + eventEmitter: EventEmitter, + credentialRepository: CredentialRepository, + indyProofFormatService: IndyProofFormatService, + indyHolderService: IndyHolderService, + indyRevocationService: IndyRevocationService + ) { + super(agentConfig, proofRepository, connectionService, didCommMessageRepository, wallet, eventEmitter) + this.credentialRepository = credentialRepository + this.ledgerService = ledgerService + this.wallet = wallet + this.indyProofFormatService = indyProofFormatService + this.indyHolderService = indyHolderService + this.indyRevocationService = indyRevocationService + } + + public getVersion(): ProofProtocolVersion { + return ProofProtocolVersion.V1 + } + + public async createProposal( + options: CreateProposalOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const { connectionRecord, proofFormats } = options + + // Assert + connectionRecord.assertReady() + + const presentationProposal = new PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + + // Create message + const proposalMessage = new V1ProposePresentationMessage({ + comment: options?.comment, + presentationProposal, + }) + + // Create record + const proofRecord = new ProofRecord({ + connectionId: connectionRecord.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalSent, + autoAcceptProof: options?.autoAcceptProof, + protocolVersion: ProofProtocolVersion.V1, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + + return { proofRecord, message: proposalMessage } + } + + public async createProposalAsResponse( + options: CreateProposalAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const { proofRecord, proofFormats, comment } = options + + // Assert + proofRecord.assertState(ProofState.RequestReceived) + + // Create message + const presentationPreview = new PresentationPreview({ + attributes: proofFormats.indy?.attributes, + predicates: proofFormats.indy?.predicates, + }) + let proposalMessage: V1ProposePresentationMessage + if (presentationPreview) { + proposalMessage = new V1ProposePresentationMessage({ + comment, + presentationProposal: presentationPreview, + }) + } else { + throw new AriesFrameworkError('Missing presentation preview.') + } + + proposalMessage.setThread({ threadId: proofRecord.threadId }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + void this.updateState(proofRecord, ProofState.ProposalSent) + + return { proofRecord, message: proposalMessage } + } + + public async processProposal(messageContext: InboundMessageContext): Promise { + let proofRecord: ProofRecord + const { message: proposalMessage, connection } = messageContext + + this.logger.debug(`Processing presentation proposal with id ${proposalMessage.id}`) + + try { + // Proof record already exists + proofRecord = await this.getByThreadAndConnectionId(proposalMessage.threadId, connection?.id) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage, + previousSentMessage: requestMessage ?? undefined, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + await this.updateState(proofRecord, ProofState.ProposalReceived) + } catch { + // No proof record exists with thread id + proofRecord = new ProofRecord({ + connectionId: connection?.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save record + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { + proofRecord, + previousState: null, + }, + }) + } + + return proofRecord + } + + public async createRequestAsResponse( + options: CreateRequestAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const { proofRecord, comment } = options + + // Assert + proofRecord.assertState(ProofState.ProposalReceived) + + // Create message + const { attachment } = await this.indyProofFormatService.createRequest({ + id: INDY_PROOF_REQUEST_ATTACHMENT_ID, + formats: options.proofFormats, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment, + requestPresentationAttachments: [attachment], + }) + requestPresentationMessage.setThread({ + threadId: proofRecord.threadId, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + await this.updateState(proofRecord, ProofState.RequestSent) + + return { message: requestPresentationMessage, proofRecord } + } + + public async createRequest( + options: CreateRequestOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + this.logger.debug(`Creating proof request`) + + // Assert + options.connectionRecord?.assertReady() + + // Create message + const { attachment } = await this.indyProofFormatService.createRequest({ + id: INDY_PROOF_REQUEST_ATTACHMENT_ID, + formats: options.proofFormats, + }) + + const requestPresentationMessage = new V1RequestPresentationMessage({ + comment: options?.comment, + requestPresentationAttachments: [attachment], + }) + + // Create record + const proofRecord = new ProofRecord({ + connectionId: options.connectionRecord?.id, + threadId: requestPresentationMessage.threadId, + state: ProofState.RequestSent, + autoAcceptProof: options?.autoAcceptProof, + protocolVersion: ProofProtocolVersion.V1, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: requestPresentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + + return { message: requestPresentationMessage, proofRecord } + } + + public async processRequest(messageContext: InboundMessageContext): Promise { + let proofRecord: ProofRecord + const { message: proofRequestMsg, connection } = messageContext + + const proofRequestMessage = proofRequestMsg as V1RequestPresentationMessage + + this.logger.debug(`Processing presentation request with id ${proofRequestMessage.id}`) + + const proofRequest = proofRequestMessage.indyProofRequest + + // Assert attachment + if (!proofRequest) { + throw new V1PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + await validateOrReject(proofRequest) + + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(proofRequest) + + this.logger.debug('received proof request', proofRequest) + + try { + // Proof record already exists + proofRecord = await this.getByThreadAndConnectionId(proofRequestMessage.threadId, connection?.id) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.ProposalSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestMessage ?? undefined, + previousSentMessage: proposalMessage ?? undefined, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + await this.updateState(proofRecord, ProofState.RequestReceived) + } catch { + // No proof record exists with thread id + proofRecord = new ProofRecord({ + connectionId: connection?.id, + threadId: proofRequestMessage.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save in repository + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + } + + return proofRecord + } + + public async createPresentation( + options: CreatePresentationOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const { proofRecord, proofFormats } = options + + this.logger.debug(`Creating presentation for proof record with id ${proofRecord.id}`) + + // Assert + proofRecord.assertState(ProofState.RequestReceived) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const requestAttachment = requestMessage?.indyAttachment + + if (!requestAttachment) { + throw new V1PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation with thread id ${proofRecord.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + + const proof = await this.indyProofFormatService.createPresentation({ + id: INDY_PROOF_ATTACHMENT_ID, + attachment: requestAttachment, + formats: { + indy: { + requestedAttributes: proofFormats.indy?.requestedAttributes, + requestedPredicates: proofFormats.indy?.requestedPredicates, + selfAttestedAttributes: proofFormats.indy?.selfAttestedAttributes, + }, + }, + }) + + // Extract proof request from attachment + const proofRequestJson = requestAttachment.getDataAsJson() ?? null + const proofRequest = JsonTransformer.fromJSON(proofRequestJson, ProofRequest) + + const requestedCredentials = new RequestedCredentials({ + requestedAttributes: proofFormats.indy?.requestedAttributes, + requestedPredicates: proofFormats.indy?.requestedPredicates, + selfAttestedAttributes: proofFormats.indy?.selfAttestedAttributes, + }) + + // Get the matching attachments to the requested credentials + const linkedAttachments = await this.getRequestedAttachmentsForRequestedCredentials( + proofRequest, + requestedCredentials + ) + + const presentationMessage = new V1PresentationMessage({ + comment: options?.comment, + presentationAttachments: [proof.attachment], + attachments: linkedAttachments, + }) + presentationMessage.setThread({ threadId: proofRecord.threadId }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: presentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + // Update record + await this.updateState(proofRecord, ProofState.PresentationSent) + + return { message: presentationMessage, proofRecord } + } + + public async processPresentation(messageContext: InboundMessageContext): Promise { + const { message: presentationMsg, connection } = messageContext + + const presentationMessage = presentationMsg as V1PresentationMessage + this.logger.debug(`Processing presentation with id ${presentationMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId(presentationMessage.threadId, connection?.id) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const requestMessage = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage ?? undefined, + previousSentMessage: requestMessage ?? undefined, + }) + + try { + const isValid = await this.indyProofFormatService.processPresentation({ + record: proofRecord, + presentation: { + proof: presentationMessage.getAttachmentFormats(), + request: requestMessage.getAttachmentFormats(), + }, + }) + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: presentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + proofRecord.isVerified = isValid + await this.updateState(proofRecord, ProofState.PresentationReceived) + } catch (e) { + if (e instanceof AriesFrameworkError) { + throw new V1PresentationProblemReportError(e.message, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + throw e + } + + return proofRecord + } + + public async processAck(messageContext: InboundMessageContext): Promise { + const { message: presentationAckMessage, connection } = messageContext + + this.logger.debug(`Processing presentation ack with id ${presentationAckMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId(presentationAckMessage.threadId, connection?.id) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const presentationMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1PresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.PresentationSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestMessage ?? undefined, + previousSentMessage: presentationMessage ?? undefined, + }) + + // Update record + await this.updateState(proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + options: CreateProblemReportOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const msg = new V1PresentationProblemReportMessage({ + description: { + code: PresentationProblemReportReason.Abandoned, + en: options.description, + }, + }) + + msg.setThread({ + threadId: options.proofRecord.threadId, + }) + + return { + proofRecord: options.proofRecord, + message: msg, + } + } + + public async processProblemReport(messageContext: InboundMessageContext): Promise { + const { message: presentationProblemReportMsg } = messageContext + + const presentationProblemReportMessage = presentationProblemReportMsg as V1PresentationProblemReportMessage + const connection = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) + + const proofRecord = await this.getByThreadAndConnectionId(presentationProblemReportMessage.threadId, connection?.id) + + proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` + await this.updateState(proofRecord, ProofState.Abandoned) + return proofRecord + } + + public async generateProofRequestNonce() { + return this.wallet.generateNonce() + } + + public async createProofRequestFromProposal(options: ProofRequestFromProposalOptions): Promise { + const proofRecordId = options.proofRecord.id + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V1ProposePresentationMessage, + }) + + if (!proposalMessage) { + throw new AriesFrameworkError(`Proof record with id ${proofRecordId} is missing required presentation proposal`) + } + + const indyProposeProofFormat: IndyProposeProofFormat = { + name: options?.name ?? 'Proof Request', + version: options?.version ?? '1.0', + nonce: options.nonce ?? (await this.generateProofRequestNonce()), + } + + const proofRequest = ProofsUtils.createReferentForProofRequest( + indyProposeProofFormat, + proposalMessage.presentationProposal + ) + + return { + indy: proofRequest, + } + } + + /** + * Retrieves the linked attachments for an {@link indyProofRequest} + * @param indyProofRequest The proof request for which the linked attachments have to be found + * @param requestedCredentials The requested credentials + * @returns a list of attachments that are linked to the requested credentials + */ + public async getRequestedAttachmentsForRequestedCredentials( + indyProofRequest: ProofRequest, + requestedCredentials: RequestedCredentials + ): Promise { + const attachments: Attachment[] = [] + const credentialIds = new Set() + const requestedAttributesNames: (string | undefined)[] = [] + + // Get the credentialIds if it contains a hashlink + for (const [referent, requestedAttribute] of Object.entries(requestedCredentials.requestedAttributes)) { + // Find the requested Attributes + const requestedAttributes = indyProofRequest.requestedAttributes.get(referent) as ProofAttributeInfo + + // List the requested attributes + requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) + + //Get credentialInfo + if (!requestedAttribute.credentialInfo) { + const indyCredentialInfo = await this.indyHolderService.getCredential(requestedAttribute.credentialId) + requestedAttribute.credentialInfo = JsonTransformer.fromJSON(indyCredentialInfo, IndyCredentialInfo) + } + + // Find the attributes that have a hashlink as a value + for (const attribute of Object.values(requestedAttribute.credentialInfo.attributes)) { + if (attribute.toLowerCase().startsWith('hl:')) { + credentialIds.add(requestedAttribute.credentialId) + } + } + } + + // Only continues if there is an attribute value that contains a hashlink + for (const credentialId of credentialIds) { + // Get the credentialRecord that matches the ID + + const credentialRecord = await this.credentialRepository.getSingleByQuery({ credentialIds: [credentialId] }) + + if (credentialRecord.linkedAttachments) { + // Get the credentials that have a hashlink as value and are requested + const requestedCredentials = credentialRecord.credentialAttributes?.filter( + (credential) => + credential.value.toLowerCase().startsWith('hl:') && requestedAttributesNames.includes(credential.name) + ) + + // Get the linked attachments that match the requestedCredentials + const linkedAttachments = credentialRecord.linkedAttachments.filter((attachment) => + requestedCredentials?.map((credential) => credential.value.split(':')[1]).includes(attachment.id) + ) + + if (linkedAttachments) { + attachments.push(...linkedAttachments) + } + } + } + + return attachments.length ? attachments : undefined + } + + public async shouldAutoRespondToRequest(proofRecord: ProofRecord): Promise { + const proposal = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + if (!proposal) { + return false + } + + const request = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + if (!request) { + throw new AriesFrameworkError(`Expected to find a request message for ProofRecord with id ${proofRecord.id}`) + } + + const proofRequest = request.indyProofRequest + + // Assert attachment + if (!proofRequest) { + throw new V1PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${request.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + await validateOrReject(proofRequest) + + // Assert attribute and predicate (group) names do not match + checkProofRequestForDuplicates(proofRequest) + + const proposalAttributes = proposal.presentationProposal.attributes + const requestedAttributes = proofRequest.requestedAttributes + + const proposedAttributeNames = proposalAttributes.map((x) => x.name) + let requestedAttributeNames: string[] = [] + + const requestedAttributeList = Array.from(requestedAttributes.values()) + + requestedAttributeList.forEach((x) => { + if (x.name) { + requestedAttributeNames.push(x.name) + } else if (x.names) { + requestedAttributeNames = requestedAttributeNames.concat(x.names) + } + }) + + if (requestedAttributeNames.length > proposedAttributeNames.length) { + // more attributes are requested than have been proposed + return false + } + + requestedAttributeNames.forEach((x) => { + if (!proposedAttributeNames.includes(x)) { + this.logger.debug(`Attribute ${x} was requested but wasn't proposed.`) + return false + } + }) + + // assert that all requested attributes are provided + const providedPredicateNames = proposal.presentationProposal.predicates.map((x) => x.name) + proofRequest.requestedPredicates.forEach((x) => { + if (!providedPredicateNames.includes(x.name)) { + return false + } + }) + + return true + } + + public async shouldAutoRespondToPresentation(proofRecord: ProofRecord): Promise { + this.logger.debug(`Should auto respond to presentation for proof record id: ${proofRecord.id}`) + return true + } + + public async getRequestedCredentialsForProofRequest( + options: GetRequestedCredentialsForProofRequestOptions + ): Promise { + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + const indyProofRequest = requestMessage?.requestPresentationAttachments + + if (!indyProofRequest) { + throw new AriesFrameworkError('Could not find proof request') + } + + return await this.indyProofFormatService.getRequestedCredentialsForProofRequest({ + attachment: indyProofRequest[0], + presentationProposal: proposalMessage?.presentationProposal, + config: options.config ?? undefined, + }) + } + + public async autoSelectCredentialsForProofRequest( + options: RetrievedCredentialOptions + ): Promise { + return await this.indyProofFormatService.autoSelectCredentialsForProofRequest(options) + } + + public async registerHandlers( + dispatcher: Dispatcher, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + mediationRecipientService: MediationRecipientService + ): Promise { + dispatcher.registerHandler( + new V1ProposePresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) + ) + + dispatcher.registerHandler( + new V1RequestPresentationHandler( + this, + agentConfig, + proofResponseCoordinator, + mediationRecipientService, + this.didCommMessageRepository + ) + ) + + dispatcher.registerHandler( + new V1PresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) + ) + dispatcher.registerHandler(new V1PresentationAckHandler(this)) + dispatcher.registerHandler(new V1PresentationProblemReportHandler(this)) + } + + public async findRequestMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V1RequestPresentationMessage, + }) + } + public async findPresentationMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V1PresentationMessage, + }) + } + + public async findProposalMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V1ProposePresentationMessage, + }) + } + + /** + * Retrieve all proof records + * + * @returns List containing all proof records + */ + public async getAll(): Promise { + return this.proofRepository.getAll() + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @throws {RecordNotFoundError} If no record is found + * @return The proof record + * + */ + public async getById(proofRecordId: string): Promise { + return this.proofRepository.getById(proofRecordId) + } + + /** + * Retrieve a proof record by id + * + * @param proofRecordId The proof record id + * @return The proof record or null if not found + * + */ + public async findById(proofRecordId: string): Promise { + return this.proofRepository.findById(proofRecordId) + } + + /** + * Delete a proof record by id + * + * @param proofId the proof record id + */ + public async deleteById(proofId: string) { + const proofRecord = await this.getById(proofId) + return this.proofRepository.delete(proofRecord) + } + + /** + * Retrieve a proof record by connection id and thread id + * + * @param connectionId The connection id + * @param threadId The thread id + * @throws {RecordNotFoundError} If no record is found + * @throws {RecordDuplicateError} If multiple records are found + * @returns The proof record + */ + public async getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { + return this.proofRepository.getSingleByQuery({ threadId, connectionId }) + } + + public update(proofRecord: ProofRecord) { + return this.proofRepository.update(proofRecord) + } + + public async createAck(options: CreateAckOptions): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const { proofRecord } = options + this.logger.debug(`Creating presentation ack for proof record with id ${proofRecord.id}`) + + // Assert + proofRecord.assertState(ProofState.PresentationReceived) + + // Create message + const ackMessage = new V1PresentationAckMessage({ + status: AckStatus.OK, + threadId: proofRecord.threadId, + }) + + // Update record + await this.updateState(proofRecord, ProofState.Done) + + return { message: ackMessage, proofRecord } + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts new file mode 100644 index 0000000000..f4ca50c809 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-presentation.test.ts @@ -0,0 +1,252 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { + AcceptPresentationOptions, + AcceptProposalOptions, + ProposeProofOptions, +} from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { PresentationPreview } from '../models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage/didcomm' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V1PresentationMessage, V1ProposePresentationMessage, V1RequestPresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V1 propose proof test', + } + + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId: presentationPreview.attributes[0].credentialDefinitionId, + value: 'John', + referent: '0', + }, + { + name: 'image_0', + credentialDefinitionId: presentationPreview.attributes[1].credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId: presentationPreview.predicates[0].credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) + + test(`Faber accepts the Proposal send by Alice`, async () => { + // Accept Proposal + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestPresentationAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(aliceProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) + + test(`Alice accepts presentation request from Faber`, async () => { + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + appendedAttachments: [ + { + id: expect.any(String), + filename: expect.any(String), + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) + + test(`Faber accepts the presentation provided by Alice`, async () => { + // Faber accepts the presentation provided by Alice + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts new file mode 100644 index 0000000000..e701d57ecc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-proposal.test.ts @@ -0,0 +1,108 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { ProposeProofOptions } from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { PresentationPreview } from '../models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V1ProposePresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V1 propose proof test', + } + + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId: presentationPreview.attributes[0].credentialDefinitionId, + value: 'John', + referent: '0', + }, + { + name: 'image_0', + credentialDefinitionId: presentationPreview.attributes[1].credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId: presentationPreview.predicates[0].credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + + expect(faberProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts new file mode 100644 index 0000000000..f7a3b462e8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/__tests__/indy-proof-request.test.ts @@ -0,0 +1,157 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { AcceptProposalOptions, ProposeProofOptions } from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { PresentationPreview } from '../models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage/didcomm' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V1ProposePresentationMessage, V1RequestPresentationMessage } from '../messages' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V1 propose proof test', + } + + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + comment: 'V1 propose proof test', + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId: presentationPreview.attributes[0].credentialDefinitionId, + value: 'John', + referent: '0', + }, + { + name: 'image_0', + credentialDefinitionId: presentationPreview.attributes[1].credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId: presentationPreview.predicates[0].credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) + + test(`Faber accepts the Proposal send by Alice`, async () => { + // Accept Proposal + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestPresentationAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(aliceProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v1/errors/V1PresentationProblemReportError.ts b/packages/core/src/modules/proofs/protocol/v1/errors/V1PresentationProblemReportError.ts new file mode 100644 index 0000000000..27c77c0f82 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/errors/V1PresentationProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' + +import { ProblemReportError } from '../../../../problem-reports' +import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' + +interface V1PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason +} + +export class V1PresentationProblemReportError extends ProblemReportError { + public problemReport: V1PresentationProblemReportMessage + + public constructor(public message: string, { problemCode }: V1PresentationProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V1PresentationProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/errors/index.ts b/packages/core/src/modules/proofs/protocol/v1/errors/index.ts new file mode 100644 index 0000000000..75d23e13a1 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/errors/index.ts @@ -0,0 +1 @@ +export * from './V1PresentationProblemReportError' diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts new file mode 100644 index 0000000000..aa0c050c82 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationAckHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V1ProofService } from '../V1ProofService' + +import { V1PresentationAckMessage } from '../messages' + +export class V1PresentationAckHandler implements Handler { + private proofService: V1ProofService + public supportedMessages = [V1PresentationAckMessage] + + public constructor(proofService: V1ProofService) { + this.proofService = proofService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.proofService.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts new file mode 100644 index 0000000000..9b1d9e21b8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationHandler.ts @@ -0,0 +1,72 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRecord } from '../../../repository' +import type { V1ProofService } from '../V1ProofService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { V1PresentationMessage, V1RequestPresentationMessage } from '../messages' + +export class V1PresentationHandler implements Handler { + private proofService: V1ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1PresentationMessage] + + public constructor( + proofService: V1ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + didCommMessageRepository: DidCommMessageRepository + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processPresentation(messageContext) + + if (this.proofResponseCoordinator.shouldAutoRespondToPresentation(proofRecord)) { + return await this.createAck(proofRecord, messageContext) + } + } + + private async createAck(record: ProofRecord, messageContext: HandlerInboundMessage) { + this.agentConfig.logger.info( + `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + const { message, proofRecord } = await this.proofService.createAck({ + proofRecord: record, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + const presentationMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1PresentationMessage, + }) + + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage?.service && presentationMessage?.service) { + const recipientService = presentationMessage?.service + const ourService = requestMessage?.service + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create presentation ack`) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..da2af78c18 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V1ProofService } from '../V1ProofService' + +import { V1PresentationProblemReportMessage } from '../messages/V1PresentationProblemReportMessage' + +export class V1PresentationProblemReportHandler implements Handler { + private proofService: V1ProofService + public supportedMessages = [V1PresentationProblemReportMessage] + + public constructor(proofService: V1ProofService) { + this.proofService = proofService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.proofService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts new file mode 100644 index 0000000000..722a79bf6f --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1ProposePresentationHandler.ts @@ -0,0 +1,97 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRequestFromProposalOptions } from '../../../models/ProofServiceOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { V1ProofService } from '../V1ProofService' + +import { createOutboundMessage } from '../../../../../agent/helpers' +import { V1ProposePresentationMessage } from '../messages' + +export class V1ProposePresentationHandler implements Handler { + private proofService: V1ProofService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + private proofResponseCoordinator: ProofResponseCoordinator + public supportedMessages = [V1ProposePresentationMessage] + + public constructor( + proofService: V1ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + didCommMessageRepository: DidCommMessageRepository + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processProposal(messageContext) + if (this.proofResponseCoordinator.shouldAutoRespondToProposal(proofRecord)) { + return await this.createRequest(proofRecord, messageContext) + } + } + + private async createRequest( + proofRecord: ProofRecord, + messageContext: HandlerInboundMessage + ) { + this.agentConfig.logger.info( + `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext') + return + } + + const proposalMessage = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + if (!proposalMessage) { + this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) + return + } + + const proofRequestFromProposalOptions: ProofRequestFromProposalOptions = { + name: 'proof-request', + version: '1.0', + nonce: await this.proofService.generateProofRequestNonce(), + proofRecord, + } + + const proofRequest = await this.proofService.createProofRequestFromProposal(proofRequestFromProposalOptions) + + const indyProofRequest = proofRequest.indy + + if (!indyProofRequest) { + this.agentConfig.logger.error(`Indy proof request is missing required proof request`) + return + } + + const { message } = await this.proofService.createRequestAsResponse({ + proofFormats: { + indy: { + name: indyProofRequest.name, + version: indyProofRequest.version, + nonRevoked: indyProofRequest.nonRevoked, + requestedAttributes: indyProofRequest.requestedAttributes, + requestedPredicates: indyProofRequest.requestedPredicates, + ver: indyProofRequest.ver, + proofRequest: indyProofRequest, + nonce: indyProofRequest.nonce, + }, + }, + proofRecord: proofRecord, + autoAcceptProof: proofRecord.autoAcceptProof, + willConfirm: true, + }) + + return createOutboundMessage(messageContext.connection, message) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts new file mode 100644 index 0000000000..0d3d98de55 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/V1RequestPresentationHandler.ts @@ -0,0 +1,120 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' +import type { MediationRecipientService } from '../../../../routing' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { V1ProofService } from '../V1ProofService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' +import { DidCommMessageRole } from '../../../../../storage' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { V1RequestPresentationMessage } from '../messages' + +export class V1RequestPresentationHandler implements Handler { + private proofService: V1ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator + private mediationRecipientService: MediationRecipientService + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V1RequestPresentationMessage] + + public constructor( + proofService: V1ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator + this.mediationRecipientService = mediationRecipientService + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processRequest(messageContext) + if (this.proofResponseCoordinator.shouldAutoRespondToRequest(proofRecord)) { + return await this.createPresentation(proofRecord, messageContext) + } + } + + private async createPresentation( + record: ProofRecord, + messageContext: HandlerInboundMessage + ) { + const requestMessage = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: record.id, + messageClass: V1RequestPresentationMessage, + }) + + const indyProofRequest = requestMessage.indyProofRequest + + this.agentConfig.logger.info( + `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!indyProofRequest) { + this.agentConfig.logger.error('Proof request is undefined.') + return + } + + const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest({ + proofRecord: record, + config: { + filterByPresentationPreview: false, + }, + }) + + if (!retrievedCredentials.indy) { + this.agentConfig.logger.error('No matching Indy credentials could be retrieved.') + return + } + + const requestedCredentials = await this.proofService.autoSelectCredentialsForProofRequest({ + indy: retrievedCredentials.indy, + }) + + const { message, proofRecord } = await this.proofService.createPresentation({ + proofRecord: record, + proofFormats: { + indy: requestedCredentials.indy, + }, + protocolVersion: ProofProtocolVersion.V1, + willConfirm: true, + }) + + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage.service) { + // Create ~service decorator + const routing = await this.mediationRecipientService.getRouting() + const ourService = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + + const recipientService = requestMessage.service + + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create presentation`) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v1/handlers/index.ts b/packages/core/src/modules/proofs/protocol/v1/handlers/index.ts new file mode 100644 index 0000000000..c202042b9b --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/handlers/index.ts @@ -0,0 +1,5 @@ +export * from './V1PresentationAckHandler' +export * from './V1PresentationHandler' +export * from './V1ProposePresentationHandler' +export * from './V1RequestPresentationHandler' +export * from './V1PresentationProblemReportHandler' diff --git a/packages/core/src/modules/proofs/protocol/v1/index.ts b/packages/core/src/modules/proofs/protocol/v1/index.ts new file mode 100644 index 0000000000..1b43254564 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/index.ts @@ -0,0 +1 @@ +export * from './V1ProofService' diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationAckMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationAckMessage.ts new file mode 100644 index 0000000000..9e73ad1ba9 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationAckMessage.ts @@ -0,0 +1,17 @@ +import type { AckMessageOptions } from '../../../../common' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common' + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V1PresentationAckMessage extends AckMessage { + public constructor(options: AckMessageOptions) { + super(options) + } + + @IsValidMessageType(V1PresentationAckMessage.type) + public readonly type = V1PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/ack') +} diff --git a/packages/core/src/modules/proofs/messages/PresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts similarity index 57% rename from packages/core/src/modules/proofs/messages/PresentationMessage.ts rename to packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts index 72d68cbdcc..0cff834083 100644 --- a/packages/core/src/modules/proofs/messages/PresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationMessage.ts @@ -1,11 +1,15 @@ +import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' import type { IndyProof } from 'indy-sdk' import { Expose, Type } from 'class-transformer' import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' -import { AgentMessage } from '../../../agent/AgentMessage' -import { Attachment } from '../../../decorators/attachment/Attachment' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V2_INDY_PRESENTATION } from '../../../formats/ProofFormats' +import { ProofFormatSpec } from '../../../formats/models/ProofFormatSpec' export const INDY_PROOF_ATTACHMENT_ID = 'libindy-presentation-0' @@ -22,7 +26,7 @@ export interface PresentationOptions { * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#presentation */ -export class PresentationMessage extends AgentMessage { +export class V1PresentationMessage extends AgentMessage { public constructor(options: PresentationOptions) { super() @@ -34,8 +38,8 @@ export class PresentationMessage extends AgentMessage { } } - @IsValidMessageType(PresentationMessage.type) - public readonly type = PresentationMessage.type.messageTypeUri + @IsValidMessageType(V1PresentationMessage.type) + public readonly type = V1PresentationMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/presentation') /** @@ -57,8 +61,27 @@ export class PresentationMessage extends AgentMessage { @IsInstance(Attachment, { each: true }) public presentationAttachments!: Attachment[] + public getAttachmentFormats(): ProofAttachmentFormat[] { + const attachment = this.indyAttachment + + if (!attachment) { + throw new AriesFrameworkError(`Could not find a presentation attachment`) + } + + return [ + { + format: new ProofFormatSpec({ format: V2_INDY_PRESENTATION }), + attachment: attachment, + }, + ] + } + + public get indyAttachment(): Attachment | null { + return this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) ?? null + } + public get indyProof(): IndyProof | null { - const attachment = this.presentationAttachments.find((attachment) => attachment.id === INDY_PROOF_ATTACHMENT_ID) + const attachment = this.indyAttachment const proofJson = attachment?.getDataAsJson() ?? null diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..87901ce6a8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1PresentationProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +export type V1PresentationProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V1PresentationProblemReportMessage extends ProblemReportMessage { + /** + * Create new PresentationProblemReportMessage instance. + * @param options description of error and multiple optional fields for reporting problem + */ + public constructor(options: V1PresentationProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V1PresentationProblemReportMessage.type) + public readonly type = V1PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/problem-report') +} diff --git a/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts similarity index 75% rename from packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts rename to packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts index 409b1cfe23..d6392f5d61 100644 --- a/packages/core/src/modules/proofs/messages/ProposePresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1ProposePresentationMessage.ts @@ -1,10 +1,9 @@ import { Expose, Type } from 'class-transformer' import { IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' -import { AgentMessage } from '../../../agent/AgentMessage' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' - -import { PresentationPreview } from './PresentationPreview' +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { PresentationPreview } from '../models/V1PresentationPreview' export interface ProposePresentationMessageOptions { id?: string @@ -17,7 +16,7 @@ export interface ProposePresentationMessageOptions { * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#propose-presentation */ -export class ProposePresentationMessage extends AgentMessage { +export class V1ProposePresentationMessage extends AgentMessage { public constructor(options: ProposePresentationMessageOptions) { super() @@ -28,8 +27,8 @@ export class ProposePresentationMessage extends AgentMessage { } } - @IsValidMessageType(ProposePresentationMessage.type) - public readonly type = ProposePresentationMessage.type.messageTypeUri + @IsValidMessageType(V1ProposePresentationMessage.type) + public readonly type = V1ProposePresentationMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/propose-presentation') /** diff --git a/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts similarity index 56% rename from packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts rename to packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts index e03b4f2fa4..14a6f12c67 100644 --- a/packages/core/src/modules/proofs/messages/RequestPresentationMessage.ts +++ b/packages/core/src/modules/proofs/protocol/v1/messages/V1RequestPresentationMessage.ts @@ -1,11 +1,16 @@ +import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' + import { Expose, Type } from 'class-transformer' import { IsArray, IsString, ValidateNested, IsOptional, IsInstance } from 'class-validator' -import { AgentMessage } from '../../../agent/AgentMessage' -import { Attachment } from '../../../decorators/attachment/Attachment' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { IsValidMessageType, parseMessageType } from '../../../utils/messageType' -import { ProofRequest } from '../models' +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { V2_INDY_PRESENTATION_REQUEST } from '../../../formats/ProofFormats' +import { ProofRequest } from '../../../formats/indy/models/ProofRequest' +import { ProofFormatSpec } from '../../../formats/models/ProofFormatSpec' export interface RequestPresentationOptions { id?: string @@ -20,7 +25,7 @@ export const INDY_PROOF_REQUEST_ATTACHMENT_ID = 'libindy-request-presentation-0' * * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#request-presentation */ -export class RequestPresentationMessage extends AgentMessage { +export class V1RequestPresentationMessage extends AgentMessage { public constructor(options: RequestPresentationOptions) { super() @@ -31,8 +36,8 @@ export class RequestPresentationMessage extends AgentMessage { } } - @IsValidMessageType(RequestPresentationMessage.type) - public readonly type = RequestPresentationMessage.type.messageTypeUri + @IsValidMessageType(V1RequestPresentationMessage.type) + public readonly type = V1RequestPresentationMessage.type.messageTypeUri public static readonly type = parseMessageType('https://didcomm.org/present-proof/1.0/request-presentation') /** @@ -64,4 +69,26 @@ export class RequestPresentationMessage extends AgentMessage { return proofRequest } + + public getAttachmentFormats(): ProofAttachmentFormat[] { + const attachment = this.indyAttachment + + if (!attachment) { + throw new AriesFrameworkError(`Could not find a request presentation attachment`) + } + + return [ + { + format: new ProofFormatSpec({ format: V2_INDY_PRESENTATION_REQUEST }), + attachment: attachment, + }, + ] + } + + public get indyAttachment(): Attachment | null { + return ( + this.requestPresentationAttachments.find((attachment) => attachment.id === INDY_PROOF_REQUEST_ATTACHMENT_ID) ?? + null + ) + } } diff --git a/packages/core/src/modules/proofs/protocol/v1/messages/index.ts b/packages/core/src/modules/proofs/protocol/v1/messages/index.ts new file mode 100644 index 0000000000..01d16d4e87 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v1/messages/index.ts @@ -0,0 +1,4 @@ +export * from './V1ProposePresentationMessage' +export * from './V1RequestPresentationMessage' +export * from './V1PresentationMessage' +export * from './V1PresentationAckMessage' diff --git a/packages/core/src/modules/proofs/models/AttributeFilter.ts b/packages/core/src/modules/proofs/protocol/v1/models/AttributeFilter.ts similarity index 98% rename from packages/core/src/modules/proofs/models/AttributeFilter.ts rename to packages/core/src/modules/proofs/protocol/v1/models/AttributeFilter.ts index 90b628799e..b2a804ab2d 100644 --- a/packages/core/src/modules/proofs/models/AttributeFilter.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/AttributeFilter.ts @@ -1,7 +1,7 @@ import { Expose, Transform, TransformationType, Type } from 'class-transformer' import { IsInstance, IsOptional, IsString, Matches, ValidateNested } from 'class-validator' -import { credDefIdRegex, indyDidRegex, schemaIdRegex, schemaVersionRegex } from '../../../utils' +import { credDefIdRegex, indyDidRegex, schemaIdRegex, schemaVersionRegex } from '../../../../../utils/regex' export class AttributeValue { public constructor(options: AttributeValue) { diff --git a/packages/core/src/modules/proofs/models/PartialProof.ts b/packages/core/src/modules/proofs/protocol/v1/models/PartialProof.ts similarity index 100% rename from packages/core/src/modules/proofs/models/PartialProof.ts rename to packages/core/src/modules/proofs/protocol/v1/models/PartialProof.ts diff --git a/packages/core/src/modules/proofs/models/PredicateType.ts b/packages/core/src/modules/proofs/protocol/v1/models/PredicateType.ts similarity index 100% rename from packages/core/src/modules/proofs/models/PredicateType.ts rename to packages/core/src/modules/proofs/protocol/v1/models/PredicateType.ts diff --git a/packages/core/src/modules/proofs/models/ProofAttribute.ts b/packages/core/src/modules/proofs/protocol/v1/models/ProofAttribute.ts similarity index 100% rename from packages/core/src/modules/proofs/models/ProofAttribute.ts rename to packages/core/src/modules/proofs/protocol/v1/models/ProofAttribute.ts diff --git a/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts b/packages/core/src/modules/proofs/protocol/v1/models/ProofAttributeInfo.ts similarity index 94% rename from packages/core/src/modules/proofs/models/ProofAttributeInfo.ts rename to packages/core/src/modules/proofs/protocol/v1/models/ProofAttributeInfo.ts index 56c98888a0..c306fc0e18 100644 --- a/packages/core/src/modules/proofs/models/ProofAttributeInfo.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/ProofAttributeInfo.ts @@ -1,7 +1,7 @@ import { Expose, Type } from 'class-transformer' import { IsString, IsOptional, IsArray, ValidateNested, IsInstance, ValidateIf, ArrayNotEmpty } from 'class-validator' -import { RevocationInterval } from '../../credentials' +import { RevocationInterval } from '../../../../credentials' import { AttributeFilter } from './AttributeFilter' diff --git a/packages/core/src/modules/proofs/models/ProofIdentifier.ts b/packages/core/src/modules/proofs/protocol/v1/models/ProofIdentifier.ts similarity index 92% rename from packages/core/src/modules/proofs/models/ProofIdentifier.ts rename to packages/core/src/modules/proofs/protocol/v1/models/ProofIdentifier.ts index 66f337e8b2..241ac74aaa 100644 --- a/packages/core/src/modules/proofs/models/ProofIdentifier.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/ProofIdentifier.ts @@ -1,7 +1,7 @@ import { Expose } from 'class-transformer' import { IsNumber, IsOptional, IsString, Matches } from 'class-validator' -import { credDefIdRegex } from '../../../utils' +import { credDefIdRegex } from '../../../../../utils/regex' export class ProofIdentifier { public constructor(options: ProofIdentifier) { diff --git a/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts b/packages/core/src/modules/proofs/protocol/v1/models/ProofPredicateInfo.ts similarity index 95% rename from packages/core/src/modules/proofs/models/ProofPredicateInfo.ts rename to packages/core/src/modules/proofs/protocol/v1/models/ProofPredicateInfo.ts index ba2ecdde81..22f7e6f593 100644 --- a/packages/core/src/modules/proofs/models/ProofPredicateInfo.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/ProofPredicateInfo.ts @@ -1,7 +1,7 @@ import { Expose, Type } from 'class-transformer' import { IsArray, IsEnum, IsInstance, IsInt, IsOptional, IsString, ValidateNested } from 'class-validator' -import { RevocationInterval } from '../../credentials' +import { RevocationInterval } from '../../../../credentials' import { AttributeFilter } from './AttributeFilter' import { PredicateType } from './PredicateType' diff --git a/packages/core/src/modules/proofs/models/RequestedAttribute.ts b/packages/core/src/modules/proofs/protocol/v1/models/RequestedAttribute.ts similarity index 91% rename from packages/core/src/modules/proofs/models/RequestedAttribute.ts rename to packages/core/src/modules/proofs/protocol/v1/models/RequestedAttribute.ts index 4998a8b097..293756870a 100644 --- a/packages/core/src/modules/proofs/models/RequestedAttribute.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/RequestedAttribute.ts @@ -1,7 +1,7 @@ import { Exclude, Expose } from 'class-transformer' import { IsBoolean, IsInt, IsOptional, IsString } from 'class-validator' -import { IndyCredentialInfo } from '../../credentials' +import { IndyCredentialInfo } from '../../../../credentials/protocol/v1/models' /** * Requested Attribute for Indy proof creation diff --git a/packages/core/src/modules/proofs/models/RequestedPredicate.ts b/packages/core/src/modules/proofs/protocol/v1/models/RequestedPredicate.ts similarity index 92% rename from packages/core/src/modules/proofs/models/RequestedPredicate.ts rename to packages/core/src/modules/proofs/protocol/v1/models/RequestedPredicate.ts index 5e7d4dc5f9..9109b51a4d 100644 --- a/packages/core/src/modules/proofs/models/RequestedPredicate.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/RequestedPredicate.ts @@ -1,7 +1,7 @@ import { Exclude, Expose } from 'class-transformer' import { IsInt, IsOptional, IsString } from 'class-validator' -import { IndyCredentialInfo } from '../../credentials' +import { IndyCredentialInfo } from '../../../../credentials' /** * Requested Predicate for Indy proof creation diff --git a/packages/core/src/modules/proofs/models/RequestedProof.ts b/packages/core/src/modules/proofs/protocol/v1/models/RequestedProof.ts similarity index 100% rename from packages/core/src/modules/proofs/models/RequestedProof.ts rename to packages/core/src/modules/proofs/protocol/v1/models/RequestedProof.ts diff --git a/packages/core/src/modules/proofs/messages/PresentationPreview.ts b/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts similarity index 95% rename from packages/core/src/modules/proofs/messages/PresentationPreview.ts rename to packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts index c309aaee85..25a87555e6 100644 --- a/packages/core/src/modules/proofs/messages/PresentationPreview.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/V1PresentationPreview.ts @@ -11,9 +11,9 @@ import { ValidateNested, } from 'class-validator' -import { credDefIdRegex } from '../../../utils' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../utils/messageType' +import { credDefIdRegex } from '../../../../../utils' +import { JsonTransformer } from '../../../../../utils/JsonTransformer' +import { IsValidMessageType, parseMessageType, replaceLegacyDidSovPrefix } from '../../../../../utils/messageType' import { PredicateType } from '../models/PredicateType' export interface PresentationPreviewAttributeOptions { diff --git a/packages/core/src/modules/proofs/models/index.ts b/packages/core/src/modules/proofs/protocol/v1/models/index.ts similarity index 75% rename from packages/core/src/modules/proofs/models/index.ts rename to packages/core/src/modules/proofs/protocol/v1/models/index.ts index d313158b63..0c5bf61d92 100644 --- a/packages/core/src/modules/proofs/models/index.ts +++ b/packages/core/src/modules/proofs/protocol/v1/models/index.ts @@ -5,9 +5,6 @@ export * from './ProofAttribute' export * from './ProofAttributeInfo' export * from './ProofIdentifier' export * from './ProofPredicateInfo' -export * from './ProofRequest' export * from './RequestedAttribute' -export * from './RequestedCredentials' export * from './RequestedPredicate' export * from './RequestedProof' -export * from './RetrievedCredentials' diff --git a/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts b/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts new file mode 100644 index 0000000000..9b8229d5b5 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/V2ProofService.ts @@ -0,0 +1,808 @@ +import type { AgentMessage } from '../../../../agent/AgentMessage' +import type { Dispatcher } from '../../../../agent/Dispatcher' +import type { InboundMessageContext } from '../../../../agent/models/InboundMessageContext' +import type { MediationRecipientService } from '../../../routing/services/MediationRecipientService' +import type { ProofStateChangedEvent } from '../../ProofEvents' +import type { ProofResponseCoordinator } from '../../ProofResponseCoordinator' +import type { ProofFormatService } from '../../formats/ProofFormatService' +import type { ProofRequest } from '../../formats/indy/models/ProofRequest' +import type { CreateProblemReportOptions } from '../../formats/models/ProofFormatServiceOptions' +import type { ProofFormatSpec } from '../../formats/models/ProofFormatSpec' +import type { + CreateAckOptions, + CreatePresentationOptions, + CreateProposalAsResponseOptions, + CreateProposalOptions, + CreateRequestAsResponseOptions, + CreateRequestOptions, + GetRequestedCredentialsForProofRequestOptions, + ProofRequestFromProposalOptions, +} from '../../models/ProofServiceOptions' +import type { + RetrievedCredentialOptions, + ProofRequestFormats, + RequestedCredentialsFormats, +} from '../../models/SharedOptions' + +import { inject, Lifecycle, scoped } from 'tsyringe' + +import { AgentConfig } from '../../../../agent/AgentConfig' +import { EventEmitter } from '../../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../../constants' +import { AriesFrameworkError } from '../../../../error' +import { DidCommMessageRepository, DidCommMessageRole } from '../../../../storage' +import { Wallet } from '../../../../wallet/Wallet' +import { AckStatus } from '../../../common' +import { ConnectionService } from '../../../connections' +import { ProofEventTypes } from '../../ProofEvents' +import { ProofService } from '../../ProofService' +import { ProofsUtils } from '../../ProofsUtil' +import { PresentationProblemReportReason } from '../../errors/PresentationProblemReportReason' +import { V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST } from '../../formats/ProofFormats' +import { IndyProofFormatService } from '../../formats/indy/IndyProofFormatService' +import { ProofProtocolVersion } from '../../models/ProofProtocolVersion' +import { ProofState } from '../../models/ProofState' +import { PresentationRecordType, ProofRecord, ProofRepository } from '../../repository' + +import { V2PresentationProblemReportError } from './errors' +import { V2PresentationAckHandler } from './handlers/V2PresentationAckHandler' +import { V2PresentationHandler } from './handlers/V2PresentationHandler' +import { V2PresentationProblemReportHandler } from './handlers/V2PresentationProblemReportHandler' +import { V2ProposePresentationHandler } from './handlers/V2ProposePresentationHandler' +import { V2RequestPresentationHandler } from './handlers/V2RequestPresentationHandler' +import { V2PresentationAckMessage } from './messages' +import { V2PresentationMessage } from './messages/V2PresentationMessage' +import { V2PresentationProblemReportMessage } from './messages/V2PresentationProblemReportMessage' +import { V2ProposalPresentationMessage } from './messages/V2ProposalPresentationMessage' +import { V2RequestPresentationMessage } from './messages/V2RequestPresentationMessage' + +@scoped(Lifecycle.ContainerScoped) +export class V2ProofService extends ProofService { + public async createProofRequestFromProposal(options: ProofRequestFromProposalOptions): Promise { + const proofRecordId = options.proofRecord.id + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V2ProposalPresentationMessage, + }) + + if (!proposalMessage) { + throw new AriesFrameworkError(`Proof record with id ${proofRecordId} is missing required presentation proposal`) + } + + let result = {} + for (const key of proposalMessage.formats) { + if (key.format === V2_INDY_PRESENTATION_PROPOSAL) { + for (const attachment of proposalMessage.proposalsAttach) { + const proofRequestJson = attachment.getDataAsJson() ?? null + result = { + indy: proofRequestJson, + } + } + } else { + // PK-TODO create Presentation Exchange request format + } + } + + return result + } + + private protocolVersion: ProofProtocolVersion + private formatServiceMap: { [key: string]: ProofFormatService } + + public constructor( + agentConfig: AgentConfig, + connectionService: ConnectionService, + proofRepository: ProofRepository, + didCommMessageRepository: DidCommMessageRepository, + eventEmitter: EventEmitter, + indyProofFormatService: IndyProofFormatService, + @inject(InjectionSymbols.Wallet) wallet: Wallet + ) { + super(agentConfig, proofRepository, connectionService, didCommMessageRepository, wallet, eventEmitter) + this.protocolVersion = ProofProtocolVersion.V2 + this.wallet = wallet + this.formatServiceMap = { + [PresentationRecordType.Indy]: indyProofFormatService, + } + } + + public getVersion(): ProofProtocolVersion { + return this.protocolVersion + } + + public async createProposal( + options: CreateProposalOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const formats = [] + for (const key of Object.keys(options.proofFormats)) { + const service = this.formatServiceMap[key] + formats.push( + await service.createRequest({ + formats: + key === PresentationRecordType.Indy + ? await ProofsUtils.createRequestFromPreview(options) + : options.proofFormats, + }) + ) + } + + const proposalMessage = new V2ProposalPresentationMessage({ + attachmentInfo: formats, + comment: options.comment, + willConfirm: options.willConfirm, + goalCode: options.goalCode, + }) + + const proofRecord = new ProofRecord({ + connectionId: options.connectionRecord.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalSent, + protocolVersion: ProofProtocolVersion.V2, + }) + + await this.proofRepository.save(proofRecord) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + + return { + proofRecord: proofRecord, + message: proposalMessage, + } + } + + public async createProposalAsResponse( + options: CreateProposalAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + options.proofRecord.assertState(ProofState.RequestReceived) + + const formats = [] + for (const key of Object.keys(options.proofFormats)) { + const service = this.formatServiceMap[key] + formats.push( + await service.createProposal({ + formats: options.proofFormats, + }) + ) + } + + const proposalMessage = new V2ProposalPresentationMessage({ + attachmentInfo: formats, + comment: options.comment, + goalCode: options.goalCode, + willConfirm: options.willConfirm, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: options.proofRecord.id, + }) + + void this.updateState(options.proofRecord, ProofState.ProposalSent) + + return { message: proposalMessage, proofRecord: options.proofRecord } + } + + public async processProposal(messageContext: InboundMessageContext): Promise { + const { message: _proposalMessage, connection: connectionRecord } = messageContext + let proofRecord: ProofRecord + + const proposalMessage = _proposalMessage as V2ProposalPresentationMessage + + const proposalAttachments = proposalMessage.getAttachmentFormats() + + for (const attachmentFormat of proposalAttachments) { + const service = this.getFormatServiceForFormat(attachmentFormat.format) + service?.processProposal({ + proposal: attachmentFormat, + }) + } + + try { + proofRecord = await this.proofRepository.getSingleByQuery({ + threadId: proposalMessage.threadId, + connectionId: connectionRecord?.id, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage, + previousSentMessage: requestMessage ?? undefined, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + await this.updateState(proofRecord, ProofState.ProposalReceived) + } catch { + // No proof record exists with thread id + proofRecord = new ProofRecord({ + connectionId: connectionRecord?.id, + threadId: proposalMessage.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save record + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proposalMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { + proofRecord, + previousState: null, + }, + }) + } + + return proofRecord + } + + public async createRequest( + options: CreateRequestOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + // create attachment formats + const formats = [] + for (const key of Object.keys(options.proofFormats)) { + const service = this.formatServiceMap[key] + formats.push( + await service.createRequest({ + formats: options.proofFormats, + }) + ) + } + + // create request message + const requestMessage = new V2RequestPresentationMessage({ + attachmentInfo: formats, + comment: options.comment, + willConfirm: options.willConfirm, + goalCode: options.goalCode, + }) + + // create & store proof record + const proofRecord = new ProofRecord({ + connectionId: options.connectionRecord?.id, + threadId: requestMessage.threadId, + state: ProofState.RequestSent, + protocolVersion: ProofProtocolVersion.V2, + }) + + await this.proofRepository.save(proofRecord) + + // create DIDComm message + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: requestMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: proofRecord.id, + }) + + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + + return { + proofRecord: proofRecord, + message: requestMessage, + } + } + + public async createRequestAsResponse( + options: CreateRequestAsResponseOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + options.proofRecord.assertState(ProofState.ProposalReceived) + + const proposal = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + if (!proposal) { + throw new AriesFrameworkError( + `Proof record with id ${options.proofRecord.id} is missing required presentation proposal` + ) + } + + // create attachment formats + const formats = [] + + for (const key of Object.keys(options.proofFormats)) { + const service = this.formatServiceMap[key] + formats.push( + await service.createRequestAsResponse({ + formats: options.proofFormats, + }) + ) + } + + // create request message + const requestMessage = new V2RequestPresentationMessage({ + attachmentInfo: formats, + comment: options.comment, + willConfirm: options.willConfirm, + goalCode: options.goalCode, + }) + requestMessage.setThread({ threadId: options.proofRecord.threadId }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: requestMessage, + role: DidCommMessageRole.Sender, + associatedRecordId: options.proofRecord.id, + }) + + void this.updateState(options.proofRecord, ProofState.RequestSent) + + return { message: requestMessage, proofRecord: options.proofRecord } + } + + public async processRequest(messageContext: InboundMessageContext): Promise { + const { message: _proofRequestMessage, connection: connectionRecord } = messageContext + + const proofRequestMessage = _proofRequestMessage as V2RequestPresentationMessage + + const requestAttachments = proofRequestMessage.getAttachmentFormats() + + for (const attachmentFormat of requestAttachments) { + const service = this.getFormatServiceForFormat(attachmentFormat.format) + service?.processRequest({ + request: attachmentFormat, + }) + } + + // assert + if (proofRequestMessage.requestPresentationsAttach.length === 0) { + throw new V2PresentationProblemReportError( + `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, + { problemCode: PresentationProblemReportReason.Abandoned } + ) + } + + this.logger.debug(`Received proof request`, proofRequestMessage) + + let proofRecord: ProofRecord + + try { + proofRecord = await this.proofRepository.getSingleByQuery({ + threadId: proofRequestMessage.threadId, + connectionId: connectionRecord?.id, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.ProposalSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestMessage ?? undefined, + previousSentMessage: proposalMessage ?? undefined, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + await this.updateState(proofRecord, ProofState.RequestReceived) + } catch { + // No proof record exists with thread id + proofRecord = new ProofRecord({ + connectionId: connectionRecord?.id, + threadId: proofRequestMessage.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: proofRequestMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Assert + this.connectionService.assertConnectionOrServiceDecorator(messageContext) + + // Save in repository + await this.proofRepository.save(proofRecord) + this.eventEmitter.emit({ + type: ProofEventTypes.ProofStateChanged, + payload: { proofRecord, previousState: null }, + }) + } + + return proofRecord + } + + public async createPresentation( + options: CreatePresentationOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + // assert state + options.proofRecord.assertState(ProofState.RequestReceived) + + const proofRequest = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const formats = [] + for (const key of Object.keys(options.proofFormats)) { + const service = this.formatServiceMap[key] + formats.push( + await service.createPresentation({ + attachment: proofRequest.getAttachmentByFormatIdentifier(V2_INDY_PRESENTATION_REQUEST), + formats: options.proofFormats, + }) + ) + } + + const presentationMessage = new V2PresentationMessage({ + comment: options.comment, + attachmentInfo: formats, + goalCode: options.goalCode, + lastPresentation: options.lastPresentation, + }) + presentationMessage.setThread({ threadId: options.proofRecord.threadId }) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: presentationMessage, + associatedRecordId: options.proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + await this.updateState(options.proofRecord, ProofState.PresentationSent) + + return { message: presentationMessage, proofRecord: options.proofRecord } + } + + public async processPresentation(messageContext: InboundMessageContext): Promise { + const { message: _presentationMessage, connection: connectionRecord } = messageContext + + const presentationMessage = _presentationMessage as V2PresentationMessage + + this.logger.debug(`Processing presentation with id ${presentationMessage.id}`) + + const proofRecord = await this.proofRepository.getSingleByQuery({ + threadId: presentationMessage.threadId, + connectionId: connectionRecord?.id, + }) + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + const requestMessage = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.RequestSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: proposalMessage ?? undefined, + previousSentMessage: requestMessage ?? undefined, + }) + + const formatVerificationResults = [] + for (const attachmentFormat of presentationMessage.getAttachmentFormats()) { + const service = this.getFormatServiceForFormat(attachmentFormat.format) + if (service) { + try { + formatVerificationResults.push( + await service.processPresentation({ + record: proofRecord, + presentation: { + request: requestMessage?.getAttachmentFormats(), + proof: presentationMessage.getAttachmentFormats(), + }, + }) + ) + } catch (e) { + if (e instanceof AriesFrameworkError) { + throw new V2PresentationProblemReportError(e.message, { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + throw e + } + } + } + if (formatVerificationResults.length === 0) { + throw new V2PresentationProblemReportError('None of the received formats are supported.', { + problemCode: PresentationProblemReportReason.Abandoned, + }) + } + + const isValid = formatVerificationResults.every((x) => x === true) + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: presentationMessage, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Receiver, + }) + + // Update record + proofRecord.isVerified = isValid + await this.updateState(proofRecord, ProofState.PresentationReceived) + + return proofRecord + } + + public async createAck(options: CreateAckOptions): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + // assert we've received the final presentation + const presentation = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V2PresentationMessage, + }) + + if (!presentation.lastPresentation) { + throw new AriesFrameworkError( + `Trying to send an ack message while presentation with id ${presentation.id} indicates this is not the last presentation (presentation.lastPresentation is set to false)` + ) + } + + const msg = new V2PresentationAckMessage({ + threadId: options.proofRecord.threadId, + status: AckStatus.OK, + }) + + await this.updateState(options.proofRecord, ProofState.Done) + + return { + message: msg, + proofRecord: options.proofRecord, + } + } + + public async processAck(messageContext: InboundMessageContext): Promise { + const { message: _ackMessage, connection: connectionRecord } = messageContext + + const ackMessage = _ackMessage as V2PresentationAckMessage + + const proofRecord = await this.proofRepository.getSingleByQuery({ + threadId: ackMessage.threadId, + connectionId: connectionRecord?.id, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const presentationMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + }) + + // Assert + proofRecord.assertState(ProofState.PresentationSent) + this.connectionService.assertConnectionOrServiceDecorator(messageContext, { + previousReceivedMessage: requestMessage ?? undefined, + previousSentMessage: presentationMessage ?? undefined, + }) + + // Update record + await this.updateState(proofRecord, ProofState.Done) + + return proofRecord + } + + public async createProblemReport( + options: CreateProblemReportOptions + ): Promise<{ proofRecord: ProofRecord; message: AgentMessage }> { + const msg = new V2PresentationProblemReportMessage({ + description: { + code: PresentationProblemReportReason.Abandoned, + en: options.description, + }, + }) + + msg.setThread({ + threadId: options.proofRecord.threadId, + }) + + return { + proofRecord: options.proofRecord, + message: msg, + } + } + + public async processProblemReport(messageContext: InboundMessageContext): Promise { + const { message: presentationProblemReportMsg } = messageContext + + const presentationProblemReportMessage = presentationProblemReportMsg as V2PresentationProblemReportMessage + const connectionRecord = messageContext.assertReadyConnection() + + this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) + + const proofRecord = await this.proofRepository.getSingleByQuery({ + threadId: presentationProblemReportMessage.threadId, + connectionId: connectionRecord?.id, + }) + + proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` + await this.updateState(proofRecord, ProofState.Abandoned) + return proofRecord + } + + public async shouldAutoRespondToRequest(proofRecord: ProofRecord): Promise { + const proposal = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + if (!proposal) { + return false + } + + const request = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + if (!request) { + throw new AriesFrameworkError(`Expected to find a request message for ProofRecord with id ${proofRecord.id}`) + } + + const proposalAttachments = proposal.getAttachmentFormats() + const requestAttachments = request.getAttachmentFormats() + + const equalityResults = [] + for (const attachmentFormat of proposalAttachments) { + const service = this.getFormatServiceForFormat(attachmentFormat.format) + equalityResults.push(service?.proposalAndRequestAreEqual(proposalAttachments, requestAttachments)) + } + + return equalityResults.every((x) => x === true) + } + + public async shouldAutoRespondToPresentation(proofRecord: ProofRecord): Promise { + const request = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + return request.willConfirm + } + + public async findRequestMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V2RequestPresentationMessage, + }) + } + + public async findPresentationMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V2PresentationMessage, + }) + } + + public async findProposalMessage(proofRecordId: string): Promise { + return await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecordId, + messageClass: V2ProposalPresentationMessage, + }) + } + + public async getRequestedCredentialsForProofRequest( + options: GetRequestedCredentialsForProofRequestOptions + ): Promise { + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: options.proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + if (!requestMessage) { + throw new AriesFrameworkError('No proof request found.') + } + + const requestAttachments = requestMessage.getAttachmentFormats() + + let result = {} + for (const attachmentFormat of requestAttachments) { + const service = this.getFormatServiceForFormat(attachmentFormat.format) + + if (!service) { + throw new AriesFrameworkError('No format service found for getting requested.') + } + + result = { + ...result, + ...(await service.getRequestedCredentialsForProofRequest({ + attachment: attachmentFormat.attachment, + presentationProposal: undefined, + config: options.config, + })), + } + } + + return result + } + + public async autoSelectCredentialsForProofRequest( + options: RetrievedCredentialOptions + ): Promise { + let returnValue = {} + + for (const [id] of Object.entries(options)) { + const service = this.formatServiceMap[id] + const credentials = await service.autoSelectCredentialsForProofRequest(options) + returnValue = { ...returnValue, ...credentials } + } + + return returnValue + } + + public async registerHandlers( + dispatcher: Dispatcher, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + mediationRecipientService: MediationRecipientService + ): Promise { + dispatcher.registerHandler( + new V2ProposePresentationHandler(this, agentConfig, this.didCommMessageRepository, proofResponseCoordinator) + ) + + dispatcher.registerHandler( + new V2RequestPresentationHandler( + this, + agentConfig, + proofResponseCoordinator, + mediationRecipientService, + this.didCommMessageRepository + ) + ) + + dispatcher.registerHandler( + new V2PresentationHandler(this, agentConfig, proofResponseCoordinator, this.didCommMessageRepository) + ) + dispatcher.registerHandler(new V2PresentationAckHandler(this)) + dispatcher.registerHandler(new V2PresentationProblemReportHandler(this)) + } + + private getFormatServiceForFormat(format: ProofFormatSpec) { + for (const service of Object.values(this.formatServiceMap)) { + if (service.supportsFormat(format.format)) { + return service + } + } + return null + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts new file mode 100644 index 0000000000..2792b10684 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-presentation.test.ts @@ -0,0 +1,256 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { + AcceptPresentationOptions, + AcceptProposalOptions, + ProposeProofOptions, +} from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage' +import { + V2_INDY_PRESENTATION, + V2_INDY_PRESENTATION_PROPOSAL, + V2_INDY_PRESENTATION_REQUEST, +} from '../../../formats/ProofFormats' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' +import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '947121108704767252195126', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V2 propose proof test', + } + + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_PROPOSAL, + }, + ], + proposalsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test', + }) + expect(faberProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) + + test(`Faber accepts the Proposal send by Alice`, async () => { + // Accept Proposal + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_REQUEST, + }, + ], + requestPresentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(aliceProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) + + test(`Alice accepts presentation request from Faber`, async () => { + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION, + }, + ], + presentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) + + test(`Faber accepts the presentation provided by Alice`, async () => { + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts new file mode 100644 index 0000000000..d7707f999d --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-proposal.test.ts @@ -0,0 +1,100 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { ProposeProofOptions } from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository' +import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage' +import { V2_INDY_PRESENTATION_PROPOSAL } from '../../../formats/ProofFormats' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberPresentationRecord: ProofRecord + let alicePresentationRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V2 propose proof test', + } + + alicePresentationRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberPresentationRecord = await waitForProofRecord(faberAgent, { + threadId: alicePresentationRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberPresentationRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_PROPOSAL, + }, + ], + proposalsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test', + }) + expect(faberPresentationRecord).toMatchObject({ + id: expect.anything(), + threadId: faberPresentationRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts new file mode 100644 index 0000000000..9100c0a15c --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/__tests__/indy-proof-request.test.ts @@ -0,0 +1,157 @@ +import type { Agent } from '../../../../../agent/Agent' +import type { ConnectionRecord } from '../../../../connections/repository/ConnectionRecord' +import type { AcceptProposalOptions, ProposeProofOptions } from '../../../models/ModuleOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { PresentationPreview } from '../../v1/models/V1PresentationPreview' + +import { setupProofsTest, waitForProofRecord } from '../../../../../../tests/helpers' +import testLogger from '../../../../../../tests/logger' +import { DidCommMessageRepository } from '../../../../../storage' +import { V2_INDY_PRESENTATION_PROPOSAL, V2_INDY_PRESENTATION_REQUEST } from '../../../formats/ProofFormats' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { ProofState } from '../../../models/ProofState' +import { V2RequestPresentationMessage } from '../messages' +import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, aliceConnection, presentationPreview } = await setupProofsTest( + 'Faber agent', + 'Alice agent' + )) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test(`Alice Creates and sends Proof Proposal to Faber`, async () => { + testLogger.test('Alice sends proof proposal to Faber') + + const proposeOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'ProofRequest', + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + comment: 'V2 propose proof test', + } + + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_PROPOSAL, + }, + ], + proposalsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + comment: 'V2 propose proof test', + }) + expect(faberProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) + + test(`Faber accepts the Proposal send by Alice`, async () => { + // Accept Proposal + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Alice waits for proof request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_REQUEST, + }, + ], + requestPresentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(aliceProofRecord).toMatchObject({ + id: expect.anything(), + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + }) +}) diff --git a/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts b/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts new file mode 100644 index 0000000000..76fa789a6e --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/errors/V2PresentationProblemReportError.ts @@ -0,0 +1,23 @@ +import type { ProblemReportErrorOptions } from '../../../../problem-reports' +import type { PresentationProblemReportReason } from '../../../errors/PresentationProblemReportReason' + +import { ProblemReportError } from '../../../../problem-reports/errors/ProblemReportError' +import { V2PresentationProblemReportMessage } from '../messages' + +interface V2PresentationProblemReportErrorOptions extends ProblemReportErrorOptions { + problemCode: PresentationProblemReportReason +} + +export class V2PresentationProblemReportError extends ProblemReportError { + public problemReport: V2PresentationProblemReportMessage + + public constructor(public message: string, { problemCode }: V2PresentationProblemReportErrorOptions) { + super(message, { problemCode }) + this.problemReport = new V2PresentationProblemReportMessage({ + description: { + en: message, + code: problemCode, + }, + }) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/errors/index.ts b/packages/core/src/modules/proofs/protocol/v2/errors/index.ts new file mode 100644 index 0000000000..7064b070aa --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/errors/index.ts @@ -0,0 +1 @@ +export * from './V2PresentationProblemReportError' diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts new file mode 100644 index 0000000000..9ffa2f32cc --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationAckHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { ProofService } from '../../../ProofService' + +import { V2PresentationAckMessage } from '../messages' + +export class V2PresentationAckHandler implements Handler { + private proofService: ProofService + public supportedMessages = [V2PresentationAckMessage] + + public constructor(proofService: ProofService) { + this.proofService = proofService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.proofService.processAck(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts new file mode 100644 index 0000000000..438659a98c --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationHandler.ts @@ -0,0 +1,72 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRecord } from '../../../repository' +import type { V2ProofService } from '../V2ProofService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { V2PresentationMessage, V2RequestPresentationMessage } from '../messages' + +export class V2PresentationHandler implements Handler { + private proofService: V2ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V2PresentationMessage] + + public constructor( + proofService: V2ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + didCommMessageRepository: DidCommMessageRepository + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processPresentation(messageContext) + + if (this.proofResponseCoordinator.shouldAutoRespondToPresentation(proofRecord)) { + return await this.createAck(proofRecord, messageContext) + } + } + + private async createAck(record: ProofRecord, messageContext: HandlerInboundMessage) { + this.agentConfig.logger.info( + `Automatically sending acknowledgement with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + const { message, proofRecord } = await this.proofService.createAck({ + proofRecord: record, + }) + + const requestMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + const presentationMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2PresentationMessage, + }) + + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage?.service && presentationMessage?.service) { + const recipientService = presentationMessage?.service + const ourService = requestMessage?.service + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create presentation ack`) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts new file mode 100644 index 0000000000..77bdab2160 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2PresentationProblemReportHandler.ts @@ -0,0 +1,17 @@ +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { V2ProofService } from '../V2ProofService' + +import { V2PresentationProblemReportMessage } from '../messages' + +export class V2PresentationProblemReportHandler implements Handler { + private proofService: V2ProofService + public supportedMessages = [V2PresentationProblemReportMessage] + + public constructor(proofService: V2ProofService) { + this.proofService = proofService + } + + public async handle(messageContext: HandlerInboundMessage) { + await this.proofService.processProblemReport(messageContext) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts new file mode 100644 index 0000000000..9387680ee8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2ProposePresentationHandler.ts @@ -0,0 +1,94 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRequestFromProposalOptions } from '../../../models/ProofServiceOptions' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { V2ProofService } from '../V2ProofService' + +import { createOutboundMessage } from '../../../../../agent/helpers' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { V2_INDY_PRESENTATION_PROPOSAL } from '../../../formats/ProofFormats' +import { V2ProposalPresentationMessage } from '../messages/V2ProposalPresentationMessage' + +export class V2ProposePresentationHandler implements Handler { + private proofService: V2ProofService + private agentConfig: AgentConfig + private didCommMessageRepository: DidCommMessageRepository + private proofResponseCoordinator: ProofResponseCoordinator + public supportedMessages = [V2ProposalPresentationMessage] + + public constructor( + proofService: V2ProofService, + agentConfig: AgentConfig, + didCommMessageRepository: DidCommMessageRepository, + proofResponseCoordinator: ProofResponseCoordinator + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.didCommMessageRepository = didCommMessageRepository + this.proofResponseCoordinator = proofResponseCoordinator + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processProposal(messageContext) + + if (this.proofResponseCoordinator.shouldAutoRespondToProposal(proofRecord)) { + return this.createRequest(proofRecord, messageContext) + } + } + + private async createRequest( + proofRecord: ProofRecord, + messageContext: HandlerInboundMessage + ) { + this.agentConfig.logger.info( + `Automatically sending request with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + if (!messageContext.connection) { + this.agentConfig.logger.error('No connection on the messageContext') + return + } + + const proposalMessage = await this.didCommMessageRepository.findAgentMessage({ + associatedRecordId: proofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + if (!proposalMessage) { + this.agentConfig.logger.error(`Proof record with id ${proofRecord.id} is missing required credential proposal`) + return + } + + const proposalAttachment = proposalMessage + .getAttachmentFormats() + .find((x) => x.format.format === V2_INDY_PRESENTATION_PROPOSAL) + + if (!proposalAttachment) { + throw new AriesFrameworkError('No proposal message could be found') + } + + const proofRequestFromProposalOptions: ProofRequestFromProposalOptions = { + name: 'proof-request', + version: '1.0', + nonce: await this.proofService.generateProofRequestNonce(), + proofRecord, + } + + const proofRequest = await this.proofService.createProofRequestFromProposal(proofRequestFromProposalOptions) + + if (!proofRequest.indy) { + throw new AriesFrameworkError('Failed to create proof request') + } + + const { message } = await this.proofService.createRequestAsResponse({ + proofRecord: proofRecord, + autoAcceptProof: proofRecord.autoAcceptProof, + proofFormats: proofRequest, + willConfirm: true, + }) + + return createOutboundMessage(messageContext.connection, message) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts new file mode 100644 index 0000000000..8c0cf599bb --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/handlers/V2RequestPresentationHandler.ts @@ -0,0 +1,106 @@ +import type { AgentConfig } from '../../../../../agent/AgentConfig' +import type { Handler, HandlerInboundMessage } from '../../../../../agent/Handler' +import type { DidCommMessageRepository } from '../../../../../storage/didcomm/DidCommMessageRepository' +import type { MediationRecipientService } from '../../../../routing' +import type { ProofResponseCoordinator } from '../../../ProofResponseCoordinator' +import type { ProofRecord } from '../../../repository/ProofRecord' +import type { V2ProofService } from '../V2ProofService' + +import { createOutboundMessage, createOutboundServiceMessage } from '../../../../../agent/helpers' +import { ServiceDecorator } from '../../../../../decorators/service/ServiceDecorator' +import { DidCommMessageRole } from '../../../../../storage' +import { ProofProtocolVersion } from '../../../models/ProofProtocolVersion' +import { V2RequestPresentationMessage } from '../messages/V2RequestPresentationMessage' + +export class V2RequestPresentationHandler implements Handler { + private proofService: V2ProofService + private agentConfig: AgentConfig + private proofResponseCoordinator: ProofResponseCoordinator + private mediationRecipientService: MediationRecipientService + private didCommMessageRepository: DidCommMessageRepository + public supportedMessages = [V2RequestPresentationMessage] + + public constructor( + proofService: V2ProofService, + agentConfig: AgentConfig, + proofResponseCoordinator: ProofResponseCoordinator, + mediationRecipientService: MediationRecipientService, + didCommMessageRepository: DidCommMessageRepository + ) { + this.proofService = proofService + this.agentConfig = agentConfig + this.proofResponseCoordinator = proofResponseCoordinator + this.mediationRecipientService = mediationRecipientService + this.didCommMessageRepository = didCommMessageRepository + } + + public async handle(messageContext: HandlerInboundMessage) { + const proofRecord = await this.proofService.processRequest(messageContext) + if (this.proofResponseCoordinator.shouldAutoRespondToRequest(proofRecord)) { + return await this.createPresentation(proofRecord, messageContext) + } + } + + private async createPresentation( + record: ProofRecord, + messageContext: HandlerInboundMessage + ) { + const requestMessage = await this.didCommMessageRepository.getAgentMessage({ + associatedRecordId: record.id, + messageClass: V2RequestPresentationMessage, + }) + + this.agentConfig.logger.info( + `Automatically sending presentation with autoAccept on ${this.agentConfig.autoAcceptProofs}` + ) + + const retrievedCredentials = await this.proofService.getRequestedCredentialsForProofRequest({ + proofRecord: record, + config: { + filterByPresentationPreview: false, + }, + }) + + const requestedCredentials = await this.proofService.autoSelectCredentialsForProofRequest(retrievedCredentials) + + const { message, proofRecord } = await this.proofService.createPresentation({ + proofRecord: record, + proofFormats: { + indy: requestedCredentials.indy, + }, + protocolVersion: ProofProtocolVersion.V2, + // Not sure to what to do with goalCode, willConfirm and comment fields here + }) + + if (messageContext.connection) { + return createOutboundMessage(messageContext.connection, message) + } else if (requestMessage.service) { + // Create ~service decorator + const routing = await this.mediationRecipientService.getRouting() + const ourService = new ServiceDecorator({ + serviceEndpoint: routing.endpoints[0], + recipientKeys: [routing.verkey], + routingKeys: routing.routingKeys, + }) + + const recipientService = requestMessage.service + + // Set and save ~service decorator to record (to remember our verkey) + message.service = ourService + + await this.didCommMessageRepository.saveOrUpdateAgentMessage({ + agentMessage: message, + associatedRecordId: proofRecord.id, + role: DidCommMessageRole.Sender, + }) + + return createOutboundServiceMessage({ + payload: message, + service: recipientService.resolvedDidCommService, + senderKey: ourService.resolvedDidCommService.recipientKeys[0], + }) + } + + this.agentConfig.logger.error(`Could not automatically create presentation`) + } +} diff --git a/packages/core/src/modules/proofs/protocol/v2/index.ts b/packages/core/src/modules/proofs/protocol/v2/index.ts new file mode 100644 index 0000000000..960bb99e85 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/index.ts @@ -0,0 +1 @@ +export * from './V2ProofService' diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts new file mode 100644 index 0000000000..bc4fb541c8 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationAckMessage.ts @@ -0,0 +1,17 @@ +import type { PresentationAckMessageOptions } from '../../../messages/PresentationAckMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { AckMessage } from '../../../../common/messages/AckMessage' + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0015-acks/README.md#explicit-acks + */ +export class V2PresentationAckMessage extends AckMessage { + public constructor(options: PresentationAckMessageOptions) { + super(options) + } + + @IsValidMessageType(V2PresentationAckMessage.type) + public readonly type = V2PresentationAckMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/ack') +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts new file mode 100644 index 0000000000..9b2db03334 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationMessage.ts @@ -0,0 +1,94 @@ +import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../formats/models/ProofFormatSpec' + +export interface V2PresentationMessageOptions { + id?: string + goalCode?: string + comment?: string + lastPresentation?: boolean + attachmentInfo: ProofAttachmentFormat[] +} + +export class V2PresentationMessage extends AgentMessage { + public constructor(options: V2PresentationMessageOptions) { + super() + + this.formats = [] + this.presentationsAttach = [] + + if (options) { + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.lastPresentation = options.lastPresentation ?? true + + for (const entry of options.attachmentInfo) { + this.addPresentationsAttachment(entry) + } + } + } + + public addPresentationsAttachment(attachment: ProofAttachmentFormat) { + this.formats.push(attachment.format) + this.presentationsAttach.push(attachment.attachment) + } + + /** + * Every attachment has a corresponding entry in the formats array. + * This method pairs those together in a {@link ProofAttachmentFormat} object. + */ + public getAttachmentFormats(): ProofAttachmentFormat[] { + const attachmentFormats: ProofAttachmentFormat[] = [] + + this.formats.forEach((format) => { + const attachment = this.presentationsAttach.find((attachment) => attachment.id === format.attachmentId) + + if (!attachment) { + throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) + } + + attachmentFormats.push({ format, attachment }) + }) + return attachmentFormats + } + + @IsValidMessageType(V2PresentationMessage.type) + public readonly type = V2PresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Expose({ name: 'last_presentation' }) + @IsBoolean() + public lastPresentation = true + + @Expose({ name: 'formats' }) + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public presentationsAttach!: Attachment[] +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts new file mode 100644 index 0000000000..b36a69a7fe --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2PresentationProblemReportMessage.ts @@ -0,0 +1,23 @@ +import type { ProblemReportMessageOptions } from '../../../../problem-reports/messages/ProblemReportMessage' + +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { ProblemReportMessage } from '../../../../problem-reports/messages/ProblemReportMessage' + +export type V2PresentationProblemReportMessageOptions = ProblemReportMessageOptions + +/** + * @see https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md + */ +export class V2PresentationProblemReportMessage extends ProblemReportMessage { + /** + * Create new PresentationProblemReportMessage instance. + * @param options + */ + public constructor(options: V2PresentationProblemReportMessageOptions) { + super(options) + } + + @IsValidMessageType(V2PresentationProblemReportMessage.type) + public readonly type = V2PresentationProblemReportMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/problem-report') +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts new file mode 100644 index 0000000000..085d80cbde --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2ProposalPresentationMessage.ts @@ -0,0 +1,94 @@ +import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../../error/AriesFrameworkError' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../formats/models/ProofFormatSpec' + +export interface V2ProposePresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + willConfirm?: boolean + attachmentInfo: ProofAttachmentFormat[] +} + +export class V2ProposalPresentationMessage extends AgentMessage { + public constructor(options: V2ProposePresentationMessageOptions) { + super() + + this.formats = [] + this.proposalsAttach = [] + + if (options) { + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.willConfirm = options.willConfirm ?? false + + for (const entry of options.attachmentInfo) { + this.addProposalsAttachment(entry) + } + } + } + + public addProposalsAttachment(attachment: ProofAttachmentFormat) { + this.formats.push(attachment.format) + this.proposalsAttach.push(attachment.attachment) + } + + /** + * Every attachment has a corresponding entry in the formats array. + * This method pairs those together in a {@link ProofAttachmentFormat} object. + */ + public getAttachmentFormats(): ProofAttachmentFormat[] { + const attachmentFormats: ProofAttachmentFormat[] = [] + + this.formats.forEach((format) => { + const attachment = this.proposalsAttach.find((attachment) => attachment.id === format.attachmentId) + + if (!attachment) { + throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) + } + + attachmentFormats.push({ format, attachment }) + }) + return attachmentFormats + } + + @IsValidMessageType(V2ProposalPresentationMessage.type) + public readonly type = V2ProposalPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType(`https://didcomm.org/present-proof/2.0/propose-presentation`) + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Expose({ name: 'will_confirm' }) + @IsBoolean() + public willConfirm = false + + @Expose({ name: 'formats' }) + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'proposals~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public proposalsAttach!: Attachment[] +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts new file mode 100644 index 0000000000..15667cb778 --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/V2RequestPresentationMessage.ts @@ -0,0 +1,118 @@ +import type { ProofAttachmentFormat } from '../../../formats/models/ProofAttachmentFormat' + +import { Expose, Type } from 'class-transformer' +import { IsArray, IsBoolean, IsInstance, IsOptional, IsString, ValidateNested } from 'class-validator' + +import { AgentMessage } from '../../../../../agent/AgentMessage' +import { Attachment } from '../../../../../decorators/attachment/Attachment' +import { AriesFrameworkError } from '../../../../../error' +import { IsValidMessageType, parseMessageType } from '../../../../../utils/messageType' +import { uuid } from '../../../../../utils/uuid' +import { ProofFormatSpec } from '../../../formats/models/ProofFormatSpec' + +export interface V2RequestPresentationMessageOptions { + id?: string + comment?: string + goalCode?: string + presentMultiple?: boolean + willConfirm?: boolean + attachmentInfo: ProofAttachmentFormat[] +} + +export class V2RequestPresentationMessage extends AgentMessage { + public constructor(options: V2RequestPresentationMessageOptions) { + super() + + this.formats = [] + this.requestPresentationsAttach = [] + + if (options) { + this.id = options.id ?? uuid() + this.comment = options.comment + this.goalCode = options.goalCode + this.willConfirm = options.willConfirm ?? true + this.presentMultiple = options.presentMultiple ?? false + for (const entry of options.attachmentInfo) { + this.addRequestPresentationsAttachment(entry) + } + } + } + + public addRequestPresentationsAttachment(attachment: ProofAttachmentFormat) { + this.formats.push(attachment.format) + this.requestPresentationsAttach.push(attachment.attachment) + } + + public getAttachmentByFormatIdentifier(formatIdentifier: string) { + const format = this.formats.find((x) => x.format === formatIdentifier) + if (!format) { + throw new AriesFrameworkError( + `Expected to find a format entry of type: ${formatIdentifier}, but none could be found.` + ) + } + + const attachment = this.requestPresentationsAttach.find((x) => x.id === format.attachmentId) + + if (!attachment) { + throw new AriesFrameworkError( + `Expected to find an attachment entry with id: ${format.attachmentId}, but none could be found.` + ) + } + + return attachment + } + + /** + * Every attachment has a corresponding entry in the formats array. + * This method pairs those together in a {@link ProofAttachmentFormat} object. + */ + public getAttachmentFormats(): ProofAttachmentFormat[] { + const attachmentFormats: ProofAttachmentFormat[] = [] + + this.formats.forEach((format) => { + const attachment = this.requestPresentationsAttach.find((attachment) => attachment.id === format.attachmentId) + + if (!attachment) { + throw new AriesFrameworkError(`Could not find a matching attachment with attachmentId: ${format.attachmentId}`) + } + + attachmentFormats.push({ format, attachment }) + }) + return attachmentFormats + } + + @IsValidMessageType(V2RequestPresentationMessage.type) + public readonly type = V2RequestPresentationMessage.type.messageTypeUri + public static readonly type = parseMessageType('https://didcomm.org/present-proof/2.0/request-presentation') + + @IsString() + @IsOptional() + public comment?: string + + @Expose({ name: 'goal_code' }) + @IsString() + @IsOptional() + public goalCode?: string + + @Expose({ name: 'will_confirm' }) + @IsBoolean() + public willConfirm = false + + @Expose({ name: 'present_multiple' }) + @IsBoolean() + public presentMultiple = false + + @Expose({ name: 'formats' }) + @Type(() => ProofFormatSpec) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(ProofFormatSpec, { each: true }) + public formats!: ProofFormatSpec[] + + @Expose({ name: 'request_presentations~attach' }) + @Type(() => Attachment) + @IsArray() + @ValidateNested({ each: true }) + @IsInstance(Attachment, { each: true }) + public requestPresentationsAttach!: Attachment[] +} diff --git a/packages/core/src/modules/proofs/protocol/v2/messages/index.ts b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts new file mode 100644 index 0000000000..8b0c4a005d --- /dev/null +++ b/packages/core/src/modules/proofs/protocol/v2/messages/index.ts @@ -0,0 +1,5 @@ +export * from './V2PresentationAckMessage' +export * from './V2PresentationMessage' +export * from './V2PresentationProblemReportMessage' +export * from './V2ProposalPresentationMessage' +export * from './V2RequestPresentationMessage' diff --git a/packages/core/src/modules/proofs/repository/PresentationExchangeRecord.ts b/packages/core/src/modules/proofs/repository/PresentationExchangeRecord.ts new file mode 100644 index 0000000000..83988150a8 --- /dev/null +++ b/packages/core/src/modules/proofs/repository/PresentationExchangeRecord.ts @@ -0,0 +1,4 @@ +export enum PresentationRecordType { + Indy = 'indy', + JsonLD = 'JSON-LD', +} diff --git a/packages/core/src/modules/proofs/repository/ProofRecord.ts b/packages/core/src/modules/proofs/repository/ProofRecord.ts index bf4faa5435..8d87cd8d68 100644 --- a/packages/core/src/modules/proofs/repository/ProofRecord.ts +++ b/packages/core/src/modules/proofs/repository/ProofRecord.ts @@ -1,31 +1,23 @@ import type { TagsBase } from '../../../storage/BaseRecord' -import type { AutoAcceptProof } from '../ProofAutoAcceptType' -import type { ProofState } from '../ProofState' - -import { Type } from 'class-transformer' +import type { AutoAcceptProof } from '../models/ProofAutoAcceptType' +import type { ProofProtocolVersion } from '../models/ProofProtocolVersion' +import type { ProofState } from '../models/ProofState' import { AriesFrameworkError } from '../../../error' import { BaseRecord } from '../../../storage/BaseRecord' import { uuid } from '../../../utils/uuid' -import { ProposePresentationMessage, RequestPresentationMessage, PresentationMessage } from '../messages' export interface ProofRecordProps { id?: string createdAt?: Date - + protocolVersion: ProofProtocolVersion isVerified?: boolean state: ProofState connectionId?: string threadId: string - presentationId?: string tags?: CustomProofTags autoAcceptProof?: AutoAcceptProof errorMessage?: string - - // message data - proposalMessage?: ProposePresentationMessage - requestMessage?: RequestPresentationMessage - presentationMessage?: PresentationMessage } export type CustomProofTags = TagsBase @@ -38,20 +30,12 @@ export type DefaultProofTags = { export class ProofRecord extends BaseRecord { public connectionId?: string public threadId!: string + public protocolVersion!: ProofProtocolVersion public isVerified?: boolean - public presentationId?: string public state!: ProofState public autoAcceptProof?: AutoAcceptProof public errorMessage?: string - // message data - @Type(() => ProposePresentationMessage) - public proposalMessage?: ProposePresentationMessage - @Type(() => RequestPresentationMessage) - public requestMessage?: RequestPresentationMessage - @Type(() => PresentationMessage) - public presentationMessage?: PresentationMessage - public static readonly type = 'ProofRecord' public readonly type = ProofRecord.type @@ -61,14 +45,13 @@ export class ProofRecord extends BaseRecord { if (props) { this.id = props.id ?? uuid() this.createdAt = props.createdAt ?? new Date() - this.proposalMessage = props.proposalMessage - this.requestMessage = props.requestMessage - this.presentationMessage = props.presentationMessage + this.protocolVersion = props.protocolVersion + this.isVerified = props.isVerified this.state = props.state this.connectionId = props.connectionId this.threadId = props.threadId - this.presentationId = props.presentationId + this.autoAcceptProof = props.autoAcceptProof this._tags = props.tags ?? {} this.errorMessage = props.errorMessage diff --git a/packages/core/src/modules/proofs/repository/index.ts b/packages/core/src/modules/proofs/repository/index.ts index 23ca5ba9a3..8c1bf5235e 100644 --- a/packages/core/src/modules/proofs/repository/index.ts +++ b/packages/core/src/modules/proofs/repository/index.ts @@ -1,2 +1,3 @@ export * from './ProofRecord' export * from './ProofRepository' +export * from './PresentationExchangeRecord' diff --git a/packages/core/src/modules/proofs/services/ProofService.ts b/packages/core/src/modules/proofs/services/ProofService.ts deleted file mode 100644 index 24ce0ab284..0000000000 --- a/packages/core/src/modules/proofs/services/ProofService.ts +++ /dev/null @@ -1,1151 +0,0 @@ -import type { AgentMessage } from '../../../agent/AgentMessage' -import type { InboundMessageContext } from '../../../agent/models/InboundMessageContext' -import type { Logger } from '../../../logger' -import type { ConnectionRecord } from '../../connections' -import type { AutoAcceptProof } from '../ProofAutoAcceptType' -import type { ProofStateChangedEvent } from '../ProofEvents' -import type { PresentationPreview, PresentationPreviewAttribute } from '../messages' -import type { PresentationProblemReportMessage } from './../messages/PresentationProblemReportMessage' -import type { CredDef, IndyProof, Schema } from 'indy-sdk' - -import { validateOrReject } from 'class-validator' -import { inject, Lifecycle, scoped } from 'tsyringe' - -import { AgentConfig } from '../../../agent/AgentConfig' -import { EventEmitter } from '../../../agent/EventEmitter' -import { InjectionSymbols } from '../../../constants' -import { Attachment, AttachmentData } from '../../../decorators/attachment/Attachment' -import { AriesFrameworkError } from '../../../error' -import { JsonEncoder } from '../../../utils/JsonEncoder' -import { JsonTransformer } from '../../../utils/JsonTransformer' -import { checkProofRequestForDuplicates } from '../../../utils/indyProofRequest' -import { uuid } from '../../../utils/uuid' -import { Wallet } from '../../../wallet/Wallet' -import { AckStatus } from '../../common' -import { ConnectionService } from '../../connections' -import { CredentialUtils, Credential, CredentialRepository, IndyCredentialInfo } from '../../credentials' -import { IndyHolderService, IndyVerifierService, IndyRevocationService } from '../../indy' -import { IndyLedgerService } from '../../ledger/services/IndyLedgerService' -import { ProofEventTypes } from '../ProofEvents' -import { ProofState } from '../ProofState' -import { PresentationProblemReportError, PresentationProblemReportReason } from '../errors' -import { - INDY_PROOF_ATTACHMENT_ID, - INDY_PROOF_REQUEST_ATTACHMENT_ID, - PresentationAckMessage, - PresentationMessage, - ProposePresentationMessage, - RequestPresentationMessage, -} from '../messages' -import { - AttributeFilter, - PartialProof, - ProofAttributeInfo, - ProofPredicateInfo, - ProofRequest, - RequestedAttribute, - RequestedCredentials, - RequestedPredicate, - RetrievedCredentials, -} from '../models' -import { ProofRepository } from '../repository' -import { ProofRecord } from '../repository/ProofRecord' - -/** - * @todo add method to check if request matches proposal. Useful to see if a request I received is the same as the proposal I sent. - * @todo add method to reject / revoke messages - * @todo validate attachments / messages - */ -@scoped(Lifecycle.ContainerScoped) -export class ProofService { - private proofRepository: ProofRepository - private credentialRepository: CredentialRepository - private ledgerService: IndyLedgerService - private wallet: Wallet - private logger: Logger - private indyHolderService: IndyHolderService - private indyVerifierService: IndyVerifierService - private indyRevocationService: IndyRevocationService - private connectionService: ConnectionService - private eventEmitter: EventEmitter - - public constructor( - proofRepository: ProofRepository, - ledgerService: IndyLedgerService, - @inject(InjectionSymbols.Wallet) wallet: Wallet, - agentConfig: AgentConfig, - indyHolderService: IndyHolderService, - indyVerifierService: IndyVerifierService, - indyRevocationService: IndyRevocationService, - connectionService: ConnectionService, - eventEmitter: EventEmitter, - credentialRepository: CredentialRepository - ) { - this.proofRepository = proofRepository - this.credentialRepository = credentialRepository - this.ledgerService = ledgerService - this.wallet = wallet - this.logger = agentConfig.logger - this.indyHolderService = indyHolderService - this.indyVerifierService = indyVerifierService - this.indyRevocationService = indyRevocationService - this.connectionService = connectionService - this.eventEmitter = eventEmitter - } - - /** - * Create a {@link ProposePresentationMessage} not bound to an existing presentation exchange. - * To create a proposal as response to an existing presentation exchange, use {@link ProofService.createProposalAsResponse}. - * - * @param connectionRecord The connection for which to create the presentation proposal - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated proof record - * - */ - public async createProposal( - connectionRecord: ConnectionRecord, - presentationProposal: PresentationPreview, - config?: { - comment?: string - autoAcceptProof?: AutoAcceptProof - } - ): Promise> { - // Assert - connectionRecord.assertReady() - - // Create message - const proposalMessage = new ProposePresentationMessage({ - comment: config?.comment, - presentationProposal, - }) - - // Create record - const proofRecord = new ProofRecord({ - connectionId: connectionRecord.id, - threadId: proposalMessage.threadId, - state: ProofState.ProposalSent, - proposalMessage, - autoAcceptProof: config?.autoAcceptProof, - }) - await this.proofRepository.save(proofRecord) - this.eventEmitter.emit({ - type: ProofEventTypes.ProofStateChanged, - payload: { proofRecord, previousState: null }, - }) - - return { message: proposalMessage, proofRecord } - } - - /** - * Create a {@link ProposePresentationMessage} as response to a received presentation request. - * To create a proposal not bound to an existing presentation exchange, use {@link ProofService.createProposal}. - * - * @param proofRecord The proof record for which to create the presentation proposal - * @param presentationProposal The presentation proposal to include in the message - * @param config Additional configuration to use for the proposal - * @returns Object containing proposal message and associated proof record - * - */ - public async createProposalAsResponse( - proofRecord: ProofRecord, - presentationProposal: PresentationPreview, - config?: { - comment?: string - } - ): Promise> { - // Assert - proofRecord.assertState(ProofState.RequestReceived) - - // Create message - const proposalMessage = new ProposePresentationMessage({ - comment: config?.comment, - presentationProposal, - }) - proposalMessage.setThread({ threadId: proofRecord.threadId }) - - // Update record - proofRecord.proposalMessage = proposalMessage - await this.updateState(proofRecord, ProofState.ProposalSent) - - return { message: proposalMessage, proofRecord } - } - - /** - * Decline a proof request - * @param proofRecord The proof request to be declined - */ - public async declineRequest(proofRecord: ProofRecord): Promise { - proofRecord.assertState(ProofState.RequestReceived) - - await this.updateState(proofRecord, ProofState.Declined) - - return proofRecord - } - - /** - * Process a received {@link ProposePresentationMessage}. This will not accept the presentation proposal - * or send a presentation request. It will only create a new, or update the existing proof record with - * the information from the presentation proposal message. Use {@link ProofService.createRequestAsResponse} - * after calling this method to create a presentation request. - * - * @param messageContext The message context containing a presentation proposal message - * @returns proof record associated with the presentation proposal message - * - */ - public async processProposal( - messageContext: InboundMessageContext - ): Promise { - let proofRecord: ProofRecord - const { message: proposalMessage, connection } = messageContext - - this.logger.debug(`Processing presentation proposal with id ${proposalMessage.id}`) - - try { - // Proof record already exists - proofRecord = await this.getByThreadAndConnectionId(proposalMessage.threadId, connection?.id) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proofRecord.proposalMessage, - previousSentMessage: proofRecord.requestMessage, - }) - - // Update record - proofRecord.proposalMessage = proposalMessage - await this.updateState(proofRecord, ProofState.ProposalReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofRecord({ - connectionId: connection?.id, - threadId: proposalMessage.threadId, - proposalMessage, - state: ProofState.ProposalReceived, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save record - await this.proofRepository.save(proofRecord) - this.eventEmitter.emit({ - type: ProofEventTypes.ProofStateChanged, - payload: { - proofRecord, - previousState: null, - }, - }) - } - - return proofRecord - } - - /** - * Create a {@link RequestPresentationMessage} as response to a received presentation proposal. - * To create a request not bound to an existing presentation exchange, use {@link ProofService.createRequest}. - * - * @param proofRecord The proof record for which to create the presentation request - * @param proofRequest The proof request to include in the message - * @param config Additional configuration to use for the request - * @returns Object containing request message and associated proof record - * - */ - public async createRequestAsResponse( - proofRecord: ProofRecord, - proofRequest: ProofRequest, - config?: { - comment?: string - } - ): Promise> { - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - // Assert - proofRecord.assertState(ProofState.ProposalReceived) - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proofRequest), - }), - }) - const requestPresentationMessage = new RequestPresentationMessage({ - comment: config?.comment, - requestPresentationAttachments: [attachment], - }) - requestPresentationMessage.setThread({ - threadId: proofRecord.threadId, - }) - - // Update record - proofRecord.requestMessage = requestPresentationMessage - await this.updateState(proofRecord, ProofState.RequestSent) - - return { message: requestPresentationMessage, proofRecord } - } - - /** - * Create a {@link RequestPresentationMessage} not bound to an existing presentation exchange. - * To create a request as response to an existing presentation exchange, use {@link ProofService#createRequestAsResponse}. - * - * @param proofRequestTemplate The proof request template - * @param connectionRecord The connection for which to create the presentation request - * @returns Object containing request message and associated proof record - * - */ - public async createRequest( - proofRequest: ProofRequest, - connectionRecord?: ConnectionRecord, - config?: { - comment?: string - autoAcceptProof?: AutoAcceptProof - } - ): Promise> { - this.logger.debug(`Creating proof request`) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - // Assert - connectionRecord?.assertReady() - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_REQUEST_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proofRequest), - }), - }) - const requestPresentationMessage = new RequestPresentationMessage({ - comment: config?.comment, - requestPresentationAttachments: [attachment], - }) - - // Create record - const proofRecord = new ProofRecord({ - connectionId: connectionRecord?.id, - threadId: requestPresentationMessage.threadId, - requestMessage: requestPresentationMessage, - state: ProofState.RequestSent, - autoAcceptProof: config?.autoAcceptProof, - }) - - await this.proofRepository.save(proofRecord) - this.eventEmitter.emit({ - type: ProofEventTypes.ProofStateChanged, - payload: { proofRecord, previousState: null }, - }) - - return { message: requestPresentationMessage, proofRecord } - } - - /** - * Process a received {@link RequestPresentationMessage}. This will not accept the presentation request - * or send a presentation. It will only create a new, or update the existing proof record with - * the information from the presentation request message. Use {@link ProofService.createPresentation} - * after calling this method to create a presentation. - * - * @param messageContext The message context containing a presentation request message - * @returns proof record associated with the presentation request message - * - */ - public async processRequest(messageContext: InboundMessageContext): Promise { - let proofRecord: ProofRecord - const { message: proofRequestMessage, connection } = messageContext - - this.logger.debug(`Processing presentation request with id ${proofRequestMessage.id}`) - - const proofRequest = proofRequestMessage.indyProofRequest - - // Assert attachment - if (!proofRequest) { - throw new PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${proofRequestMessage.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - await validateOrReject(proofRequest) - - // Assert attribute and predicate (group) names do not match - checkProofRequestForDuplicates(proofRequest) - - this.logger.debug('received proof request', proofRequest) - - try { - // Proof record already exists - proofRecord = await this.getByThreadAndConnectionId(proofRequestMessage.threadId, connection?.id) - - // Assert - proofRecord.assertState(ProofState.ProposalSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proofRecord.requestMessage, - previousSentMessage: proofRecord.proposalMessage, - }) - - // Update record - proofRecord.requestMessage = proofRequestMessage - await this.updateState(proofRecord, ProofState.RequestReceived) - } catch { - // No proof record exists with thread id - proofRecord = new ProofRecord({ - connectionId: connection?.id, - threadId: proofRequestMessage.threadId, - requestMessage: proofRequestMessage, - state: ProofState.RequestReceived, - }) - - // Assert - this.connectionService.assertConnectionOrServiceDecorator(messageContext) - - // Save in repository - await this.proofRepository.save(proofRecord) - this.eventEmitter.emit({ - type: ProofEventTypes.ProofStateChanged, - payload: { proofRecord, previousState: null }, - }) - } - - return proofRecord - } - - /** - * Create a {@link PresentationMessage} as response to a received presentation request. - * - * @param proofRecord The proof record for which to create the presentation - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @param config Additional configuration to use for the presentation - * @returns Object containing presentation message and associated proof record - * - */ - public async createPresentation( - proofRecord: ProofRecord, - requestedCredentials: RequestedCredentials, - config?: { - comment?: string - } - ): Promise> { - this.logger.debug(`Creating presentation for proof record with id ${proofRecord.id}`) - - // Assert - proofRecord.assertState(ProofState.RequestReceived) - - const indyProofRequest = proofRecord.requestMessage?.indyProofRequest - if (!indyProofRequest) { - throw new PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation with thread id ${proofRecord.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - - // Get the matching attachments to the requested credentials - const attachments = await this.getRequestedAttachmentsForRequestedCredentials( - indyProofRequest, - requestedCredentials - ) - - // Create proof - const proof = await this.createProof(indyProofRequest, requestedCredentials) - - // Create message - const attachment = new Attachment({ - id: INDY_PROOF_ATTACHMENT_ID, - mimeType: 'application/json', - data: new AttachmentData({ - base64: JsonEncoder.toBase64(proof), - }), - }) - - const presentationMessage = new PresentationMessage({ - comment: config?.comment, - presentationAttachments: [attachment], - attachments, - }) - presentationMessage.setThread({ threadId: proofRecord.threadId }) - - // Update record - proofRecord.presentationMessage = presentationMessage - await this.updateState(proofRecord, ProofState.PresentationSent) - - return { message: presentationMessage, proofRecord } - } - - /** - * Process a received {@link PresentationMessage}. This will not accept the presentation - * or send a presentation acknowledgement. It will only update the existing proof record with - * the information from the presentation message. Use {@link ProofService.createAck} - * after calling this method to create a presentation acknowledgement. - * - * @param messageContext The message context containing a presentation message - * @returns proof record associated with the presentation message - * - */ - public async processPresentation(messageContext: InboundMessageContext): Promise { - const { message: presentationMessage, connection } = messageContext - - this.logger.debug(`Processing presentation with id ${presentationMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId(presentationMessage.threadId, connection?.id) - - // Assert - proofRecord.assertState(ProofState.RequestSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proofRecord.proposalMessage, - previousSentMessage: proofRecord.requestMessage, - }) - - // TODO: add proof class with validator - const indyProofJson = presentationMessage.indyProof - const indyProofRequest = proofRecord.requestMessage?.indyProofRequest - - if (!indyProofJson) { - throw new PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation with thread id ${presentationMessage.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - - if (!indyProofRequest) { - throw new PresentationProblemReportError( - `Missing required base64 or json encoded attachment data for presentation request with thread id ${presentationMessage.threadId}`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - - const isValid = await this.verifyProof(indyProofJson, indyProofRequest) - - // Update record - proofRecord.isVerified = isValid - proofRecord.presentationMessage = presentationMessage - await this.updateState(proofRecord, ProofState.PresentationReceived) - - return proofRecord - } - - /** - * Create a {@link PresentationAckMessage} as response to a received presentation. - * - * @param proofRecord The proof record for which to create the presentation acknowledgement - * @returns Object containing presentation acknowledgement message and associated proof record - * - */ - public async createAck(proofRecord: ProofRecord): Promise> { - this.logger.debug(`Creating presentation ack for proof record with id ${proofRecord.id}`) - - // Assert - proofRecord.assertState(ProofState.PresentationReceived) - - // Create message - const ackMessage = new PresentationAckMessage({ - status: AckStatus.OK, - threadId: proofRecord.threadId, - }) - - // Update record - await this.updateState(proofRecord, ProofState.Done) - - return { message: ackMessage, proofRecord } - } - - /** - * Process a received {@link PresentationAckMessage}. - * - * @param messageContext The message context containing a presentation acknowledgement message - * @returns proof record associated with the presentation acknowledgement message - * - */ - public async processAck(messageContext: InboundMessageContext): Promise { - const { message: presentationAckMessage, connection } = messageContext - - this.logger.debug(`Processing presentation ack with id ${presentationAckMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId(presentationAckMessage.threadId, connection?.id) - - // Assert - proofRecord.assertState(ProofState.PresentationSent) - this.connectionService.assertConnectionOrServiceDecorator(messageContext, { - previousReceivedMessage: proofRecord.requestMessage, - previousSentMessage: proofRecord.presentationMessage, - }) - - // Update record - await this.updateState(proofRecord, ProofState.Done) - - return proofRecord - } - - /** - * Process a received {@link PresentationProblemReportMessage}. - * - * @param messageContext The message context containing a presentation problem report message - * @returns proof record associated with the presentation acknowledgement message - * - */ - public async processProblemReport( - messageContext: InboundMessageContext - ): Promise { - const { message: presentationProblemReportMessage } = messageContext - - const connection = messageContext.assertReadyConnection() - - this.logger.debug(`Processing problem report with id ${presentationProblemReportMessage.id}`) - - const proofRecord = await this.getByThreadAndConnectionId(presentationProblemReportMessage.threadId, connection?.id) - - proofRecord.errorMessage = `${presentationProblemReportMessage.description.code}: ${presentationProblemReportMessage.description.en}` - await this.update(proofRecord) - return proofRecord - } - - public async generateProofRequestNonce() { - return this.wallet.generateNonce() - } - - /** - * Create a {@link ProofRequest} from a presentation proposal. This method can be used to create the - * proof request from a received proposal for use in {@link ProofService.createRequestAsResponse} - * - * @param presentationProposal The presentation proposal to create a proof request from - * @param config Additional configuration to use for the proof request - * @returns proof request object - * - */ - public async createProofRequestFromProposal( - presentationProposal: PresentationPreview, - config: { name: string; version: string; nonce?: string } - ): Promise { - const nonce = config.nonce ?? (await this.generateProofRequestNonce()) - - const proofRequest = new ProofRequest({ - name: config.name, - version: config.version, - nonce, - }) - - /** - * Create mapping of attributes by referent. This required the - * attributes to come from the same credential. - * @see https://github.com/hyperledger/aries-rfcs/blob/master/features/0037-present-proof/README.md#referent - * - * { - * "referent1": [Attribute1, Attribute2], - * "referent2": [Attribute3] - * } - */ - const attributesByReferent: Record = {} - for (const proposedAttributes of presentationProposal.attributes) { - if (!proposedAttributes.referent) proposedAttributes.referent = uuid() - - const referentAttributes = attributesByReferent[proposedAttributes.referent] - - // Referent key already exist, add to list - if (referentAttributes) { - referentAttributes.push(proposedAttributes) - } - // Referent key does not exist yet, create new entry - else { - attributesByReferent[proposedAttributes.referent] = [proposedAttributes] - } - } - - // Transform attributes by referent to requested attributes - for (const [referent, proposedAttributes] of Object.entries(attributesByReferent)) { - // Either attributeName or attributeNames will be undefined - const attributeName = proposedAttributes.length == 1 ? proposedAttributes[0].name : undefined - const attributeNames = proposedAttributes.length > 1 ? proposedAttributes.map((a) => a.name) : undefined - - const requestedAttribute = new ProofAttributeInfo({ - name: attributeName, - names: attributeNames, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedAttributes[0].credentialDefinitionId, - }), - ], - }) - - proofRequest.requestedAttributes.set(referent, requestedAttribute) - } - - this.logger.debug('proposal predicates', presentationProposal.predicates) - // Transform proposed predicates to requested predicates - for (const proposedPredicate of presentationProposal.predicates) { - const requestedPredicate = new ProofPredicateInfo({ - name: proposedPredicate.name, - predicateType: proposedPredicate.predicate, - predicateValue: proposedPredicate.threshold, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: proposedPredicate.credentialDefinitionId, - }), - ], - }) - - proofRequest.requestedPredicates.set(uuid(), requestedPredicate) - } - - return proofRequest - } - - /** - * Retrieves the linked attachments for an {@link indyProofRequest} - * @param indyProofRequest The proof request for which the linked attachments have to be found - * @param requestedCredentials The requested credentials - * @returns a list of attachments that are linked to the requested credentials - */ - public async getRequestedAttachmentsForRequestedCredentials( - indyProofRequest: ProofRequest, - requestedCredentials: RequestedCredentials - ): Promise { - const attachments: Attachment[] = [] - const credentialIds = new Set() - const requestedAttributesNames: (string | undefined)[] = [] - - // Get the credentialIds if it contains a hashlink - for (const [referent, requestedAttribute] of Object.entries(requestedCredentials.requestedAttributes)) { - // Find the requested Attributes - const requestedAttributes = indyProofRequest.requestedAttributes.get(referent) as ProofAttributeInfo - - // List the requested attributes - requestedAttributesNames.push(...(requestedAttributes.names ?? [requestedAttributes.name])) - - //Get credentialInfo - if (!requestedAttribute.credentialInfo) { - const indyCredentialInfo = await this.indyHolderService.getCredential(requestedAttribute.credentialId) - requestedAttribute.credentialInfo = JsonTransformer.fromJSON(indyCredentialInfo, IndyCredentialInfo) - } - - // Find the attributes that have a hashlink as a value - for (const attribute of Object.values(requestedAttribute.credentialInfo.attributes)) { - if (attribute.toLowerCase().startsWith('hl:')) { - credentialIds.add(requestedAttribute.credentialId) - } - } - } - - // Only continues if there is an attribute value that contains a hashlink - for (const credentialId of credentialIds) { - // Get the credentialRecord that matches the ID - - const credentialRecord = await this.credentialRepository.getSingleByQuery({ credentialIds: [credentialId] }) - - if (credentialRecord.linkedAttachments) { - // Get the credentials that have a hashlink as value and are requested - const requestedCredentials = credentialRecord.credentialAttributes?.filter( - (credential) => - credential.value.toLowerCase().startsWith('hl:') && requestedAttributesNames.includes(credential.name) - ) - - // Get the linked attachments that match the requestedCredentials - const linkedAttachments = credentialRecord.linkedAttachments.filter((attachment) => - requestedCredentials?.map((credential) => credential.value.split(':')[1]).includes(attachment.id) - ) - - if (linkedAttachments) { - attachments.push(...linkedAttachments) - } - } - } - - return attachments.length ? attachments : undefined - } - - /** - * Create a {@link RetrievedCredentials} object. Given input proof request and presentation proposal, - * use credentials in the wallet to build indy requested credentials object for input to proof creation. - * If restrictions allow, self attested attributes will be used. - * - * - * @param proofRequest The proof request to build the requested credentials object from - * @param presentationProposal Optional presentation proposal to improve credential selection algorithm - * @returns RetrievedCredentials object - */ - public async getRequestedCredentialsForProofRequest( - proofRequest: ProofRequest, - config: { - presentationProposal?: PresentationPreview - filterByNonRevocationRequirements?: boolean - } = {} - ): Promise { - const retrievedCredentials = new RetrievedCredentials({}) - - for (const [referent, requestedAttribute] of proofRequest.requestedAttributes.entries()) { - let credentialMatch: Credential[] = [] - const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) - - // If we have exactly one credential, or no proposal to pick preferences - // on the credentials to use, we will use the first one - if (credentials.length === 1 || !config.presentationProposal) { - credentialMatch = credentials - } - // If we have a proposal we will use that to determine the credentials to use - else { - const names = requestedAttribute.names ?? [requestedAttribute.name] - - // Find credentials that matches all parameters from the proposal - credentialMatch = credentials.filter((credential) => { - const { attributes, credentialDefinitionId } = credential.credentialInfo - - // Check if credentials matches all parameters from proposal - return names.every((name) => - config.presentationProposal?.attributes.find( - (a) => - a.name === name && - a.credentialDefinitionId === credentialDefinitionId && - (!a.value || a.value === attributes[name]) - ) - ) - }) - } - - retrievedCredentials.requestedAttributes[referent] = await Promise.all( - credentialMatch.map(async (credential: Credential) => { - const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ - proofRequest, - requestedItem: requestedAttribute, - credential, - }) - - return new RequestedAttribute({ - credentialId: credential.credentialInfo.referent, - revealed: true, - credentialInfo: credential.credentialInfo, - timestamp: deltaTimestamp, - revoked, - }) - }) - ) - - // We only attach revoked state if non-revocation is requested. So if revoked is true it means - // the credential is not applicable to the proof request - if (config.filterByNonRevocationRequirements) { - retrievedCredentials.requestedAttributes[referent] = retrievedCredentials.requestedAttributes[referent].filter( - (r) => !r.revoked - ) - } - } - - for (const [referent, requestedPredicate] of proofRequest.requestedPredicates.entries()) { - const credentials = await this.getCredentialsForProofRequest(proofRequest, referent) - - retrievedCredentials.requestedPredicates[referent] = await Promise.all( - credentials.map(async (credential) => { - const { revoked, deltaTimestamp } = await this.getRevocationStatusForRequestedItem({ - proofRequest, - requestedItem: requestedPredicate, - credential, - }) - - return new RequestedPredicate({ - credentialId: credential.credentialInfo.referent, - credentialInfo: credential.credentialInfo, - timestamp: deltaTimestamp, - revoked, - }) - }) - ) - - // We only attach revoked state if non-revocation is requested. So if revoked is true it means - // the credential is not applicable to the proof request - if (config.filterByNonRevocationRequirements) { - retrievedCredentials.requestedPredicates[referent] = retrievedCredentials.requestedPredicates[referent].filter( - (r) => !r.revoked - ) - } - } - - return retrievedCredentials - } - - /** - * Takes a RetrievedCredentials object and auto selects credentials in a RequestedCredentials object - * - * Use the return value of this method as input to {@link ProofService.createPresentation} to - * automatically accept a received presentation request. - * - * @param retrievedCredentials The retrieved credentials object to get credentials from - * - * @returns RequestedCredentials - */ - public autoSelectCredentialsForProofRequest(retrievedCredentials: RetrievedCredentials): RequestedCredentials { - const requestedCredentials = new RequestedCredentials({}) - - Object.keys(retrievedCredentials.requestedAttributes).forEach((attributeName) => { - const attributeArray = retrievedCredentials.requestedAttributes[attributeName] - - if (attributeArray.length === 0) { - throw new AriesFrameworkError('Unable to automatically select requested attributes.') - } else { - requestedCredentials.requestedAttributes[attributeName] = attributeArray[0] - } - }) - - Object.keys(retrievedCredentials.requestedPredicates).forEach((attributeName) => { - if (retrievedCredentials.requestedPredicates[attributeName].length === 0) { - throw new AriesFrameworkError('Unable to automatically select requested predicates.') - } else { - requestedCredentials.requestedPredicates[attributeName] = - retrievedCredentials.requestedPredicates[attributeName][0] - } - }) - - return requestedCredentials - } - - /** - * Verify an indy proof object. Will also verify raw values against encodings. - * - * @param proofRequest The proof request to use for proof verification - * @param proofJson The proof object to verify - * @throws {Error} If the raw values do not match the encoded values - * @returns Boolean whether the proof is valid - * - */ - public async verifyProof(proofJson: IndyProof, proofRequest: ProofRequest): Promise { - const proof = JsonTransformer.fromJSON(proofJson, PartialProof) - - for (const [referent, attribute] of proof.requestedProof.revealedAttributes.entries()) { - if (!CredentialUtils.checkValidEncoding(attribute.raw, attribute.encoded)) { - throw new PresentationProblemReportError( - `The encoded value for '${referent}' is invalid. ` + - `Expected '${CredentialUtils.encode(attribute.raw)}'. ` + - `Actual '${attribute.encoded}'`, - { problemCode: PresentationProblemReportReason.Abandoned } - ) - } - } - - // TODO: pre verify proof json - // I'm not 100% sure how much indy does. Also if it checks whether the proof requests matches the proof - // @see https://github.com/hyperledger/aries-cloudagent-python/blob/master/aries_cloudagent/indy/sdk/verifier.py#L79-L164 - - const schemas = await this.getSchemas(new Set(proof.identifiers.map((i) => i.schemaId))) - const credentialDefinitions = await this.getCredentialDefinitions( - new Set(proof.identifiers.map((i) => i.credentialDefinitionId)) - ) - - return await this.indyVerifierService.verifyProof({ - proofRequest: proofRequest.toJSON(), - proof: proofJson, - schemas, - credentialDefinitions, - }) - } - - /** - * Retrieve all proof records - * - * @returns List containing all proof records - */ - public async getAll(): Promise { - return this.proofRepository.getAll() - } - - /** - * Retrieve a proof record by id - * - * @param proofRecordId The proof record id - * @throws {RecordNotFoundError} If no record is found - * @return The proof record - * - */ - public async getById(proofRecordId: string): Promise { - return this.proofRepository.getById(proofRecordId) - } - - /** - * Retrieve a proof record by id - * - * @param proofRecordId The proof record id - * @return The proof record or null if not found - * - */ - public async findById(proofRecordId: string): Promise { - return this.proofRepository.findById(proofRecordId) - } - - /** - * Delete a proof record by id - * - * @param proofId the proof record id - */ - public async deleteById(proofId: string) { - const proofRecord = await this.getById(proofId) - return this.proofRepository.delete(proofRecord) - } - - /** - * Retrieve a proof record by connection id and thread id - * - * @param connectionId The connection id - * @param threadId The thread id - * @throws {RecordNotFoundError} If no record is found - * @throws {RecordDuplicateError} If multiple records are found - * @returns The proof record - */ - public async getByThreadAndConnectionId(threadId: string, connectionId?: string): Promise { - return this.proofRepository.getSingleByQuery({ threadId, connectionId }) - } - - public update(proofRecord: ProofRecord) { - return this.proofRepository.update(proofRecord) - } - - /** - * Create indy proof from a given proof request and requested credential object. - * - * @param proofRequest The proof request to create the proof for - * @param requestedCredentials The requested credentials object specifying which credentials to use for the proof - * @returns indy proof object - */ - private async createProof( - proofRequest: ProofRequest, - requestedCredentials: RequestedCredentials - ): Promise { - const credentialObjects = await Promise.all( - [ - ...Object.values(requestedCredentials.requestedAttributes), - ...Object.values(requestedCredentials.requestedPredicates), - ].map(async (c) => { - if (c.credentialInfo) { - return c.credentialInfo - } - const credentialInfo = await this.indyHolderService.getCredential(c.credentialId) - return JsonTransformer.fromJSON(credentialInfo, IndyCredentialInfo) - }) - ) - - const schemas = await this.getSchemas(new Set(credentialObjects.map((c) => c.schemaId))) - const credentialDefinitions = await this.getCredentialDefinitions( - new Set(credentialObjects.map((c) => c.credentialDefinitionId)) - ) - - return this.indyHolderService.createProof({ - proofRequest: proofRequest.toJSON(), - requestedCredentials: requestedCredentials, - schemas, - credentialDefinitions, - }) - } - - private async getCredentialsForProofRequest( - proofRequest: ProofRequest, - attributeReferent: string - ): Promise { - const credentialsJson = await this.indyHolderService.getCredentialsForProofRequest({ - proofRequest: proofRequest.toJSON(), - attributeReferent, - }) - - return JsonTransformer.fromJSON(credentialsJson, Credential) as unknown as Credential[] - } - - private async getRevocationStatusForRequestedItem({ - proofRequest, - requestedItem, - credential, - }: { - proofRequest: ProofRequest - requestedItem: ProofAttributeInfo | ProofPredicateInfo - credential: Credential - }) { - const requestNonRevoked = requestedItem.nonRevoked ?? proofRequest.nonRevoked - const credentialRevocationId = credential.credentialInfo.credentialRevocationId - const revocationRegistryId = credential.credentialInfo.revocationRegistryId - - // If revocation interval is present and the credential is revocable then fetch the revocation status of credentials for display - if (requestNonRevoked && credentialRevocationId && revocationRegistryId) { - this.logger.trace( - `Presentation is requesting proof of non revocation, getting revocation status for credential`, - { - requestNonRevoked, - credentialRevocationId, - revocationRegistryId, - } - ) - - // Note presentation from-to's vs ledger from-to's: https://github.com/hyperledger/indy-hipe/blob/master/text/0011-cred-revocation/README.md#indy-node-revocation-registry-intervals - const status = await this.indyRevocationService.getRevocationStatus( - credentialRevocationId, - revocationRegistryId, - requestNonRevoked - ) - - return status - } - - return { revoked: undefined, deltaTimestamp: undefined } - } - - /** - * Update the record to a new state and emit an state changed event. Also updates the record - * in storage. - * - * @param proofRecord The proof record to update the state for - * @param newState The state to update to - * - */ - private async updateState(proofRecord: ProofRecord, newState: ProofState) { - const previousState = proofRecord.state - proofRecord.state = newState - await this.proofRepository.update(proofRecord) - - this.eventEmitter.emit({ - type: ProofEventTypes.ProofStateChanged, - payload: { proofRecord, previousState: previousState }, - }) - } - - /** - * Build schemas object needed to create and verify proof objects. - * - * Creates object with `{ schemaId: Schema }` mapping - * - * @param schemaIds List of schema ids - * @returns Object containing schemas for specified schema ids - * - */ - private async getSchemas(schemaIds: Set) { - const schemas: { [key: string]: Schema } = {} - - for (const schemaId of schemaIds) { - const schema = await this.ledgerService.getSchema(schemaId) - schemas[schemaId] = schema - } - - return schemas - } - - /** - * Build credential definitions object needed to create and verify proof objects. - * - * Creates object with `{ credentialDefinitionId: CredentialDefinition }` mapping - * - * @param credentialDefinitionIds List of credential definition ids - * @returns Object containing credential definitions for specified credential definition ids - * - */ - private async getCredentialDefinitions(credentialDefinitionIds: Set) { - const credentialDefinitions: { [key: string]: CredDef } = {} - - for (const credDefId of credentialDefinitionIds) { - const credDef = await this.ledgerService.getCredentialDefinition(credDefId) - credentialDefinitions[credDefId] = credDef - } - - return credentialDefinitions - } -} - -export interface ProofRequestTemplate { - proofRequest: ProofRequest - comment?: string -} - -export interface ProofProtocolMsgReturnType { - message: MessageType - proofRecord: ProofRecord -} 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 0233c56665..0000000000 --- a/packages/core/src/modules/proofs/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ProofService' diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index a8327e36a2..49833c595c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -82,6 +82,7 @@ export interface InitConfig { autoUpdateStorageOnStartup?: boolean } +export type ProtocolVersion = `${number}.${number}` export interface PlaintextMessage { '@type': string '@id': string diff --git a/packages/core/src/utils/__tests__/indyProofRequest.test.ts b/packages/core/src/utils/__tests__/indyProofRequest.test.ts index 547745bcc7..5b08490d17 100644 --- a/packages/core/src/utils/__tests__/indyProofRequest.test.ts +++ b/packages/core/src/utils/__tests__/indyProofRequest.test.ts @@ -1,4 +1,4 @@ -import { checkProofRequestForDuplicates } from '..' +import { checkProofRequestForDuplicates } from '../indyProofRequest' import { AriesFrameworkError, diff --git a/packages/core/src/utils/indyProofRequest.ts b/packages/core/src/utils/indyProofRequest.ts index 7c24a4b05c..bf74c38d96 100644 --- a/packages/core/src/utils/indyProofRequest.ts +++ b/packages/core/src/utils/indyProofRequest.ts @@ -1,4 +1,4 @@ -import type { ProofRequest } from '../modules/proofs/models/ProofRequest' +import type { ProofRequest } from '../modules/proofs/formats/indy/models/ProofRequest' import { assertNoDuplicatesInArray } from './assertNoDuplicates' diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 53cd3402c1..fe43ffecfb 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -8,13 +8,14 @@ import type { CredentialDefinitionTemplate, CredentialStateChangedEvent, InitConfig, - ProofAttributeInfo, - ProofPredicateInfo, ProofStateChangedEvent, SchemaTemplate, + ProofPredicateInfo, + ProofAttributeInfo, } from '../src' import type { AcceptOfferOptions, OfferCredentialOptions } from '../src/modules/credentials/CredentialsModuleOptions' import type { CredentialOfferTemplate } from '../src/modules/credentials/protocol' +import type { AcceptPresentationOptions, RequestProofOptions } from '../src/modules/proofs/models/ModuleOptions' import type { Schema, CredDef } from 'indy-sdk' import type { Observable } from 'rxjs' @@ -26,9 +27,7 @@ import { SubjectInboundTransport } from '../../../tests/transport/SubjectInbound import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' import { agentDependencies, WalletScheme } from '../../node/src' import { - PresentationPreview, - PresentationPreviewAttribute, - PresentationPreviewPredicate, + ProofProtocolVersion, HandshakeProtocol, DidExchangeState, DidExchangeRole, @@ -54,6 +53,11 @@ import { OutOfBandRole } from '../src/modules/oob/domain/OutOfBandRole' import { OutOfBandState } from '../src/modules/oob/domain/OutOfBandState' import { OutOfBandInvitation } from '../src/modules/oob/messages' import { OutOfBandRecord } from '../src/modules/oob/repository' +import { + PresentationPreview, + PresentationPreviewAttribute, + PresentationPreviewPredicate, +} from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' import { LinkedAttachment } from '../src/utils/LinkedAttachment' import { uuid } from '../src/utils/uuid' @@ -539,20 +543,39 @@ export async function presentProof({ verifierAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(verifierReplay) holderAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(holderReplay) - let verifierRecord = await verifierAgent.proofs.requestProof(verifierConnectionId, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: verifierConnectionId, + proofFormats: { + indy: { + name: 'test-proof-request', + requestedAttributes: attributes, + requestedPredicates: predicates, + version: '1.0', + nonce: '947121108704767252195123', + }, + }, + } + + let verifierRecord = await verifierAgent.proofs.requestProof(requestProofsOptions) let holderRecord = await waitForProofRecordSubject(holderReplay, { threadId: verifierRecord.threadId, state: ProofState.RequestReceived, }) - const retrievedCredentials = await holderAgent.proofs.getRequestedCredentialsForProofRequest(holderRecord.id) - const requestedCredentials = holderAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await holderAgent.proofs.acceptRequest(holderRecord.id, requestedCredentials) + const requestedCredentials = await holderAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: holderRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: holderRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + await holderAgent.proofs.acceptRequest(acceptPresentationOptions) verifierRecord = await waitForProofRecordSubject(verifierReplay, { threadId: holderRecord.threadId, diff --git a/packages/core/tests/proofs.test.ts b/packages/core/tests/proofs.test.ts deleted file mode 100644 index 38c96c03ce..0000000000 --- a/packages/core/tests/proofs.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -import type { Agent, ConnectionRecord, PresentationPreview } from '../src' -import type { CredDefId } from 'indy-sdk' - -import { - AttributeFilter, - JsonTransformer, - PredicateType, - PresentationMessage, - ProofAttributeInfo, - ProofPredicateInfo, - ProofRecord, - ProofState, - ProposePresentationMessage, - RequestPresentationMessage, -} from '../src' - -import { setupProofsTest, waitForProofRecord } from './helpers' -import testLogger from './logger' - -describe('Present Proof', () => { - let faberAgent: Agent - let aliceAgent: Agent - let credDefId: CredDefId - let faberConnection: ConnectionRecord - let aliceConnection: ConnectionRecord - let presentationPreview: PresentationPreview - - beforeAll(async () => { - testLogger.test('Initializing the agents') - ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = - await setupProofsTest('Faber agent', 'Alice agent')) - testLogger.test('Issuing second credential') - }) - - afterAll(async () => { - testLogger.test('Shutting down both agents') - await faberAgent.shutdown() - await faberAgent.wallet.delete() - await aliceAgent.shutdown() - await aliceAgent.wallet.delete() - }) - - test('Alice starts with proof proposal to Faber', async () => { - // Alice sends a presentation proposal to Faber - testLogger.test('Alice sends a presentation proposal to Faber') - let aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview) - - // Faber waits for a presentation proposal from Alice - testLogger.test('Faber waits for a presentation proposal from Alice') - let faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.ProposalReceived, - }) - - expect(JsonTransformer.toJSON(aliceProofRecord)).toMatchObject({ - createdAt: expect.any(String), - id: expect.any(String), - proposalMessage: { - '@type': 'https://didcomm.org/present-proof/1.0/propose-presentation', - '@id': expect.any(String), - presentation_proposal: { - '@type': 'https://didcomm.org/present-proof/1.0/presentation-preview', - attributes: [ - { - name: 'name', - value: 'John', - }, - { - name: 'image_0', - value: undefined, - }, - ], - predicates: [ - { - name: 'age', - predicate: '>=', - threshold: 50, - }, - ], - }, - }, - }) - - // Faber accepts the presentation proposal from Alice - testLogger.test('Faber accepts the presentation proposal from Alice') - faberProofRecord = await faberAgent.proofs.acceptProposal(faberProofRecord.id) - - // Alice waits for presentation request from Faber - testLogger.test('Alice waits for presentation request from Faber') - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.RequestReceived, - }) - - // Alice retrieves the requested credentials and accepts the presentation request - testLogger.test('Alice accepts presentation request from Faber') - const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(aliceProofRecord.id, { - filterByPresentationPreview: true, - }) - const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials) - - // Faber waits for the presentation from Alice - testLogger.test('Faber waits for presentation from Alice') - faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.PresentationReceived, - }) - expect(JsonTransformer.toJSON(faberProofRecord)).toMatchObject({ - createdAt: expect.any(String), - state: ProofState.PresentationReceived, - isVerified: true, - presentationMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/present-proof/1.0/presentation', - 'presentations~attach': [ - { - '@id': 'libindy-presentation-0', - 'mime-type': 'application/json', - }, - ], - '~attach': [ - { - '@id': expect.any(String), - filename: 'picture-of-a-cat.png', - }, - ], - }, - }) - - expect(aliceProofRecord).toMatchObject({ - type: ProofRecord.name, - id: expect.any(String), - _tags: { - threadId: faberProofRecord.threadId, - connectionId: aliceProofRecord.connectionId, - state: ProofState.ProposalSent, - }, - }) - - // Faber accepts the presentation provided by Alice - testLogger.test('Faber accepts the presentation provided by Alice') - await faberAgent.proofs.acceptPresentation(faberProofRecord.id) - - // Alice waits until she received a presentation acknowledgement - testLogger.test('Alice waits until she receives a presentation acknowledgement') - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.Done, - }) - - expect(faberProofRecord).toMatchObject({ - type: ProofRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: aliceProofRecord.threadId, - connectionId: expect.any(String), - isVerified: true, - state: ProofState.PresentationReceived, - proposalMessage: expect.any(ProposePresentationMessage), - requestMessage: expect.any(RequestPresentationMessage), - presentationMessage: expect.any(PresentationMessage), - }) - - expect(aliceProofRecord).toMatchObject({ - type: ProofRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: faberProofRecord.threadId, - connectionId: expect.any(String), - state: ProofState.Done, - proposalMessage: expect.any(ProposePresentationMessage), - requestMessage: expect.any(RequestPresentationMessage), - presentationMessage: expect.any(PresentationMessage), - }) - }) - - test('Faber starts with proof request to Alice', async () => { - // Sample attributes - const attributes = { - name: new ProofAttributeInfo({ - name: 'name', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - image_0: new ProofAttributeInfo({ - name: 'image_0', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - image_1: new ProofAttributeInfo({ - name: 'image_1', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - // Sample predicates - const predicates = { - age: new ProofPredicateInfo({ - name: 'age', - predicateType: PredicateType.GreaterThanOrEqualTo, - predicateValue: 50, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - // Faber sends a presentation request to Alice - testLogger.test('Faber sends a presentation request to Alice') - let faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) - - // Alice waits for presentation request from Faber - testLogger.test('Alice waits for presentation request from Faber') - let aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: faberProofRecord.threadId, - state: ProofState.RequestReceived, - }) - - expect(JsonTransformer.toJSON(aliceProofRecord)).toMatchObject({ - id: expect.any(String), - createdAt: expect.any(String), - requestMessage: { - '@id': expect.any(String), - '@type': 'https://didcomm.org/present-proof/1.0/request-presentation', - 'request_presentations~attach': [ - { - '@id': 'libindy-request-presentation-0', - 'mime-type': 'application/json', - }, - ], - }, - }) - - // Alice retrieves the requested credentials and accepts the presentation request - testLogger.test('Alice accepts presentation request from Faber') - const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(aliceProofRecord.id, { - filterByPresentationPreview: true, - }) - const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials) - - // Faber waits until it receives a presentation from Alice - testLogger.test('Faber waits for presentation from Alice') - faberProofRecord = await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.PresentationReceived, - }) - - expect(faberProofRecord).toMatchObject({ - id: expect.any(String), - createdAt: expect.any(Date), - state: ProofState.PresentationReceived, - requestMessage: expect.any(RequestPresentationMessage), - isVerified: true, - presentationMessage: { - type: 'https://didcomm.org/present-proof/1.0/presentation', - id: expect.any(String), - presentationAttachments: [ - { - id: 'libindy-presentation-0', - mimeType: 'application/json', - }, - ], - appendedAttachments: [ - { - id: 'zQmfDXo7T3J43j3CTkEZaz7qdHuABhWktksZ7JEBueZ5zUS', - filename: 'picture-of-a-cat.png', - data: { - base64: expect.any(String), - }, - }, - { - id: 'zQmRHBT9rDs5QhsnYuPY3mNpXxgLcnNXkhjWJvTSAPMmcVd', - filename: 'picture-of-a-dog.png', - }, - ], - thread: { - threadId: aliceProofRecord.threadId, - }, - }, - }) - - // Faber accepts the presentation - testLogger.test('Faber accept the presentation from Alice') - await faberAgent.proofs.acceptPresentation(faberProofRecord.id) - - // Alice waits until she receives a presentation acknowledgement - testLogger.test('Alice waits for acceptance by Faber') - aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.threadId, - state: ProofState.Done, - }) - - expect(faberProofRecord).toMatchObject({ - type: ProofRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: aliceProofRecord.threadId, - connectionId: expect.any(String), - isVerified: true, - state: ProofState.PresentationReceived, - requestMessage: expect.any(RequestPresentationMessage), - presentationMessage: expect.any(PresentationMessage), - }) - - expect(aliceProofRecord).toMatchObject({ - type: ProofRecord.name, - id: expect.any(String), - createdAt: expect.any(Date), - threadId: faberProofRecord.threadId, - connectionId: expect.any(String), - state: ProofState.Done, - requestMessage: expect.any(RequestPresentationMessage), - presentationMessage: expect.any(PresentationMessage), - }) - }) - - test('an attribute group name matches with a predicate group name so an error is thrown', async () => { - // Age attribute - const attributes = { - age: new ProofAttributeInfo({ - name: 'age', - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - // Age predicate - const predicates = { - age: new ProofPredicateInfo({ - name: 'age', - predicateType: PredicateType.GreaterThanOrEqualTo, - predicateValue: 50, - restrictions: [ - new AttributeFilter({ - credentialDefinitionId: credDefId, - }), - ], - }), - } - - await expect( - faberAgent.proofs.requestProof(faberConnection.id, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) - ).rejects.toThrowError(`The proof request contains duplicate items: age`) - }) -}) diff --git a/packages/core/tests/connectionless-proofs.test.ts b/packages/core/tests/v1-connectionless-proofs.test.ts similarity index 81% rename from packages/core/tests/connectionless-proofs.test.ts rename to packages/core/tests/v1-connectionless-proofs.test.ts index f6bee43a02..4f97d1c2bb 100644 --- a/packages/core/tests/connectionless-proofs.test.ts +++ b/packages/core/tests/v1-connectionless-proofs.test.ts @@ -1,5 +1,6 @@ import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' import type { ProofStateChangedEvent } from '../src/modules/proofs' +import type { AcceptPresentationOptions, OutOfBandRequestOptions } from '../src/modules/proofs/models/ModuleOptions' import { Subject, ReplaySubject } from 'rxjs' @@ -10,6 +11,7 @@ import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachm import { HandshakeProtocol } from '../src/modules/connections' import { V1CredentialPreview } from '../src/modules/credentials' import { + ProofProtocolVersion, PredicateType, ProofState, ProofAttributeInfo, @@ -76,14 +78,25 @@ describe('Present Proof', () => { }), } + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + // eslint-disable-next-line prefer-const - let { proofRecord: faberProofRecord, requestMessage } = await faberAgent.proofs.createOutOfBandRequest({ - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions + ) - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.receiveMessage(message.toJSON()) testLogger.test('Alice waits for presentation request from Faber') let aliceProofRecord = await waitForProofRecordSubject(aliceReplay, { @@ -92,11 +105,19 @@ describe('Present Proof', () => { }) testLogger.test('Alice accepts presentation request from Faber') - const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(aliceProofRecord.id, { - filterByPresentationPreview: true, + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, }) - const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) testLogger.test('Faber waits for presentation from Alice') faberProofRecord = await waitForProofRecordSubject(faberReplay, { @@ -152,19 +173,26 @@ describe('Present Proof', () => { }), } - // eslint-disable-next-line prefer-const - let { proofRecord: faberProofRecord, requestMessage } = await faberAgent.proofs.createOutOfBandRequest( - { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, }, - { - autoAcceptProof: AutoAcceptProof.ContentApproved, - } + autoAcceptProof: AutoAcceptProof.ContentApproved, + } + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions ) - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.receiveMessage(message.toJSON()) await waitForProofRecordSubject(aliceReplay, { threadId: faberProofRecord.threadId, @@ -306,16 +334,22 @@ describe('Present Proof', () => { }), } - // eslint-disable-next-line prefer-const - let { proofRecord: faberProofRecord, requestMessage } = await faberAgent.proofs.createOutOfBandRequest( - { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, }, - { - autoAcceptProof: AutoAcceptProof.ContentApproved, - } + autoAcceptProof: AutoAcceptProof.ContentApproved, + } + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions ) const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator() @@ -323,7 +357,7 @@ describe('Present Proof', () => { throw new Error('Faber agent has no default mediator') } - expect(requestMessage).toMatchObject({ + expect(message).toMatchObject({ service: { recipientKeys: [expect.any(String)], routingKeys: mediationRecord.routingKeys, @@ -331,7 +365,7 @@ describe('Present Proof', () => { }, }) - await aliceAgent.receiveMessage(requestMessage.toJSON()) + await aliceAgent.receiveMessage(message.toJSON()) await waitForProofRecordSubject(aliceReplay, { threadId: faberProofRecord.threadId, diff --git a/packages/core/tests/v1-indy-proofs.test.ts b/packages/core/tests/v1-indy-proofs.test.ts new file mode 100644 index 0000000000..c6873046b4 --- /dev/null +++ b/packages/core/tests/v1-indy-proofs.test.ts @@ -0,0 +1,579 @@ +import type { Agent, ConnectionRecord, ProofRecord } from '../src' +import type { + AcceptPresentationOptions, + AcceptProposalOptions, + ProposeProofOptions, + RequestProofOptions, +} from '../src/modules/proofs/models/ModuleOptions' +import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import type { CredDefId } from 'indy-sdk' + +import { + V1PresentationMessage, + V1RequestPresentationMessage, + V1ProposePresentationMessage, + AttributeFilter, + PredicateType, + ProofAttributeInfo, + ProofPredicateInfo, + ProofState, +} from '../src' +import { ProofProtocolVersion } from '../src/modules/proofs/models/ProofProtocolVersion' +import { DidCommMessageRepository } from '../src/storage/didcomm' + +import { setupProofsTest, waitForProofRecord } from './helpers' +import testLogger from './logger' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: CredDefId + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let presentationPreview: PresentationPreview + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest('Faber agent', 'Alice agent')) + testLogger.test('Issuing second credential') + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + const proposeProofOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + nonce: '947121108704767252195126', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + } + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeProofOptions) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1ProposePresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/propose-presentation', + id: expect.any(String), + presentationProposal: { + type: 'https://didcomm.org/present-proof/1.0/presentation-preview', + attributes: [ + { + name: 'name', + credentialDefinitionId: presentationPreview.attributes[0].credentialDefinitionId, + value: 'John', + referent: '0', + }, + { + name: 'image_0', + credentialDefinitionId: presentationPreview.attributes[1].credentialDefinitionId, + }, + ], + predicates: [ + { + name: 'age', + credentialDefinitionId: presentationPreview.predicates[0].credentialDefinitionId, + predicate: '>=', + threshold: 50, + }, + ], + }, + }) + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestPresentationAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: faberProofRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + appendedAttachments: [ + { + id: expect.any(String), + filename: expect.any(String), + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + image_0: new ProofAttributeInfo({ + name: 'image_0', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Sample predicates + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestPresentationAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + + expect(aliceProofRecord.id).not.toBeNull() + expect(aliceProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/presentation', + id: expect.any(String), + presentationAttachments: [ + { + id: 'libindy-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + appendedAttachments: [ + { + id: expect.any(String), + filename: expect.any(String), + data: { + base64: expect.any(String), + }, + }, + ], + thread: { + threadId: expect.any(String), + }, + }) + + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('an attribute group name matches with a predicate group name so an error is thrown', async () => { + // Age attribute + const attributes = { + age: new ProofAttributeInfo({ + name: 'age', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Age predicate + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + await expect(faberAgent.proofs.requestProof(requestProofsOptions)).rejects.toThrowError( + `The proof request contains duplicate items: age` + ) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + image_0: new ProofAttributeInfo({ + name: 'image_0', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Sample predicates + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V1RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/1.0/request-presentation', + id: expect.any(String), + requestPresentationAttachments: [ + { + id: 'libindy-request-presentation-0', + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + }) + + expect(aliceProofRecord.id).not.toBeNull() + expect(aliceProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V1, + }) + + aliceProofRecord = await aliceAgent.proofs.sendProblemReport(aliceProofRecord.id, 'Problem inside proof request') + + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Abandoned, + }) + + expect(faberProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: ProofProtocolVersion.V1, + }) + }) +}) diff --git a/packages/core/tests/v1-proofs-auto-accept.test.ts b/packages/core/tests/v1-proofs-auto-accept.test.ts new file mode 100644 index 0000000000..334b2465b3 --- /dev/null +++ b/packages/core/tests/v1-proofs-auto-accept.test.ts @@ -0,0 +1,254 @@ +import type { Agent, ConnectionRecord } from '../src' +import type { + AcceptProposalOptions, + ProposeProofOptions, + RequestProofOptions, +} from '../src/modules/proofs/models/ModuleOptions' +import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' + +import { + AutoAcceptProof, + ProofState, + ProofAttributeInfo, + AttributeFilter, + ProofPredicateInfo, + PredicateType, +} from '../src' +import { ProofProtocolVersion } from '../src/modules/proofs/models/ProofProtocolVersion' + +import { setupProofsTest, waitForProofRecord } from './helpers' +import testLogger from './logger' + +describe('Auto accept present proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: string + let faberConnection: ConnectionRecord + let aliceConnection: ConnectionRecord + let presentationPreview: PresentationPreview + + describe('Auto accept on `always`', () => { + beforeAll(async () => { + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest( + 'Faber Auto Accept Always Proofs', + 'Alice Auto Accept Always Proofs', + AutoAcceptProof.Always + )) + }) + afterAll(async () => { + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `always`', async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + const proposeProofOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + nonce: '58d223e5-fc4d-4448-b74c-5eb11c6b558f', + name: 'abc', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + } + + const aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeProofOptions) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + testLogger.test('Alice waits till it receives presentation ack') + await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with proof requests to Alice, both with autoAcceptProof on `always`', async () => { + testLogger.test('Faber sends presentation request to Alice') + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + const faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + }) + }) + + describe('Auto accept on `contentApproved`', () => { + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest( + 'Faber Auto Accept Content Approved Proofs', + 'Alice Auto Accept Content Approved Proofs', + AutoAcceptProof.ContentApproved + )) + }) + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber, both with autoacceptproof on `contentApproved`', async () => { + testLogger.test('Alice sends presentation proposal to Faber') + + const proposal: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V1, + proofFormats: { + indy: { + nonce: '1298236324864', + name: 'abc', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + } + + const aliceProofRecord = await aliceAgent.proofs.proposeProof(proposal) + + testLogger.test('Faber waits for presentation proposal from Alice') + + const faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + testLogger.test('Faber accepts presentation proposal from Alice') + + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + testLogger.test('Faber waits for presentation from Alice') + + await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with proof requests to Alice, both with autoacceptproof on `contentApproved`', async () => { + testLogger.test('Faber sends presentation request to Alice') + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V1, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324866', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + const faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + + testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + // Alice waits till it receives presentation ack + await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + }) + }) +}) diff --git a/packages/core/tests/v2-connectionless-proofs.test.ts b/packages/core/tests/v2-connectionless-proofs.test.ts new file mode 100644 index 0000000000..c469ba69e5 --- /dev/null +++ b/packages/core/tests/v2-connectionless-proofs.test.ts @@ -0,0 +1,381 @@ +import type { SubjectMessage } from '../../../tests/transport/SubjectInboundTransport' +import type { ProofStateChangedEvent } from '../src/modules/proofs' +import type { AcceptPresentationOptions, OutOfBandRequestOptions } from '../src/modules/proofs/models/ModuleOptions' + +import { Subject, ReplaySubject } from 'rxjs' + +import { SubjectInboundTransport } from '../../../tests/transport/SubjectInboundTransport' +import { SubjectOutboundTransport } from '../../../tests/transport/SubjectOutboundTransport' +import { Agent } from '../src/agent/Agent' +import { Attachment, AttachmentData } from '../src/decorators/attachment/Attachment' +import { HandshakeProtocol } from '../src/modules/connections/models/HandshakeProtocol' +import { V1CredentialPreview } from '../src/modules/credentials/protocol/v1/V1CredentialPreview' +import { + PredicateType, + ProofState, + ProofAttributeInfo, + AttributeFilter, + ProofPredicateInfo, + AutoAcceptProof, + ProofEventTypes, +} from '../src/modules/proofs' +import { ProofProtocolVersion } from '../src/modules/proofs/models/ProofProtocolVersion' +import { MediatorPickupStrategy } from '../src/modules/routing/MediatorPickupStrategy' +import { LinkedAttachment } from '../src/utils/LinkedAttachment' +import { uuid } from '../src/utils/uuid' + +import { + getBaseConfig, + issueCredential, + makeConnection, + prepareForIssuance, + setupProofsTest, + waitForProofRecordSubject, +} from './helpers' +import testLogger from './logger' + +describe('Present Proof', () => { + let agents: Agent[] + + afterEach(async () => { + for (const agent of agents) { + await agent.shutdown() + await agent.wallet.delete() + } + }) + + test('Faber starts with connection-less proof requests to Alice', async () => { + const { aliceAgent, faberAgent, aliceReplay, credDefId, faberReplay } = await setupProofsTest( + 'Faber connection-less Proofs', + 'Alice connection-less Proofs', + AutoAcceptProof.Never + ) + agents = [aliceAgent, faberAgent] + testLogger.test('Faber sends presentation request to Alice') + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions + ) + + await aliceAgent.receiveMessage(message.toJSON()) + + testLogger.test('Alice waits for presentation request from Faber') + let aliceProofRecord = await waitForProofRecordSubject(aliceReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecordSubject(faberReplay, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + // assert presentation is valid + expect(faberProofRecord.isVerified).toBe(true) + + // Faber accepts presentation + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits till it receives presentation ack + aliceProofRecord = await waitForProofRecordSubject(aliceReplay, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const { aliceAgent, faberAgent, aliceReplay, credDefId, faberReplay } = await setupProofsTest( + 'Faber connection-less Proofs - Auto Accept', + 'Alice connection-less Proofs - Auto Accept', + AutoAcceptProof.Always + ) + + agents = [aliceAgent, faberAgent] + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + } + + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions + ) + + await aliceAgent.receiveMessage(message.toJSON()) + + await waitForProofRecordSubject(aliceReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + await waitForProofRecordSubject(faberReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + }) + + test('Faber starts with connection-less proof requests to Alice with auto-accept enabled and both agents having a mediator', async () => { + testLogger.test('Faber sends presentation request to Alice') + + const credentialPreview = V1CredentialPreview.fromRecord({ + name: 'John', + age: '99', + }) + + const unique = uuid().substring(0, 4) + + const mediatorConfig = getBaseConfig(`Connectionless proofs with mediator Mediator-${unique}`, { + autoAcceptMediationRequests: true, + endpoints: ['rxjs:mediator'], + }) + + const faberMessages = new Subject() + const aliceMessages = new Subject() + const mediatorMessages = new Subject() + + const subjectMap = { + 'rxjs:mediator': mediatorMessages, + } + + // Initialize mediator + const mediatorAgent = new Agent(mediatorConfig.config, mediatorConfig.agentDependencies) + mediatorAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + mediatorAgent.registerInboundTransport(new SubjectInboundTransport(mediatorMessages)) + await mediatorAgent.initialize() + + const faberMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'faber invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const aliceMediationOutOfBandRecord = await mediatorAgent.oob.createInvitation({ + label: 'alice invitation', + handshakeProtocols: [HandshakeProtocol.Connections], + }) + + const faberConfig = getBaseConfig(`Connectionless proofs with mediator Faber-${unique}`, { + autoAcceptProofs: AutoAcceptProof.Always, + mediatorConnectionsInvite: faberMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }) + + const aliceConfig = getBaseConfig(`Connectionless proofs with mediator Alice-${unique}`, { + autoAcceptProofs: AutoAcceptProof.Always, + // logger: new TestLogger(LogLevel.test), + mediatorConnectionsInvite: aliceMediationOutOfBandRecord.outOfBandInvitation.toUrl({ + domain: 'https://example.com', + }), + mediatorPickupStrategy: MediatorPickupStrategy.PickUpV1, + }) + + const faberAgent = new Agent(faberConfig.config, faberConfig.agentDependencies) + faberAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + faberAgent.registerInboundTransport(new SubjectInboundTransport(faberMessages)) + await faberAgent.initialize() + + const aliceAgent = new Agent(aliceConfig.config, aliceConfig.agentDependencies) + aliceAgent.registerOutboundTransport(new SubjectOutboundTransport(subjectMap)) + aliceAgent.registerInboundTransport(new SubjectInboundTransport(aliceMessages)) + await aliceAgent.initialize() + + agents = [aliceAgent, faberAgent, mediatorAgent] + + const { definition } = await prepareForIssuance(faberAgent, ['name', 'age', 'image_0', 'image_1']) + + const [faberConnection, aliceConnection] = await makeConnection(faberAgent, aliceAgent) + expect(faberConnection.isReady).toBe(true) + expect(aliceConnection.isReady).toBe(true) + + await issueCredential({ + issuerAgent: faberAgent, + issuerConnectionId: faberConnection.id, + holderAgent: aliceAgent, + credentialTemplate: { + credentialDefinitionId: definition.id, + comment: 'some comment about credential', + preview: credentialPreview, + linkedAttachments: [ + new LinkedAttachment({ + name: 'image_0', + attachment: new Attachment({ + filename: 'picture-of-a-cat.png', + data: new AttachmentData({ base64: 'cGljdHVyZSBvZiBhIGNhdA==' }), + }), + }), + new LinkedAttachment({ + name: 'image_1', + attachment: new Attachment({ + filename: 'picture-of-a-dog.png', + data: new AttachmentData({ base64: 'UGljdHVyZSBvZiBhIGRvZw==' }), + }), + }), + ], + }, + }) + const faberReplay = new ReplaySubject() + const aliceReplay = new ReplaySubject() + + faberAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(faberReplay) + aliceAgent.events.observable(ProofEventTypes.ProofStateChanged).subscribe(aliceReplay) + + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: definition.id, + }), + ], + }), + } + + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: definition.id, + }), + ], + }), + } + + const outOfBandRequestOptions: OutOfBandRequestOptions = { + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'test-proof-request', + version: '1.0', + nonce: '12345678901', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + autoAcceptProof: AutoAcceptProof.ContentApproved, + } + // eslint-disable-next-line prefer-const + let { proofRecord: faberProofRecord, message } = await faberAgent.proofs.createOutOfBandRequest( + outOfBandRequestOptions + ) + + const mediationRecord = await faberAgent.mediationRecipient.findDefaultMediator() + if (!mediationRecord) { + throw new Error('Faber agent has no default mediator') + } + + expect(message).toMatchObject({ + service: { + recipientKeys: [expect.any(String)], + routingKeys: mediationRecord.routingKeys, + serviceEndpoint: mediationRecord.endpoint, + }, + }) + + await aliceAgent.receiveMessage(message.toJSON()) + + await waitForProofRecordSubject(aliceReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + + await waitForProofRecordSubject(faberReplay, { + threadId: faberProofRecord.threadId, + state: ProofState.Done, + }) + }) +}) diff --git a/packages/core/tests/v2-indy-proofs.test.ts b/packages/core/tests/v2-indy-proofs.test.ts new file mode 100644 index 0000000000..ff034371d6 --- /dev/null +++ b/packages/core/tests/v2-indy-proofs.test.ts @@ -0,0 +1,531 @@ +import type { Agent, ConnectionRecord, ProofRecord } from '../src' +import type { + AcceptPresentationOptions, + AcceptProposalOptions, + ProposeProofOptions, + RequestProofOptions, +} from '../src/modules/proofs/models/ModuleOptions' +import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' +import type { CredDefId } from 'indy-sdk' + +import { AttributeFilter, PredicateType, ProofAttributeInfo, ProofPredicateInfo, ProofState } from '../src' +import { + V2_INDY_PRESENTATION, + V2_INDY_PRESENTATION_PROPOSAL, + V2_INDY_PRESENTATION_REQUEST, +} from '../src/modules/proofs/formats/ProofFormats' +import { ProofProtocolVersion } from '../src/modules/proofs/models/ProofProtocolVersion' +import { + V2PresentationMessage, + V2ProposalPresentationMessage, + V2RequestPresentationMessage, +} from '../src/modules/proofs/protocol/v2/messages' +import { DidCommMessageRepository } from '../src/storage/didcomm' + +import { setupProofsTest, waitForProofRecord } from './helpers' +import testLogger from './logger' + +describe('Present Proof', () => { + let faberAgent: Agent + let aliceAgent: Agent + let credDefId: CredDefId + let aliceConnection: ConnectionRecord + let faberConnection: ConnectionRecord + let faberProofRecord: ProofRecord + let aliceProofRecord: ProofRecord + let presentationPreview: PresentationPreview + let didCommMessageRepository: DidCommMessageRepository + + beforeAll(async () => { + testLogger.test('Initializing the agents') + ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = + await setupProofsTest('Faber agent', 'Alice agent')) + }) + + afterAll(async () => { + testLogger.test('Shutting down both agents') + await faberAgent.shutdown() + await faberAgent.wallet.delete() + await aliceAgent.shutdown() + await aliceAgent.wallet.delete() + }) + + test('Alice starts with proof proposal to Faber', async () => { + // Alice sends a presentation proposal to Faber + testLogger.test('Alice sends a presentation proposal to Faber') + + const proposeProofOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + name: 'abc', + version: '1.0', + nonce: '947121108704767252195126', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + } + aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeProofOptions) + + // Faber waits for a presentation proposal from Alice + testLogger.test('Faber waits for a presentation proposal from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.ProposalReceived, + }) + + didCommMessageRepository = faberAgent.injectionContainer.resolve(DidCommMessageRepository) + + const proposal = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2ProposalPresentationMessage, + }) + + expect(proposal).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/propose-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_PROPOSAL, + }, + ], + proposalsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.ProposalReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + // Faber accepts the presentation proposal from Alice + testLogger.test('Faber accepts presentation proposal from Alice') + faberProofRecord = await faberAgent.proofs.acceptProposal(acceptProposalOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_REQUEST, + }, + ], + requestPresentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits for the presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION, + }, + ], + presentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + // Faber accepts the presentation provided by Alice + testLogger.test('Faber accepts the presentation provided by Alice') + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she received a presentation acknowledgement + testLogger.test('Alice waits until she receives a presentation acknowledgement') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Faber starts with proof request to Alice', async () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + image_0: new ProofAttributeInfo({ + name: 'image_0', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Sample predicates + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V2, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_REQUEST, + }, + ], + requestPresentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofRecord.id).not.toBeNull() + expect(aliceProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + // Alice retrieves the requested credentials and accepts the presentation request + testLogger.test('Alice accepts presentation request from Faber') + + const requestedCredentials = await aliceAgent.proofs.autoSelectCredentialsForProofRequest({ + proofRecordId: aliceProofRecord.id, + config: { + filterByPresentationPreview: true, + }, + }) + + const acceptPresentationOptions: AcceptPresentationOptions = { + proofRecordId: aliceProofRecord.id, + proofFormats: { indy: requestedCredentials.indy }, + } + + await aliceAgent.proofs.acceptRequest(acceptPresentationOptions) + + // Faber waits until it receives a presentation from Alice + testLogger.test('Faber waits for presentation from Alice') + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.PresentationReceived, + }) + + const presentation = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2PresentationMessage, + }) + + expect(presentation).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION, + }, + ], + presentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + thread: { + threadId: faberProofRecord.threadId, + }, + }) + expect(faberProofRecord.id).not.toBeNull() + expect(faberProofRecord).toMatchObject({ + threadId: faberProofRecord.threadId, + state: ProofState.PresentationReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + // Faber accepts the presentation + testLogger.test('Faber accept the presentation from Alice') + await faberAgent.proofs.acceptPresentation(faberProofRecord.id) + + // Alice waits until she receives a presentation acknowledgement + testLogger.test('Alice waits for acceptance by Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Done, + }) + + expect(faberProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: aliceProofRecord.threadId, + connectionId: expect.any(String), + isVerified: true, + state: ProofState.PresentationReceived, + }) + + expect(aliceProofRecord).toMatchObject({ + // type: ProofRecord.name, + id: expect.any(String), + createdAt: expect.any(Date), + threadId: faberProofRecord.threadId, + connectionId: expect.any(String), + state: ProofState.Done, + }) + }) + + test('Faber starts with proof request to Alice but gets Problem Reported', async () => { + const attributes = { + name: new ProofAttributeInfo({ + name: 'name', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + image_0: new ProofAttributeInfo({ + name: 'image_0', + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + // Sample predicates + const predicates = { + age: new ProofPredicateInfo({ + name: 'age', + predicateType: PredicateType.GreaterThanOrEqualTo, + predicateValue: 50, + restrictions: [ + new AttributeFilter({ + credentialDefinitionId: credDefId, + }), + ], + }), + } + + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V2, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + + // Faber sends a presentation request to Alice + testLogger.test('Faber sends a presentation request to Alice') + faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) + + // Alice waits for presentation request from Faber + testLogger.test('Alice waits for presentation request from Faber') + aliceProofRecord = await waitForProofRecord(aliceAgent, { + threadId: faberProofRecord.threadId, + state: ProofState.RequestReceived, + }) + + const request = await didCommMessageRepository.findAgentMessage({ + associatedRecordId: faberProofRecord.id, + messageClass: V2RequestPresentationMessage, + }) + + expect(request).toMatchObject({ + type: 'https://didcomm.org/present-proof/2.0/request-presentation', + formats: [ + { + attachmentId: expect.any(String), + format: V2_INDY_PRESENTATION_REQUEST, + }, + ], + requestPresentationsAttach: [ + { + id: expect.any(String), + mimeType: 'application/json', + data: { + base64: expect.any(String), + }, + }, + ], + id: expect.any(String), + }) + + expect(aliceProofRecord.id).not.toBeNull() + expect(aliceProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.RequestReceived, + protocolVersion: ProofProtocolVersion.V2, + }) + + aliceProofRecord = await aliceAgent.proofs.sendProblemReport(aliceProofRecord.id, 'Problem inside proof request') + + faberProofRecord = await waitForProofRecord(faberAgent, { + threadId: aliceProofRecord.threadId, + state: ProofState.Abandoned, + }) + + expect(faberProofRecord).toMatchObject({ + threadId: aliceProofRecord.threadId, + state: ProofState.Abandoned, + protocolVersion: ProofProtocolVersion.V2, + }) + }) +}) diff --git a/packages/core/tests/proofs-auto-accept.test.ts b/packages/core/tests/v2-proofs-auto-accept.test.ts similarity index 68% rename from packages/core/tests/proofs-auto-accept.test.ts rename to packages/core/tests/v2-proofs-auto-accept.test.ts index a990c3d070..47dd8355f7 100644 --- a/packages/core/tests/proofs-auto-accept.test.ts +++ b/packages/core/tests/v2-proofs-auto-accept.test.ts @@ -1,4 +1,10 @@ -import type { Agent, ConnectionRecord, PresentationPreview } from '../src' +import type { Agent, ConnectionRecord } from '../src' +import type { + AcceptProposalOptions, + ProposeProofOptions, + RequestProofOptions, +} from '../src/modules/proofs/models/ModuleOptions' +import type { PresentationPreview } from '../src/modules/proofs/protocol/v1/models/V1PresentationPreview' import { AutoAcceptProof, @@ -8,6 +14,7 @@ import { ProofPredicateInfo, PredicateType, } from '../src' +import { ProofProtocolVersion } from '../src/modules/proofs/models/ProofProtocolVersion' import { setupProofsTest, waitForProofRecord } from './helpers' import testLogger from './logger' @@ -29,7 +36,6 @@ describe('Auto accept present proof', () => { AutoAcceptProof.Always )) }) - afterAll(async () => { await faberAgent.shutdown() await faberAgent.wallet.delete() @@ -39,7 +45,22 @@ describe('Auto accept present proof', () => { test('Alice starts with proof proposal to Faber, both with autoAcceptProof on `always`', async () => { testLogger.test('Alice sends presentation proposal to Faber') - const aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview) + + const proposeProofOptions: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + nonce: '1298236324864', + name: 'abc', + version: '1.0', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + }, + }, + } + + const aliceProofRecord = await aliceAgent.proofs.proposeProof(proposeProofOptions) testLogger.test('Faber waits for presentation from Alice') await waitForProofRecord(faberAgent, { @@ -56,7 +77,6 @@ describe('Auto accept present proof', () => { test('Faber starts with proof requests to Alice, both with autoAcceptProof on `always`', async () => { testLogger.test('Faber sends presentation request to Alice') - const attributes = { name: new ProofAttributeInfo({ name: 'name', @@ -67,7 +87,6 @@ describe('Auto accept present proof', () => { ], }), } - const predicates = { age: new ProofPredicateInfo({ name: 'age', @@ -81,18 +100,26 @@ describe('Auto accept present proof', () => { }), } - const faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V2, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324864', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } + const faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) testLogger.test('Faber waits for presentation from Alice') await waitForProofRecord(faberAgent, { threadId: faberProofRecord.threadId, state: ProofState.Done, }) - // Alice waits till it receives presentation ack await waitForProofRecord(aliceAgent, { threadId: faberProofRecord.threadId, @@ -103,6 +130,7 @@ describe('Auto accept present proof', () => { describe('Auto accept on `contentApproved`', () => { beforeAll(async () => { + testLogger.test('Initializing the agents') ;({ faberAgent, aliceAgent, credDefId, faberConnection, aliceConnection, presentationPreview } = await setupProofsTest( 'Faber Auto Accept Content Approved Proofs', @@ -110,8 +138,8 @@ describe('Auto accept present proof', () => { AutoAcceptProof.ContentApproved )) }) - afterAll(async () => { + testLogger.test('Shutting down both agents') await faberAgent.shutdown() await faberAgent.wallet.delete() await aliceAgent.shutdown() @@ -120,23 +148,48 @@ describe('Auto accept present proof', () => { test('Alice starts with proof proposal to Faber, both with autoacceptproof on `contentApproved`', async () => { testLogger.test('Alice sends presentation proposal to Faber') - const aliceProofRecord = await aliceAgent.proofs.proposeProof(aliceConnection.id, presentationPreview) + + const proposal: ProposeProofOptions = { + connectionId: aliceConnection.id, + protocolVersion: ProofProtocolVersion.V2, + proofFormats: { + indy: { + nonce: '1298236324864', + attributes: presentationPreview.attributes, + predicates: presentationPreview.predicates, + name: 'abc', + version: '1.0', + }, + }, + } + + const aliceProofRecord = await aliceAgent.proofs.proposeProof(proposal) testLogger.test('Faber waits for presentation proposal from Alice') + const faberProofRecord = await waitForProofRecord(faberAgent, { threadId: aliceProofRecord.threadId, state: ProofState.ProposalReceived, }) testLogger.test('Faber accepts presentation proposal from Alice') - await faberAgent.proofs.acceptProposal(faberProofRecord.id) + + const acceptProposalOptions: AcceptProposalOptions = { + config: { + name: 'proof-request', + version: '1.0', + }, + proofRecordId: faberProofRecord.id, + } + + await faberAgent.proofs.acceptProposal(acceptProposalOptions) testLogger.test('Faber waits for presentation from Alice') + await waitForProofRecord(faberAgent, { threadId: aliceProofRecord.threadId, state: ProofState.Done, }) - // Alice waits till it receives presentation ack await waitForProofRecord(aliceAgent, { threadId: aliceProofRecord.threadId, @@ -146,7 +199,6 @@ describe('Auto accept present proof', () => { test('Faber starts with proof requests to Alice, both with autoacceptproof on `contentApproved`', async () => { testLogger.test('Faber sends presentation request to Alice') - const attributes = { name: new ProofAttributeInfo({ name: 'name', @@ -157,7 +209,6 @@ describe('Auto accept present proof', () => { ], }), } - const predicates = { age: new ProofPredicateInfo({ name: 'age', @@ -171,34 +222,31 @@ describe('Auto accept present proof', () => { }), } - const faberProofRecord = await faberAgent.proofs.requestProof(faberConnection.id, { - name: 'test-proof-request', - requestedAttributes: attributes, - requestedPredicates: predicates, - }) - - testLogger.test('Alice waits for presentation request from Faber') - const aliceProofRecord = await waitForProofRecord(aliceAgent, { - threadId: faberProofRecord.threadId, - state: ProofState.RequestReceived, - }) + const requestProofsOptions: RequestProofOptions = { + protocolVersion: ProofProtocolVersion.V2, + connectionId: faberConnection.id, + proofFormats: { + indy: { + name: 'proof-request', + version: '1.0', + nonce: '1298236324866', + requestedAttributes: attributes, + requestedPredicates: predicates, + }, + }, + } - testLogger.test('Alice accepts presentation request from Faber') - const retrievedCredentials = await aliceAgent.proofs.getRequestedCredentialsForProofRequest(aliceProofRecord.id, { - filterByPresentationPreview: true, - }) - const requestedCredentials = aliceAgent.proofs.autoSelectCredentialsForProofRequest(retrievedCredentials) - await aliceAgent.proofs.acceptRequest(aliceProofRecord.id, requestedCredentials) + const faberProofRecord = await faberAgent.proofs.requestProof(requestProofsOptions) testLogger.test('Faber waits for presentation from Alice') await waitForProofRecord(faberAgent, { - threadId: aliceProofRecord.threadId, + threadId: faberProofRecord.threadId, state: ProofState.Done, }) // Alice waits till it receives presentation ack await waitForProofRecord(aliceAgent, { - threadId: aliceProofRecord.threadId, + threadId: faberProofRecord.threadId, state: ProofState.Done, }) })