Skip to content

Commit

Permalink
add RevocationStatusHandler (#197)
Browse files Browse the repository at this point in the history
* add RevocationStatusHandler


* fix W3CCredential fromJSON param
  • Loading branch information
volodymyr-basiuk authored Mar 20, 2024
1 parent 44f87ca commit 3dff826
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/iden3comm/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './auth';
export * from './fetch';
export * from './contract-request';
export * from './refresh';
export * from './revocation-status';
export * from './common';
160 changes: 160 additions & 0 deletions src/iden3comm/handlers/revocation-status.ts
Original file line number Diff line number Diff line change
@@ -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<RevocationStatusRequestMessage>`
*/
parseRevocationStatusRequest(request: Uint8Array): Promise<RevocationStatusRequestMessage>;

/**
* handle revocation status request
* @param {did} did - sender DID
* @param {Uint8Array} request - raw byte message
* @param {RevocationStatusHandlerOptions} opts - handler options
* @returns {Promise<Uint8Array>}` - revocation status response message
*/
handleRevocationStatusRequest(
did: DID,
request: Uint8Array,
opts?: RevocationStatusHandlerOptions
): Promise<Uint8Array>;
}

/** 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<RevocationStatusRequestMessage> {
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<Uint8Array> {
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
});
}
}
25 changes: 24 additions & 1 deletion src/identity/identity-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export interface IIdentityWallet {
): Promise<MerkleTreeProofWithTreeState>;

/**
* 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
Expand All @@ -181,6 +181,21 @@ export interface IIdentityWallet {
treeState?: TreeState
): Promise<MerkleTreeProofWithTreeState>;

/**
* 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<MerkleTreeProofWithTreeState>` - MerkleTreeProof and TreeState on which proof has been generated
*/
generateNonRevocationMtpWithNonce(
did: DID,
revNonce: bigint,
treeState?: TreeState
): Promise<MerkleTreeProofWithTreeState>;

/**
* Signs a payload of arbitrary size with an Auth BJJ Credential that identifies a key for signing.
*
Expand Down Expand Up @@ -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<MerkleTreeProofWithTreeState> {
const treesModel = await this.getDIDTreeModel(did);

const revocationTree = await this._storage.mt.getMerkleTreeByIdentifierAndType(
Expand Down
2 changes: 1 addition & 1 deletion src/verifiable/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
123 changes: 123 additions & 0 deletions tests/handlers/revocation-status.test.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
});
});

0 comments on commit 3dff826

Please sign in to comment.