diff --git a/src/credentials/credential-wallet.ts b/src/credentials/credential-wallet.ts index 9deb87dc..a64b1193 100644 --- a/src/credentials/credential-wallet.ts +++ b/src/credentials/credential-wallet.ts @@ -16,6 +16,8 @@ import { JSONSchema } from '../schema-processor'; import * as uuid from 'uuid'; import { CredentialStatusResolverRegistry } from './status/resolver'; import { IssuerResolver } from './status/sparse-merkle-tree'; +import { AgentResolver } from './status/agent-revocation'; +import { CredentialStatusResolveOptions } from './status/resolver'; // ErrAllClaimsRevoked all claims are revoked. const ErrAllClaimsRevoked = 'all claims are revoked'; @@ -166,8 +168,7 @@ export interface ICredentialWallet { */ getRevocationStatus( credStatus: CredentialStatus, - issuerDID: DID, - issuerData: IssuerData + credentialStatusResolveOptions?: CredentialStatusResolveOptions ): Promise; /** * Creates a W3C verifiable Credential object @@ -224,6 +225,10 @@ export class CredentialWallet implements ICredentialWallet { CredentialStatusType.SparseMerkleTreeProof, new IssuerResolver() ); + this._credentialStatusResolverRegistry.register( + CredentialStatusType.Iden3commRevocationStatusV1, + new AgentResolver() + ); } } @@ -269,7 +274,26 @@ export class CredentialWallet implements ICredentialWallet { } const issuerDID = DID.parse(cred.issuer); - return await this.getRevocationStatus(cred.credentialStatus, issuerDID, issuerData); + let userDID: DID; + if (!cred.credentialSubject.id) { + userDID = issuerDID; + } else { + try { + if (typeof cred.credentialSubject.id !== 'string') { + throw new Error('credential status `id` is not a string'); + } + userDID = DID.parse(cred.credentialSubject.id); + } catch (e) { + throw new Error('credential status `id` is not a valid DID'); + } + } + + const opts: CredentialStatusResolveOptions = { + issuerData, + issuerDID, + userDID + }; + return await this.getRevocationStatus(cred.credentialStatus, opts); } /** @@ -277,18 +301,14 @@ export class CredentialWallet implements ICredentialWallet { */ async getRevocationStatus( credStatus: CredentialStatus, - issuerDID: DID, - issuerData: IssuerData + credentialStatusResolveOptions?: CredentialStatusResolveOptions ): Promise { const statusResolver = this._credentialStatusResolverRegistry.get(credStatus.type); if (!statusResolver) { throw new Error(`credential status resolver does not exist for ${credStatus.type} type`); } - const cs = await statusResolver.resolve(credStatus, { - issuer: issuerDID, - issuerData: issuerData - }); + const cs = await statusResolver.resolve(credStatus, credentialStatusResolveOptions); return cs; } diff --git a/src/credentials/index.ts b/src/credentials/index.ts index b857efa6..ebb898db 100644 --- a/src/credentials/index.ts +++ b/src/credentials/index.ts @@ -2,5 +2,6 @@ export * from './status/on-chain-revocation'; export * from './status/reverse-sparse-merkle-tree'; export * from './status/sparse-merkle-tree'; export * from './status/resolver'; +export * from './status/agent-revocation'; export * from './credential-wallet'; export * from './rhs'; diff --git a/src/credentials/status/agent-revocation.ts b/src/credentials/status/agent-revocation.ts new file mode 100644 index 00000000..f7910205 --- /dev/null +++ b/src/credentials/status/agent-revocation.ts @@ -0,0 +1,65 @@ +import { CredentialStatus, RevocationStatus } from '../../verifiable'; +import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver'; +import { + RevocationStatusRequestMessage, + RevocationStatusResponseMessage +} from '../../iden3comm/types'; +import { MediaType, PROTOCOL_MESSAGE_TYPE } from '../../iden3comm/constants'; +import * as uuid from 'uuid'; + +export class AgentResolver implements CredentialStatusResolver { + async resolve( + credentialStatus: CredentialStatus, + credentialStatusResolveOptions?: CredentialStatusResolveOptions + ): Promise { + if (!credentialStatusResolveOptions?.issuerDID) { + throw new Error('IssuerDID is not set in options'); + } + if (!credentialStatusResolveOptions?.userDID) { + throw new Error('UserDID is not set in options'); + } + + const from = credentialStatusResolveOptions.userDID.toString(); + const to = credentialStatusResolveOptions.issuerDID.toString(); + const msg = buildRevocationMessageRequest(from, to, credentialStatus.revocationNonce); + const response = await fetch(credentialStatus.id, { + method: 'POST', + body: JSON.stringify(msg), + headers: { + 'Content-Type': 'application/json' + } + }); + const agentResponse = await response.json(); + return Object.assign(new RevocationStatusAgent(), { agentResponse }).toRevocationStatus(); + } +} + +function buildRevocationMessageRequest( + from: string, + to: string, + revocationNonce: number +): RevocationStatusRequestMessage { + const revocationStatusRequestMessage: RevocationStatusRequestMessage = { + id: uuid.v4(), + typ: MediaType.PlainMessage, + type: PROTOCOL_MESSAGE_TYPE.REVOCATION_STATUS_REQUEST_MESSAGE_TYPE, + body: { + revocation_nonce: revocationNonce + }, + thid: uuid.v4(), + from: from, + to: to + }; + return revocationStatusRequestMessage; +} + +class RevocationStatusAgent { + agentResponse: RevocationStatusResponseMessage; + + toRevocationStatus(): RevocationStatus { + return { + mtp: this.agentResponse.body.mtp, + issuer: this.agentResponse.body.issuer + }; + } +} diff --git a/src/credentials/status/on-chain-revocation.ts b/src/credentials/status/on-chain-revocation.ts index ef9ad907..d4cba5ed 100644 --- a/src/credentials/status/on-chain-revocation.ts +++ b/src/credentials/status/on-chain-revocation.ts @@ -1,6 +1,6 @@ import { RevocationStatus, CredentialStatus } from '../../verifiable'; import { EthConnectionConfig } from '../../storage/blockchain'; -import { CredentialStatusResolver } from './resolver'; +import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver'; import { OnChainRevocationStorage } from '../../storage/blockchain/onchain-revocation'; import { DID } from '@iden3/js-iden3-core'; @@ -23,11 +23,12 @@ export class OnChainResolver implements CredentialStatusResolver { async resolve( credentialStatus: CredentialStatus, - opts: { - issuer: DID; - } + credentialStatusResolveOptions?: CredentialStatusResolveOptions ): Promise { - return this.getRevocationOnChain(credentialStatus, opts.issuer); + if (!credentialStatusResolveOptions?.issuerDID) { + throw new Error('IssuerDID is not set in options'); + } + return this.getRevocationOnChain(credentialStatus, credentialStatusResolveOptions.issuerDID); } /** @@ -74,8 +75,8 @@ export class OnChainResolver implements CredentialStatusResolver { throw new Error('revocationNonce not found'); } - const issuerDID = id.split('/')[0]; - if (!issuerDID) { + const issuer = id.split('/')[0]; + if (!issuer) { throw new Error('issuer not found in credentialStatus id'); } // TODO (illia-korotia): after merging core v2 need to parse contract address from did if `contractAddress` is not present in id as param @@ -89,7 +90,7 @@ export class OnChainResolver implements CredentialStatusResolver { const chainId = parseInt(parts[0], 10); const contractAddress = parts[1]; - return { contractAddress, chainId, revocationNonce, issuer: issuerDID }; + return { contractAddress, chainId, revocationNonce, issuer }; } networkByChainId(chainId: number): EthConnectionConfig { diff --git a/src/credentials/status/resolver.ts b/src/credentials/status/resolver.ts index ea88627f..f1cc421e 100644 --- a/src/credentials/status/resolver.ts +++ b/src/credentials/status/resolver.ts @@ -1,7 +1,22 @@ -import { CredentialStatusType, CredentialStatus, RevocationStatus } from '../../verifiable'; +import { DID } from '@iden3/js-iden3-core'; +import { + IssuerData, + RevocationStatus, + CredentialStatus, + CredentialStatusType +} from '../../verifiable'; + +export type CredentialStatusResolveOptions = { + issuerData?: IssuerData; + issuerDID?: DID; + userDID?: DID; +}; export interface CredentialStatusResolver { - resolve(credentialStatus: CredentialStatus, opts?: object): Promise; + resolve( + credentialStatus: CredentialStatus, + opts?: CredentialStatusResolveOptions + ): Promise; } export class CredentialStatusResolverRegistry { diff --git a/src/credentials/status/reverse-sparse-merkle-tree.ts b/src/credentials/status/reverse-sparse-merkle-tree.ts index 2929b7f4..634ff38b 100644 --- a/src/credentials/status/reverse-sparse-merkle-tree.ts +++ b/src/credentials/status/reverse-sparse-merkle-tree.ts @@ -10,8 +10,8 @@ import { testBit } from '@iden3/js-merkletree'; import { IStateStorage } from '../../storage'; -import { CredentialStatusResolver } from './resolver'; -import { CredentialStatus, IssuerData, RevocationStatus } from '../../verifiable'; +import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver'; +import { CredentialStatus, RevocationStatus } from '../../verifiable'; import { strMTHex } from '../../circuits'; import { VerifiableConstants, CredentialStatusType } from '../../verifiable/constants'; @@ -104,12 +104,9 @@ export class RHSResolver implements CredentialStatusResolver { public async getStatus( credentialStatus: CredentialStatus, - opts: { - issuer: DID; - issuerData?: IssuerData; - } + issuerDID: DID ): Promise { - const latestStateInfo = await this._state.getLatestStateById(opts.issuer.id.bigInt()); + const latestStateInfo = await this._state.getLatestStateById(issuerDID.id.bigInt()); const hashedRevNonce = newHashFromBigInt(BigInt(credentialStatus.revocationNonce ?? 0)); const hashedIssuerRoot = newHashFromBigInt(BigInt(latestStateInfo?.state ?? 0)); return await this.getRevocationStatusFromRHS( @@ -119,29 +116,33 @@ export class RHSResolver implements CredentialStatusResolver { ); } - public async resolve( + async resolve( credentialStatus: CredentialStatus, - opts: { - issuer: DID; - issuerData?: IssuerData; - } + credentialStatusResolveOptions?: CredentialStatusResolveOptions ): Promise { + if (!credentialStatusResolveOptions?.issuerDID) { + throw new Error('IssuerDID is not set in options'); + } + try { - return await this.getStatus(credentialStatus, opts); + return await this.getStatus(credentialStatus, credentialStatusResolveOptions.issuerDID); } catch (e) { const errMsg = e.reason ?? e.message; if ( - !!opts.issuerData && + !!credentialStatusResolveOptions.issuerData && errMsg.includes(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST) && - isIssuerGenesis(opts.issuer.toString(), opts.issuerData.state.value) + isIssuerGenesis( + credentialStatusResolveOptions.issuerDID.toString(), + credentialStatusResolveOptions.issuerData.state.value + ) ) { return { mtp: new Proof(), issuer: { - state: opts.issuerData.state.value, - revocationTreeRoot: opts.issuerData.state.revocationTreeRoot, - rootOfRoots: opts.issuerData.state.rootOfRoots, - claimsTreeRoot: opts.issuerData.state.claimsTreeRoot + state: credentialStatusResolveOptions.issuerData.state.value, + revocationTreeRoot: credentialStatusResolveOptions.issuerData.state.revocationTreeRoot, + rootOfRoots: credentialStatusResolveOptions.issuerData.state.rootOfRoots, + claimsTreeRoot: credentialStatusResolveOptions.issuerData.state.claimsTreeRoot } }; } diff --git a/src/iden3comm/handlers/auth.ts b/src/iden3comm/handlers/auth.ts index 01dfd6fa..abcddca1 100644 --- a/src/iden3comm/handlers/auth.ts +++ b/src/iden3comm/handlers/auth.ts @@ -168,11 +168,12 @@ export class AuthHandler implements IAuthHandler { if (credsForGenesisDID.length == 0) { throw new Error(`no credential were issued on the given id ${did.toString()}`); } + const { cred } = await this._credentialWallet.findNonRevokedCredential(credsForGenesisDID); const zkpRes: ZeroKnowledgeProofResponse = await this._proofService.generateProof( zkpReq, did, - credsForGenesisDID[0] + cred ); authResponse.body.scope.push(zkpRes); diff --git a/src/iden3comm/types/protocol/revocation.ts b/src/iden3comm/types/protocol/revocation.ts index ab52f691..52f6e10a 100644 --- a/src/iden3comm/types/protocol/revocation.ts +++ b/src/iden3comm/types/protocol/revocation.ts @@ -6,7 +6,8 @@ import { MediaType } from '../../constants'; export type RevocationStatusRequestMessage = { id: string; typ?: MediaType; - type: RTCIceProtocol; + type: ProtocolMessage; + thid?: string; body?: RevocationStatusRequestMessageBody; from?: string; to?: string; diff --git a/src/proof/proof-service.ts b/src/proof/proof-service.ts index c79a229d..ec64df7e 100644 --- a/src/proof/proof-service.ts +++ b/src/proof/proof-service.ts @@ -24,7 +24,7 @@ import { TreeState, ValueProof } from '../circuits'; -import { ICredentialWallet } from '../credentials'; +import { CredentialStatusResolveOptions, ICredentialWallet } from '../credentials'; import { IIdentityWallet } from '../identity'; import { createVerifiablePresentation, @@ -572,15 +572,31 @@ export class ProofService implements IProofService { if (sigProof) { const signature = await bJJSignatureFromHexString(sigProof.signature); - const issuer = DID.parse(sigProof.issuerData.id); + const issuerDID = DID.parse(sigProof.issuerData.id); + let userDID: DID; + if (!credential.credentialSubject.id) { + userDID = issuerDID; + } else { + try { + if (typeof credential.credentialSubject.id !== 'string') { + throw new Error('credential status `id` is not a string'); + } + userDID = DID.parse(credential.credentialSubject.id); + } catch (e) { + throw new Error('credential status `id` is not a valid DID'); + } + } + const opts: CredentialStatusResolveOptions = { + issuerData: sigProof.issuerData, + issuerDID, + userDID + }; const rs: RevocationStatus = await this._credentialWallet.getRevocationStatus( sigProof.issuerData.credentialStatus, - issuer, - sigProof.issuerData + opts ); - //todo: check if this is correct const issuerAuthNonRevProof: MTProof = { treeState: { state: strMTHex(rs.issuer.state), diff --git a/tests/rhs/rhs.test.ts b/tests/rhs/rhs.test.ts index 36d6eb1a..3a44d086 100644 --- a/tests/rhs/rhs.test.ts +++ b/tests/rhs/rhs.test.ts @@ -186,7 +186,7 @@ describe('rhs', () => { const rhsResolver = new RHSResolver(mockStateStorageForGenesisState); return rhsResolver - .getStatus(credRHSStatus, { issuer: issuerDID }) + .getStatus(credRHSStatus, issuerDID) .then(function (m) { throw new Error('was not supposed to succeed'); }) @@ -262,7 +262,7 @@ describe('rhs', () => { await idWallet.publishStateToRHS(issuerDID, rhsUrl); const rhsResolver = new RHSResolver(mockStateStorageForDefinedState); - const rhsStatus = await rhsResolver.resolve(credRHSStatus, { issuer: issuerDID }); + const rhsStatus = await rhsResolver.resolve(credRHSStatus, { issuerDID: issuerDID }); expect(rhsStatus.issuer.state).to.equal(res.newTreeState.state.hex()); expect(rhsStatus.issuer.claimsTreeRoot).to.equal(res.newTreeState.claimsRoot.hex()); @@ -369,7 +369,7 @@ describe('rhs', () => { // state is published to blockchain (2) const rhsResolver = new RHSResolver(dataStorage.states); - const rhsStatus = await rhsResolver.getStatus(credRHSStatus, { issuer: issuerDID }); + const rhsStatus = await rhsResolver.getStatus(credRHSStatus, issuerDID); expect(rhsStatus.issuer.state).to.equal(latestTree.state.hex()); expect(rhsStatus.issuer.claimsTreeRoot).to.equal((await latestTree.claimsTree.root()).hex());