From 4d2c00a3c1c659212dd94c27bba38e4814d4b72f Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Tue, 3 Dec 2024 17:13:23 +0100 Subject: [PATCH 1/6] feat(DataIntegrity): blocked - prepare tests and logic for proofPurpose in other suites --- src/suites/EcdsaSd2023.ts | 11 ++++++++++- .../verifier/ecdsa-sd-2023-derived.test.ts | 19 +++++++++++++++++-- .../proof-chain-example-secp256k1.test.ts | 15 ++++++++++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/suites/EcdsaSd2023.ts b/src/suites/EcdsaSd2023.ts index dab4b798a..79516121b 100644 --- a/src/suites/EcdsaSd2023.ts +++ b/src/suites/EcdsaSd2023.ts @@ -15,7 +15,7 @@ import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3'; import type { IDidDocument } from '../models/DidDocument'; import { VerifierError } from '../models'; -const { purposes: { AssertionProofPurpose } } = jsigs; +const { purposes: { AssertionProofPurpose, AuthenticationProofPurpose } } = jsigs; enum SUB_STEPS { retrieveVerificationMethodPublicKey = 'retrieveVerificationMethodPublicKey', @@ -35,6 +35,8 @@ export default class EcdsaSd2023 extends Suite { public cryptosuite = 'ecdsa-sd-2023'; public publicKey: string; public verificationKey: any; + public proofPurpose: string; + private readonly proofPurposeMap: any; constructor (props: SuiteAPI) { super(props); @@ -44,6 +46,11 @@ export default class EcdsaSd2023 extends Suite { this.documentToVerify = props.document as BlockcertsV3; this.issuer = props.issuer; this.proof = props.proof as VCProof; + this.proofPurpose = props.proofPurpose ?? 'assertionMethod'; + this.proofPurposeMap = { + authentication: AuthenticationProofPurpose, + assertionMethod: AssertionProofPurpose + }; this.validateProofType(); } @@ -180,6 +187,8 @@ export default class EcdsaSd2023 extends Suite { const verificationMethod = (this.documentToVerify.proof as VCProof).verificationMethod; const verificationStatus = await jsigs.verify(this.documentToVerify, { suite, + // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 + // purpose: new this.proofPurposeMap[this.proofPurpose](), purpose: new AssertionProofPurpose(), documentLoader: this.generateDocumentLoader([ { diff --git a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts index 2e93efbe7..4ed60f78a 100644 --- a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts +++ b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; import { Certificate, VERIFICATION_STATUSES } from '../../../src'; import fixture from '../../fixtures/v3/ecdsa-sd-2023-derived-credential.json'; import fixtureIssuerProfile from '../../fixtures/issuer-blockcerts.json'; describe('ecdsa-sd-2023 signed and derived document test suite', function () { - it('should verify successfully', async function () { + beforeAll(function () { vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => { const explorerLookup = await importOriginal(); return { @@ -16,10 +16,25 @@ describe('ecdsa-sd-2023 signed and derived document test suite', function () { } }; }); + }); + + it('should verify successfully', async function () { const certificate = new Certificate(fixture as any); await certificate.init(); const result = await certificate.verify(); expect(result.status).toBe(VERIFICATION_STATUSES.SUCCESS); }); + + // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 + // describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { + // it('should fail verification', async function () { + // const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + // await certificate.init(); + // const result = await certificate.verify(); + // + // expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + // expect(result.message).toBe(VERIFICATION_STATUSES.FAILURE); + // }); + // }); }); diff --git a/test/e2e/verifier/proof-chain-example-secp256k1.test.ts b/test/e2e/verifier/proof-chain-example-secp256k1.test.ts index 1a0fba5e6..38e0a89d0 100644 --- a/test/e2e/verifier/proof-chain-example-secp256k1.test.ts +++ b/test/e2e/verifier/proof-chain-example-secp256k1.test.ts @@ -32,9 +32,6 @@ describe('proof chain example', function () { } }; }); - - instance = new Certificate(fixture as any); - await instance.init(); }); afterAll(function () { @@ -42,6 +39,8 @@ describe('proof chain example', function () { }); it('verifies as expected', async function () { + instance = new Certificate(fixture as any); + await instance.init(); const result = await instance.verify(); expect(result.message).toEqual({ description: 'All the signatures of this certificate have successfully verified.', @@ -49,4 +48,14 @@ describe('proof chain example', function () { }); expect(result.status).toBe('success'); }); + // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 + // describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { + // it('should fail verification', async function () { + // const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + // await certificate.init(); + // const result = await certificate.verify(); + // + // expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + // }); + // }); }); From ca6d814821f2be9e33180e9c0669463fecc7dfb3 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Wed, 4 Dec 2024 13:00:10 +0100 Subject: [PATCH 2/6] feat(ProofPurpose): attempt passing proof purpose to other suites --- package-lock.json | 23 +++++++++++++++---- package.json | 2 +- src/suites/EcdsaSd2023.ts | 4 ++-- .../verifier/ecdsa-sd-2023-derived.test.ts | 20 ++++++++-------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f82b450e..d5d2630ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "buffer": "^6.0.3", "jsonld": "^8.3.2", "jsonld-checker": "npm:@blockcerts/jsonld-checker@^0.1.9", - "jsonld-signatures": "^11.2.1", + "jsonld-signatures": "^11.3.2", "jsonld-signatures-merkleproof2019": "^2.9.0", "lodash.clonedeep": "^4.5.0", "secp256k1": "^5.0.0", @@ -13091,17 +13091,18 @@ } }, "node_modules/jsonld-signatures": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.2.1.tgz", - "integrity": "sha512-RNaHTEeRrX0jWeidPCwxMq/E/Ze94zFyEZz/v267ObbCHQlXhPO7GtkY6N5PSHQfQhZPXa8NlMBg5LiDF4dNbA==", + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-11.3.2.tgz", + "integrity": "sha512-nwmhZLFbg/hc+0ZTLxa76T8Ll4I+V3b8XQvNevuaUS7ocnPIBmtqWmmfLjbH8hnxkkh7EospZYQvrOIUzVvJqw==", "license": "BSD-3-Clause", "dependencies": { "@digitalbazaar/security-context": "^1.0.0", "jsonld": "^8.0.0", + "rdf-canonize": "^4.0.1", "serialize-error": "^8.1.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/jsonld-signatures-merkleproof2019": { @@ -13146,6 +13147,18 @@ "base-x": "^3.0.2" } }, + "node_modules/jsonld-signatures/node_modules/rdf-canonize": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-4.0.1.tgz", + "integrity": "sha512-B5ynHt4sasbUafzrvYI2GFARgeFcD8Sx9yXPbg7gEyT2EH76rlCv84kyO6tnxzVbxUN/uJDbK1S/MXh+DsnuTA==", + "license": "BSD-3-Clause", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsonld/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index 4fae9393a..d12926c7d 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "buffer": "^6.0.3", "jsonld": "^8.3.2", "jsonld-checker": "npm:@blockcerts/jsonld-checker@^0.1.9", - "jsonld-signatures": "^11.2.1", + "jsonld-signatures": "^11.3.2", "jsonld-signatures-merkleproof2019": "^2.9.0", "lodash.clonedeep": "^4.5.0", "secp256k1": "^5.0.0", diff --git a/src/suites/EcdsaSd2023.ts b/src/suites/EcdsaSd2023.ts index 79516121b..c8036cc69 100644 --- a/src/suites/EcdsaSd2023.ts +++ b/src/suites/EcdsaSd2023.ts @@ -188,8 +188,8 @@ export default class EcdsaSd2023 extends Suite { const verificationStatus = await jsigs.verify(this.documentToVerify, { suite, // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 - // purpose: new this.proofPurposeMap[this.proofPurpose](), - purpose: new AssertionProofPurpose(), + purpose: new this.proofPurposeMap[this.proofPurpose](), + // purpose: new AssertionProofPurpose(), documentLoader: this.generateDocumentLoader([ { url: verificationMethod, diff --git a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts index 4ed60f78a..95532fb53 100644 --- a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts +++ b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts @@ -27,14 +27,14 @@ describe('ecdsa-sd-2023 signed and derived document test suite', function () { }); // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 - // describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { - // it('should fail verification', async function () { - // const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); - // await certificate.init(); - // const result = await certificate.verify(); - // - // expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); - // expect(result.message).toBe(VERIFICATION_STATUSES.FAILURE); - // }); - // }); + describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { + it('should fail verification', async function () { + const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + await certificate.init(); + const result = await certificate.verify(); + + expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + expect(result.message).toBe(VERIFICATION_STATUSES.FAILURE); + }); + }); }); From 8143660b9e90fa9c9c36369834c9706dafbb8e30 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 9 Dec 2024 13:19:37 +0100 Subject: [PATCH 3/6] feat(DataIntegrity): pass proof challenge to merkle proof 2019 --- package-lock.json | 8 +-- package.json | 2 +- src/certificate.ts | 11 +++- src/models/Suite.ts | 1 + src/suites/MerkleProof2019.ts | 8 ++- src/verifier.ts | 52 ++++++++++++------- ...cknet-vc-v2-proofChallenge-invalid.test.ts | 42 +++++++++++++++ 7 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 test/e2e/verifier/mocknet-vc-v2-proofChallenge-invalid.test.ts diff --git a/package-lock.json b/package-lock.json index 15098deab..967b524fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "jsonld": "^8.3.2", "jsonld-checker": "npm:@blockcerts/jsonld-checker@^0.1.9", "jsonld-signatures": "^11.3.2", - "jsonld-signatures-merkleproof2019": "^2.9.0", + "jsonld-signatures-merkleproof2019": "^2.11.0", "lodash.clonedeep": "^4.5.0", "secp256k1": "^5.0.0", "sha256": "^0.2.0" @@ -13120,9 +13120,9 @@ } }, "node_modules/jsonld-signatures-merkleproof2019": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/jsonld-signatures-merkleproof2019/-/jsonld-signatures-merkleproof2019-2.10.0.tgz", - "integrity": "sha512-Zn56rbXW0nvdAvWYOVVr8tczJoEFD7tZUzx7BS++ngY61JOxd/gVZSuQoeyQo/A2RAbTOsmEYT8qwq3tCvVxQA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/jsonld-signatures-merkleproof2019/-/jsonld-signatures-merkleproof2019-2.11.0.tgz", + "integrity": "sha512-4GAt7Uj5iYRJIQ5APV+v2yAuutPKtTQ+OPFCvJUsuE6k2IPsPQuuj+4xvYaK99UG3epNZm0Ii4hWtKep+qZtEg==", "license": "MIT", "dependencies": { "@blockcerts/explorer-lookup": "^1.5.2", diff --git a/package.json b/package.json index f3ffda8c1..53dfd2d5a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "jsonld": "^8.3.2", "jsonld-checker": "npm:@blockcerts/jsonld-checker@^0.1.9", "jsonld-signatures": "^11.3.2", - "jsonld-signatures-merkleproof2019": "^2.9.0", + "jsonld-signatures-merkleproof2019": "^2.11.0", "lodash.clonedeep": "^4.5.0", "secp256k1": "^5.0.0", "sha256": "^0.2.0" diff --git a/src/certificate.ts b/src/certificate.ts index 8765fc56f..e7ff30bd4 100644 --- a/src/certificate.ts +++ b/src/certificate.ts @@ -36,6 +36,10 @@ export interface CertificateOptions { // restricts the verification to (a) specific domain(s) - useful for authentication, should match the domain property in the proof // https://www.w3.org/TR/vc-data-integrity/#defn-domain domain?: string | string[]; + // the property must match the one provided by the proof + // https://www.w3.org/TR/vc-data-integrity/#defn-challenge + // https://github.com/w3c/vc-data-integrity/issues/324#issuecomment-2521054375 + challenge?: string; } export interface Signers { @@ -56,7 +60,6 @@ export default class Certificate { public certificateJson: Blockcerts; public description?: string; // v1, v3.2 public display?: BlockcertsV3Display; - public proofDomain?: string | string[]; public expires: string; public validFrom: string; public explorerAPIs: ExplorerAPI[] = []; @@ -70,6 +73,8 @@ export default class Certificate { public name?: string; public options: CertificateOptions; public proofPurpose: string; + public proofDomain?: string | string[]; + public proofChallenge?: string; public recipientFullName: string; public recordLink: string; public revocationKey: string; @@ -133,7 +138,8 @@ export default class Certificate { revocationKey: this.revocationKey, explorerAPIs: deepCopy(this.explorerAPIs), proofPurpose: this.proofPurpose, - proofDomain: this.proofDomain + proofDomain: this.proofDomain, + proofChallenge: this.proofChallenge }); await this.verifier.init(); this.verificationSteps = this.verifier.getVerificationSteps(); @@ -189,6 +195,7 @@ export default class Certificate { this.explorerAPIs = this.options.explorerAPIs ?? []; this.proofPurpose = this.options.proofPurpose; this.proofDomain = this.options.domain; + this.proofChallenge = this.options.challenge; if (options.didResolverUrl) { domain.did.didResolver.url = options.didResolverUrl; diff --git a/src/models/Suite.ts b/src/models/Suite.ts index ad8056cf0..aaa850c4e 100644 --- a/src/models/Suite.ts +++ b/src/models/Suite.ts @@ -14,6 +14,7 @@ export interface SuiteAPI { issuer: Issuer; proofPurpose?: string; proofDomain?: string | string[]; + proofChallenge?: string; } export abstract class Suite { diff --git a/src/suites/MerkleProof2019.ts b/src/suites/MerkleProof2019.ts index 34dcec3a0..bcd627812 100644 --- a/src/suites/MerkleProof2019.ts +++ b/src/suites/MerkleProof2019.ts @@ -56,8 +56,9 @@ export default class MerkleProof2019 extends Suite { public type = 'MerkleProof2019'; public cryptosuite = 'merkle-proof-2019'; public suite: LDMerkleProof2019; - public proofPurpose: string; - public proofDomain: string | string[]; + public proofPurpose?: string; + public proofDomain?: string | string[]; + public proofChallenge?: string; constructor (props: SuiteAPI) { super(props); @@ -68,6 +69,7 @@ export default class MerkleProof2019 extends Suite { this.issuer = props.issuer; this.proofPurpose = props.proofPurpose; this.proofDomain = props.proofDomain; + this.proofChallenge = props.proofChallenge; this.validateProofType(); this.receipt = parseReceipt(this.proof); this.transactionId = domain.certificates.getTransactionId(this.receipt); @@ -169,12 +171,14 @@ export default class MerkleProof2019 extends Suite { this.getTargetVerificationMethodContainer(), getVCProofVerificationMethod(this.proof) ); + this.suite = new LDMerkleProof2019({ document: this.documentToVerify, proof: this.proof, verificationMethod: this.verificationMethodPublicKey, proofPurpose: this.proofPurpose, domain: this.proofDomain, + challenge: this.proofChallenge, options: { explorerAPIs: this.explorerAPIs, executeStepMethod: this.executeStep diff --git a/src/verifier.ts b/src/verifier.ts index c222e97b9..0f6cca044 100644 --- a/src/verifier.ts +++ b/src/verifier.ts @@ -51,10 +51,23 @@ export enum SupportedVerificationSuites { EcdsaSd2023 = 'EcdsaSd2023' } +export interface VerifierAPI { + certificateJson: Blockcerts; + expires: string; + validFrom?: string; + id: string; + issuer: Issuer; + hashlinkVerifier: HashlinkVerifier; + revocationKey: string; + explorerAPIs?: ExplorerAPI[]; + proofPurpose?: string; + proofDomain?: string | string[]; + proofChallenge?: string; +} + export default class Verifier { public expires: string; public validFrom: string; - public proofDomain: string | string[]; public id: string; public issuer: Issuer; public revocationKey: string; @@ -75,22 +88,23 @@ export default class Verifier { public proofVerifiers: Suite[] = []; public verificationProcess: SUB_STEPS[]; public proofMap: TVerifierProofMap; - public proofPurpose: string; - - constructor ( - { certificateJson, expires, hashlinkVerifier, id, issuer, revocationKey, explorerAPIs, validFrom, proofPurpose, proofDomain }: { - certificateJson: Blockcerts; - expires: string; - validFrom?: string; - id: string; - issuer: Issuer; - hashlinkVerifier: HashlinkVerifier; - revocationKey: string; - explorerAPIs?: ExplorerAPI[]; - proofPurpose?: string; - proofDomain?: string | string[]; - } - ) { + public proofPurpose?: string; + public proofDomain?: string | string[]; + public proofChallenge?: string; + + constructor ({ + certificateJson, + expires, + hashlinkVerifier, + id, + issuer, + revocationKey, + explorerAPIs, + validFrom, + proofPurpose, + proofDomain, + proofChallenge + }: VerifierAPI) { this.expires = expires; this.validFrom = validFrom; this.id = id; @@ -100,6 +114,7 @@ export default class Verifier { this.explorerAPIs = explorerAPIs; this.proofPurpose = proofPurpose; this.proofDomain = proofDomain; + this.proofChallenge = proofChallenge; this.documentToVerify = Object.assign({}, certificateJson); } @@ -224,7 +239,8 @@ export default class Verifier { explorerAPIs: this.explorerAPIs, issuer: this.issuer, proofPurpose: this.proofPurpose, - proofDomain: this.proofDomain + proofDomain: this.proofDomain, + proofChallenge: this.proofChallenge }; this.proofVerifiers.push(new this.supportedVerificationSuites[proofTypes[index]](suiteOptions)); diff --git a/test/e2e/verifier/mocknet-vc-v2-proofChallenge-invalid.test.ts b/test/e2e/verifier/mocknet-vc-v2-proofChallenge-invalid.test.ts new file mode 100644 index 000000000..23644b854 --- /dev/null +++ b/test/e2e/verifier/mocknet-vc-v2-proofChallenge-invalid.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, vi } from 'vitest'; +import { Certificate, VERIFICATION_STATUSES } from '../../../src'; +import MocknetVCV2ValidFromValid from '../../fixtures/v3/mocknet-vc-v2-validFrom-valid.json'; +import fixtureBlockcertsIssuerProfile from '../../fixtures/issuer-blockcerts.json'; + +describe('given the proofPurpose of the certificate\'s proof does not match the verifier\'s purpose', function () { + // this test will expire in 2039 + it('should fail verification', async function () { + vi.mock('@blockcerts/explorer-lookup', async (importOriginal) => { + const explorerLookup = await importOriginal(); + return { + ...explorerLookup, + request: async function ({ url }) { + if (url === 'https://www.blockcerts.org/samples/3.0/issuer-blockcerts.json') { + return JSON.stringify(fixtureBlockcertsIssuerProfile); + } + } + }; + }); + + const fixture = { + ...MocknetVCV2ValidFromValid, + proof: { + ...MocknetVCV2ValidFromValid.proof, + proofPurpose: 'authentication', + challenge: 'another-challenge', + domain: 'blockcerts.org' + } + }; + + const certificate = new Certificate(fixture, { + proofPurpose: 'authentication', + challenge: 'a challenge', + domain: 'blockcerts.org' + }); + await certificate.init(); + const result = await certificate.verify(); + expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + expect(result.message).toBe('The proof\'s challenge does not match the verifier\'s challenge'); + vi.restoreAllMocks(); + }); +}); From 7e4e0c9efb359e938e324b5eff92e76b1d9db273 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 9 Dec 2024 15:18:52 +0100 Subject: [PATCH 4/6] feat(DataIntegrity): make EcdsaSD2023, Ed25519 and Secp256k1 proofs verify authentication --- src/models/BlockcertsV3.ts | 4 ++- src/suites/EcdsaSd2023.ts | 16 +++++++--- src/suites/EcdsaSecp256k1Signature2019.ts | 30 +++++++++++++++++-- src/suites/Ed25519Signature2020.ts | 29 ++++++++++++++++-- .../verifier/ecdsa-sd-2023-derived.test.ts | 10 +++++-- .../e2e/verifier/ecdsa-sd-2023-signed.test.ts | 2 +- .../proof-chain-example-ed25519.test.ts | 13 +++++++- .../proof-chain-example-secp256k1.test.ts | 23 +++++++------- 8 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/models/BlockcertsV3.ts b/src/models/BlockcertsV3.ts index 16c682ba2..8f5d1e181 100644 --- a/src/models/BlockcertsV3.ts +++ b/src/models/BlockcertsV3.ts @@ -7,7 +7,9 @@ export interface VCProof { created: string; proofValue?: string; jws?: string; - proofPurpose: string; + proofPurpose?: string; + domain?: string; + challenge?: string; verificationMethod: string; chainedProofType?: string; previousProof?: any; diff --git a/src/suites/EcdsaSd2023.ts b/src/suites/EcdsaSd2023.ts index c8036cc69..4e4fffd55 100644 --- a/src/suites/EcdsaSd2023.ts +++ b/src/suites/EcdsaSd2023.ts @@ -36,6 +36,8 @@ export default class EcdsaSd2023 extends Suite { public publicKey: string; public verificationKey: any; public proofPurpose: string; + public challenge: string; + public domain: string | string[]; private readonly proofPurposeMap: any; constructor (props: SuiteAPI) { @@ -47,6 +49,8 @@ export default class EcdsaSd2023 extends Suite { this.issuer = props.issuer; this.proof = props.proof as VCProof; this.proofPurpose = props.proofPurpose ?? 'assertionMethod'; + this.challenge = props.proofChallenge ?? ''; + this.domain = props.proofDomain; this.proofPurposeMap = { authentication: AuthenticationProofPurpose, assertionMethod: AssertionProofPurpose @@ -146,7 +150,7 @@ export default class EcdsaSd2023 extends Suite { } private getErrorMessage (verificationStatus): string { - return verificationStatus.results[0].error.cause.message; + return verificationStatus.error.errors[0].message; } private getTargetVerificationMethodContainer (): Issuer | IDidDocument { @@ -185,11 +189,15 @@ export default class EcdsaSd2023 extends Suite { cryptosuite: createVerifyCryptosuite({ requiredAlgorithm: 'K-256' }) }); const verificationMethod = (this.documentToVerify.proof as VCProof).verificationMethod; + if (this.proofPurpose === 'authentication' && !this.proof.challenge) { + this.proof.challenge = ''; + } const verificationStatus = await jsigs.verify(this.documentToVerify, { suite, - // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 - purpose: new this.proofPurposeMap[this.proofPurpose](), - // purpose: new AssertionProofPurpose(), + purpose: new this.proofPurposeMap[this.proofPurpose]({ + challenge: this.challenge, + domain: this.domain + }), documentLoader: this.generateDocumentLoader([ { url: verificationMethod, diff --git a/src/suites/EcdsaSecp256k1Signature2019.ts b/src/suites/EcdsaSecp256k1Signature2019.ts index afdac12a4..bec712eb6 100644 --- a/src/suites/EcdsaSecp256k1Signature2019.ts +++ b/src/suites/EcdsaSecp256k1Signature2019.ts @@ -17,7 +17,7 @@ import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3'; import type { ISecp256k1PublicKeyJwk } from '../helpers/keyUtils'; import type { IDidDocument } from '../models/DidDocument'; -const { purposes: { AssertionProofPurpose } } = jsigs; +const { purposes: { AssertionProofPurpose, AuthenticationProofPurpose } } = jsigs; enum SUB_STEPS { retrieveVerificationMethodPublicKey = 'retrieveVerificationMethodPublicKey', @@ -36,6 +36,10 @@ export default class EcdsaSecp256k1Signature2019 extends Suite { public type = 'EcdsaSecp256k1Signature2019'; public verificationKey: EcdsaSecp256k1VerificationKey2019; public publicKey: string; + public proofPurpose: string; + public challenge: string; + public domain: string | string[]; + private readonly proofPurposeMap: any; constructor (props: SuiteAPI) { super(props); @@ -45,6 +49,13 @@ export default class EcdsaSecp256k1Signature2019 extends Suite { this.documentToVerify = props.document; this.issuer = props.issuer; this.proof = props.proof as VCProof; + this.proofPurpose = props.proofPurpose ?? 'assertionMethod'; + this.challenge = props.proofChallenge ?? ''; + this.domain = props.proofDomain; + this.proofPurposeMap = { + authentication: AuthenticationProofPurpose, + assertionMethod: AssertionProofPurpose + }; this.validateProofType(); } @@ -146,6 +157,10 @@ export default class EcdsaSecp256k1Signature2019 extends Suite { return this.issuer.didDocument ?? this.issuer; } + private getErrorMessage (verificationStatus): string { + return verificationStatus.error.errors[0].message; + } + private async retrieveVerificationMethodPublicKey (): Promise { this.verificationKey = await this.executeStep( SUB_STEPS.retrieveVerificationMethodPublicKey, @@ -188,15 +203,24 @@ export default class EcdsaSecp256k1Signature2019 extends Suite { // TODO: date property should exist but we are currently using a forked implementation which does not expose it (suite as any).date = new Date(Date.now()).toISOString(); + if (this.proofPurpose === 'authentication' && !this.proof.challenge) { + this.proof.challenge = ''; + } + const verificationStatus = await jsigs.verify(this.retrieveInitialDocument(), { suite, - purpose: new AssertionProofPurpose(), + purpose: new this.proofPurposeMap[this.proofPurpose]({ + challenge: this.challenge, + domain: this.domain + }), documentLoader: this.generateDocumentLoader() }); if (!verificationStatus.verified) { console.error(JSON.stringify(verificationStatus, null, 2)); - throw new VerifierError(SUB_STEPS.checkDocumentSignature, `The document's ${this.type} signature could not be confirmed`); + throw new VerifierError(SUB_STEPS.checkDocumentSignature, + `The document's ${this.type} signature could not be confirmed: ${this.getErrorMessage(verificationStatus)}` + ); } else { console.log('Credential Secp256k1 signature successfully verified'); } diff --git a/src/suites/Ed25519Signature2020.ts b/src/suites/Ed25519Signature2020.ts index f4b1d21f5..dca080462 100644 --- a/src/suites/Ed25519Signature2020.ts +++ b/src/suites/Ed25519Signature2020.ts @@ -16,7 +16,7 @@ import type VerificationSubstep from '../domain/verifier/valueObjects/Verificati import type { SuiteAPI } from '../models/Suite'; import type { BlockcertsV3, VCProof } from '../models/BlockcertsV3'; -const { purposes: { AssertionProofPurpose } } = jsigs; +const { purposes: { AssertionProofPurpose, AuthenticationProofPurpose } } = jsigs; enum SUB_STEPS { retrieveVerificationMethodPublicKey = 'retrieveVerificationMethodPublicKey', @@ -35,6 +35,10 @@ export default class Ed25519Signature2020 extends Suite { public type = 'Ed25519Signature2020'; public verificationKey: Ed25519VerificationKey2020; public publicKey: string; + public proofPurpose: string; + public challenge: string; + public domain: string | string[]; + private readonly proofPurposeMap: any; constructor (props: SuiteAPI) { super(props); @@ -44,6 +48,13 @@ export default class Ed25519Signature2020 extends Suite { this.documentToVerify = props.document; this.issuer = props.issuer; this.proof = props.proof as VCProof; + this.proofPurpose = props.proofPurpose ?? 'assertionMethod'; + this.challenge = props.proofChallenge ?? ''; + this.domain = props.proofDomain; + this.proofPurposeMap = { + authentication: AuthenticationProofPurpose, + assertionMethod: AssertionProofPurpose + }; this.validateProofType(); } @@ -146,6 +157,10 @@ export default class Ed25519Signature2020 extends Suite { return document; } + private getErrorMessage (verificationStatus): string { + return verificationStatus.error.errors[0].message; + } + private async retrieveVerificationMethodPublicKey (): Promise { this.verificationKey = await this.executeStep( SUB_STEPS.retrieveVerificationMethodPublicKey, @@ -190,15 +205,23 @@ export default class Ed25519Signature2020 extends Suite { const suite = new Ed25519VerificationSuite({ key: this.verificationKey }); suite.date = new Date(Date.now()).toISOString(); + if (this.proofPurpose === 'authentication' && !this.proof.challenge) { + this.proof.challenge = ''; + } + const verificationStatus = await jsigs.verify(this.retrieveInitialDocument(), { suite, - purpose: new AssertionProofPurpose(), + purpose: new this.proofPurposeMap[this.proofPurpose]({ + challenge: this.challenge, + domain: this.domain + }), documentLoader: this.generateDocumentLoader() }); if (!verificationStatus.verified) { console.error(JSON.stringify(verificationStatus, null, 2)); - throw new VerifierError(SUB_STEPS.checkDocumentSignature, `The document's ${this.type} signature could not be confirmed`); + throw new VerifierError(SUB_STEPS.checkDocumentSignature, + `The document's ${this.type} signature could not be confirmed: ${this.getErrorMessage(verificationStatus)}`); } else { console.log('Credential Ed25519 signature successfully verified'); } diff --git a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts index 95532fb53..29fd5e18e 100644 --- a/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts +++ b/test/e2e/verifier/ecdsa-sd-2023-derived.test.ts @@ -26,15 +26,19 @@ describe('ecdsa-sd-2023 signed and derived document test suite', function () { expect(result.status).toBe(VERIFICATION_STATUSES.SUCCESS); }); - // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { it('should fail verification', async function () { - const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + const certificate = new Certificate(fixture as any, { + proofPurpose: 'authentication', + // cannot test without these values too + domain: 'blockcerts.org', + challenge: 'a challenge' + }); await certificate.init(); const result = await certificate.verify(); expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); - expect(result.message).toBe(VERIFICATION_STATUSES.FAILURE); + expect(result.message).toBe('The document\'s EcdsaSd2023 signature could not be confirmed: Did not verify any proofs; insufficient proofs matched the acceptable suite(s) and required purpose(s).'); }); }); }); diff --git a/test/e2e/verifier/ecdsa-sd-2023-signed.test.ts b/test/e2e/verifier/ecdsa-sd-2023-signed.test.ts index 9081a4efe..eefe484ce 100644 --- a/test/e2e/verifier/ecdsa-sd-2023-signed.test.ts +++ b/test/e2e/verifier/ecdsa-sd-2023-signed.test.ts @@ -32,6 +32,6 @@ describe('ecdsa-sd-2023 signed and derived document test suite', function () { }); it('should expose the expected error message', function () { - expect(result.message).toBe('The document\'s EcdsaSd2023 signature could not be confirmed: "proof.proofValue" must be a derived proof.'); + expect(result.message).toBe('The document\'s EcdsaSd2023 signature could not be confirmed: The proof does not include a valid "proofValue" property.'); }); }); diff --git a/test/e2e/verifier/proof-chain-example-ed25519.test.ts b/test/e2e/verifier/proof-chain-example-ed25519.test.ts index 972ae5eeb..980c1c1cf 100644 --- a/test/e2e/verifier/proof-chain-example-ed25519.test.ts +++ b/test/e2e/verifier/proof-chain-example-ed25519.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { Certificate } from '../../../src'; +import { Certificate, VERIFICATION_STATUSES } from '../../../src'; import { universalResolverUrl } from '../../../src/domain/did/valueObjects/didResolver'; import multipleProofsVerificationSteps from '../../assertions/verification-steps-v3-multiple-proofs'; import didDocument from '../../fixtures/did/did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ.json'; @@ -55,4 +55,15 @@ describe('proof chain example', function () { label: 'Verified' }); }); + + describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { + it('should fail verification', async function () { + const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + await certificate.init(); + const result = await certificate.verify(); + + expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + expect(result.message).toBe('The document\'s Ed25519Signature2020 signature could not be confirmed: Did not verify any proofs; insufficient proofs matched the acceptable suite(s) and required purpose(s).'); + }); + }); }); diff --git a/test/e2e/verifier/proof-chain-example-secp256k1.test.ts b/test/e2e/verifier/proof-chain-example-secp256k1.test.ts index 38e0a89d0..d47149eae 100644 --- a/test/e2e/verifier/proof-chain-example-secp256k1.test.ts +++ b/test/e2e/verifier/proof-chain-example-secp256k1.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; -import { Certificate } from '../../../src'; +import { Certificate, VERIFICATION_STATUSES } from '../../../src'; import fixture from '../../fixtures/v3/proof-chain-example-secp256k1.json'; import { universalResolverUrl } from '../../../src/domain/did/valueObjects/didResolver'; import didDocument from '../../fixtures/did/did:ion:EiA_Z6LQILbB2zj_eVrqfQ2xDm4HNqeJUw5Kj2Z7bFOOeQ.json'; @@ -48,14 +48,15 @@ describe('proof chain example', function () { }); expect(result.status).toBe('success'); }); - // TODO: uncomment the following if jsonld-signatures follows the spec https://github.com/digitalbazaar/jsonld-signatures/issues/185 - // describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { - // it('should fail verification', async function () { - // const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); - // await certificate.init(); - // const result = await certificate.verify(); - // - // expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); - // }); - // }); + + describe('when the verifier\'s proofPurpose does not match the document\'s proof purpose', function () { + it('should fail verification', async function () { + const certificate = new Certificate(fixture as any, { proofPurpose: 'authentication' }); + await certificate.init(); + const result = await certificate.verify(); + + expect(result.status).toBe(VERIFICATION_STATUSES.FAILURE); + expect(result.message).toBe('The document\'s EcdsaSecp256k1Signature2019 signature could not be confirmed: Did not verify any proofs; insufficient proofs matched the acceptable suite(s) and required purpose(s).'); + }); + }); }); From c57161196cb4937789ce1ce0e8747bfe3244dd7e Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 9 Dec 2024 15:33:33 +0100 Subject: [PATCH 5/6] types(VCProof): make compulsory again --- src/models/BlockcertsV3.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/BlockcertsV3.ts b/src/models/BlockcertsV3.ts index 8f5d1e181..d6148fb07 100644 --- a/src/models/BlockcertsV3.ts +++ b/src/models/BlockcertsV3.ts @@ -7,7 +7,7 @@ export interface VCProof { created: string; proofValue?: string; jws?: string; - proofPurpose?: string; + proofPurpose: string; domain?: string; challenge?: string; verificationMethod: string; From e9bae53d2077f13c51dd2109605110bffd9d0cf0 Mon Sep 17 00:00:00 2001 From: Julien Fraichot Date: Mon, 9 Dec 2024 18:23:58 +0100 Subject: [PATCH 6/6] fix(Build): fix iife build after jsonld-signature dep update --- bundle-esm-stats.html | 5 +- package-lock.json | 104 +++++++++++++++++++++++++++++++++++++++++ package.json | 1 + rollup.iife.config.mjs | 4 +- 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/bundle-esm-stats.html b/bundle-esm-stats.html index 87a14f8c2..34af9eb2b 100644 --- a/bundle-esm-stats.html +++ b/bundle-esm-stats.html @@ -4873,7 +4873,7 @@