From 6c2dfdb625f7a8f2504f8bc8cf878e01ee1c50cc Mon Sep 17 00:00:00 2001 From: NB-MikeRichardson <93971245+NB-MikeRichardson@users.noreply.github.com> Date: Wed, 18 May 2022 17:56:05 +0300 Subject: [PATCH] fix: propose payload attachment in in snake_case JSON format (#775) Signed-off-by: Mike Richardson --- .../formats/CredentialFormatService.ts | 2 +- .../indy/IndyCredentialFormatService.ts | 51 +++++++++++++------ .../protocol/v1/V1CredentialService.ts | 2 +- .../protocol/v2/CredentialMessageBuilder.ts | 6 +-- .../protocol/v2/V2CredentialService.ts | 5 +- .../v2credentials-architecture.test.ts | 12 ++--- .../v2credentials.propose-offer.test.ts | 39 +++++++++++--- 7 files changed, 80 insertions(+), 37 deletions(-) diff --git a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts index 23fbcc1385..dc7bfd2ed2 100644 --- a/packages/core/src/modules/credentials/formats/CredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/CredentialFormatService.ts @@ -31,7 +31,7 @@ export abstract class CredentialFormatService { this.eventEmitter = eventEmitter } - abstract createProposal(options: ProposeCredentialOptions): FormatServiceProposeAttachmentFormats + abstract createProposal(options: ProposeCredentialOptions): Promise abstract processProposal( options: ServiceAcceptProposalOptions, diff --git a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts index f5c77071d1..e8473988db 100644 --- a/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts +++ b/packages/core/src/modules/credentials/formats/indy/IndyCredentialFormatService.ts @@ -18,7 +18,6 @@ import type { } from '../../protocol' import type { V1CredentialPreview } from '../../protocol/v1/V1CredentialPreview' import type { CredentialExchangeRecord } from '../../repository/CredentialExchangeRecord' -import type { CredPropose } from '../models/CredPropose' import type { FormatServiceCredentialAttachmentFormats, CredentialFormatSpec, @@ -34,6 +33,7 @@ import { Lifecycle, scoped } from 'tsyringe' import { AgentConfig } from '../../../../agent/AgentConfig' import { EventEmitter } from '../../../../agent/EventEmitter' import { AriesFrameworkError } from '../../../../error' +import { JsonTransformer } from '../../../../utils/JsonTransformer' import { MessageValidator } from '../../../../utils/MessageValidator' import { uuid } from '../../../../utils/uuid' import { IndyHolderService, IndyIssuerService } from '../../../indy' @@ -47,6 +47,7 @@ import { V2CredentialPreview } from '../../protocol/v2/V2CredentialPreview' import { CredentialMetadataKeys } from '../../repository/CredentialMetadataTypes' import { CredentialRepository } from '../../repository/CredentialRepository' import { CredentialFormatService } from '../CredentialFormatService' +import { CredPropose } from '../models/CredPropose' @scoped(Lifecycle.ContainerScoped) export class IndyCredentialFormatService extends CredentialFormatService { @@ -80,7 +81,7 @@ export class IndyCredentialFormatService extends CredentialFormatService { * @returns object containing associated attachment, formats and filtersAttach elements * */ - public createProposal(options: ProposeCredentialOptions): FormatServiceProposeAttachmentFormats { + public async createProposal(options: ProposeCredentialOptions): Promise { const formats: CredentialFormatSpec = { attachId: this.generateId(), format: 'hlindy/cred-filter@v2.0', @@ -89,7 +90,19 @@ export class IndyCredentialFormatService extends CredentialFormatService { throw new AriesFrameworkError('Missing payload in createProposal') } - const attachment: Attachment = this.getFormatData(options.credentialFormats.indy?.payload, formats.attachId) + // Use class instance instead of interface, otherwise this causes interoperability problems + let proposal = new CredPropose(options.credentialFormats.indy?.payload) + + try { + await MessageValidator.validate(proposal) + } catch (error) { + throw new AriesFrameworkError(`Invalid credPropose class instance: ${proposal} in Indy Format Service`) + } + + proposal = JsonTransformer.toJSON(proposal) + + const attachment = this.getFormatData(proposal, formats.attachId) + const { previewWithAttachments } = this.getCredentialLinkedAttachments(options) return { format: formats, attachment, preview: previewWithAttachments } @@ -99,7 +112,9 @@ export class IndyCredentialFormatService extends CredentialFormatService { options: ServiceAcceptProposalOptions, credentialRecord: CredentialExchangeRecord ): Promise { - const credPropose = options.proposalAttachment?.getDataAsJson() + let credPropose = options.proposalAttachment?.getDataAsJson() + credPropose = JsonTransformer.fromJSON(credPropose, CredPropose) + if (!credPropose) { throw new AriesFrameworkError('Missing indy credential proposal data payload') } @@ -246,17 +261,20 @@ export class IndyCredentialFormatService extends CredentialFormatService { }) } + if (!options.credentialFormats.indy.attributes) { + throw new AriesFrameworkError('Missing attributes from credential proposal') + } + if (options.credentialFormats.indy && options.credentialFormats.indy.linkedAttachments) { // there are linked attachments so transform into the attribute field of the CredentialPreview object for // this proposal - if (options.credentialFormats.indy.attributes) { - previewWithAttachments = CredentialUtils.createAndLinkAttachmentsToPreview( - options.credentialFormats.indy.linkedAttachments, - new V2CredentialPreview({ - attributes: options.credentialFormats.indy.attributes, - }) - ) - } + previewWithAttachments = CredentialUtils.createAndLinkAttachmentsToPreview( + options.credentialFormats.indy.linkedAttachments, + new V2CredentialPreview({ + attributes: options.credentialFormats.indy.attributes, + }) + ) + attachments = options.credentialFormats.indy.linkedAttachments.map( (linkedAttachment) => linkedAttachment.attachment ) @@ -364,7 +382,7 @@ export class IndyCredentialFormatService extends CredentialFormatService { } const attachmentId = options.attachId ? options.attachId : formats.attachId - const issueAttachment: Attachment = this.getFormatData(credential, attachmentId) + const issueAttachment = this.getFormatData(credential, attachmentId) return { format: formats, attachment: issueAttachment } } /** @@ -502,11 +520,11 @@ export class IndyCredentialFormatService extends CredentialFormatService { private areProposalAndOfferDefinitionIdEqual(proposalAttachment?: Attachment, offerAttachment?: Attachment) { const credOffer = offerAttachment?.getDataAsJson() - const credPropose = proposalAttachment?.getDataAsJson() + let credPropose = proposalAttachment?.getDataAsJson() + credPropose = JsonTransformer.fromJSON(credPropose, CredPropose) const proposalCredentialDefinitionId = credPropose?.credentialDefinitionId const offerCredentialDefinitionId = credOffer?.cred_def_id - return proposalCredentialDefinitionId === offerCredentialDefinitionId } @@ -561,7 +579,8 @@ export class IndyCredentialFormatService extends CredentialFormatService { proposeAttachment?: Attachment ) { const indyCredentialRequest = requestAttachment?.getDataAsJson() - const indyCredentialProposal = proposeAttachment?.getDataAsJson() + let indyCredentialProposal = proposeAttachment?.getDataAsJson() + indyCredentialProposal = JsonTransformer.fromJSON(indyCredentialProposal, CredPropose) const indyCredentialOffer = offerAttachment?.getDataAsJson() diff --git a/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts index 3f04dbd6ef..bcf68b4109 100644 --- a/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts +++ b/packages/core/src/modules/credentials/protocol/v1/V1CredentialService.ts @@ -129,7 +129,7 @@ export class V1CredentialService extends CredentialService { const options = { ...config } - const { attachment: filtersAttach } = this.formatService.createProposal(proposal) + const { attachment: filtersAttach } = await this.formatService.createProposal(proposal) if (!filtersAttach) { throw new AriesFrameworkError('Missing filters attach in Proposal') diff --git a/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts b/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts index 8f0d72128b..1dce4cb9b8 100644 --- a/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts +++ b/packages/core/src/modules/credentials/protocol/v2/CredentialMessageBuilder.ts @@ -44,10 +44,10 @@ export class CredentialMessageBuilder { * @param _threadId optional thread id for this message service * @return a version 2.0 credential propose message see {@link V2ProposeCredentialMessage} */ - public createProposal( + public async createProposal( formatServices: CredentialFormatService[], proposal: ProposeCredentialOptions - ): CredentialProtocolMsgReturnType { + ): Promise> { if (formatServices.length === 0) { throw new AriesFrameworkError('no format services provided to createProposal') } @@ -58,7 +58,7 @@ export class CredentialMessageBuilder { const filtersAttachArray: Attachment[] | undefined = [] let previewAttachments: V2CredentialPreview | undefined for (const formatService of formatServices) { - const { format: formats, attachment, preview } = formatService.createProposal(proposal) + const { format: formats, attachment, preview } = await formatService.createProposal(proposal) if (attachment) { filtersAttachArray.push(attachment) } else { diff --git a/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts index 469f7dcfb5..9636d8dd39 100644 --- a/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts +++ b/packages/core/src/modules/credentials/protocol/v2/V2CredentialService.ts @@ -115,7 +115,7 @@ export class V2CredentialService extends CredentialService { if (!formats || formats.length === 0) { throw new AriesFrameworkError(`Unable to create proposal. No supported formats`) } - const { message: proposalMessage, credentialRecord } = this.credentialMessageBuilder.createProposal( + const { message: proposalMessage, credentialRecord } = await this.credentialMessageBuilder.createProposal( formats, proposal ) @@ -316,7 +316,7 @@ export class V2CredentialService extends CredentialService { if (!formats || formats.length === 0) { throw new AriesFrameworkError(`Unable to negotiate offer. No supported formats`) } - const { message: credentialProposalMessage } = this.credentialMessageBuilder.createProposal(formats, options) + const { message: credentialProposalMessage } = await this.credentialMessageBuilder.createProposal(formats, options) credentialProposalMessage.setThread({ threadId: credentialRecord.threadId }) // Update record @@ -1078,7 +1078,6 @@ export class V2CredentialService extends CredentialService { */ public getFormatsFromMessage(messageFormats: CredentialFormatSpec[]): CredentialFormatService[] { const formats: CredentialFormatService[] = [] - for (const msg of messageFormats) { if (msg.format.includes('indy')) { formats.push(this.getFormatService(CredentialFormatType.Indy)) diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts index 538d730658..e122206d1f 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials-architecture.test.ts @@ -81,27 +81,27 @@ describe('V2 Credential Architecture', () => { expect(type).toEqual('IndyCredentialFormatService') }) - test('propose credential format service returns correct format and filters~attach', () => { + test('propose credential format service returns correct format and filters~attach', async () => { const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 const service: CredentialService = api.getService(version) const formatService: CredentialFormatService = service.getFormatService(CredentialFormatType.Indy) - const { format: formats, attachment: filtersAttach } = formatService.createProposal(proposal) + const { format: formats, attachment: filtersAttach } = await formatService.createProposal(proposal) expect(formats.attachId.length).toBeGreaterThan(0) expect(formats.format).toEqual('hlindy/cred-filter@v2.0') expect(filtersAttach).toBeTruthy() }) - test('propose credential format service transforms and validates CredPropose payload correctly', () => { + test('propose credential format service transforms and validates CredPropose payload correctly', async () => { const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 const service: CredentialService = api.getService(version) const formatService: CredentialFormatService = service.getFormatService(CredentialFormatType.Indy) - const { format: formats, attachment: filtersAttach } = formatService.createProposal(proposal) + const { format: formats, attachment: filtersAttach } = await formatService.createProposal(proposal) expect(formats.attachId.length).toBeGreaterThan(0) expect(formats.format).toEqual('hlindy/cred-filter@v2.0') expect(filtersAttach).toBeTruthy() }) - test('propose credential format service creates message with multiple formats', () => { + test('propose credential format service creates message with multiple formats', async () => { const version: CredentialProtocolVersion = CredentialProtocolVersion.V2 const service: CredentialService = api.getService(version) @@ -111,7 +111,7 @@ describe('V2 Credential Architecture', () => { expect(formats.length).toBe(1) // for now will be added to with jsonld const messageBuilder: CredentialMessageBuilder = new CredentialMessageBuilder() - const v2Proposal = messageBuilder.createProposal(formats, multiFormatProposal) + const v2Proposal = await messageBuilder.createProposal(formats, multiFormatProposal) expect(v2Proposal.message.formats.length).toBe(1) expect(v2Proposal.message.formats[0].format).toEqual('hlindy/cred-filter@v2.0') diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts index 9cb9bbebcd..e641a915be 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2credentials.propose-offer.test.ts @@ -78,13 +78,13 @@ describe('credentials', () => { const testAttributes = { attributes: credentialPreview.attributes, - credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', payload: { schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', schemaName: 'ahoy', schemaVersion: '1.0', schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', }, } testLogger.test('Alice sends (v1) credential proposal to Faber') @@ -227,18 +227,13 @@ describe('credentials', () => { }) const testAttributes = { attributes: credentialPreview.attributes, - schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', - schemaName: 'ahoy', - schemaVersion: '1.0', - schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', - issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', - credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', payload: { schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', schemaName: 'ahoy', schemaVersion: '1.0', schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', }, } testLogger.test('Alice sends (v2) credential proposal to Faber') @@ -382,6 +377,36 @@ describe('credentials', () => { } }) + test('Ensure missing attributes are caught if absent from in V2 (Indy) Proposal Message', async () => { + // Note missing attributes... + const testAttributes = { + payload: { + schemaIssuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + schemaName: 'ahoy', + schemaVersion: '1.0', + schemaId: 'q7ATwTYbQDgiigVijUAej:2:test:1.0', + issuerDid: 'GMm4vMw8LLrLJjp81kRRLp', + credentialDefinitionId: 'GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag', + }, + } + testLogger.test('Alice sends (v2) credential proposal to Faber') + // set the propose options + // we should set the version to V1.0 and V2.0 in separate tests, one as a regression test + const proposeOptions: ProposeCredentialOptions = { + connectionId: aliceConnection.id, + protocolVersion: CredentialProtocolVersion.V2, + credentialFormats: { + indy: testAttributes, + }, + comment: 'v2 propose credential test', + } + testLogger.test('Alice sends (v2, Indy) credential proposal to Faber') + + await expect(aliceAgent.credentials.proposeCredential(proposeOptions)).rejects.toThrow( + 'Missing attributes from credential proposal' + ) + }) + test('Faber Issues Credential which is then deleted from Alice`s wallet', async () => { const credentialPreview = V2CredentialPreview.fromRecord({ name: 'John',