From faf713f1a8d635e588a3fc3c2fbdc6efafa3f77a Mon Sep 17 00:00:00 2001 From: volodymyr-basiuk <31999965+volodymyr-basiuk@users.noreply.github.com> Date: Thu, 21 Nov 2024 17:22:55 +0200 Subject: [PATCH 1/4] feat: accept header (#253) * accept-profile --- src/iden3comm/constants.ts | 27 ++++ src/iden3comm/handlers/auth.ts | 53 +++++++- src/iden3comm/index.ts | 1 + src/iden3comm/packageManager.ts | 15 +++ src/iden3comm/packers/jws.ts | 29 ++++- src/iden3comm/packers/plain.ts | 24 ++++ src/iden3comm/packers/zkp.ts | 37 +++++- src/iden3comm/types/index.ts | 1 + src/iden3comm/types/packageManager.ts | 16 +++ src/iden3comm/types/packer.ts | 15 +++ .../types/protocol/accept-profile.ts | 14 ++ src/iden3comm/types/protocol/auth.ts | 1 + src/iden3comm/utils/accept-profile.ts | 121 ++++++++++++++++++ src/iden3comm/utils/index.ts | 1 + tests/handlers/auth.test.ts | 84 +++++++++++- tests/iden3comm/accept-profile.test.ts | 75 +++++++++++ 16 files changed, 507 insertions(+), 7 deletions(-) create mode 100644 src/iden3comm/types/protocol/accept-profile.ts create mode 100644 src/iden3comm/utils/accept-profile.ts create mode 100644 tests/iden3comm/accept-profile.test.ts diff --git a/src/iden3comm/constants.ts b/src/iden3comm/constants.ts index a2d4e1c2..398767f1 100644 --- a/src/iden3comm/constants.ts +++ b/src/iden3comm/constants.ts @@ -1,3 +1,5 @@ +import { AcceptProfile } from './types'; + const IDEN3_PROTOCOL = 'https://iden3-communication.io/'; /** * Constants for Iden3 protocol @@ -79,5 +81,30 @@ export const SUPPORTED_PUBLIC_KEY_TYPES = { ] }; +export enum ProtocolVersion { + V1 = 'iden3comm/v1' +} + +export enum AcceptAuthCircuits { + AuthV2 = 'authV2', + AuthV3 = 'authV3' +} + +export enum AcceptJwzAlgorithms { + Groth16 = 'groth16' +} + +export enum AcceptJwsAlgorithms { + ES256K = 'ES256K', + ES256KR = 'ES256K-R' +} + +export const defaultAcceptProfile: AcceptProfile = { + protocolVersion: ProtocolVersion.V1, + env: MediaType.ZKPMessage, + circuits: [AcceptAuthCircuits.AuthV2], + alg: [AcceptJwzAlgorithms.Groth16] +}; + export const DEFAULT_PROOF_VERIFY_DELAY = 1 * 60 * 60 * 1000; // 1 hour export const DEFAULT_AUTH_VERIFY_DELAY = 5 * 60 * 1000; // 5 minutes diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index ff13cb72..028c8737 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -1,4 +1,4 @@ -import { MediaType } from '../constants'; +import { MediaType, ProtocolVersion } from '../constants'; import { IProofService } from '../../proof/proof-service'; import { PROTOCOL_MESSAGE_TYPE } from '../constants'; @@ -21,6 +21,7 @@ import { byteDecoder, byteEncoder } from '../../utils'; import { processZeroKnowledgeProofRequests } from './common'; import { CircuitId } from '../../circuits'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { parseAcceptProfile } from '../utils'; /** * createAuthorizationRequest is a function to create protocol authorization request @@ -231,13 +232,18 @@ export class AuthHandler // override sender did if it's explicitly specified in the auth request const to = authRequest.to ? DID.parse(authRequest.to) : ctx.senderDid; - const mediaType = ctx.mediaType || MediaType.ZKPMessage; const guid = uuid.v4(); if (!authRequest.from) { throw new Error('auth request should contain from field'); } + const responseType = PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE; + const mediaType = this.getSupportedMediaTypeByProfile( + ctx, + responseType, + authRequest.body.accept + ); const from = DID.parse(authRequest.from); const responseScope = await processZeroKnowledgeProofRequests( @@ -250,8 +256,8 @@ export class AuthHandler return { id: guid, - typ: ctx.mediaType, - type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_RESPONSE_MESSAGE_TYPE, + typ: mediaType, + type: responseType, thid: authRequest.thid ?? guid, body: { message: authRequest?.body?.message, @@ -450,4 +456,43 @@ export class AuthHandler } } } + + private getSupportedMediaTypeByProfile( + ctx: AuthReqOptions, + responseType: string, + profile?: string[] | undefined + ): MediaType { + let mediaType: MediaType; + if (profile?.length) { + const supportedMediaTypes: MediaType[] = []; + for (const acceptProfile of profile) { + // 1. check protocol version + const { protocolVersion, env } = parseAcceptProfile(acceptProfile); + const responseTypeVersion = Number(responseType.split('/').at(-2)); + if ( + protocolVersion !== ProtocolVersion.V1 || + (protocolVersion === ProtocolVersion.V1 && + (responseTypeVersion < 1 || responseTypeVersion >= 2)) + ) { + continue; + } + // 2. check packer support + if (this._packerMgr.isProfileSupported(env, acceptProfile)) { + supportedMediaTypes.push(env); + } + } + + if (!supportedMediaTypes.length) { + throw new Error('no packer with profile which meets `accept` header requirements'); + } + + mediaType = supportedMediaTypes[0]; + if (ctx.mediaType && supportedMediaTypes.includes(ctx.mediaType)) { + mediaType = ctx.mediaType; + } + } else { + mediaType = ctx.mediaType || MediaType.ZKPMessage; + } + return mediaType; + } } diff --git a/src/iden3comm/index.ts b/src/iden3comm/index.ts index 870e7f69..f1e27864 100644 --- a/src/iden3comm/index.ts +++ b/src/iden3comm/index.ts @@ -3,6 +3,7 @@ export * from './packers'; export * from './types'; export * from './handlers'; export * from './utils/did'; +export * from './utils/accept-profile'; import * as PROTOCOL_CONSTANTS from './constants'; export { PROTOCOL_CONSTANTS }; diff --git a/src/iden3comm/packageManager.ts b/src/iden3comm/packageManager.ts index c211568f..2a73e1ca 100644 --- a/src/iden3comm/packageManager.ts +++ b/src/iden3comm/packageManager.ts @@ -21,6 +21,21 @@ export class PackageManager implements IPackageManager { this.packers = new Map(); } + /** {@inheritDoc IPackageManager.isProfileSupported} */ + isProfileSupported(mediaType: MediaType, profile: string): boolean { + const p = this.packers.get(mediaType); + if (!p) { + return false; + } + + return p.isProfileSupported(profile); + } + + /** {@inheritDoc IPackageManager.getSupportedMediaTypes} */ + getSupportedMediaTypes(): MediaType[] { + return [...this.packers.keys()]; + } + /** {@inheritDoc IPackageManager.registerPackers} */ registerPackers(packers: Array): void { packers.forEach((p) => { diff --git a/src/iden3comm/packers/jws.ts b/src/iden3comm/packers/jws.ts index 17218961..e382aaad 100644 --- a/src/iden3comm/packers/jws.ts +++ b/src/iden3comm/packers/jws.ts @@ -1,5 +1,5 @@ import { BasicMessage, IPacker, JWSPackerParams } from '../types'; -import { MediaType, SUPPORTED_PUBLIC_KEY_TYPES } from '../constants'; +import { AcceptJwsAlgorithms, MediaType, SUPPORTED_PUBLIC_KEY_TYPES } from '../constants'; import { extractPublicKeyBytes, resolveVerificationMethods } from '../utils/did'; import { keyPath, KMS } from '../../kms/'; @@ -13,6 +13,7 @@ import { decodeBase64url, encodeBase64url } from '../../utils'; +import { parseAcceptProfile } from '../utils'; /** * Packer that can pack message to JWZ token, @@ -102,6 +103,32 @@ export class JWSPacker implements IPacker { return MediaType.SignedMessage; } + /** {@inheritDoc IPacker.getSupportedProfiles} */ + getSupportedProfiles(): string[] { + return [`env=${this.mediaType()}&alg=${this.getSupportedAlgorithms().join(',')}`]; + } + + /** {@inheritDoc IPacker.isProfileSupported} */ + isProfileSupported(profile: string) { + const { env, circuits, alg } = parseAcceptProfile(profile); + if (env !== this.mediaType()) { + return false; + } + + if (circuits) { + throw new Error(`Circuits are not supported for ${env} media type`); + } + + const supportedAlgArr = this.getSupportedAlgorithms(); + const algSupported = + !alg?.length || alg.some((a) => supportedAlgArr.includes(a as AcceptJwsAlgorithms)); + return algSupported; + } + + private getSupportedAlgorithms(): AcceptJwsAlgorithms[] { + return [AcceptJwsAlgorithms.ES256K, AcceptJwsAlgorithms.ES256KR]; + } + private async resolveDidDoc(from: string) { let didDocument: DIDDocument; try { diff --git a/src/iden3comm/packers/plain.ts b/src/iden3comm/packers/plain.ts index 55b1c813..b5eaeb7d 100644 --- a/src/iden3comm/packers/plain.ts +++ b/src/iden3comm/packers/plain.ts @@ -1,6 +1,7 @@ import { BasicMessage, IPacker } from '../types'; import { MediaType } from '../constants'; import { byteDecoder, byteEncoder } from '../../utils'; +import { parseAcceptProfile } from '../utils'; /** * Plain packer just serializes bytes to JSON and adds media type @@ -53,4 +54,27 @@ export class PlainPacker implements IPacker { mediaType(): MediaType { return MediaType.PlainMessage; } + + /** {@inheritDoc IPacker.getSupportedProfiles} */ + getSupportedProfiles(): string[] { + return [`env=${this.mediaType()}`]; + } + + /** {@inheritDoc IPacker.isProfileSupported} */ + isProfileSupported(profile: string) { + const { env, circuits, alg } = parseAcceptProfile(profile); + if (env !== this.mediaType()) { + return false; + } + + if (circuits) { + throw new Error(`Circuits are not supported for ${env} media type`); + } + + if (alg) { + throw new Error(`Algorithms are not supported for ${env} media type`); + } + + return true; + } } diff --git a/src/iden3comm/packers/zkp.ts b/src/iden3comm/packers/zkp.ts index 93453cdb..94d7e9ba 100644 --- a/src/iden3comm/packers/zkp.ts +++ b/src/iden3comm/packers/zkp.ts @@ -20,9 +20,10 @@ import { ErrStateVerificationFailed, ErrUnknownCircuitID } from '../errors'; -import { MediaType } from '../constants'; +import { AcceptAuthCircuits, AcceptJwzAlgorithms, MediaType } from '../constants'; import { byteDecoder, byteEncoder } from '../../utils'; import { DEFAULT_AUTH_VERIFY_DELAY } from '../constants'; +import { parseAcceptProfile } from '../utils'; const { getProvingMethod } = proving; @@ -174,6 +175,40 @@ export class ZKPPacker implements IPacker { mediaType(): MediaType { return MediaType.ZKPMessage; } + + /** {@inheritDoc IPacker.getSupportedProfiles} */ + getSupportedProfiles(): string[] { + return [ + `env=${this.mediaType()}&alg=${this.getSupportedAlgorithms().join( + ',' + )}&circuitIds=${this.getSupportedCircuitIds().join(',')}` + ]; + } + + /** {@inheritDoc IPacker.isProfileSupported} */ + isProfileSupported(profile: string) { + const { env, circuits, alg } = parseAcceptProfile(profile); + if (env !== this.mediaType()) { + return false; + } + + const supportedCircuitIds = this.getSupportedCircuitIds(); + const circuitIdSupported = + !circuits?.length || circuits.some((c) => supportedCircuitIds.includes(c)); + + const supportedAlgArr = this.getSupportedAlgorithms(); + const algSupported = + !alg?.length || alg.some((a) => supportedAlgArr.includes(a as AcceptJwzAlgorithms)); + return algSupported && circuitIdSupported; + } + + private getSupportedAlgorithms(): AcceptJwzAlgorithms[] { + return [AcceptJwzAlgorithms.Groth16]; + } + + private getSupportedCircuitIds(): AcceptAuthCircuits[] { + return [AcceptAuthCircuits.AuthV2]; + } } const verifySender = async (token: Token, msg: BasicMessage): Promise => { diff --git a/src/iden3comm/types/index.ts b/src/iden3comm/types/index.ts index 9f08e4cb..da011009 100644 --- a/src/iden3comm/types/index.ts +++ b/src/iden3comm/types/index.ts @@ -6,6 +6,7 @@ export * from './protocol/revocation'; export * from './protocol/contract-request'; export * from './protocol/proposal-request'; export * from './protocol/payment'; +export * from './protocol/accept-profile'; export * from './packer'; export * from './models'; diff --git a/src/iden3comm/types/packageManager.ts b/src/iden3comm/types/packageManager.ts index d94af86b..554663bd 100644 --- a/src/iden3comm/types/packageManager.ts +++ b/src/iden3comm/types/packageManager.ts @@ -74,6 +74,22 @@ export interface IPackageManager { * @returns MediaType */ getMediaType(envelope: string): MediaType; + + /** + * gets supported media types by packer manager + * + * @returns MediaType[] + */ + getSupportedMediaTypes(): MediaType[]; + + /** + * returns true if media type and algorithms supported by packer manager + * + * @param {MediaType} mediaType + * @param {string} profile + * @returns {boolean} + */ + isProfileSupported(mediaType: MediaType, profile: string): boolean; } /** * EnvelopeStub is used to stub the jwt based envelops diff --git a/src/iden3comm/types/packer.ts b/src/iden3comm/types/packer.ts index db4ca2a6..1d7b6f90 100644 --- a/src/iden3comm/types/packer.ts +++ b/src/iden3comm/types/packer.ts @@ -137,6 +137,21 @@ export interface IPacker { * @returns The media type as a MediaType. */ mediaType(): MediaType; + + /** + * gets packer envelope (supported profiles) with options + * + * @returns {string} + */ + getSupportedProfiles(): string[]; + + /** + * returns true if profile is supported by packer + * + * @param {string} profile + * @returns {boolean} + */ + isProfileSupported(profile: string): boolean; } /** * Params for verification of auth circuit public signals diff --git a/src/iden3comm/types/protocol/accept-profile.ts b/src/iden3comm/types/protocol/accept-profile.ts new file mode 100644 index 00000000..f7f001ba --- /dev/null +++ b/src/iden3comm/types/protocol/accept-profile.ts @@ -0,0 +1,14 @@ +import { + AcceptAuthCircuits, + AcceptJwsAlgorithms, + AcceptJwzAlgorithms, + MediaType, + ProtocolVersion +} from '../../constants'; + +export type AcceptProfile = { + protocolVersion: ProtocolVersion; + env: MediaType; + circuits?: AcceptAuthCircuits[]; + alg?: AcceptJwsAlgorithms[] | AcceptJwzAlgorithms[]; +}; diff --git a/src/iden3comm/types/protocol/auth.ts b/src/iden3comm/types/protocol/auth.ts index 48080219..0d48d8c0 100644 --- a/src/iden3comm/types/protocol/auth.ts +++ b/src/iden3comm/types/protocol/auth.ts @@ -38,6 +38,7 @@ export type AuthorizationRequestMessageBody = { message?: string; did_doc?: DIDDocument; scope: Array; + accept?: string[]; }; /** ZeroKnowledgeProofRequest represents structure of zkp request object */ diff --git a/src/iden3comm/utils/accept-profile.ts b/src/iden3comm/utils/accept-profile.ts new file mode 100644 index 00000000..7c86a36d --- /dev/null +++ b/src/iden3comm/utils/accept-profile.ts @@ -0,0 +1,121 @@ +import { + MediaType, + ProtocolVersion, + AcceptAuthCircuits, + AcceptJwzAlgorithms, + AcceptJwsAlgorithms +} from '../constants'; +import { AcceptProfile } from '../types'; + +function isProtocolVersion(value: string): boolean { + return Object.values(ProtocolVersion).includes(value as ProtocolVersion); +} + +function isMediaType(value: string): boolean { + return Object.values(MediaType).includes(value as MediaType); +} + +function isAcceptAuthCircuits(value: string): boolean { + return Object.values(AcceptAuthCircuits).includes(value as AcceptAuthCircuits); +} + +function isAcceptJwsAlgorithms(value: string): boolean { + return Object.values(AcceptJwsAlgorithms).includes(value as AcceptJwsAlgorithms); +} + +function isAcceptJwzAlgorithms(value: string): boolean { + return Object.values(AcceptJwzAlgorithms).includes(value as AcceptJwzAlgorithms); +} + +export const buildAccept = (profiles: AcceptProfile[]): string[] => { + const result = []; + for (const profile of profiles) { + let accept = `${profile.protocolVersion};env=${profile.env}`; + if (profile.circuits?.length) { + accept += `;circuitId=${profile.circuits.join(',')}`; + } + if (profile.alg?.length) { + accept += `;alg=${profile.alg.join(',')}`; + } + result.push(accept); + } + + return result; +}; + +export const parseAcceptProfile = (profile: string): AcceptProfile => { + const params = profile.split(';'); + + if (params.length < 2) { + throw new Error('Invalid accept profile'); + } + const protocolVersion = params[0].trim() as ProtocolVersion; + if (!isProtocolVersion(protocolVersion)) { + throw new Error(`Protocol version '${protocolVersion}' not supported`); + } + + const envParam = params[1].split('='); + if (envParam.length !== 2) { + throw new Error(`Invalid accept profile 'env' parameter`); + } + const env = params[1].split('=')[1].trim() as MediaType; + if (!isMediaType(env)) { + throw new Error(`Envelop '${env}' not supported`); + } + + const circuitsIndex = params.findIndex((i: string) => i.includes('circuitId=')); + if (env !== MediaType.ZKPMessage && circuitsIndex > 0) { + throw new Error(`Circuits not supported for env '${env}'`); + } + + let circuits: AcceptAuthCircuits[] | undefined = undefined; + if (circuitsIndex > 0) { + circuits = params[circuitsIndex] + .split('=')[1] + .split(',') + .map((i) => i.trim()) + .map((i) => { + if (!isAcceptAuthCircuits(i)) { + throw new Error(`Circuit '${i}' not supported`); + } + return i as AcceptAuthCircuits; + }); + } + + const algIndex = params.findIndex((i: string) => i.includes('alg=')); + let alg: AcceptJwsAlgorithms[] | AcceptJwzAlgorithms[] | undefined = undefined; + if (algIndex > 0) { + if (env === MediaType.ZKPMessage) { + alg = params[algIndex] + .split('=')[1] + .split(',') + .map((i) => { + i = i.trim(); + if (!isAcceptJwzAlgorithms(i)) { + throw new Error(`Algorithm '${i}' not supported for '${env}'`); + } + return i as AcceptJwzAlgorithms; + }); + } else if (env === MediaType.SignedMessage) { + alg = params[algIndex] + .split('=')[1] + .split(',') + .map((i) => { + i = i.trim(); + if (!isAcceptJwsAlgorithms(i)) { + throw new Error(`Algorithm '${i}' not supported for '${env}'`); + } + return i as AcceptJwsAlgorithms; + }); + } else { + throw new Error(`Algorithm not supported for '${env}'`); + } + } + + return { + protocolVersion, + env, + circuits, + alg + }; +}; diff --git a/src/iden3comm/utils/index.ts b/src/iden3comm/utils/index.ts index 351289d8..68f7ae88 100644 --- a/src/iden3comm/utils/index.ts +++ b/src/iden3comm/utils/index.ts @@ -1,3 +1,4 @@ export * from './envelope'; export * from './message'; export * from './did'; +export * from './accept-profile'; diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 124fb0a9..34ff1df4 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -41,7 +41,9 @@ import { StateInfo, hexToBytes, NativeProver, - VerifiableConstants + VerifiableConstants, + buildAccept, + AcceptProfile } from '../../src'; import { ProvingMethodAlg, Token } from '@iden3/js-jwz'; import { Blockchain, DID, DidMethod, NetworkId } from '@iden3/js-iden3-core'; @@ -64,6 +66,12 @@ import { MOCK_STATE_STORAGE } from '../helpers'; import { getRandomBytes } from '@iden3/js-crypto'; +import { + AcceptAuthCircuits, + defaultAcceptProfile, + MediaType, + ProtocolVersion +} from '../../src/iden3comm/constants'; describe('auth', () => { let idWallet: IdentityWallet; @@ -158,10 +166,17 @@ describe('auth', () => { } }; + const profile: AcceptProfile = { + protocolVersion: ProtocolVersion.V1, + env: MediaType.ZKPMessage, + circuits: [AcceptAuthCircuits.AuthV2] + }; + const authReqBody: AuthorizationRequestMessageBody = { callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', reason: 'reason', message: 'message', + accept: buildAccept([profile]), scope: [proofReq] }; @@ -231,6 +246,7 @@ describe('auth', () => { callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', reason: 'reason', message: 'message', + accept: buildAccept([defaultAcceptProfile]), scope: [proofReq as ZeroKnowledgeProofRequest] }; @@ -2362,4 +2378,70 @@ describe('auth', () => { VerifiableConstants.ERRORS.NO_AUTH_CRED_FOUND ); }); + + it('request-response flow identity - accept header not supported', async () => { + const claimReq: CredentialRequest = { + credentialSchema: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json/kyc-nonmerklized.json', + type: 'KYCAgeCredential', + credentialSubject: { + id: userDID.string(), + birthday: 19960424, + documentType: 99 + }, + expiration: 2793526400, + revocationOpts: { + type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + id: RHS_URL + } + }; + const issuerCred = await idWallet.issueCredential(issuerDID, claimReq); + + await credWallet.save(issuerCred); + + const proofReq: ZeroKnowledgeProofRequest = { + id: 1, + circuitId: CircuitId.AtomicQuerySigV2, + optional: false, + query: { + allowedIssuers: ['*'], + type: claimReq.type, + context: + 'https://raw.githubusercontent.com/iden3/claim-schema-vocab/main/schemas/json-ld/kyc-nonmerklized.jsonld', + credentialSubject: { + documentType: { + $eq: 99 + } + } + } + }; + + const authV3NotSupportedProfile: AcceptProfile = { + protocolVersion: ProtocolVersion.V1, + env: MediaType.ZKPMessage, + circuits: [AcceptAuthCircuits.AuthV3] + }; + const authReqBody: AuthorizationRequestMessageBody = { + callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', + reason: 'reason', + message: 'message', + accept: buildAccept([authV3NotSupportedProfile]), + scope: [proofReq as ZeroKnowledgeProofRequest] + }; + + const id = uuid.v4(); + const authReq: AuthorizationRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, + thid: id, + body: authReqBody, + from: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + await expect(authHandler.handleAuthorizationRequest(userDID, msgBytes)).to.be.rejectedWith( + 'no packer with profile which meets `accept` header requirements' + ); + }); }); diff --git a/tests/iden3comm/accept-profile.test.ts b/tests/iden3comm/accept-profile.test.ts new file mode 100644 index 00000000..b856b8cb --- /dev/null +++ b/tests/iden3comm/accept-profile.test.ts @@ -0,0 +1,75 @@ +import { expect } from 'chai'; +import { parseAcceptProfile, buildAccept } from '../../src/iden3comm/utils/accept-profile'; +import { + ProtocolVersion, + MediaType, + AcceptAuthCircuits, + AcceptJwzAlgorithms, + AcceptJwsAlgorithms +} from '../../src/iden3comm/constants'; + +describe('accept profile utils test', () => { + it('parse accept profile', async () => { + const accept = [ + 'iden3comm/v1;env=application/iden3-zkp-json;circuitId=authV2,authV3;alg=groth16', + 'iden3comm/v1;env=application/iden3comm-signed-json;alg=ES256K-R' + ]; + + const { protocolVersion, env, circuits, alg } = parseAcceptProfile(accept[0]); + expect(protocolVersion).to.be.eq('iden3comm/v1'); + expect(env).to.be.eq('application/iden3-zkp-json'); + expect(circuits).to.be.deep.eq(['authV2', 'authV3']); + expect(alg).to.be.deep.eq(['groth16']); + }); + + it('build accept profile', async () => { + const expectedAccept = [ + 'iden3comm/v1;env=application/iden3-zkp-json;circuitId=authV2,authV3;alg=groth16', + 'iden3comm/v1;env=application/iden3comm-signed-json;alg=ES256K-R' + ]; + + const accept = buildAccept([ + { + protocolVersion: ProtocolVersion.V1, + env: MediaType.ZKPMessage, + circuits: [AcceptAuthCircuits.AuthV2, AcceptAuthCircuits.AuthV3], + alg: [AcceptJwzAlgorithms.Groth16] + }, + { + protocolVersion: ProtocolVersion.V1, + env: MediaType.SignedMessage, + alg: [AcceptJwsAlgorithms.ES256KR] + } + ]); + expect(expectedAccept).to.be.deep.eq(accept); + }); + + it('not supported protocol version', async () => { + const expectedAcceptProfile = + 'iden3comm/v0.1;env=application/iden3-zkp-json;circuitId=authV2,authV3;alg=groth16'; + expect(() => parseAcceptProfile(expectedAcceptProfile)).to.throw( + `Protocol version 'iden3comm/v0.1' not supported` + ); + }); + + it('not supported envelop', async () => { + const acceptProfile = 'iden3comm/v1;env=application/iden3-zkt-json'; + expect(() => parseAcceptProfile(acceptProfile)).to.throw( + `Envelop 'application/iden3-zkt-json' not supported` + ); + }); + + it('invalid alg for jwz', async () => { + const acceptProfile = 'iden3comm/v1;env=application/iden3-zkp-json;alg=ES256K-R'; + expect(() => parseAcceptProfile(acceptProfile)).to.throw( + `Algorithm 'ES256K-R' not supported for 'application/iden3-zkp-json` + ); + }); + + it('circuits for jws', async () => { + const acceptProfile = 'iden3comm/v1;env=application/iden3comm-signed-json;circuitId=authV2'; + expect(() => parseAcceptProfile(acceptProfile)).to.throw( + `Circuits not supported for env 'application/iden3comm-signed-json'` + ); + }); +}); From d55d5e76be1d81109bea5cf12f9602596e1d3e9b Mon Sep 17 00:00:00 2001 From: volodymyr-basiuk <31999965+volodymyr-basiuk@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:53:36 +0200 Subject: [PATCH 2/4] add opts to createAuthorizationRequest (#289) --- src/iden3comm/handlers/auth.ts | 22 ++++++++++--- tests/handlers/auth.test.ts | 58 ++++++++++++++-------------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index 028c8737..b57a9df0 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -23,19 +23,30 @@ import { CircuitId } from '../../circuits'; import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; import { parseAcceptProfile } from '../utils'; +/** + * Options to pass to createAuthorizationRequest function + * @public + */ +export type AuthorizationRequestCreateOptions = { + accept?: string[]; + scope?: ZeroKnowledgeProofRequest[]; +}; + /** * createAuthorizationRequest is a function to create protocol authorization request * @param {string} reason - reason to request proof * @param {string} sender - sender did * @param {string} callbackUrl - callback that user should use to send response + * @param {AuthorizationRequestCreateOptions} opts - authorization request options * @returns `Promise` */ export function createAuthorizationRequest( reason: string, sender: string, - callbackUrl: string + callbackUrl: string, + opts?: AuthorizationRequestCreateOptions ): AuthorizationRequestMessage { - return createAuthorizationRequestWithMessage(reason, '', sender, callbackUrl); + return createAuthorizationRequestWithMessage(reason, '', sender, callbackUrl, opts); } /** * createAuthorizationRequestWithMessage is a function to create protocol authorization request with explicit message to sign @@ -43,13 +54,15 @@ export function createAuthorizationRequest( * @param {string} message - message to sign in the response * @param {string} sender - sender did * @param {string} callbackUrl - callback that user should use to send response + * @param {AuthorizationRequestCreateOptions} opts - authorization request options * @returns `Promise` */ export function createAuthorizationRequestWithMessage( reason: string, message: string, sender: string, - callbackUrl: string + callbackUrl: string, + opts?: AuthorizationRequestCreateOptions ): AuthorizationRequestMessage { const uuidv4 = uuid.v4(); const request: AuthorizationRequestMessage = { @@ -59,10 +72,11 @@ export function createAuthorizationRequestWithMessage( typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, body: { + accept: opts?.accept, reason: reason, message: message, callbackUrl: callbackUrl, - scope: [] + scope: opts?.scope ?? [] } }; return request; diff --git a/tests/handlers/auth.test.ts b/tests/handlers/auth.test.ts index 34ff1df4..b15dbce5 100644 --- a/tests/handlers/auth.test.ts +++ b/tests/handlers/auth.test.ts @@ -43,7 +43,8 @@ import { NativeProver, VerifiableConstants, buildAccept, - AcceptProfile + AcceptProfile, + createAuthorizationRequest } from '../../src'; import { ProvingMethodAlg, Token } from '@iden3/js-jwz'; import { Blockchain, DID, DidMethod, NetworkId } from '@iden3/js-iden3-core'; @@ -172,23 +173,15 @@ describe('auth', () => { circuits: [AcceptAuthCircuits.AuthV2] }; - const authReqBody: AuthorizationRequestMessageBody = { - callbackUrl: 'http://localhost:8080/callback?id=1234442-123123-123123', - reason: 'reason', - message: 'message', - accept: buildAccept([profile]), - scope: [proofReq] - }; - - const id = uuid.v4(); - const authReq: AuthorizationRequestMessage = { - id, - typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, - type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE, - thid: id, - body: authReqBody, - from: issuerDID.string() - }; + const authReq = createAuthorizationRequest( + 'reason', + issuerDID.string(), + 'http://localhost:8080/callback?id=1234442-123123-123123', + { + scope: [proofReq], + accept: buildAccept([profile]) + } + ); const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); const authRes = await authHandler.handleAuthorizationRequest(userDID, msgBytes); @@ -836,19 +829,6 @@ describe('auth', () => { const userId = 'did:polygonid:polygon:mumbai:2qPDLXDaU1xa1ERTb1XKBfPCB3o2wA46q49neiXWwY'; const reason = 'test'; const message = 'message to sign'; - const request: AuthorizationRequestMessage = createAuthorizationRequestWithMessage( - reason, - message, - sender, - callback - ); - expect(request.body.scope.length).to.be.eq(0); - expect(request.body.callbackUrl).to.be.eq(callback); - expect(request.body.reason).to.be.eq(reason); - expect(request.from).to.be.eq(sender); - - request.thid = '7f38a193-0918-4a48-9fac-36adfdb8b542'; - const proofRequest: ZeroKnowledgeProofRequest = { id: 23, circuitId: CircuitId.AtomicQueryMTPV2, @@ -864,10 +844,20 @@ describe('auth', () => { } } }; - request.body.scope.push(proofRequest); - + const request: AuthorizationRequestMessage = createAuthorizationRequestWithMessage( + reason, + message, + sender, + callback, + { + scope: [proofRequest] + } + ); expect(request.body.scope.length).to.be.eq(1); - + expect(request.body.callbackUrl).to.be.eq(callback); + expect(request.body.reason).to.be.eq(reason); + expect(request.from).to.be.eq(sender); + request.thid = '7f38a193-0918-4a48-9fac-36adfdb8b542'; const mtpProof: ZeroKnowledgeProofResponse = { id: proofRequest.id, circuitId: 'credentialAtomicQueryMTPV2', From cd001c6985389d9db0afce2b8be47c99ffcb186a Mon Sep 17 00:00:00 2001 From: volodymyr-basiuk <31999965+volodymyr-basiuk@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:23:35 +0200 Subject: [PATCH 3/4] DIDComm alignment: expires_time & created_time (#288) * DIDComm alignement: expires_time & created_time --- package-lock.json | 4 +- package.json | 2 +- src/iden3comm/handlers/auth.ts | 36 +++++++---- src/iden3comm/handlers/common.ts | 14 ++++- src/iden3comm/handlers/contract-request.ts | 19 ++++-- src/iden3comm/handlers/credential-proposal.ts | 36 ++++++++--- src/iden3comm/handlers/fetch.ts | 60 +++++++++++++++---- src/iden3comm/handlers/message-handler.ts | 12 ++++ src/iden3comm/handlers/payment.ts | 44 +++++++++++--- src/iden3comm/handlers/revocation-status.ts | 13 +++- src/iden3comm/types/packer.ts | 13 ++++ src/iden3comm/types/protocol/credentials.ts | 12 ++-- src/iden3comm/types/protocol/messages.ts | 4 +- src/iden3comm/types/protocol/proof.ts | 6 +- src/iden3comm/types/protocol/revocation.ts | 4 +- 15 files changed, 215 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index b326fce8..be85aa4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.22.0", + "version": "1.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@0xpolygonid/js-sdk", - "version": "1.22.0", + "version": "1.23.0", "license": "MIT or Apache-2.0", "dependencies": { "@noble/curves": "^1.4.0", diff --git a/package.json b/package.json index bc2d2a21..75485e01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.22.0", + "version": "1.23.0", "description": "SDK to work with Polygon ID", "main": "dist/node/cjs/index.js", "module": "dist/node/esm/index.js", diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index b57a9df0..9ae9bfe6 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -12,15 +12,19 @@ import { ZeroKnowledgeProofRequest, JSONObject } from '../types'; -import { DID } from '@iden3/js-iden3-core'; +import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import { proving } from '@iden3/js-jwz'; import * as uuid from 'uuid'; import { ProofQuery } from '../../verifiable'; import { byteDecoder, byteEncoder } from '../../utils'; -import { processZeroKnowledgeProofRequests } from './common'; +import { processZeroKnowledgeProofRequests, verifyExpiresTime } from './common'; import { CircuitId } from '../../circuits'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; import { parseAcceptProfile } from '../utils'; /** @@ -30,6 +34,7 @@ import { parseAcceptProfile } from '../utils'; export type AuthorizationRequestCreateOptions = { accept?: string[]; scope?: ZeroKnowledgeProofRequest[]; + expires_time?: Date; }; /** @@ -77,7 +82,9 @@ export function createAuthorizationRequestWithMessage( message: message, callbackUrl: callbackUrl, scope: opts?.scope ?? [] - } + }, + created_time: getUnixTimestamp(new Date()), + expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } @@ -88,10 +95,11 @@ export function createAuthorizationRequestWithMessage( * * @public */ -export type AuthResponseHandlerOptions = StateVerificationOpts & { - // acceptedProofGenerationDelay is the period of time in milliseconds that a generated proof remains valid. - acceptedProofGenerationDelay?: number; -}; +export type AuthResponseHandlerOptions = StateVerificationOpts & + BasicHandlerOptions & { + // acceptedProofGenerationDelay is the period of time in milliseconds that a generated proof remains valid. + acceptedProofGenerationDelay?: number; + }; /** * Interface that allows the processing of the authorization request in the raw format for given identifier @@ -169,10 +177,10 @@ export type AuthMessageHandlerOptions = AuthReqOptions | AuthRespOptions; * @public * @interface AuthHandlerOptions */ -export interface AuthHandlerOptions { +export type AuthHandlerOptions = BasicHandlerOptions & { mediaType: MediaType; packerOptions?: JWSPackerParams; -} +}; /** * @@ -243,7 +251,6 @@ export class AuthHandler if (authRequest.type !== PROTOCOL_MESSAGE_TYPE.AUTHORIZATION_REQUEST_MESSAGE_TYPE) { throw new Error('Invalid message type for authorization request'); } - // override sender did if it's explicitly specified in the auth request const to = authRequest.to ? DID.parse(authRequest.to) : ctx.senderDid; const guid = uuid.v4(); @@ -295,7 +302,9 @@ export class AuthHandler authResponse: AuthorizationResponseMessage; }> { const authRequest = await this.parseAuthorizationRequest(request); - + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(authRequest); + } if (!opts) { opts = { mediaType: MediaType.ZKPMessage @@ -428,6 +437,9 @@ export class AuthHandler request: AuthorizationRequestMessage; response: AuthorizationResponseMessage; }> { + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(response); + } const authResp = (await this.handleAuthResponse(response, { request, acceptedStateTransitionDelay: opts?.acceptedStateTransitionDelay, diff --git a/src/iden3comm/handlers/common.ts b/src/iden3comm/handlers/common.ts index 3a3a49aa..711ab45c 100644 --- a/src/iden3comm/handlers/common.ts +++ b/src/iden3comm/handlers/common.ts @@ -1,5 +1,6 @@ import { getRandomBytes } from '@iden3/js-crypto'; import { + BasicMessage, JsonDocumentObject, JWSPackerParams, ZeroKnowledgeProofQuery, @@ -8,7 +9,7 @@ import { } from '../types'; import { mergeObjects } from '../../utils'; import { RevocationStatus, W3CCredential } from '../../verifiable'; -import { DID } from '@iden3/js-iden3-core'; +import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import { IProofService } from '../../proof'; import { CircuitId } from '../../circuits'; import { MediaType } from '../constants'; @@ -134,3 +135,14 @@ export const processZeroKnowledgeProofRequests = async ( return zkpResponses; }; + +/** + * Verifies that the expires_time field of a message is not in the past. Throws an error if it is. + * + * @param message - Basic message to verify. + */ +export const verifyExpiresTime = (message: BasicMessage) => { + if (message?.expires_time && message.expires_time < getUnixTimestamp(new Date())) { + throw new Error('Message expired'); + } +}; diff --git a/src/iden3comm/handlers/contract-request.ts b/src/iden3comm/handlers/contract-request.ts index 2a1a4a20..bb055320 100644 --- a/src/iden3comm/handlers/contract-request.ts +++ b/src/iden3comm/handlers/contract-request.ts @@ -3,11 +3,15 @@ import { IProofService } from '../../proof/proof-service'; import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { BasicMessage, IPackageManager, ZeroKnowledgeProofResponse } from '../types'; import { ContractInvokeRequest, ContractInvokeResponse } from '../types/protocol/contract-request'; -import { DID, ChainIds } from '@iden3/js-iden3-core'; +import { DID, ChainIds, getUnixTimestamp } from '@iden3/js-iden3-core'; import { FunctionSignatures, IOnChainZKPVerifier } from '../../storage'; import { Signer } from 'ethers'; -import { processZeroKnowledgeProofRequests } from './common'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { processZeroKnowledgeProofRequests, verifyExpiresTime } from './common'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; /** * Interface that allows the processing of the contract request @@ -40,7 +44,7 @@ export interface IContractRequestHandler { } /** ContractInvokeHandlerOptions represents contract invoke handler options */ -export type ContractInvokeHandlerOptions = { +export type ContractInvokeHandlerOptions = BasicHandlerOptions & { ethSigner: Signer; challenge?: bigint; }; @@ -193,7 +197,8 @@ export class ContractRequestHandler body: { transaction_data: request.body.transaction_data, scope: [] - } + }, + created_time: getUnixTimestamp(new Date()) }; for (const [txHash, zkpResponses] of txHashToZkpResponseMap) { for (const zkpResponse of zkpResponses) { @@ -222,7 +227,9 @@ export class ContractRequestHandler opts: ContractInvokeHandlerOptions ): Promise> { const ciRequest = await this.parseContractInvokeRequest(request); - + if (!opts.allowExpiredMessages) { + verifyExpiresTime(ciRequest); + } if (ciRequest.body.transaction_data.method_id !== FunctionSignatures.SubmitZKPResponseV1) { throw new Error(`please use handle method to work with other method ids`); } diff --git a/src/iden3comm/handlers/credential-proposal.ts b/src/iden3comm/handlers/credential-proposal.ts index 2471481d..82336729 100644 --- a/src/iden3comm/handlers/credential-proposal.ts +++ b/src/iden3comm/handlers/credential-proposal.ts @@ -9,7 +9,7 @@ import { PackerParams } from '../types'; -import { DID } from '@iden3/js-iden3-core'; +import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import * as uuid from 'uuid'; import { proving } from '@iden3/js-jwz'; import { @@ -21,13 +21,24 @@ import { import { IIdentityWallet } from '../../identity'; import { byteEncoder } from '../../utils'; import { W3CCredential } from '../../verifiable'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; +import { verifyExpiresTime } from './common'; /** @beta ProposalRequestCreationOptions represents proposal-request creation options */ export type ProposalRequestCreationOptions = { credentials: ProposalRequestCredential[]; metadata?: { type: string; data?: JsonDocumentObject }; did_doc?: DIDDocument; + expires_time?: Date; +}; + +/** @beta ProposalCreationOptions represents proposal creation options */ +export type ProposalCreationOptions = { + expires_time?: Date; }; /** @@ -51,7 +62,9 @@ export function createProposalRequest( to: receiver.string(), typ: MediaType.PlainMessage, type: PROTOCOL_MESSAGE_TYPE.PROPOSAL_REQUEST_MESSAGE_TYPE, - body: opts + body: opts, + created_time: getUnixTimestamp(new Date()), + expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } @@ -67,7 +80,8 @@ export function createProposalRequest( export function createProposal( sender: DID, receiver: DID, - proposals?: Proposal[] + proposals?: Proposal[], + opts?: ProposalCreationOptions ): ProposalMessage { const uuidv4 = uuid.v4(); const request: ProposalMessage = { @@ -79,7 +93,9 @@ export function createProposal( type: PROTOCOL_MESSAGE_TYPE.PROPOSAL_MESSAGE_TYPE, body: { proposals: proposals || [] - } + }, + created_time: getUnixTimestamp(new Date()), + expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } @@ -129,10 +145,10 @@ export interface ICredentialProposalHandler { } /** @beta ProposalRequestHandlerOptions represents proposal-request handler options */ -export type ProposalRequestHandlerOptions = object; +export type ProposalRequestHandlerOptions = BasicHandlerOptions; /** @beta ProposalHandlerOptions represents proposal handler options */ -export type ProposalHandlerOptions = { +export type ProposalHandlerOptions = BasicHandlerOptions & { proposalRequest?: ProposalRequestMessage; }; @@ -310,6 +326,9 @@ export class CredentialProposalHandler if (!proposalRequest.from) { throw new Error(`failed request. empty 'from' field`); } + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(proposalRequest); + } const senderDID = DID.parse(proposalRequest.from); const message = await this.handleProposalRequestMessage(proposalRequest); @@ -332,6 +351,9 @@ export class CredentialProposalHandler * @inheritdoc ICredentialProposalHandler#handleProposal */ async handleProposal(proposal: ProposalMessage, opts?: ProposalHandlerOptions) { + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(proposal); + } if (opts?.proposalRequest && opts.proposalRequest.from !== proposal.to) { throw new Error( `sender of the request is not a target of response - expected ${opts.proposalRequest.from}, given ${proposal.to}` diff --git a/src/iden3comm/handlers/fetch.ts b/src/iden3comm/handlers/fetch.ts index dd5a197c..a2f82afc 100644 --- a/src/iden3comm/handlers/fetch.ts +++ b/src/iden3comm/handlers/fetch.ts @@ -18,7 +18,12 @@ import { byteDecoder, byteEncoder } from '../../utils'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; import * as uuid from 'uuid'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; +import { verifyExpiresTime } from './common'; /** * @@ -27,7 +32,7 @@ import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handl * @public * @interface FetchHandlerOptions */ -export type FetchHandlerOptions = { +export type FetchHandlerOptions = BasicHandlerOptions & { mediaType: MediaType; packerOptions?: JWSPackerParams; headers?: { @@ -35,6 +40,24 @@ export type FetchHandlerOptions = { }; }; +/** + * + * Options to pass to fetch request handler + * + * @public + * @interface FetchRequestOptions + */ +export type FetchRequestOptions = BasicHandlerOptions; + +/** + * + * Options to pass to issuance response handler + * + * @public + * @interface IssuanceResponseOptions + */ +export type IssuanceResponseOptions = BasicHandlerOptions; + export type FetchMessageHandlerOptions = FetchHandlerOptions; /** @@ -64,7 +87,10 @@ export interface IFetchHandler { * @returns A promise that resolves to the response message. * @throws An error if the request is invalid or if the credential is not found. */ - handleCredentialFetchRequest(basicMessage: Uint8Array): Promise; + handleCredentialFetchRequest( + basicMessage: Uint8Array, + opts?: FetchRequestOptions + ): Promise; /** * Handles the issuance response message. @@ -73,7 +99,10 @@ export interface IFetchHandler { * @returns A promise that resolves to a Uint8Array. * @throws An error if the credential wallet is not provided in the options or if the credential is missing in the issuance response message. */ - handleIssuanceResponseMessage(basicMessage: Uint8Array): Promise; + handleIssuanceResponseMessage( + basicMessage: Uint8Array, + opts?: IssuanceResponseOptions + ): Promise; } /** * @@ -230,7 +259,9 @@ export class FetchHandler offer, PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE ); - + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(offerMessage); + } const result = await this.handleOfferMessage(offerMessage, { mediaType: opts?.mediaType, headers: opts?.headers, @@ -292,13 +323,18 @@ export class FetchHandler /** * @inheritdoc IFetchHandler#handleCredentialFetchRequest */ - async handleCredentialFetchRequest(envelope: Uint8Array): Promise { + async handleCredentialFetchRequest( + envelope: Uint8Array, + opts?: FetchRequestOptions + ): Promise { const msgRequest = await FetchHandler.unpackMessage( this._packerMgr, envelope, PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE ); - + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(msgRequest); + } const request = await this.handleFetchRequest(msgRequest); return this._packerMgr.pack( @@ -325,15 +361,19 @@ export class FetchHandler /** * @inheritdoc IFetchHandler#handleIssuanceResponseMessage */ - async handleIssuanceResponseMessage(envelop: Uint8Array): Promise { + async handleIssuanceResponseMessage( + envelop: Uint8Array, + opts?: IssuanceResponseOptions + ): Promise { const issuanceMsg = await FetchHandler.unpackMessage( this._packerMgr, envelop, PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE ); - + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(issuanceMsg); + } await this.handleIssuanceResponseMsg(issuanceMsg); - return Uint8Array.from([]); } diff --git a/src/iden3comm/handlers/message-handler.ts b/src/iden3comm/handlers/message-handler.ts index 69ab8ce1..e77dd99a 100644 --- a/src/iden3comm/handlers/message-handler.ts +++ b/src/iden3comm/handlers/message-handler.ts @@ -6,6 +6,15 @@ import { PaymentHandlerOptions, PaymentRequestMessageHandlerOptions } from './pa import { MediaType } from '../constants'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; +import { verifyExpiresTime } from './common'; + +/** + * iden3 Basic protocol message handler options + */ +export type BasicHandlerOptions = { + allowExpiredMessages?: boolean; +}; + /** * iden3 Protocol message handler interface */ @@ -41,6 +50,9 @@ export abstract class AbstractMessageHandler implements IProtocolMessageHandler message: BasicMessage, context: { [key: string]: unknown } ): Promise { + if (!context.allowExpiredMessages) { + verifyExpiresTime(message); + } if (this.nextMessageHandler) return this.nextMessageHandler.handle(message, context); return Promise.reject('Message handler not provided or message not supported'); } diff --git a/src/iden3comm/handlers/payment.ts b/src/iden3comm/handlers/payment.ts index 8225ab1c..03154e99 100644 --- a/src/iden3comm/handlers/payment.ts +++ b/src/iden3comm/handlers/payment.ts @@ -2,11 +2,15 @@ import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { MediaType } from '../constants'; import { BasicMessage, IPackageManager, PackerParams } from '../types'; -import { DID } from '@iden3/js-iden3-core'; +import { DID, getUnixTimestamp } from '@iden3/js-iden3-core'; import * as uuid from 'uuid'; import { proving } from '@iden3/js-jwz'; import { byteEncoder } from '../../utils'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; import { EthereumEip712Signature2021, Iden3PaymentCryptoV1, @@ -31,6 +35,17 @@ import { } from '../../verifiable'; import { Signer, ethers } from 'ethers'; import { Resolvable } from 'did-resolver'; +import { verifyExpiresTime } from './common'; + +/** @beta PaymentRequestCreationOptions represents payment-request creation options */ +export type PaymentRequestCreationOptions = { + expires_time?: Date; +}; + +/** @beta PaymentCreationOptions represents payment creation options */ +export type PaymentCreationOptions = { + expires_time?: Date; +}; /** * @beta @@ -45,7 +60,8 @@ export function createPaymentRequest( sender: DID, receiver: DID, agent: string, - payments: PaymentRequestInfo[] + payments: PaymentRequestInfo[], + opts?: PaymentRequestCreationOptions ): PaymentRequestMessage { const uuidv4 = uuid.v4(); const request: PaymentRequestMessage = { @@ -58,7 +74,9 @@ export function createPaymentRequest( body: { agent, payments - } + }, + created_time: getUnixTimestamp(new Date()), + expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } @@ -152,7 +170,8 @@ export type PaymentRailsChainInfo = { export function createPayment( sender: DID, receiver: DID, - payments: PaymentTypeUnion[] + payments: PaymentTypeUnion[], + opts?: PaymentCreationOptions ): PaymentMessage { const uuidv4 = uuid.v4(); const request: PaymentMessage = { @@ -164,7 +183,9 @@ export function createPayment( type: PROTOCOL_MESSAGE_TYPE.PAYMENT_MESSAGE_TYPE, body: { payments - } + }, + created_time: getUnixTimestamp(new Date()), + expires_time: opts?.expires_time ? getUnixTimestamp(opts.expires_time) : undefined }; return request; } @@ -225,7 +246,7 @@ export interface IPaymentHandler { } /** @beta PaymentRequestMessageHandlerOptions represents payment-request handler options */ -export type PaymentRequestMessageHandlerOptions = { +export type PaymentRequestMessageHandlerOptions = BasicHandlerOptions & { paymentHandler: (data: PaymentRequestTypeUnion) => Promise; /* selected payment nonce (for Iden3PaymentRequestCryptoV1 type it should be equal to Payment id field) @@ -235,7 +256,7 @@ export type PaymentRequestMessageHandlerOptions = { }; /** @beta PaymentHandlerOptions represents payment handler options */ -export type PaymentHandlerOptions = { +export type PaymentHandlerOptions = BasicHandlerOptions & { paymentRequest: PaymentRequestMessage; paymentValidationHandler: (txId: string, data: PaymentRequestTypeUnion) => Promise; }; @@ -408,7 +429,9 @@ export class PaymentHandler if (!paymentRequest.to) { throw new Error(`failed request. empty 'to' field`); } - + if (!opts?.allowExpiredMessages) { + verifyExpiresTime(paymentRequest); + } const agentMessage = await this.handlePaymentRequestMessage(paymentRequest, opts); if (!agentMessage) { return null; @@ -422,6 +445,9 @@ export class PaymentHandler * @inheritdoc IPaymentHandler#handlePayment */ async handlePayment(payment: PaymentMessage, params: PaymentHandlerOptions) { + if (!params?.allowExpiredMessages) { + verifyExpiresTime(payment); + } if (params.paymentRequest.from !== payment.to) { throw new Error( `sender of the request is not a target of response - expected ${params.paymentRequest.from}, given ${payment.to}` diff --git a/src/iden3comm/handlers/revocation-status.ts b/src/iden3comm/handlers/revocation-status.ts index c1c40402..327987a4 100644 --- a/src/iden3comm/handlers/revocation-status.ts +++ b/src/iden3comm/handlers/revocation-status.ts @@ -15,7 +15,12 @@ import { TreeState } from '../../circuits'; import { byteEncoder } from '../../utils'; import { proving } from '@iden3/js-jwz'; import { IIdentityWallet } from '../../identity'; -import { AbstractMessageHandler, IProtocolMessageHandler } from './message-handler'; +import { + AbstractMessageHandler, + BasicHandlerOptions, + IProtocolMessageHandler +} from './message-handler'; +import { verifyExpiresTime } from './common'; /** * Defines the options for a RevocationStatusMessageHandler. @@ -59,7 +64,7 @@ export interface IRevocationStatusHandler { } /** RevocationStatusHandlerOptions represents revocation status handler options */ -export type RevocationStatusHandlerOptions = { +export type RevocationStatusHandlerOptions = BasicHandlerOptions & { mediaType: MediaType; packerOptions?: JWSPackerParams; treeState?: TreeState; @@ -193,7 +198,9 @@ export class RevocationStatusHandler } const rsRequest = await this.parseRevocationStatusRequest(request); - + if (!opts.allowExpiredMessages) { + verifyExpiresTime(rsRequest); + } const response = await this.handleRevocationStatusRequestMessage(rsRequest, { senderDid: did, mediaType: opts.mediaType, diff --git a/src/iden3comm/types/packer.ts b/src/iden3comm/types/packer.ts index 1d7b6f90..aed4cd92 100644 --- a/src/iden3comm/types/packer.ts +++ b/src/iden3comm/types/packer.ts @@ -38,6 +38,9 @@ export type JsonDocumentObjectValue = | JsonDocumentObject | JsonDocumentObjectValue[]; +/** + * Basic message with all possible fields optional + */ export type BasicMessage = { id: string; typ?: MediaType; @@ -46,6 +49,16 @@ export type BasicMessage = { body?: unknown; from?: string; to?: string; + created_time?: number; + expires_time?: number; +}; + +/** + * Basic message with all possible fields required + */ +export type RequiredBasicMessage = Omit, 'created_time' | 'expires_time'> & { + created_time?: number; + expires_time?: number; }; /** diff --git a/src/iden3comm/types/protocol/credentials.ts b/src/iden3comm/types/protocol/credentials.ts index 099a7832..3491a220 100644 --- a/src/iden3comm/types/protocol/credentials.ts +++ b/src/iden3comm/types/protocol/credentials.ts @@ -1,6 +1,6 @@ import { W3CCredential } from '../../../verifiable'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; -import { BasicMessage, JsonDocumentObject } from '../packer'; +import { BasicMessage, JsonDocumentObject, RequiredBasicMessage } from '../packer'; import { ContractInvokeTransactionData } from './contract-request'; /** CredentialIssuanceRequestMessageBody represents data for credential issuance request */ @@ -11,13 +11,13 @@ export type CredentialIssuanceRequestMessageBody = { }; /** CredentialIssuanceRequestMessage represent Iden3message for credential request */ -export type CredentialIssuanceRequestMessage = Required & { +export type CredentialIssuanceRequestMessage = RequiredBasicMessage & { body: CredentialIssuanceRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_REQUEST_MESSAGE_TYPE; }; /** CredentialsOfferMessage represent Iden3message for credential offer */ -export type CredentialsOfferMessage = Required & { +export type CredentialsOfferMessage = RequiredBasicMessage & { body: CredentialsOfferMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE; }; @@ -29,7 +29,7 @@ export type CredentialsOfferMessageBody = { }; /** CredentialsOnchainOfferMessage represent Iden3message for credential onchain offer message */ -export type CredentialsOnchainOfferMessage = Required & { +export type CredentialsOnchainOfferMessage = RequiredBasicMessage & { body: CredentialsOnchainOfferMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ONCHAIN_OFFER_MESSAGE_TYPE; }; @@ -55,7 +55,7 @@ export type CredentialOffer = { }; /** CredentialIssuanceMessage represent Iden3message for credential issuance */ -export type CredentialIssuanceMessage = Required & { +export type CredentialIssuanceMessage = RequiredBasicMessage & { body: IssuanceMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE; }; @@ -84,7 +84,7 @@ export type Schema = { }; /** CredentialRefreshMessage represent Iden3message for credential refresh request */ -export type CredentialRefreshMessage = Required & { +export type CredentialRefreshMessage = RequiredBasicMessage & { body: CredentialRefreshMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_REFRESH_MESSAGE_TYPE; }; diff --git a/src/iden3comm/types/protocol/messages.ts b/src/iden3comm/types/protocol/messages.ts index d06cf1bc..eb57b5ee 100644 --- a/src/iden3comm/types/protocol/messages.ts +++ b/src/iden3comm/types/protocol/messages.ts @@ -1,8 +1,8 @@ -import { BasicMessage } from '../'; +import { RequiredBasicMessage } from '../'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; /** MessageFetchRequestMessage represent Iden3message for message fetch request. */ -export type MessageFetchRequestMessage = Required & { +export type MessageFetchRequestMessage = RequiredBasicMessage & { body: MessageFetchRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE; }; diff --git a/src/iden3comm/types/protocol/proof.ts b/src/iden3comm/types/protocol/proof.ts index 45c559e4..ae217513 100644 --- a/src/iden3comm/types/protocol/proof.ts +++ b/src/iden3comm/types/protocol/proof.ts @@ -1,9 +1,9 @@ -import { BasicMessage } from '../'; +import { RequiredBasicMessage } from '../'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; import { ZeroKnowledgeProofRequest, ZeroKnowledgeProofResponse } from './auth'; /** ProofGenerationRequestMessage is struct the represents body for proof generation request */ -export type ProofGenerationRequestMessage = Required & { +export type ProofGenerationRequestMessage = RequiredBasicMessage & { body: ProofGenerationRequestMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.PROOF_GENERATION_REQUEST_MESSAGE_TYPE; }; @@ -14,7 +14,7 @@ export type ProofGenerationRequestMessageBody = { }; /** ProofGenerationResponseMessage is struct the represents body for proof generation request */ -export type ProofGenerationResponseMessage = Required & { +export type ProofGenerationResponseMessage = RequiredBasicMessage & { body: ResponseMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.PROOF_GENERATION_RESPONSE_MESSAGE_TYPE; }; diff --git a/src/iden3comm/types/protocol/revocation.ts b/src/iden3comm/types/protocol/revocation.ts index d1024f4e..baec2f52 100644 --- a/src/iden3comm/types/protocol/revocation.ts +++ b/src/iden3comm/types/protocol/revocation.ts @@ -1,4 +1,4 @@ -import { BasicMessage } from '../'; +import { BasicMessage, RequiredBasicMessage } from '../'; import { RevocationStatus } from '../../../verifiable'; import { PROTOCOL_MESSAGE_TYPE } from '../../constants'; @@ -14,7 +14,7 @@ export type RevocationStatusRequestMessageBody = { }; /** RevocationStatusResponseMessage is struct the represents body for proof generation request */ -export type RevocationStatusResponseMessage = Required & { +export type RevocationStatusResponseMessage = RequiredBasicMessage & { body: RevocationStatusResponseMessageBody; type: typeof PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_RESPONSE_MESSAGE_TYPE; }; From 339a4cc2f30178c93e55b8d8a93f1fcf7ae7cb8d Mon Sep 17 00:00:00 2001 From: volodymyr-basiuk <31999965+volodymyr-basiuk@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:58:54 +0200 Subject: [PATCH 4/4] add missed domain name in getPermitSignature (#291) * add missed domain name in getPermitSignature * bump version --- package-lock.json | 4 ++-- package.json | 2 +- src/storage/blockchain/erc20-permit-sig.ts | 1 + tests/handlers/payment.test.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index be85aa4a..dd604793 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.23.0", + "version": "1.23.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@0xpolygonid/js-sdk", - "version": "1.23.0", + "version": "1.23.1", "license": "MIT or Apache-2.0", "dependencies": { "@noble/curves": "^1.4.0", diff --git a/package.json b/package.json index 75485e01..7681b70a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.23.0", + "version": "1.23.1", "description": "SDK to work with Polygon ID", "main": "dist/node/cjs/index.js", "module": "dist/node/esm/index.js", diff --git a/src/storage/blockchain/erc20-permit-sig.ts b/src/storage/blockchain/erc20-permit-sig.ts index 4d89d659..86b9ff7b 100644 --- a/src/storage/blockchain/erc20-permit-sig.ts +++ b/src/storage/blockchain/erc20-permit-sig.ts @@ -23,6 +23,7 @@ export async function getPermitSignature( const nonce = await erc20PermitContract.nonces(await signer.getAddress()); const domainData = await erc20PermitContract.eip712Domain(); const domain = { + name: domainData[1], version: domainData[2], chainId: domainData[3], verifyingContract: tokenAddress diff --git a/tests/handlers/payment.test.ts b/tests/handlers/payment.test.ts index 6972d96a..af08c630 100644 --- a/tests/handlers/payment.test.ts +++ b/tests/handlers/payment.test.ts @@ -361,7 +361,7 @@ describe('payment-request handler', () => { }; if (data.features?.includes(PaymentFeatures.EIP_2612)) { - const permitSignature = getPermitSignature( + const permitSignature = await getPermitSignature( ethSigner, data.tokenAddress, await payContract.getAddress(),