diff --git a/e2e/claims.service.e2e.ts b/e2e/claims.service.e2e.ts index cf1840db..ef2955a8 100644 --- a/e2e/claims.service.e2e.ts +++ b/e2e/claims.service.e2e.ts @@ -20,6 +20,7 @@ import { MessagingService, IClaimIssuance, SignerT, + Claim, } from '../src'; import { replenish, root, rpcUrl, setupENS } from './utils/setup-contracts'; import { ClaimManager__factory } from '../ethers/factories/ClaimManager__factory'; @@ -86,6 +87,8 @@ const mockCachedDocument = jest.fn().mockImplementation((did: string) => { ], }; // all documents are created }); +const getClamsBySubjectInitMock: (did: string) => Partial[] = () => []; + const mockGetCachedOwnedAssets = jest.fn(); const mockGetAssetById = jest.fn(); const mockGetClaimsBySubject = jest.fn(); @@ -123,7 +126,7 @@ jest.mock('../src/modules/messaging/messaging.service', () => { }; }); -describe('Enrollment claim tests', () => { +describe('Сlaim tests', () => { let claimsService: ClaimsService; let signerService: SignerService; let assetsService: AssetsService; @@ -133,7 +136,7 @@ describe('Enrollment claim tests', () => { beforeEach(async () => { jest.clearAllMocks(); - //mockGetClaimsBySubject.mockReset(); + mockGetClaimsBySubject.mockImplementation(getClamsBySubjectInitMock); await replenish(await staticIssuer.getAddress()); await replenish(await rootOwner.getAddress()); await replenish(await dynamicIssuer.getAddress()); @@ -189,7 +192,7 @@ describe('Enrollment claim tests', () => { setLogger(new ConsoleLogger(LogLevel.warn)); }); - describe('Enrollment tests', () => { + describe('Role claim tests', () => { async function enrolAndIssue( requestSigner: Required, issueSigner: Required, @@ -211,7 +214,11 @@ describe('Enrollment claim tests', () => { const requesterDID = signerService.did; const requestorFields = [{ key: 'temperature', value: 36 }]; await claimsService.createClaimRequest({ - claim: { claimType, claimTypeVersion: version, requestorFields }, + claim: { + claimType, + claimTypeVersion: version, + requestorFields, + }, registrationTypes, subject: subjectDID, }); @@ -228,6 +235,22 @@ describe('Enrollment claim tests', () => { mockIssueClaim.mock.calls.pop() ); + const currentGetClaimsBySubjectMock = + mockGetClaimsBySubject.getMockImplementation() as jest.Mock; + mockGetClaimsBySubject.mockImplementation((did) => + [...currentGetClaimsBySubjectMock(did)].concat( + did === subjectDID + ? [ + { + claimType, + claimTypeVersion: version, + issuedToken: issuedClaim.issuedToken, + }, + ] + : [] + ) + ); + const { issuedToken, requester, @@ -409,6 +432,11 @@ describe('Enrollment claim tests', () => { await enrolAndIssue(dynamicIssuer, staticIssuer, { subjectDID: dynamicIssuerDID, claimType: `${roleName1}.${root}`, + registrationTypes: [ + RegistrationTypes.OnChain, + RegistrationTypes.OffChain, // role type issuer should have offchain claim + ], + issuerFields: [], }); expect( @@ -423,7 +451,6 @@ describe('Enrollment claim tests', () => { subjectDID: rootOwnerDID, claimType: `${roleName2}.${root}`, }); - return expect( await claimsService.hasOnChainRole( rootOwnerDID, @@ -442,16 +469,10 @@ describe('Enrollment claim tests', () => { returnSteps: false, }); - const role1Claim = { - claimType: `${roleName1}.${root}`, - isAccepted: true, - }; await enrolAndIssue(rootOwner, staticIssuer, { subjectDID: rootOwnerDID, claimType: `${roleName1}.${root}`, }); - mockGetClaimsBySubject.mockImplementationOnce(() => [role1Claim]); // to verify requesting - mockGetClaimsBySubject.mockImplementationOnce(() => [role1Claim]); // to verify issuance await enrolAndIssue(rootOwner, staticIssuer, { subjectDID: rootOwnerDID, @@ -475,8 +496,6 @@ describe('Enrollment claim tests', () => { returnSteps: false, }); - mockGetClaimsBySubject.mockImplementationOnce(() => []); - return expect( enrolAndIssue(rootOwner, staticIssuer, { subjectDID: rootOwnerDID, @@ -567,7 +586,7 @@ describe('Enrollment claim tests', () => { await claimsService.createClaimRequest({ claim: { claimType: `${roleName1}.${root}`, - claimTypeVersion: 1, + claimTypeVersion: version, requestorFields: [], }, registrationTypes, diff --git a/package-lock.json b/package-lock.json index 61bbc508..63e969a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@energyweb/ekc": "^0.6.6", "@energyweb/onchain-claims": "^1.0.1-alpha.46.0", "@energyweb/staking-pool": "^1.0.0-rc.14", + "@energyweb/vc-verification": "^1.0.1-alpha.36.0", "@ensdomains/ens": "^0.6.2", "@ew-did-registry/claims": "^0.6.3-alpha.577.0", "@ew-did-registry/credentials-interface": "^0.6.3-alpha.577.0", diff --git a/package.json b/package.json index bded4d16..be967915 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@energyweb/ekc": "^0.6.6", "@energyweb/onchain-claims": "^1.0.1-alpha.46.0", "@energyweb/staking-pool": "^1.0.0-rc.14", + "@energyweb/vc-verification": "^1.0.1-alpha.36.0", "@ensdomains/ens": "^0.6.2", "@ew-did-registry/claims": "^0.6.3-alpha.577.0", "@ew-did-registry/credentials-interface": "^0.6.3-alpha.577.0", diff --git a/src/modules/claims/claims.service.ts b/src/modules/claims/claims.service.ts index 3d68f291..19f97cf8 100644 --- a/src/modules/claims/claims.service.ts +++ b/src/modules/claims/claims.service.ts @@ -6,6 +6,12 @@ import { IRoleDefinition, PreconditionType, } from '@energyweb/credential-governance'; +import { + CredentialResolver, + EthersProviderIssuerDefinitionResolver, + IpfsCredentialResolver, + VCIssuerVerification, +} from '@energyweb/vc-verification'; import { ClaimRevocation } from '@energyweb/onchain-claims'; import { Methods } from '@ew-did-registry/did'; import { Algorithms } from '@ew-did-registry/jwt'; @@ -16,6 +22,7 @@ import { IServiceEndpoint, ProviderTypes, } from '@ew-did-registry/did-resolver-interface'; +import { VerifiableCredential } from '@ew-did-registry/credentials-interface'; import { ClaimManager__factory } from '../../../ethers/factories/ClaimManager__factory'; import { ERROR_MESSAGES } from '../../errors'; import { emptyAddress } from '../../utils/constants'; @@ -64,7 +71,10 @@ import { compareDID, isValidDID } from '../../utils/did'; import { JWT } from '@ew-did-registry/jwt'; import { privToPem, KeyType } from '@ew-did-registry/keys'; import { readyToBeRegisteredOnchain } from './claims.types'; -import { VerifiableCredentialsServiceBase } from '../verifiable-credentials'; +import { + RoleCredentialSubject, + VerifiableCredentialsServiceBase, +} from '../verifiable-credentials'; import { NotAuthorizedIssuer } from '../../errors/not-authorized-issuer'; const { @@ -90,6 +100,7 @@ const { export class ClaimsService { private _claimManager: string; private _claimManagerInterface = ClaimManager__factory.createInterface(); + private _vcIssuerVerifier: VCIssuerVerification; private _claimRevocation: ClaimRevocation; constructor( @@ -100,6 +111,7 @@ export class ClaimsService { private _verifiableCredentialService: VerifiableCredentialsServiceBase ) { this._signerService.onInit(this.init.bind(this)); + this._setClaimIssuerVerifier(); } static async create( @@ -374,8 +386,6 @@ export class ClaimsService { claimData: { claimType: string; claimTypeVersion: number }; sub: string; }; - await this.verifyIssuer(claimData.claimType); - await this.verifyEnrolmentPrerequisites({ subject: sub, role: claimData.claimType, @@ -415,6 +425,7 @@ export class ClaimsService { } if (registrationTypes.includes(RegistrationTypes.OffChain)) { + await this.verifyVcIssuer(claimData.claimType); const publicClaim: IPublicClaim = { did: sub, signer: this._signerService.did, @@ -441,6 +452,19 @@ export class ClaimsService { await this._cacheClient.issueClaim(this._signerService.did, message); } + async verifyVc(vc: VerifiableCredential) { + const issuerDID = this._signerService.did; + const role = vc.credentialSubject.role.namespace; + if ( + !( + (await this._vcIssuerVerifier.verifyIssuerAuthority(role, issuerDID)) || + (await this._vcIssuerVerifier.verifyChainOfTrustByRoleDefinition(vc)) + ) + ) { + throw new NotAuthorizedIssuer(issuerDID, role); + } + } + /** * Register issued on-chain claim on Claim Manager contract. * @@ -572,7 +596,7 @@ export class ClaimsService { registrationTypes = [RegistrationTypes.OffChain], claim, }: IssueClaimOptions): Promise { - await this.verifyIssuer(claim.claimType); + await this.verifyVcIssuer(claim.claimType); await this.verifyEnrolmentPrerequisites({ subject, role: claim.claimType }); const message: IClaimIssuance = { @@ -1114,15 +1138,16 @@ export class ClaimsService { } /** - * Verify if the user is able to issue the given role. Throws an error when the user is not able to issue the given role. + * Verify if the user is issuer of the role verifiable credential * * @param {String} role Registration types of the claim */ - private async verifyIssuer(role: string): Promise { + private async verifyVcIssuer(role: string): Promise { if ( - !( - await this._cacheClient.getAllowedRolesByIssuer(this._signerService.did) - ).some((r) => r.namespace === role) + !(await this._vcIssuerVerifier.verifyIssuerAuthority( + role, + this._signerService.did + )) ) { throw new NotAuthorizedIssuer(this._signerService.did, role); } @@ -1312,4 +1337,22 @@ export class ClaimsService { await this._signerService.signMessage(arrayify(proofHash)) ); } + + private _setClaimIssuerVerifier() { + const credentialResolver: CredentialResolver = new IpfsCredentialResolver( + this._signerService.provider, + this._didRegistry.registrySettings, + this._didRegistry.ipfsStore + ); + const issuerResolver = new EthersProviderIssuerDefinitionResolver( + this._signerService.provider, + chainConfigs()[this._signerService.chainId].ensResolverV2Address + ); + this._vcIssuerVerifier = new VCIssuerVerification( + this._signerService.provider, + this._didRegistry.registrySettings, + credentialResolver, + issuerResolver + ); + } } diff --git a/src/modules/did-registry/did-registry.service.ts b/src/modules/did-registry/did-registry.service.ts index 7aca4feb..e5a2e450 100644 --- a/src/modules/did-registry/did-registry.service.ts +++ b/src/modules/did-registry/did-registry.service.ts @@ -18,6 +18,7 @@ import { KeyTags, ProviderTypes, PubKeyType, + RegistrySettings, } from '@ew-did-registry/did-resolver-interface'; import { DIDDocumentFull, @@ -226,6 +227,12 @@ export class DidRegistry { return didDocument?.delegates; } + get registrySettings(): RegistrySettings { + return { + address: chainConfigs()[this._signerService.chainId].didRegistryAddress, + }; + } + /** * Create a public claim with provided data. *