diff --git a/src/iden3comm/handlers/discovery-protocol.ts b/src/iden3comm/handlers/discovery-protocol.ts index 06aca696..86d97004 100644 --- a/src/iden3comm/handlers/discovery-protocol.ts +++ b/src/iden3comm/handlers/discovery-protocol.ts @@ -1,12 +1,13 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; -import { BasicMessage, IPackageManager } from '../types'; +import { BasicMessage, IPackageManager, ProtocolMessage } from '../types'; import * as uuid from 'uuid'; import { DiscoverFeatureDiscloseMessage, DiscoverFeatureDisclosure, DiscoverFeatureQueriesMessage, + DiscoverFeatureQuery, DiscoverFeatureQueryType, DiscoveryProtocolFeatureType } from '../types/protocol/discovery-protocol'; @@ -17,6 +18,7 @@ import { } from './message-handler'; import { getUnixTimestamp } from '@iden3/js-iden3-core'; import { verifyExpiresTime } from './common'; +import def from 'ajv/dist/vocabularies/discriminator'; /** * @beta @@ -26,6 +28,7 @@ import { verifyExpiresTime } from './common'; */ export interface DiscoveryProtocolOptions { packageManager: IPackageManager; + supportedProtocols?: Array; } /** @@ -46,27 +49,21 @@ export type DiscoveryProtocolHandlerOptions = BasicHandlerOptions & { * @param opts - discovery-feature query options * @returns `DiscoverFeatureQueriesMessage` */ -export function createDiscoveryFeatureQueryMessage(opts?: { - featureTypes?: DiscoveryProtocolFeatureType[]; - from?: string; - to?: string; - expires_time?: number; -}): DiscoverFeatureQueriesMessage { +export function createDiscoveryFeatureQueryMessage( + queries: DiscoverFeatureQuery[], + opts?: { + from?: string; + to?: string; + expires_time?: number; + } +): DiscoverFeatureQueriesMessage { const uuidv4 = uuid.v4(); return { id: uuidv4, thid: uuidv4, type: PROTOCOL_MESSAGE_TYPE.DISCOVERY_PROTOCOL_QUERIES_MESSAGE_TYPE, body: { - queries: opts?.featureTypes?.length - ? opts.featureTypes.map((featureType) => ({ - [DiscoverFeatureQueryType.FeatureType]: featureType - })) - : [ - { - [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept - } - ] + queries }, from: opts?.from, to: opts?.to, @@ -173,24 +170,11 @@ export class DiscoveryProtocolHandler verifyExpiresTime(message); } - if (message.body.queries.length !== 1) { - throw new Error('Invalid number of queries. Only one query is supported'); + const disclosures: DiscoverFeatureDisclosure[] = []; + for (const query of message.body.queries) { + disclosures.push(...this.handleQuery(query)); } - if ( - message.body.queries[0][DiscoverFeatureQueryType.FeatureType] !== - DiscoveryProtocolFeatureType.Accept - ) { - throw new Error('Invalid feature-type. Only "accept" is supported'); - } - - const disclosures = [ - { - [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept, - accept: this._options.packageManager.getSupportedProfiles() - } - ]; - return Promise.resolve( createDiscoveryFeatureDiscloseMessage(disclosures, { to: message.from, @@ -201,4 +185,52 @@ export class DiscoveryProtocolHandler }) ); } + + private handleQuery(query: DiscoverFeatureQuery): DiscoverFeatureDisclosure[] { + let result: DiscoverFeatureDisclosure[] = []; + switch (query[DiscoverFeatureQueryType.FeatureType]) { + case DiscoveryProtocolFeatureType.Accept: + result = this.handleAcceptQuery(); + break; + case DiscoveryProtocolFeatureType.Protocol: + result = this.handleProtocolQuery(); + break; + } + + return this.handleMatch(result, query.match); + } + + private handleAcceptQuery(): DiscoverFeatureDisclosure[] { + const acceptProfiles = this._options.packageManager.getSupportedProfiles(); + return acceptProfiles.map((profile) => ({ + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept, + id: profile + })); + } + + private handleProtocolQuery(): DiscoverFeatureDisclosure[] { + return ( + this._options.supportedProtocols?.map((protocol) => ({ + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Protocol, + id: protocol + })) ?? [] + ); + } + + private handleMatch( + disclosures: DiscoverFeatureDisclosure[], + match?: string + ): DiscoverFeatureDisclosure[] { + if (!match || match === '*') { + return disclosures; + } + const regExp = this.wildcardToRegExp(match); + return disclosures.filter((disclosure) => regExp.test(disclosure.id)); + } + + private wildcardToRegExp(match: string): RegExp { + // Escape special regex characters, then replace `*` with `.*` + const regexPattern = match.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); + return new RegExp(`^${regexPattern}$`); + } } diff --git a/src/iden3comm/types/protocol/discovery-protocol.ts b/src/iden3comm/types/protocol/discovery-protocol.ts index e9e61650..911fad15 100644 --- a/src/iden3comm/types/protocol/discovery-protocol.ts +++ b/src/iden3comm/types/protocol/discovery-protocol.ts @@ -8,7 +8,10 @@ export enum DiscoverFeatureQueryType { /** @beta DiscoveryProtocolFeatureType is enum for supported feature-types */ export enum DiscoveryProtocolFeatureType { - Accept = 'accept' + Accept = 'accept', + Protocol = 'protocol', + GoalCode = 'goal-code', + Header = 'header' } /** @beta DiscoverFeatureQueriesMessage is struct the represents discover feature queries message */ @@ -19,9 +22,13 @@ export type DiscoverFeatureQueriesMessage = BasicMessage & { /** @beta DiscoverFeatureQueriesBody is struct the represents discover feature queries body */ export type DiscoverFeatureQueriesBody = { - queries: { - [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType; - }[]; + queries: DiscoverFeatureQuery[]; +}; + +/** @beta DiscoverFeatureQuery is struct the represents discover feature query */ +export type DiscoverFeatureQuery = { + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType; + match?: string; }; /** @beta DiscoverFeatureDiscloseMessage is struct the represents discover feature disclose message */ @@ -38,5 +45,5 @@ export type DiscoverFeatureDiscloseBody = { /** @beta DiscoverFeatureDisclosure is struct the represents discover feature disclosure */ export type DiscoverFeatureDisclosure = { [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType; - accept: Array; + id: string; }; diff --git a/tests/handlers/discover-protocol.test.ts b/tests/handlers/discover-protocol.test.ts index 638067d1..c64c279f 100644 --- a/tests/handlers/discover-protocol.test.ts +++ b/tests/handlers/discover-protocol.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { DiscoverFeatureQueriesMessage, DiscoverFeatureQueryType, + DiscoveryProtocolFeatureType, IPackageManager, JWSPacker, KMS, @@ -14,9 +15,10 @@ import { createDiscoveryFeatureQueryMessage } from '../../src/iden3comm/handlers/discovery-protocol'; import { DIDResolutionResult } from 'did-resolver'; +import { PROTOCOL_MESSAGE_TYPE } from '../../src/iden3comm/constants'; describe('discovery-protocol', () => { - let discoveryFeatureQueryMessage: DiscoverFeatureQueriesMessage; + let acceptQueryMessage: DiscoverFeatureQueriesMessage; let jwsPacker: JWSPacker; let zkpPacker: ZKPPacker; let plainPacker: PlainPacker; @@ -28,7 +30,9 @@ describe('discovery-protocol', () => { zkpPacker = new ZKPPacker(new Map(), new Map()); plainPacker = new PlainPacker(); - discoveryFeatureQueryMessage = createDiscoveryFeatureQueryMessage(); + acceptQueryMessage = createDiscoveryFeatureQueryMessage([ + { [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept } + ]); }); it('plain message accept disclosures', async () => { @@ -40,11 +44,12 @@ describe('discovery-protocol', () => { const { body: { disclosures } - } = await discoveryProtocolHandler.handleDiscoveryQuery(discoveryFeatureQueryMessage); + } = await discoveryProtocolHandler.handleDiscoveryQuery(acceptQueryMessage); expect(disclosures.length).to.be.eq(1); - expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq('accept'); - expect(disclosures[0].accept.length).to.be.eq(1); - expect(disclosures[0].accept[0]).to.be.eq('env=application/iden3comm-plain-json'); + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[0].id).to.be.eq('env=application/iden3comm-plain-json'); }); it('jws and plain message accept disclosures', async () => { @@ -57,15 +62,18 @@ describe('discovery-protocol', () => { const { body: { disclosures } - } = await discoveryProtocolHandler.handleDiscoveryQuery(discoveryFeatureQueryMessage); - expect(disclosures.length).to.be.eq(1); + } = await discoveryProtocolHandler.handleDiscoveryQuery(acceptQueryMessage); + expect(disclosures.length).to.be.eq(2); - expect(disclosures[0].accept.length).to.be.eq(2); - expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq('accept'); - expect(disclosures[0].accept).to.include('env=application/iden3comm-plain-json'); - expect(disclosures[0].accept).to.include( - 'env=application/iden3comm-signed-json&alg=ES256K,ES256K-R' + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[1][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept ); + const disclosureIds = disclosures.map((d) => d.id); + expect(disclosureIds).to.include('env=application/iden3comm-plain-json'); + expect(disclosureIds).to.include('env=application/iden3comm-signed-json&alg=ES256K,ES256K-R'); }); it('zkp and plain message accept disclosures', async () => { @@ -77,13 +85,18 @@ describe('discovery-protocol', () => { const { body: { disclosures } - } = await discoveryProtocolHandler.handleDiscoveryQuery(discoveryFeatureQueryMessage); - expect(disclosures.length).to.be.eq(1); + } = await discoveryProtocolHandler.handleDiscoveryQuery(acceptQueryMessage); + expect(disclosures.length).to.be.eq(2); - expect(disclosures[0].accept.length).to.be.eq(2); - expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq('accept'); - expect(disclosures[0].accept).to.include('env=application/iden3comm-plain-json'); - expect(disclosures[0].accept).to.include( + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[1][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + const disclosureIds = disclosures.map((d) => d.id); + expect(disclosureIds).to.include('env=application/iden3comm-plain-json'); + expect(disclosureIds).to.include( 'env=application/iden3-zkp-json&alg=groth16&circuitIds=authV2' ); }); @@ -97,17 +110,157 @@ describe('discovery-protocol', () => { const { body: { disclosures } - } = await discoveryProtocolHandler.handleDiscoveryQuery(discoveryFeatureQueryMessage); + } = await discoveryProtocolHandler.handleDiscoveryQuery(acceptQueryMessage); + expect(disclosures.length).to.be.eq(3); + + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[1][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[2][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + const disclosureIds = disclosures.map((d) => d.id); + expect(disclosureIds).to.include('env=application/iden3comm-plain-json'); + expect(disclosureIds).to.include( + 'env=application/iden3-zkp-json&alg=groth16&circuitIds=authV2' + ); + expect(disclosureIds).to.include('env=application/iden3comm-signed-json&alg=ES256K,ES256K-R'); + }); + + it('zkp, jws and plain message accept disclosures with exact match', async () => { + const packageManager: IPackageManager = new PackageManager(); + packageManager.registerPackers([new PlainPacker(), plainPacker, zkpPacker, jwsPacker]); + const discoveryProtocolHandler = new DiscoveryProtocolHandler({ + packageManager + }); + + const acceptQueryMessageWithMatch = createDiscoveryFeatureQueryMessage([ + { + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept, + match: 'env=application/iden3-zkp-json&alg=groth16&circuitIds=authV2' + } + ]); + + const { + body: { disclosures } + } = await discoveryProtocolHandler.handleDiscoveryQuery(acceptQueryMessageWithMatch); expect(disclosures.length).to.be.eq(1); - expect(disclosures[0].accept.length).to.be.eq(3); - expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq('accept'); - expect(disclosures[0].accept).to.include('env=application/iden3comm-plain-json'); - expect(disclosures[0].accept).to.include( + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[0].id).to.include( 'env=application/iden3-zkp-json&alg=groth16&circuitIds=authV2' ); - expect(disclosures[0].accept).to.include( - 'env=application/iden3comm-signed-json&alg=ES256K,ES256K-R' + }); + + it('feature-type: protocol with protocol version match', async () => { + const packageManager: IPackageManager = new PackageManager(); + const discoveryProtocolHandler = new DiscoveryProtocolHandler({ + packageManager, + supportedProtocols: [ + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE + ] + }); + + const protocolQueryMessage = createDiscoveryFeatureQueryMessage([ + { + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Protocol, + match: 'https://iden3-communication.io/authorization/1.*' + } + ]); + + const { + body: { disclosures } + } = await discoveryProtocolHandler.handleDiscoveryQuery(protocolQueryMessage); + expect(disclosures.length).to.be.eq(2); + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Protocol + ); + expect(disclosures[1][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Protocol ); + const disclosureIds = disclosures.map((d) => d.id); + expect(disclosureIds).to.include(PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE); + expect(disclosureIds).to.include(PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE); + }); + + it('feature-type: protocol with protocol version mismatch', async () => { + const packageManager: IPackageManager = new PackageManager(); + const discoveryProtocolHandler = new DiscoveryProtocolHandler({ + packageManager, + supportedProtocols: [ + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE + ] + }); + + const protocolQueryMessage = createDiscoveryFeatureQueryMessage([ + { + [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Protocol, + match: 'https://iden3-communication.io/authorization/44.*' + } + ]); + + const { + body: { disclosures } + } = await discoveryProtocolHandler.handleDiscoveryQuery(protocolQueryMessage); + expect(disclosures.length).to.be.eq(0); + }); + + it('feature-type: protocol and accept', async () => { + const packageManager: IPackageManager = new PackageManager(); + packageManager.registerPackers([plainPacker]); + const discoveryProtocolHandler = new DiscoveryProtocolHandler({ + packageManager, + supportedProtocols: [ + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE + ] + }); + + const protocolQueryMessage = createDiscoveryFeatureQueryMessage([ + { [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Accept }, + { [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Protocol } + ]); + + const { + body: { disclosures } + } = await discoveryProtocolHandler.handleDiscoveryQuery(protocolQueryMessage); + expect(disclosures.length).to.be.eq(3); + expect(disclosures[0][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Accept + ); + expect(disclosures[1][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Protocol + ); + expect(disclosures[2][DiscoverFeatureQueryType.FeatureType]).to.be.eq( + DiscoveryProtocolFeatureType.Protocol + ); + const disclosureIds = disclosures.map((d) => d.id); + expect(disclosureIds).to.include('env=application/iden3comm-plain-json'); + expect(disclosureIds).to.include(PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE); + expect(disclosureIds).to.include(PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE); + }); + + it('feature-type: header', async () => { + const packageManager: IPackageManager = new PackageManager(); + packageManager.registerPackers([plainPacker]); + const discoveryProtocolHandler = new DiscoveryProtocolHandler({ + packageManager + }); + + const protocolQueryMessage = createDiscoveryFeatureQueryMessage([ + { [DiscoverFeatureQueryType.FeatureType]: DiscoveryProtocolFeatureType.Header } + ]); + + const { + body: { disclosures } + } = await discoveryProtocolHandler.handleDiscoveryQuery(protocolQueryMessage); + expect(disclosures.length).to.be.eq(0); }); });