From b0d75e9af7f28384ce2e5ef744dfbc3c302cd1a8 Mon Sep 17 00:00:00 2001 From: nickreynolds Date: Wed, 23 Nov 2022 10:09:11 -0500 Subject: [PATCH] fix(credential-w3c): correct verification of credentials given as objects with jwt proofs (#1071) * test(credential-w3c): add unit test to exercise case when credential is modified outside of jwt proof note: this test *should* pass, but currently fails since there is a bug * test(credential-w3c): add more tests exercising verifyCredential * fix(credential-w3c): better verification of credentials given as objects with jwt proofs * chore(did-provider-ion): explicitly declare dependency * chore: cleanup * chore: cleanup --- packages/credential-w3c/package.json | 1 + .../src/__tests__/issue-verify-flow.test.ts | 100 +++++++++++++++++- packages/credential-w3c/src/action-handler.ts | 17 +++ packages/did-provider-ion/package.json | 1 + 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/packages/credential-w3c/package.json b/packages/credential-w3c/package.json index 82d745008..e41eb5088 100644 --- a/packages/credential-w3c/package.json +++ b/packages/credential-w3c/package.json @@ -14,6 +14,7 @@ "@veramo/did-resolver": "^4.1.1", "@veramo/message-handler": "^4.1.1", "@veramo/utils": "^4.1.1", + "canonicalize": "^1.0.8", "debug": "^4.3.3", "did-jwt-vc": "^3.1.0", "did-resolver": "^4.0.1", diff --git a/packages/credential-w3c/src/__tests__/issue-verify-flow.test.ts b/packages/credential-w3c/src/__tests__/issue-verify-flow.test.ts index 0ed587138..d7e47b381 100644 --- a/packages/credential-w3c/src/__tests__/issue-verify-flow.test.ts +++ b/packages/credential-w3c/src/__tests__/issue-verify-flow.test.ts @@ -18,6 +18,11 @@ import { EthrDIDProvider } from '../../../did-provider-ethr/src' import { ContextDoc } from '../../../credential-ld/src/types' import { Resolver } from 'did-resolver' import { getResolver as ethrDidResolver } from 'ethr-did-resolver' +import { CredentialIssuerLD } from '../../../credential-ld/src/action-handler' +import { LdDefaultContexts } from '../../../credential-ld/src/ld-default-contexts' +import { VeramoEd25519Signature2018 } from '../../../credential-ld/src/suites/Ed25519Signature2018' +import { VeramoEcdsaSecp256k1RecoverySignature2020 } from '../../../credential-ld/src/suites/EcdsaSecp256k1RecoverySignature2020' +import { VerifiableCredential } from '../../../core/src' jest.setTimeout(300000) @@ -35,6 +40,7 @@ describe('credential-w3c full flow', () => { let didKeyIdentifier: IIdentifier let didEthrIdentifier: IIdentifier let agent: TAgent + let credential: CredentialPayload beforeAll(async () => { agent = createAgent({ @@ -63,20 +69,103 @@ describe('credential-w3c full flow', () => { }), }), new CredentialIssuer(), + new CredentialIssuerLD({ + contextMaps: [LdDefaultContexts, customContext], + suites: [new VeramoEd25519Signature2018(), new VeramoEcdsaSecp256k1RecoverySignature2020()], + }), ], }) didKeyIdentifier = await agent.didManagerCreate() didEthrIdentifier = await agent.didManagerCreate({ provider: 'did:ethr:goerli' }) - }) - - it('verify a verifiablePresentation', async () => { - const credential: CredentialPayload = { + credential = { issuer: didKeyIdentifier.did, '@context': ['custom:example.context'], credentialSubject: { nothing: 'else matters', }, } + }) + + it(`verifies a credential created with jwt proofType`, async () => { + const verifiableCredential1 = await agent.createVerifiableCredential({ + credential, + proofFormat: 'jwt', + }) + const verifyResult = await agent.verifyCredential({ credential: verifiableCredential1 }) + expect(verifyResult.verified).toBeTruthy() + }) + + it(`verifies a credential created with lds proofType`, async () => { + const verifiableCredential1 = await agent.createVerifiableCredential({ + credential, + proofFormat: 'lds', + }) + const verifyResult = await agent.verifyCredential({ credential: verifiableCredential1 }) + expect(verifyResult.verified).toBeTruthy() + }) + + it(`fails to verify a credential created with lds proofType with modified values`, async () => { + const verifiableCredential1 = await agent.createVerifiableCredential({ + credential, + proofFormat: 'lds', + }) + const modifiedCredential: VerifiableCredential = { ...verifiableCredential1, issuer: { id: 'did:fake:wrong' }} + const verifyResult = await agent.verifyCredential({ credential: modifiedCredential }) + expect(verifyResult.verified).toBeFalsy() + }) + + + it('fails the verification of a jwt credential with false value outside of proof', async () => { + const verifiableCredential1 = await agent.createVerifiableCredential({ + credential, + proofFormat: 'jwt', + }) + + const modifiedCredential: VerifiableCredential = { ...verifiableCredential1, issuer: { id: 'did:fake:wrong' }} + const verifyResult = await agent.verifyCredential({ credential: modifiedCredential }) + + expect(verifyResult.verified).toBeFalsy() + }) + + // uncomment when https://github.com/uport-project/veramo/issues/1073 is resolved + // example credential found at: https://learn.mattr.global/tutorials/web-credentials/issue/issue-basic + // it(`verifies a credential created with lds proofType via Mattr`, async () => { + // const verifiableCredential1 = { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // { + // "@vocab": "https://w3id.org/security/undefinedTerm#" + // }, + // "https://schema.org" + // ], + // "type": [ + // "VerifiableCredential", + // "CourseCredential" + // ], + // "issuer": { + // "id": "did:key:z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj", + // "name": "tenant" + // }, + // "issuanceDate": "2021-07-26T01:05:05.152Z", + // "credentialSubject": { + // "id": "did:key:z6MkfxQU7dy8eKxyHpG267FV23agZQu9zmokd8BprepfHALi", + // "givenName": "Chris", + // "familyName": "Shin", + // "educationalCredentialAwarded": "Certificate Name" + // }, + // "proof": { + // "type": "Ed25519Signature2018", + // "created": "2021-07-26T01:05:06Z", + // "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..o6hnrrWpArG8LQz2Ex_u66_BtuPdp3Hkz18nhNdNhJ7J1k_2lmCCwsNdmo-kNFirZdSIMzqO-V3wEjMDphVEAA", + // "proofPurpose": "assertionMethod", + // "verificationMethod": "did:key:z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj#z6MkndAHigYrXNpape7jgaC7jHiWwxzB3chuKUGXJg2b5RSj" + // } + // } + // const verifyResult = await agent.verifyCredential({ credential: verifiableCredential1 }) + // expect(verifyResult.verified).toBeTruthy() + // }) + + it('verify a verifiablePresentation', async () => { const verifiableCredential1 = await agent.createVerifiableCredential({ credential, proofFormat: 'jwt', @@ -101,7 +190,8 @@ describe('credential-w3c full flow', () => { expect(response.verified).toBe(true) }) - it.only('fails the verification of an expired credential', async () => { + + it('fails the verification of an expired credential', async () => { const presentationJWT = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjAyOTcyMTAsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSkZaRVJUUVNJc0luUjVjQ0k2SWtwWFZDSjkuZXlKbGVIQWlPakUyTmpBeU9UY3lNVEFzSW5aaklqcDdJa0JqYjI1MFpYaDBJanBiSW1oMGRIQnpPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1ERTRMMk55WldSbGJuUnBZV3h6TDNZeElpd2lZM1Z6ZEc5dE9tVjRZVzF3YkdVdVkyOXVkR1Y0ZENKZExDSjBlWEJsSWpwYklsWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnNJbDBzSW1OeVpXUmxiblJwWVd4VGRXSnFaV04wSWpwN0ltNXZkR2hwYm1jaU9pSmxiSE5sSUcxaGRIUmxjbk1pZlgwc0ltNWlaaUk2TVRZMk1ESTVOekl4TUN3aWFYTnpJam9pWkdsa09tdGxlVHA2TmsxcmFWVTNVbk5hVnpOeWFXVmxRMjg1U25OMVVEUnpRWEZYZFdGRE0zbGhjbWwxWVZCMlVXcHRZVzVsWTFBaWZRLkZhdzBEUWNNdXpacEVkcy1LR3dOalMyM2IzbUEzZFhQWXBQcGJzNmRVSnhIOVBrZzVieGF3UDVwMlNPajdQM25IdEpCR3lwTjJ3NzRfZjc3SjF5dUJ3Il19LCJuYmYiOjE2NjAyOTcyMTAsImlzcyI6ImRpZDprZXk6ejZNa2lVN1JzWlczcmllZUNvOUpzdVA0c0FxV3VhQzN5YXJpdWFQdlFqbWFuZWNQIn0.YcYbyqVlD8YsTjVw0kCEs0P_ie6SFMakf_ncPntEjsmS9C4cKyiS50ZhNkOv0R3Roy1NrzX7h93WBU55KeJlCw' diff --git a/packages/credential-w3c/src/action-handler.ts b/packages/credential-w3c/src/action-handler.ts index 05dd5fc05..ca8d51ba0 100644 --- a/packages/credential-w3c/src/action-handler.ts +++ b/packages/credential-w3c/src/action-handler.ts @@ -41,6 +41,8 @@ import { import Debug from 'debug' import { Resolvable } from 'did-resolver' +import canonicalize from 'canonicalize' + const enum DocumentFormat { JWT, JSONLD, @@ -274,6 +276,7 @@ export class CredentialPlugin implements IAgentPlugin { const resolver = { resolve: (didUrl: string) => context.agent.resolveDid({ didUrl }) } as Resolvable try { + // needs broader credential as well to check equivalence with jwt verificationResult = await verifyCredentialJWT(jwt, resolver, { ...otherOptions, policies: { @@ -285,6 +288,20 @@ export class CredentialPlugin implements IAgentPlugin { }, }) verifiedCredential = verificationResult.verifiableCredential + + // if credential was presented with other fields, make sure those fields match what's in the JWT + if (typeof credential !== 'string') { + const credentialCopy = JSON.parse(JSON.stringify(credential)) + delete credentialCopy.proof.jwt + + const verifiedCopy = JSON.parse(JSON.stringify(verifiedCredential)) + delete verifiedCopy.proof.jwt + + if(canonicalize(credentialCopy) !== canonicalize(verifiedCopy)) { + verificationResult.verified = false + verificationResult.error = new Error('Credential does not match JWT') + } + } } catch (e: any) { let { message, errorCode } = e return { diff --git a/packages/did-provider-ion/package.json b/packages/did-provider-ion/package.json index f48e0f160..7dd8a982e 100644 --- a/packages/did-provider-ion/package.json +++ b/packages/did-provider-ion/package.json @@ -19,6 +19,7 @@ "@veramo/key-manager": "^4.1.1", "@veramo/kms-local": "^4.1.1", "base64url": "^3.0.1", + "canonicalize": "^1.0.8", "debug": "^4.3.3", "uint8arrays": "^3.0.0" },