From 9c11cc8526f1bba66f9c78e474f6a00c6c52d17a Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Fri, 12 Dec 2025 12:57:11 +0000 Subject: [PATCH 1/2] Early out failure on delegation verify if missing credentials from chain --- examples/CHANGELOG.md | 6 ++ examples/package.json | 4 +- packages/credential-sdk/CHANGELOG.md | 7 ++ packages/credential-sdk/package.json | 4 +- packages/vc-delegation-engine/CHANGELOG.md | 6 ++ packages/vc-delegation-engine/package.json | 2 +- packages/vc-delegation-engine/src/engine.js | 24 +++++ .../tests/unit/engine.test.js | 91 +++++++++++++++++++ scripts/bench/CHANGELOG.md | 6 ++ scripts/bench/package.json | 4 +- 10 files changed, 147 insertions(+), 7 deletions(-) diff --git a/examples/CHANGELOG.md b/examples/CHANGELOG.md index b26e14512..f14a123a8 100644 --- a/examples/CHANGELOG.md +++ b/examples/CHANGELOG.md @@ -1,5 +1,11 @@ # @docknetwork/sdk-examples +## 0.21.14 + +### Patch Changes + +- @docknetwork/credential-sdk@0.54.14 + ## 0.21.13 ### Patch Changes diff --git a/examples/package.json b/examples/package.json index 4aca7f235..7540e4abd 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,7 +2,7 @@ "name": "@docknetwork/sdk-examples", "private": true, "type": "module", - "version": "0.21.13", + "version": "0.21.14", "scripts": { "bbs-dock-example": "babel-node ./src/bbs-dock.js", "claim-deduction-example": "babel-node ./src/claim-deduction.js", @@ -18,7 +18,7 @@ "lint": "eslint \"src/**/*.js\"" }, "dependencies": { - "@docknetwork/credential-sdk": "0.54.13", + "@docknetwork/credential-sdk": "0.54.14", "@docknetwork/cheqd-blockchain-api": "4.0.7", "@docknetwork/cheqd-blockchain-modules": "4.0.8" }, diff --git a/packages/credential-sdk/CHANGELOG.md b/packages/credential-sdk/CHANGELOG.md index 61a1605ee..6d3c2bb8c 100644 --- a/packages/credential-sdk/CHANGELOG.md +++ b/packages/credential-sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @docknetwork/credential-sdk +## 0.54.14 + +### Patch Changes + +- Updated dependencies + - @docknetwork/vc-delegation-engine@1.0.3 + ## 0.54.13 ### Patch Changes diff --git a/packages/credential-sdk/package.json b/packages/credential-sdk/package.json index 44de21ef6..2622b5df9 100644 --- a/packages/credential-sdk/package.json +++ b/packages/credential-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/credential-sdk", - "version": "0.54.13", + "version": "0.54.14", "license": "MIT", "type": "module", "files": [ @@ -62,7 +62,7 @@ "uuid": "^10.0.0", "uuidv4": "^6.2.13", "varint": "^6.0.0", - "@docknetwork/vc-delegation-engine": "1.0.2" + "@docknetwork/vc-delegation-engine": "1.0.3" }, "devDependencies": { "@babel/cli": "^7.24.1", diff --git a/packages/vc-delegation-engine/CHANGELOG.md b/packages/vc-delegation-engine/CHANGELOG.md index 537964176..3dc8ecaf0 100644 --- a/packages/vc-delegation-engine/CHANGELOG.md +++ b/packages/vc-delegation-engine/CHANGELOG.md @@ -1,5 +1,11 @@ # @docknetwork/vc-delegation-engine +## 1.0.3 + +### Patch Changes + +- Early out failure on delegation verify if missing credentials from chain + ## 1.0.2 ### Patch Changes diff --git a/packages/vc-delegation-engine/package.json b/packages/vc-delegation-engine/package.json index 2d17b9565..32d1c2b8c 100644 --- a/packages/vc-delegation-engine/package.json +++ b/packages/vc-delegation-engine/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/vc-delegation-engine", - "version": "1.0.2", + "version": "1.0.3", "description": "", "main": "dist/cjs/index.cjs", "module": "dist/esm/index.js", diff --git a/packages/vc-delegation-engine/src/engine.js b/packages/vc-delegation-engine/src/engine.js index 9f104565f..acb08c663 100644 --- a/packages/vc-delegation-engine/src/engine.js +++ b/packages/vc-delegation-engine/src/engine.js @@ -456,6 +456,30 @@ export async function verifyVPWithDelegation({ skippedCredentials, }; } + + // Early out failure if missing credential from chain + normalizedCredentials.forEach((credential) => { + const { id, previousCredentialId, rootCredentialId } = credential; + if (previousCredentialId && !normalizedById.has(previousCredentialId)) { + throw new DelegationError( + DelegationErrorCodes.MISSING_CREDENTIAL, + `Missing credential ${previousCredentialId} referenced as previous by ${id}`, + ); + } + if (!rootCredentialId || typeof rootCredentialId !== 'string' || rootCredentialId.length === 0) { + throw new DelegationError( + DelegationErrorCodes.INVALID_CREDENTIAL, + `Delegation credential ${id} is missing rootCredentialId`, + ); + } + if (!normalizedById.has(rootCredentialId)) { + throw new DelegationError( + DelegationErrorCodes.MISSING_CREDENTIAL, + `Missing root credential ${rootCredentialId} referenced by ${id}`, + ); + } + }); + const tailCredentials = normalizedCredentials.filter((vc) => !referencedPreviousIds.has(vc.id)); if (tailCredentials.length === 0) { throw new DelegationError( diff --git a/packages/vc-delegation-engine/tests/unit/engine.test.js b/packages/vc-delegation-engine/tests/unit/engine.test.js index d6b00ef96..9726db595 100644 --- a/packages/vc-delegation-engine/tests/unit/engine.test.js +++ b/packages/vc-delegation-engine/tests/unit/engine.test.js @@ -3,6 +3,7 @@ import { } from 'vitest'; import jsonld from 'jsonld'; import { verifyVPWithDelegation } from '../../src/engine.js'; +import { DelegationErrorCodes } from '../../src/errors.js'; import { VC_TYPE, VC_VC, @@ -17,6 +18,8 @@ import { const ROOT_CREDENTIAL_ID = 'urn:cred:root'; const LEAF_CREDENTIAL_ID = 'urn:cred:leaf'; +const MISSING_PREVIOUS_ID = 'urn:cred:missing-prev'; +const UNKNOWN_ROOT_REFERENCE = 'urn:cred:missing-root'; const W3C_CONTEXT = 'https://www.w3.org/2018/credentials/v1'; const ISSUER_ROOT = 'did:root'; const SUBJECT_DELEGATE = 'did:delegate'; @@ -124,6 +127,94 @@ describe('engine', () => { expect(result.decision).toBe('allow'); expect(result.evaluations).toHaveLength(1); }); + + it('denies when a credential references a missing previous credential', async () => { + const presentation = [ + { + '@type': [VC_TYPE], + [VC_VC]: [ + { + '@graph': [ + { + '@id': LEAF_CREDENTIAL_ID, + '@type': [VC_TYPE_DELEGATION_CREDENTIAL], + [VC_ISSUER]: [{ '@id': SUBJECT_DELEGATE }], + [VC_SUBJECT]: [{ '@id': SUBJECT_HOLDER }], + [VC_ROOT_CREDENTIAL_ID]: [{ '@id': LEAF_CREDENTIAL_ID }], + [VC_PREVIOUS_CREDENTIAL_ID]: [{ '@id': MISSING_PREVIOUS_ID }], + }, + ], + }, + ], + [SECURITY_PROOF]: [ + { + [SECURITY_VERIFICATION_METHOD]: [{ '@id': `${SUBJECT_DELEGATE}#key` }], + }, + ], + }, + ]; + const contexts = new Map([[LEAF_CREDENTIAL_ID, [W3C_CONTEXT]]]); + + const result = await verifyVPWithDelegation({ + expandedPresentation: presentation, + credentialContexts: contexts, + }); + + expect(result.decision).toBe('deny'); + expect(result.failures?.[0]?.code).toBe(DelegationErrorCodes.MISSING_CREDENTIAL); + expect(result.failures?.[0]?.message).toContain(MISSING_PREVIOUS_ID); + }); + + it('denies when a credential references a root credential that is not present', async () => { + const presentation = [ + { + '@type': [VC_TYPE], + [VC_VC]: [ + { + '@graph': [ + { + '@id': ROOT_CREDENTIAL_ID, + '@type': [VC_TYPE_DELEGATION_CREDENTIAL], + [VC_ISSUER]: [{ '@id': ISSUER_ROOT }], + [VC_SUBJECT]: [{ '@id': SUBJECT_DELEGATE }], + [VC_ROOT_CREDENTIAL_ID]: [{ '@id': ROOT_CREDENTIAL_ID }], + }, + ], + }, + { + '@graph': [ + { + '@id': LEAF_CREDENTIAL_ID, + '@type': [VC_TYPE_DELEGATION_CREDENTIAL], + [VC_ISSUER]: [{ '@id': SUBJECT_DELEGATE }], + [VC_SUBJECT]: [{ '@id': SUBJECT_HOLDER }], + [VC_ROOT_CREDENTIAL_ID]: [{ '@id': UNKNOWN_ROOT_REFERENCE }], + [VC_PREVIOUS_CREDENTIAL_ID]: [{ '@id': ROOT_CREDENTIAL_ID }], + }, + ], + }, + ], + [SECURITY_PROOF]: [ + { + [SECURITY_VERIFICATION_METHOD]: [{ '@id': `${ISSUER_ROOT}#key` }], + }, + ], + }, + ]; + const contexts = new Map([ + [ROOT_CREDENTIAL_ID, [W3C_CONTEXT]], + [LEAF_CREDENTIAL_ID, [W3C_CONTEXT]], + ]); + + const result = await verifyVPWithDelegation({ + expandedPresentation: presentation, + credentialContexts: contexts, + }); + + expect(result.decision).toBe('deny'); + expect(result.failures?.[0]?.code).toBe(DelegationErrorCodes.MISSING_CREDENTIAL); + expect(result.failures?.[0]?.message).toContain(UNKNOWN_ROOT_REFERENCE); + }); it('fails when credential contexts are missing', async () => { const result = await verifyVPWithDelegation({ expandedPresentation: buildPresentation(), diff --git a/scripts/bench/CHANGELOG.md b/scripts/bench/CHANGELOG.md index 9a0907156..c49967a7d 100644 --- a/scripts/bench/CHANGELOG.md +++ b/scripts/bench/CHANGELOG.md @@ -1,5 +1,11 @@ # @docknetwork/benchmarks +## 0.4.14 + +### Patch Changes + +- @docknetwork/credential-sdk@0.54.14 + ## 0.4.13 ### Patch Changes diff --git a/scripts/bench/package.json b/scripts/bench/package.json index 7ed1ba8c4..f69b62afc 100644 --- a/scripts/bench/package.json +++ b/scripts/bench/package.json @@ -2,14 +2,14 @@ "name": "@docknetwork/benchmarks", "private": true, "type": "module", - "version": "0.4.13", + "version": "0.4.14", "scripts": { "bench": "babel-node src/main.js" }, "dependencies": { "@docknetwork/cheqd-blockchain-api": "4.0.7", "@docknetwork/cheqd-blockchain-modules": "4.0.8", - "@docknetwork/credential-sdk": "0.54.13", + "@docknetwork/credential-sdk": "0.54.14", "@docknetwork/crypto-wasm-ts": "^0.63.0" }, "devDependencies": { From a502a113348d57bcc442234cf8cb249de47f12e4 Mon Sep 17 00:00:00 2001 From: Sam Hellawell Date: Fri, 12 Dec 2025 13:13:57 +0000 Subject: [PATCH 2/2] Test fix --- packages/vc-delegation-engine/src/engine.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vc-delegation-engine/src/engine.js b/packages/vc-delegation-engine/src/engine.js index acb08c663..2cd9ce27e 100644 --- a/packages/vc-delegation-engine/src/engine.js +++ b/packages/vc-delegation-engine/src/engine.js @@ -30,6 +30,7 @@ import { summarizeDelegationChain, summarizeStandaloneCredential } from './summa import { DelegationError, DelegationErrorCodes, normalizeDelegationFailure } from './errors.js'; const CONTROL_PREDICATES = new Set(['allows', 'delegatesTo', 'listsClaim', 'inheritsParent']); +const DELEGATION_TYPE_NAME = shortenTerm(VC_TYPE_DELEGATION_CREDENTIAL); const RESERVED_RESOURCE_TYPES = new Set([ `${VC_NS}VerifiableCredential`, 'VerifiableCredential', @@ -459,7 +460,13 @@ export async function verifyVPWithDelegation({ // Early out failure if missing credential from chain normalizedCredentials.forEach((credential) => { - const { id, previousCredentialId, rootCredentialId } = credential; + const { + id, previousCredentialId, rootCredentialId, type, + } = credential; + const isDelegationCredential = Array.isArray(type) && type.includes(DELEGATION_TYPE_NAME); + if (!isDelegationCredential) { + return; + } if (previousCredentialId && !normalizedById.has(previousCredentialId)) { throw new DelegationError( DelegationErrorCodes.MISSING_CREDENTIAL,