CVE-2024-31995 (published 2024-04-10T22:15Z)

`@digitalbazaar/zcap` provides JavaScript reference implementation for Authorization Capabilities. Prior to version 9.0.1, when invoking a capability with a chain depth of 2, i.e., it is delegated directly from the root capability, the `expires` property is not properly checked against the current date or other `date` param.  This can allow invocations outside of the original intended time period.  A zcap still cannot be invoked without being able to use the associated private key material. `@digitalbazaar/zcap` v9.0.1 fixes expiration checking. As a workaround, one may revoke a zcap at any time.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/digitalbazaar/zcap
commit_details: repo_url: https://github.com/digitalbazaar/zcap
author: Dave Longley
author_date: 2024-03-29 15:04:30-04:00
msg: Fix chain-depth-2 zcap invocation expiry check.
repository: repo_url: https://github.com/digitalbazaar/zcap
repo_name: digitalbazaar/zcap
description: Linked Data Capabilities reference implementation
repo_language: JavaScript
owner: digitalbazaar
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,12 @@
 # @digitalbazaar/zcap ChangeLog
 
+## 9.0.1 - 2024-mm-dd
+
+### Fixed
+- Ensure that when invoking a capability with a chain depth of 2, i.e.,
+  it is delegated directly from the root capability, that `expires`
+  is properly checked against the current date or other `date` param.
+
 ## 9.0.0 - 2022-10-25
 
 ### Changed

programming_language: Markdown
filename: CapabilityInvocation.js
change_type: ModificationType.MODIFY
diff: @@ -308,6 +308,25 @@ export class CapabilityInvocation extends CapabilityProofPurpose {
       throw error;
     }
 
+    // if capability is delegated, verify that it has not expired
+    if(capability.parentCapability) {
+      // verify expiration dates
+      // expires date has been previously validated, so just parse it
+      const currentCapabilityExpirationTime = Date.parse(capability.expires);
+
+      // use `utils.compareTime` to allow for allow for clock drift because
+      // we are comparing against `currentDate`
+      const {date, maxClockSkew} = this;
+      const currentDate = (date && new Date(date)) || new Date();
+      if(utils.compareTime({
+        t1: currentDate.getTime(),
+        t2: currentCapabilityExpirationTime,
+        maxClockSkew
+      }) > 0) {
+        throw new Error('The invoked capability has expired.');
+      }
+    }
+
     // run base level validation checks
     const result = await this._runBaseProofValidation({proof, validateOptions});
     if(!result.valid) {

programming_language: TypeScript
filename: CapabilityProofPurpose.js
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 /*!
- * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
  */
 import jsigs from 'jsonld-signatures';
 import * as utils from './utils.js';
@@ -317,7 +317,7 @@ export class CapabilityProofPurpose extends ControllerProofPurpose {
    * @param {Function} options.documentLoader - A configured jsonld
    *   documentLoader.
    *
-   * @returns {object} {verified, error}.
+   * @returns {object} An object with `{verified, error}`.
    */
   async _verifyCapabilityChain({
     CapabilityDelegation,
@@ -446,7 +446,7 @@ export class CapabilityProofPurpose extends ControllerProofPurpose {
           // we are comparing against `currentDate`
           if(utils.compareTime({
             t1: currentDate.getTime(),
-            t2: currentCapabilityExpirationTime,
+            t2: parentExpirationTime,
             maxClockSkew
           }) > 0) {
             throw new Error(

programming_language: TeX
filename: utils.js
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 /*!
- * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
  */
 import {
   MAX_CHAIN_LENGTH, ZCAP_CONTEXT_URL, ZCAP_ROOT_PREFIX
@@ -316,7 +316,7 @@ export function computeCapabilityChain({
  * @param {number} [options.maxChainLength=10] - The maximum length of the
  *   capability delegation chain (this is inclusive of `capability` itself).
  *
- * @returns {Promise<object>} {dereferencedChain}.
+ * @returns {Promise<object>} Resolves to `{dereferencedChain}`.
  */
 export async function dereferenceCapabilityChain({
   capability, getRootCapability, maxChainLength = MAX_CHAIN_LENGTH
@@ -522,8 +522,7 @@ export function checkCapability({capability, expectRoot}) {
         `"${ZCAP_CONTEXT_URL}".`);
     }
     if(capability.expires !== undefined) {
-      throw new Error(
-        'Root capability must not have an "expires" field.');
+      throw new Error('Root capability must not have an "expires" field.');
     }
   } else {
     if(!((Array.isArray(context) && context[0] === ZCAP_CONTEXT_URL))) {

programming_language: TypeScript
filename: test-common.js
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 /*!
- * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
  */
 
 import chai from 'chai';
@@ -2115,9 +2115,244 @@ describe('zcap', () => {
         expect(result.verified).to.be.true;
       });
 
-      it('should fail invoking a capability with `expires` ' +
+      it('should fail invoking a chain depth 2 capability with `expires` ' +
         'and `date` parameter in the past', async () => {
         const rootCapability = {...capabilities.root.beta};
+        rootCapability.id = 'urn:uuid:dada0e4d-bc52-4530-ac7f-4973eb8013e9';
+        addToLoader({doc: rootCapability});
+
+        // alice delegates to bob a capability is already expired
+        let expires = new Date();
+        expires.setHours(expires.getHours() - 50);
+        expires = expires.toISOString();
+        const bobZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: bob.id(),
+            parentCapability: rootCapability.id,
+            invocationTarget: rootCapability.invocationTarget,
+            expires
+          },
+          purposeOptions: {
+            // do not validate locally to allow expired zcap to be delegated
+            _skipLocalValidationForTesting: true
+          },
+          parentCapability: rootCapability,
+          delegator: alice
+        });
+
+        // bob invokes
+        const doc = clone(mock.exampleDoc);
+        const invocation = await _invoke({
+          doc, invoker: bob, capability: bobZcap, capabilityAction: 'read'
+        });
+        // pass current time as 20 hours ago
+        const date = new Date();
+        date.setHours(date.getHours() - 20);
+        const result = await _verifyInvocation({
+          invocation, purposeOptions: {
+            expectedAction: 'read',
+            expectedRootCapability: rootCapability.id,
+            expectedTarget: rootCapability.invocationTarget,
+            date
+          }
+        });
+        expect(result).to.exist;
+        expect(result.verified).to.be.false;
+        should.exist(result.error);
+        result.error.name.should.equal('VerificationError');
+        const [error] = result.error.errors;
+        error.message.should.equal('The invoked capability has expired.');
+      });
+
+      it('should fail invoking a chain depth 2 capability with `expires` ' +
+        'and `date` parameter in the future', async () => {
+        const rootCapability = {...capabilities.root.beta};
+        rootCapability.id = 'urn:zcap:1d0cd622-2e0a-4686-b0d7-bd71cc27db04';
+        addToLoader({doc: rootCapability});
+
+        // alice delegates to bob a capability that is presently valid
+        let expires = new Date();
+        expires.setHours(expires.getHours() + 50);
+        expires = expires.toISOString();
+        const bobZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: bob.id(),
+            parentCapability: rootCapability.id,
+            invocationTarget: rootCapability.invocationTarget,
+            expires
+          },
+          parentCapability: rootCapability,
+          delegator: alice
+        });
+
+        // bob invokes
+        const doc = clone(mock.exampleDoc);
+        const invocation = await _invoke({
+          doc, invoker: bob, capability: bobZcap, capabilityAction: 'read'
+        });
+        // the capability will have expired in 100 hours, so pass that future
+        // time in as the verification date to trigger an error
+        const date = new Date();
+        date.setHours(date.getHours() + 100);
+        const result = await _verifyInvocation({
+          invocation, purposeOptions: {
+            expectedAction: 'read',
+            expectedRootCapability: rootCapability.id,
+            expectedTarget: rootCapability.invocationTarget,
+            suite: new Ed25519Signature2020(),
+            date,
+          }
+        });
+        expect(result).to.exist;
+        expect(result.verified).to.be.false;
+        should.exist(result.error);
+        result.error.name.should.equal('VerificationError');
+        const [error] = result.error.errors;
+        error.message.should.equal('The invoked capability has expired.');
+      });
+
+      it('should fail invoking a chain depth 3 capability with `expires` ' +
+        'and `date` parameter in the past', async () => {
+        const rootCapability = {...capabilities.root.beta};
+        rootCapability.id = 'urn:zcap:59bcb98b-a7ee-4ba7-8629-8b52329bc506';
+        addToLoader({doc: rootCapability});
+
+        // alice delegates to bob a capability has not yet expired
+        let expires = new Date();
+        expires.setHours(expires.getHours() + 50);
+        expires = expires.toISOString();
+        const bobZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: bob.id(),
+            parentCapability: rootCapability.id,
+            invocationTarget: rootCapability.invocationTarget,
+            expires
+          },
+          parentCapability: rootCapability,
+          delegator: alice
+        });
+
+        // bob delegates to carol a capability that has already expired
+        expires = new Date();
+        expires.setHours(expires.getHours() - 50);
+        expires = expires.toISOString();
+        const carolZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: carol.id(),
+            parentCapability: bobZcap.id,
+            invocationTarget: bobZcap.invocationTarget,
+            expires
+          },
+          purposeOptions: {
+            // do not validate locally to allow expired zcap to be delegated
+            _skipLocalValidationForTesting: true
+          },
+          parentCapability: bobZcap,
+          delegator: bob
+        });
+
+        // carol invokes
+        const doc = clone(mock.exampleDoc);
+        const invocation = await _invoke({
+          doc, invoker: carol, capability: carolZcap, capabilityAction: 'read'
+        });
+        // pass current time as 20 hours ago
+        const date = new Date();
+        date.setHours(date.getHours() - 20);
+        const result = await _verifyInvocation({
+          invocation, purposeOptions: {
+            expectedAction: 'read',
+            expectedRootCapability: rootCapability.id,
+            expectedTarget: rootCapability.invocationTarget,
+            date
+          }
+        });
+        expect(result).to.exist;
+        expect(result.verified).to.be.false;
+        should.exist(result.error);
+        result.error.name.should.equal('VerificationError');
+        const [error] = result.error.errors;
+        error.message.should.equal('The invoked capability has expired.');
+      });
+
+      it('should fail invoking a chain depth 3 capability with `expires` ' +
+        'and `date` parameter in the future', async () => {
+        const rootCapability = {...capabilities.root.beta};
+        rootCapability.id = 'urn:zcap:80a8de39-df68-4fd8-ba78-4d65a27b2009';
+        addToLoader({doc: rootCapability});
+
+        // alice delegates to bob a capability that is presently valid
+        let expires = new Date();
+        expires.setHours(expires.getHours() + 50);
+        expires = expires.toISOString();
+        const bobZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: bob.id(),
+            parentCapability: rootCapability.id,
+            invocationTarget: rootCapability.invocationTarget,
+            expires
+          },
+          parentCapability: rootCapability,
+          delegator: alice
+        });
+
+        // bob delegates to carol with an attenuated (shorter) expiration date
+        expires = new Date();
+        expires.setHours(expires.getHours() + 25);
+        expires = expires.toISOString();
+        const carolZcap = await _delegate({
+          newCapability: {
+            '@context': ZCAP_CONTEXT_URL,
+            id: uuid(),
+            controller: carol.id(),
+            parentCapability: bobZcap.id,
+            invocationTarget: bobZcap.invocationTarget,
+            expires
+          },
+          parentCapability: bobZcap,
+          delegator: bob
+        });
+
+        // carol invokes
+        const doc = clone(mock.exampleDoc);
+        const invocation = await _invoke({
+          doc, invoker: carol, capability: carolZcap, capabilityAction: 'read'
+        });
+        // the capability will have expired in 30 hours, which is before
+        // bob's zcap's expiry but after carol's -- so pass that future
+        // time in as the verification date to trigger an error
+        const date = new Date();
+        date.setHours(date.getHours() + 30);
+        const result = await _verifyInvocation({
+          invocation, purposeOptions: {
+            expectedAction: 'read',
+            expectedRootCapability: rootCapability.id,
+            expectedTarget: rootCapability.invocationTarget,
+            suite: new Ed25519Signature2020(),
+            date,
+          }
+        });
+        expect(result).to.exist;
+        expect(result.verified).to.be.false;
+        should.exist(result.error);
+        result.error.name.should.equal('VerificationError');
+        const [error] = result.error.errors;
+        error.message.should.equal('The invoked capability has expired.');
+      });
+
+      it('should fail invoking a chain depth 3 capability with `expires` ' +
+        'and `date` parameter in the chain in the past', async () => {
+        const rootCapability = {...capabilities.root.beta};
         rootCapability.id = 'urn:zcap:142b0b4a-c664-4288-84e6-be0a59b6efa4';
         addToLoader({doc: rootCapability});
 
@@ -2165,7 +2400,7 @@ describe('zcap', () => {
         const invocation = await _invoke({
           doc, invoker: carol, capability: carolZcap, capabilityAction: 'read'
         });
-        // the capability was also expired 20 hours ago
+        // pass current time as 20 hours ago
         const date = new Date();
         date.setHours(date.getHours() - 20);
         const result = await _verifyInvocation({
@@ -2185,8 +2420,8 @@ describe('zcap', () => {
           'A capability in the delegation chain has expired.');
       });
 
-      it('should fail invoking a capability with `expires` ' +
-        'and `date` parameter in the future', async () => {
+      it('should fail invoking a chain depth 3 capability with `expires` ' +
+        'and `date` parameter in the chain in the future', async () => {
         const rootCapability = {...capabilities.root.beta};
         rootCapability.id = 'urn:zcap:bcbcde5e-d64a-4f46-a76e-daf52f63f702';
         addToLoader({doc: rootCapability});
@@ -2636,8 +2871,7 @@ describe('zcap', () => {
         should.exist(result.error);
         result.error.name.should.equal('VerificationError');
         const [error] = result.error.errors;
-        error.message.should.equal(
-          'A capability in the delegation chain has expired.');
+        error.message.should.equal('The invoked capability has expired.');
       });
 
       it('should fail invoking a capability with second delegated ' +
@@ -2700,8 +2934,7 @@ describe('zcap', () => {
         should.exist(result.error);
         result.error.errors.should.have.length(1);
         const [error] = result.error.errors;
-        error.message.should.contain(
-          'capability in the delegation chain has expired');
+        error.message.should.equal('The invoked capability has expired.');
       });
 
       it('should fail invoking a capability with ' +
@@ -2895,8 +3128,7 @@ describe('zcap', () => {
         should.exist(result.error);
         result.error.name.should.equal('VerificationError');
         const [error] = result.error.errors;
-        error.message.should.equal(
-          'A capability in the delegation chain has expired.');
+        error.message.should.equal('The invoked capability has expired.');
       });
     }); // end Expiration date
 

programming_language: TypeScript
repo_url: https://github.com/digitalbazaar/zcap
commit_details: repo_url: https://github.com/digitalbazaar/zcap
author: Dave Longley
author_date: 2024-03-29 15:12:12-04:00
msg: Express expectation of node >= 18.
repository: repo_url: https://github.com/digitalbazaar/zcap
repo_name: digitalbazaar/zcap
description: Linked Data Capabilities reference implementation
repo_language: JavaScript
owner: digitalbazaar
file_changes: filename: package.json
change_type: ModificationType.MODIFY
diff: @@ -63,7 +63,7 @@
     ]
   },
   "engines": {
-    "node": ">=14"
+    "node": ">=18"
   },
   "keywords": [
     "Authorization Capability",

programming_language: JavaScript