diff --git a/src/iden3comm/handlers/index.ts b/src/iden3comm/handlers/index.ts index 24823a2c..16f4a3c2 100644 --- a/src/iden3comm/handlers/index.ts +++ b/src/iden3comm/handlers/index.ts @@ -2,4 +2,5 @@ export * from './auth'; export * from './fetch'; export * from './contract-request'; export * from './refresh'; +export * from './revocation-status'; export * from './common'; diff --git a/src/iden3comm/handlers/revocation-status.ts b/src/iden3comm/handlers/revocation-status.ts new file mode 100644 index 00000000..e7b80d71 --- /dev/null +++ b/src/iden3comm/handlers/revocation-status.ts @@ -0,0 +1,160 @@ +import { PROTOCOL_MESSAGE_TYPE } from '../constants'; +import { MediaType } from '../constants'; +import { + IPackageManager, + JWSPackerParams, + RevocationStatusRequestMessage, + RevocationStatusResponseMessage +} from '../types'; + +import { DID } from '@iden3/js-iden3-core'; +import * as uuid from 'uuid'; +import { RevocationStatus } from '../../verifiable'; +import { TreeState } from '../../circuits'; +import { byteEncoder } from '../../utils'; +import { proving } from '@iden3/js-jwz'; +import { IIdentityWallet } from '../../identity'; + +/** + * Interface that allows the processing of the revocation status + * + * @interface IRevocationStatusHandler + */ +export interface IRevocationStatusHandler { + /** + * unpacks revocation status request + * @param {Uint8Array} request - raw byte message + * @returns `Promise` + */ + parseRevocationStatusRequest(request: Uint8Array): Promise; + + /** + * handle revocation status request + * @param {did} did - sender DID + * @param {Uint8Array} request - raw byte message + * @param {RevocationStatusHandlerOptions} opts - handler options + * @returns {Promise}` - revocation status response message + */ + handleRevocationStatusRequest( + did: DID, + request: Uint8Array, + opts?: RevocationStatusHandlerOptions + ): Promise; +} + +/** RevocationStatusHandlerOptions represents revocation status handler options */ +export type RevocationStatusHandlerOptions = { + mediaType: MediaType; + packerOptions?: JWSPackerParams; + treeState?: TreeState; +}; + +/** + * + * Allows to process RevocationStatusRequest protocol message + * + + * @class RevocationStatusHandler + * @implements implements IRevocationStatusHandler interface + */ +export class RevocationStatusHandler implements IRevocationStatusHandler { + /** + * Creates an instance of RevocationStatusHandler. + * @param {IPackageManager} _packerMgr - package manager to unpack message envelope + * @param {IIdentityWallet} _identityWallet - identity wallet + * + */ + + constructor( + private readonly _packerMgr: IPackageManager, + private readonly _identityWallet: IIdentityWallet + ) {} + + /** + * @inheritdoc IRevocationStatusHandler#parseRevocationStatusRequest + */ + async parseRevocationStatusRequest(request: Uint8Array): Promise { + const { unpackedMessage: message } = await this._packerMgr.unpack(request); + const ciRequest = message as unknown as RevocationStatusRequestMessage; + if (message.type !== PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_REQUEST_MESSAGE_TYPE) { + throw new Error('Invalid media type'); + } + return ciRequest; + } + + /** + * @inheritdoc IRevocationStatusHandler#handleRevocationStatusRequest + */ + async handleRevocationStatusRequest( + did: DID, + request: Uint8Array, + opts?: RevocationStatusHandlerOptions + ): Promise { + if (!opts) { + opts = { + mediaType: MediaType.PlainMessage + }; + } + + if (opts.mediaType === MediaType.SignedMessage && !opts.packerOptions) { + throw new Error(`jws packer options are required for ${MediaType.SignedMessage}`); + } + + const rsRequest = await this.parseRevocationStatusRequest(request); + + if (!rsRequest.to) { + throw new Error(`failed request. empty 'to' field`); + } + + if (!rsRequest.from) { + throw new Error(`failed request. empty 'from' field`); + } + + if (!rsRequest.body?.revocation_nonce) { + throw new Error(`failed request. empty 'revocation_nonce' field`); + } + + const issuerDID = DID.parse(rsRequest.to); + + const mtpWithTreeState = await this._identityWallet.generateNonRevocationMtpWithNonce( + issuerDID, + BigInt(rsRequest.body.revocation_nonce), + opts?.treeState + ); + const treeState = mtpWithTreeState.treeState; + const revStatus: RevocationStatus = { + issuer: { + state: treeState?.state.string(), + claimsTreeRoot: treeState.claimsRoot.string(), + revocationTreeRoot: treeState.revocationRoot.string(), + rootOfRoots: treeState.rootOfRoots.string() + }, + mtp: mtpWithTreeState.proof + }; + + const packerOpts = + opts.mediaType === MediaType.SignedMessage + ? opts.packerOptions + : { + provingMethodAlg: proving.provingMethodGroth16AuthV2Instance.methodAlg + }; + + const senderDID = DID.parse(rsRequest.to); + const guid = uuid.v4(); + + const response: RevocationStatusResponseMessage = { + id: guid, + typ: MediaType.PlainMessage, + type: PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_RESPONSE_MESSAGE_TYPE, + thid: rsRequest.thid ?? guid, + body: revStatus, + from: did.string(), + to: rsRequest.from + }; + + return this._packerMgr.pack(opts.mediaType, byteEncoder.encode(JSON.stringify(response)), { + senderDID, + ...packerOpts + }); + } +} diff --git a/src/identity/identity-wallet.ts b/src/identity/identity-wallet.ts index 6d0a6056..f03d4a23 100644 --- a/src/identity/identity-wallet.ts +++ b/src/identity/identity-wallet.ts @@ -167,7 +167,7 @@ export interface IIdentityWallet { ): Promise; /** - * Generates proof of credential revocation nonce inclusion / non-inclusion to the given revocation tree + * Generates proof of credential revocation nonce (with credential as a param) inclusion / non-inclusion to the given revocation tree * and its root or to the current root of the Revocation tree in the given Merkle tree storage. * * @param {DID} did @@ -181,6 +181,21 @@ export interface IIdentityWallet { treeState?: TreeState ): Promise; + /** + * Generates proof of credential revocation nonce (with revNonce as a param) inclusion / non-inclusion to the given revocation tree + * and its root or to the current root of the Revocation tree in the given Merkle tree storage. + * + * @param {DID} did + * @param {bigint} revNonce + * @param {TreeState} [treeState] + * @returns `Promise` - MerkleTreeProof and TreeState on which proof has been generated + */ + generateNonRevocationMtpWithNonce( + did: DID, + revNonce: bigint, + treeState?: TreeState + ): Promise; + /** * Signs a payload of arbitrary size with an Auth BJJ Credential that identifies a key for signing. * @@ -672,7 +687,15 @@ export class IdentityWallet implements IIdentityWallet { const coreClaim = await this.getCoreClaimFromCredential(credential); const revNonce = coreClaim.getRevocationNonce(); + return this.generateNonRevocationMtpWithNonce(did, revNonce, treeState); + } + /** {@inheritDoc IIdentityWallet.generateNonRevocationMtpWithNonce} */ + async generateNonRevocationMtpWithNonce( + did: DID, + revNonce: bigint, + treeState?: TreeState + ): Promise { const treesModel = await this.getDIDTreeModel(did); const revocationTree = await this._storage.mt.getMerkleTreeByIdentifierAndType( diff --git a/src/verifiable/credential.ts b/src/verifiable/credential.ts index 6cbc04fb..22e27140 100644 --- a/src/verifiable/credential.ts +++ b/src/verifiable/credential.ts @@ -91,7 +91,7 @@ export class W3CCredential { } }; - static fromJSON(obj: W3CCredential): W3CCredential { + static fromJSON(obj: any): W3CCredential { const w = new W3CCredential(); Object.assign(w, obj); w.proof = Array.isArray(w.proof) diff --git a/tests/handlers/revocation-status.test.ts b/tests/handlers/revocation-status.test.ts new file mode 100644 index 00000000..e80b7d24 --- /dev/null +++ b/tests/handlers/revocation-status.test.ts @@ -0,0 +1,123 @@ +import { + IPackageManager, + IdentityWallet, + CredentialWallet, + CredentialStatusResolverRegistry, + RHSResolver, + CredentialStatusType, + FSCircuitStorage, + ProofService, + CircuitId, + IRevocationStatusHandler, + RevocationStatusHandler, + RevocationStatusRequestMessage, + PROTOCOL_CONSTANTS, + byteEncoder +} from '../../src'; + +import { + MOCK_STATE_STORAGE, + getInMemoryDataStorage, + getPackageMgr, + registerBJJIntoInMemoryKMS, + createIdentity, + SEED_USER, + SEED_ISSUER +} from '../helpers'; + +import * as uuid from 'uuid'; +import { expect } from 'chai'; +import path from 'path'; + +describe('revocation status', () => { + let packageMgr: IPackageManager; + let rsHandlerr: IRevocationStatusHandler; + let idWallet: IdentityWallet; + + beforeEach(async () => { + const kms = registerBJJIntoInMemoryKMS(); + const dataStorage = getInMemoryDataStorage(MOCK_STATE_STORAGE); + const circuitStorage = new FSCircuitStorage({ + dirname: path.join(__dirname, '../proofs/testdata') + }); + const resolvers = new CredentialStatusResolverRegistry(); + resolvers.register( + CredentialStatusType.Iden3ReverseSparseMerkleTreeProof, + new RHSResolver(dataStorage.states) + ); + const credWallet = new CredentialWallet(dataStorage, resolvers); + idWallet = new IdentityWallet(kms, dataStorage, credWallet); + + 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) + ); + rsHandlerr = new RevocationStatusHandler(packageMgr, idWallet); + }); + + it('revocation status works', 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 id = uuid.v4(); + const rsReq: RevocationStatusRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_REQUEST_MESSAGE_TYPE, + thid: id, + body: { + revocation_nonce: 1000 + }, + from: userDID.string(), + to: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(rsReq)); + + await rsHandlerr.handleRevocationStatusRequest(userDID, msgBytes); + }); + + it(`revocation status - no 'from' field`, 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 id = uuid.v4(); + const rsReq: RevocationStatusRequestMessage = { + id, + typ: PROTOCOL_CONSTANTS.MediaType.PlainMessage, + type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_REQUEST_MESSAGE_TYPE, + thid: id, + body: { + revocation_nonce: 1000 + }, + to: issuerDID.string() + }; + + const msgBytes = byteEncoder.encode(JSON.stringify(rsReq)); + + try { + await rsHandlerr.handleRevocationStatusRequest(userDID, msgBytes); + expect.fail(); + } catch (err: unknown) { + expect((err as Error).message).to.be.equal(`failed request. empty 'from' field`); + } + }); +});