From 2935dc479ad199bc5433b0f5bc574d79c6901285 Mon Sep 17 00:00:00 2001 From: Dimasik Kolezhniuk Date: Wed, 24 Jan 2024 17:25:02 +0100 Subject: [PATCH 1/2] Add message fetch and issuance message types handling (#168) * Add message fetch and issuance message types handling --- src/iden3comm/handlers/fetch.ts | 140 +++++++++++++++++- tests/handlers/fetch.test.ts | 246 +++++++++++++++++--------------- 2 files changed, 262 insertions(+), 124 deletions(-) diff --git a/src/iden3comm/handlers/fetch.ts b/src/iden3comm/handlers/fetch.ts index 33f62fd9..9d99340a 100644 --- a/src/iden3comm/handlers/fetch.ts +++ b/src/iden3comm/handlers/fetch.ts @@ -2,17 +2,21 @@ import { MediaType } from '../constants'; import { PROTOCOL_MESSAGE_TYPE } from '../constants'; import { + CredentialFetchRequestMessage, + CredentialIssuanceMessage, CredentialsOfferMessage, IPackageManager, JWSPackerParams, MessageFetchRequestMessage } from '../types'; -import * as uuid from 'uuid'; import { W3CCredential } from '../../verifiable'; +import { ICredentialWallet, getUserDIDFromCredential } from '../../credentials'; + import { byteDecoder, byteEncoder } from '../../utils'; import { proving } from '@iden3/js-jwz'; import { DID } from '@iden3/js-iden3-core'; +import * as uuid from 'uuid'; /** * @@ -48,6 +52,24 @@ export interface IFetchHandler { }>` */ handleCredentialOffer(offer: Uint8Array, opts?: FetchHandlerOptions): Promise; + + /** + * Handles a credential fetch request. + * + * @param basicMessage - The basic message containing the fetch request. + * @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; + + /** + * Handles the issuance response message. + * + * @param basicMessage - The basic message containing the issuance response. + * @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; } /** * @@ -63,7 +85,12 @@ export class FetchHandler implements IFetchHandler { * Creates an instance of FetchHandler. * @param {IPackageManager} _packerMgr - package manager to unpack message envelope */ - constructor(private readonly _packerMgr: IPackageManager) {} + constructor( + private readonly _packerMgr: IPackageManager, + private readonly opts?: { + credentialWallet: ICredentialWallet; + } + ) {} /** * Handles only messages with credentials/1.0/offer type @@ -88,11 +115,12 @@ export class FetchHandler implements IFetchHandler { throw new Error(`jws packer options are required for ${MediaType.SignedMessage}`); } - const { unpackedMessage: message } = await this._packerMgr.unpack(offer); - const offerMessage = message as unknown as CredentialsOfferMessage; - if (message.type !== PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE) { - throw new Error('Invalid media type'); - } + const offerMessage = await FetchHandler.unpackMessage( + this._packerMgr, + offer, + PROTOCOL_MESSAGE_TYPE.CREDENTIAL_OFFER_MESSAGE_TYPE + ); + const credentials: W3CCredential[] = []; for (let index = 0; index < offerMessage.body.credentials.length; index++) { @@ -155,4 +183,102 @@ export class FetchHandler implements IFetchHandler { } return credentials; } + + /** + * @inheritdoc IFetchHandler#handleCredentialFetchRequest + */ + async handleCredentialFetchRequest(envelope: Uint8Array): Promise { + const msgRequest = await FetchHandler.unpackMessage( + this._packerMgr, + envelope, + PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE + ); + + if (!msgRequest.to) { + throw new Error("failed request. empty 'to' field"); + } + + if (!msgRequest.from) { + throw new Error("failed request. empty 'from' field"); + } + + const issuerDID = DID.parse(msgRequest.to); + const userDID = DID.parse(msgRequest.from); + + const credId = msgRequest.body?.id; + if (!credId) { + throw new Error('nvalid credential id in fetch request body'); + } + + if (!this.opts?.credentialWallet) { + throw new Error('please, provide credential wallet in options'); + } + + const cred = await this.opts.credentialWallet.findById(credId); + + if (!cred) { + throw new Error('credential not found'); + } + + const userToVerifyDID = getUserDIDFromCredential(issuerDID, cred); + + if (userToVerifyDID.string() !== userDID.string()) { + throw new Error('credential subject is not a sender DID'); + } + + return this._packerMgr.pack( + MediaType.PlainMessage, + byteEncoder.encode( + JSON.stringify({ + id: uuid.v4(), + type: PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE, + typ: msgRequest.typ ?? MediaType.PlainMessage, + thid: msgRequest.thid ?? uuid.v4(), + body: { credential: cred }, + from: msgRequest.to, + to: msgRequest.from + }) + ), + {} + ); + } + + /** + * @inheritdoc IFetchHandler#handleIssuanceResponseMessage + */ + async handleIssuanceResponseMessage(envelop: Uint8Array): Promise { + const issuanceMsg = await FetchHandler.unpackMessage( + this._packerMgr, + envelop, + PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE + ); + + if (!this.opts?.credentialWallet) { + throw new Error('please provide credential wallet in options'); + } + + if (!issuanceMsg.body?.credential) { + throw new Error('credential is missing in issuance response message'); + } + + await this.opts.credentialWallet.save(W3CCredential.fromJSON(issuanceMsg.body.credential)); + + return Promise.resolve(Uint8Array.from([])); + } + + /** + * @inheritdoc IFetchHandler#unpackMessage + */ + static async unpackMessage( + packerMgr: IPackageManager, + envelope: Uint8Array, + messageType: string + ): Promise { + const { unpackedMessage: message } = await packerMgr.unpack(envelope); + const msgRequest = message as unknown as T; + if (message.type !== messageType) { + throw new Error('Invalid message type'); + } + return msgRequest; + } } diff --git a/tests/handlers/fetch.test.ts b/tests/handlers/fetch.test.ts index a5e80e7e..3cd2d63a 100644 --- a/tests/handlers/fetch.test.ts +++ b/tests/handlers/fetch.test.ts @@ -1,40 +1,40 @@ -import { IdentityWallet } from '../../src/identity/identity-wallet'; import { - Identity, - Profile, - IdentityStorage, - IDataStorage, - IStateStorage, - InMemoryDataSource, - InMemoryMerkleTreeStorage, - RootInfo, - StateProof, - CredentialStorage -} from '../../src/storage'; -import { BjjProvider, KMS, KmsKeyType, InMemoryPrivateKeyStore } from '../../src/kms'; -import { - CredentialWallet, - CredentialStatusResolverRegistry, - RHSResolver -} from '../../src/credentials'; -import { CredentialStatusType, VerifiableConstants, W3CCredential } from '../../src/verifiable'; -import { - BasicMessage, CredentialsOfferMessage, - CredentialsOfferMessageBody, FetchHandler, IFetchHandler, IPackageManager, - PackageManager, - PlainPacker, - PROTOCOL_CONSTANTS -} from '../../src/iden3comm'; + IDataStorage, + IdentityWallet, + CredentialWallet, + CredentialStatusResolverRegistry, + RHSResolver, + CredentialStatusType, + W3CCredential, + PROTOCOL_CONSTANTS, + byteEncoder, + CredentialFetchRequestMessage, + CredentialRequest, + CredentialIssuanceMessage, + FSCircuitStorage, + ProofService, + CircuitId +} from '../../src'; + +import { + MOCK_STATE_STORAGE, + RHS_URL, + SEED_ISSUER, + SEED_USER, + createIdentity, + getInMemoryDataStorage, + getPackageMgr, + registerBJJIntoInMemoryKMS +} from '../helpers'; + import * as uuid from 'uuid'; -import { byteEncoder } from '../../src/utils'; -import { Blockchain, DidMethod, NetworkId } from '@iden3/js-iden3-core'; import { expect } from 'chai'; import fetchMock from '@gr2m/fetch-mock'; -import { after } from 'mocha'; +import path from 'path'; describe('fetch', () => { let idWallet: IdentityWallet; @@ -43,43 +43,9 @@ describe('fetch', () => { let dataStorage: IDataStorage; let fetchHandler: IFetchHandler; let packageMgr: IPackageManager; - const rhsUrl = process.env.RHS_URL as string; const agentUrl = 'https://testagent.com/'; - const mockedToken = 'jwz token to fetch credential'; - - const mockStateStorage: IStateStorage = { - getLatestStateById: async () => { - throw new Error(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST); - }, - publishState: async () => { - return '0xc837f95c984892dbcc3ac41812ecb145fedc26d7003202c50e1b87e226a9b33c'; - }, - getGISTProof: (): Promise => { - return Promise.resolve({ - root: 0n, - existence: false, - siblings: [], - index: 0n, - value: 0n, - auxExistence: false, - auxIndex: 0n, - auxValue: 0n - }); - }, - getGISTRootInfo: (): Promise => { - return Promise.resolve({ - root: 0n, - replacedByRoot: 0n, - createdAtTimestamp: 0n, - replacedAtTimestamp: 0n, - createdAtBlock: 0n, - replacedAtBlock: 0n - }); - } - }; - - const mockedCredResponse = `{ + const issuanceResponseMock = `{ "body": { "credential": { "@context": [ @@ -146,20 +112,11 @@ describe('fetch', () => { }`; beforeEach(async () => { - const memoryKeyStore = new InMemoryPrivateKeyStore(); - const bjjProvider = new BjjProvider(KmsKeyType.BabyJubJub, memoryKeyStore); - const kms = new KMS(); - kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider); - dataStorage = { - credential: new CredentialStorage(new InMemoryDataSource()), - identity: new IdentityStorage( - new InMemoryDataSource(), - new InMemoryDataSource() - ), - mt: new InMemoryMerkleTreeStorage(40), - states: mockStateStorage - }; - + const kms = registerBJJIntoInMemoryKMS(); + dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); const resolvers = new CredentialStatusResolverRegistry(); resolvers.register( CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, @@ -168,49 +125,28 @@ describe('fetch', () => { credWallet = new CredentialWallet(dataStorage, resolvers); idWallet = new IdentityWallet(kms, dataStorage, credWallet); - // proofService = new ProofService(idWallet, credWallet, circuitStorage, mockStateStorage); - packageMgr = {} as PackageManager; - packageMgr.unpack = async function ( - envelope: Uint8Array - ): Promise<{ unpackedMessage: BasicMessage; unpackedMediaType: PROTOCOL_CONSTANTS.MediaType }> { - const msg = await new PlainPacker().unpack(envelope); - return { unpackedMessage: msg, unpackedMediaType: PROTOCOL_CONSTANTS.MediaType.PlainMessage }; - }; - packageMgr.pack = async (): Promise => byteEncoder.encode(mockedToken); - fetchHandler = new FetchHandler(packageMgr); - fetchMock.spy(); - fetchMock.post(agentUrl, JSON.parse(mockedCredResponse)); - }); - - after(() => { - fetchMock.restore(); + const proofService = new ProofService(idWallet, credWallet, circuitStorage, MOCK_STATE_STORAGE); + packageMgr = await getPackageMgr( + await circuitStorage.loadCircuitData(CircuitId.AuthV2), + proofService.generateAuthV2Inputs.bind(proofService), + proofService.verifyState.bind(proofService) + ); + fetchHandler = new FetchHandler(packageMgr, { + credentialWallet: credWallet + }); }); it('fetch credential issued to genesis did', async () => { - const seedPhraseIssuer: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseedseed'); - const seedPhrase: Uint8Array = byteEncoder.encode('seedseedseedseedseedseedseeduser'); - - const { did: userDID, credential: cred } = await idWallet.createIdentity({ - method: DidMethod.Iden3, - blockchain: Blockchain.Polygon, - networkId: NetworkId.Mumbai, - seed: seedPhrase, - revocationOpts: { - type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, - id: rhsUrl - } + fetchMock.spy(); + fetchMock.post(agentUrl, JSON.parse(issuanceResponseMock)); + const { did: userDID, credential: cred } = await createIdentity(idWallet, { + seed: SEED_USER }); + expect(cred).not.to.be.undefined; - const { did: issuerDID, credential: issuerAuthCredential } = await idWallet.createIdentity({ - method: DidMethod.Iden3, - blockchain: Blockchain.Polygon, - networkId: NetworkId.Mumbai, - seed: seedPhraseIssuer, - revocationOpts: { - type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, - id: rhsUrl - } + const { did: issuerDID, credential: issuerAuthCredential } = await createIdentity(idWallet, { + seed: SEED_ISSUER }); expect(issuerAuthCredential).not.to.be.undefined; @@ -224,7 +160,7 @@ describe('fetch', () => { body: { url: agentUrl, credentials: [{ id: 'https://credentialId', description: 'kyc age credentials' }] - } as CredentialsOfferMessageBody, + }, from: issuerDID.string(), to: userDID.string() }; @@ -237,8 +173,84 @@ describe('fetch', () => { expect(res).to.be.a('array'); expect(res).to.have.length(1); - const w3cCred = W3CCredential.fromJSON(JSON.parse(mockedCredResponse).body.credential); + const w3cCred = W3CCredential.fromJSON(JSON.parse(issuanceResponseMock).body.credential); expect(Object.entries(res[0]).toString()).to.equal(Object.entries(w3cCred).toString()); + fetchMock.restore(); + }); + + it('handle credential fetch and issuance requests', async () => { + const { did: userDID, credential: cred } = await createIdentity(idWallet, { + seed: SEED_USER + }); + expect(cred).not.to.be.undefined; + + const { did: issuerDID, credential: issuerAuthCredential } = await createIdentity(idWallet, { + seed: SEED_ISSUER + }); + + expect(issuerAuthCredential).not.to.be.undefined; + + 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 issuedCred = await idWallet.issueCredential(issuerDID, claimReq); + await credWallet.save(issuedCred); + + const id = uuid.v4(); + const authReq: CredentialFetchRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.CREDENTIAL_FETCH_REQUEST_MESSAGE_TYPE, + thid: id, + body: { + id: issuedCred.id + }, + from: userDID.string(), + to: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(authReq)); + + const res = await fetchHandler.handleCredentialFetchRequest(msgBytes); + + expect(res).to.be.a('Uint8Array'); + + const issueanceMsg = await FetchHandler.unpackMessage( + packageMgr, + res, + PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.CREDENTIAL_ISSUANCE_RESPONSE_MESSAGE_TYPE + ); + + expect(issueanceMsg).not.to.be.undefined; + expect(issueanceMsg.body).not.to.be.undefined; + expect(issueanceMsg.body?.credential).not.to.be.undefined; + expect(issueanceMsg.body?.credential.id).to.equal(issuedCred.id); + + const newId = uuid.v4(); + + issueanceMsg.body = { + credential: { ...issueanceMsg.body?.credential, id: newId } as W3CCredential + }; + + await fetchHandler.handleIssuanceResponseMessage( + byteEncoder.encode(JSON.stringify(issueanceMsg)) + ); + + const cred2 = await credWallet.findById(newId); + expect(cred2).not.to.be.undefined; }); }); From 6737fe490ee5e3432de1497d56cfe903363d3815 Mon Sep 17 00:00:00 2001 From: volodymyr-basiuk <31999965+volodymyr-basiuk@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:25:29 +0200 Subject: [PATCH 2/2] check rev nonce in cred validate proof (#170) * check rev nonce in cred validate proof --- package-lock.json | 4 ++-- package.json | 2 +- src/verifiable/credential.ts | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cee2516b..4ff739ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.7.2", + "version": "1.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@0xpolygonid/js-sdk", - "version": "1.7.2", + "version": "1.7.3", "license": "AGPL-3.0", "dependencies": { "ajv": "8.12.0", diff --git a/package.json b/package.json index 493b3750..7ec73b3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0xpolygonid/js-sdk", - "version": "1.7.2", + "version": "1.7.3", "description": "SDK to work with Polygon ID", "main": "dist/node/cjs/index.js", "module": "dist/node/esm/index.js", diff --git a/src/verifiable/credential.ts b/src/verifiable/credential.ts index 1bc7c0b5..5c171839 100644 --- a/src/verifiable/credential.ts +++ b/src/verifiable/credential.ts @@ -231,6 +231,11 @@ export class W3CCredential { } const revocationNonce = BigInt(proof.issuerData.credentialStatus.revocationNonce || 0); + if (revocationNonce !== proof.issuerData.authCoreClaim.getRevocationNonce()) { + throw new Error( + `revocation nonce mismatch: revocation nonce from core representation of auth credential is not the same as in its credential` + ); + } const proofValid = await verifyProof( Hash.fromHex(credStatus.issuer.revocationTreeRoot), credStatus.mtp,