CVE-2024-31995: Insufficient Information (NVD-CWE-noinfo)
value: `@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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-10T22:15Z
Repository: https://github.com/digitalbazaar/zcap
Language: JavaScript
Files changed: CHANGELOG.md, lib/CapabilityInvocation.js, lib/CapabilityProofPurpose.js, lib/utils.js, tests/test-common.js
Commit date: 2024-03-29 15:14:23 -0400
Commit message: Fix chain-depth-2 zcap invocation expiry check.

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b1162c..5e32a36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,14 @@
 # @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
 - **BREAKING**: Use `jsonld-signatures@11` to get better safe mode
   protections when canonizing.
diff --git a/lib/CapabilityInvocation.js b/lib/CapabilityInvocation.js
index 194c6b8..530ef3b 100644
--- a/lib/CapabilityInvocation.js
+++ b/lib/CapabilityInvocation.js
@@ -306,10 +306,29 @@ export class CapabilityInvocation extends CapabilityProofPurpose {
         verificationMethod
       };
       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) {
       throw result.error;
     }
diff --git a/lib/CapabilityProofPurpose.js b/lib/CapabilityProofPurpose.js
index 82d05e2..f3bf177 100644
--- a/lib/CapabilityProofPurpose.js
+++ b/lib/CapabilityProofPurpose.js
@@ -1,7 +1,7 @@
 /*!
- * 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';
 const {ControllerProofPurpose} = jsigs.purposes;
 
@@ -315,11 +315,11 @@ export class CapabilityProofPurpose extends ControllerProofPurpose {
    *   chain for `capability`, starting at the root capability and ending at
    *   `capability`.
    * @param {Function} options.documentLoader - A configured jsonld
    *   documentLoader.
    *
-   * @returns {object} {verified, error}.
+   * @returns {object} An object with `{verified, error}`.
    */
   async _verifyCapabilityChain({
     CapabilityDelegation,
     capabilityChainMeta,
     dereferencedChain,
@@ -444,11 +444,11 @@ export class CapabilityProofPurpose extends ControllerProofPurpose {
           }
           // use `utils.compareTime` to allow for allow for clock drift because
           // we are comparing against `currentDate`
           if(utils.compareTime({
             t1: currentDate.getTime(),
-            t2: currentCapabilityExpirationTime,
+            t2: parentExpirationTime,
             maxClockSkew
           }) > 0) {
             throw new Error(
               'A capability in the delegation chain has expired.');
           }
diff --git a/lib/utils.js b/lib/utils.js
index a8da6a7..47c5280 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,7 +1,7 @@
 /*!
- * 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
 } from './constants.js';
 
@@ -314,11 +314,11 @@ export function computeCapabilityChain({
  *   the root capability (the root zcap must be deref'd in a trusted way by the
  *   verifier, it must not be untrusted input).
  * @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
 }) {
   // capability MUST be a string if it is root; root zcaps MUST always be
@@ -520,12 +520,11 @@ export function checkCapability({capability, expectRoot}) {
       throw new Error(
         'Root capability must have an "@context" value of ' +
         `"${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))) {
       throw new Error(
         'Delegated capability must have an "@context" array ' +
diff --git a/tests/test-common.js b/tests/test-common.js
index c5913ad..0fd4859 100644
--- a/tests/test-common.js
+++ b/tests/test-common.js
@@ -1,7 +1,7 @@
 /*!
- * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
+ * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
  */
 
 import chai from 'chai';
 
 import {
@@ -2113,13 +2113,248 @@ describe('zcap', () => {
         });
         expect(result).to.exist;
         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});
 
         // alice delegates to bob a capability is already expired
         let expires = new Date();
@@ -2163,11 +2398,11 @@ describe('zcap', () => {
         // carol invokes
         const doc = clone(mock.exampleDoc);
         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({
           invocation, purposeOptions: {
             expectedAction: 'read',
@@ -2183,12 +2418,12 @@ describe('zcap', () => {
         const [error] = result.error.errors;
         error.message.should.equal(
           '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});
 
         // alice delegates to bob a capability that is presently valid
@@ -2634,12 +2869,11 @@ describe('zcap', () => {
         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(
-          '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 ' +
         'capability that expires on the Unix epoch', async () => {
         // bob's capability expires on a date in the future, but
@@ -2698,12 +2932,11 @@ describe('zcap', () => {
         expect(result).to.exist;
         expect(result.verified).to.be.false;
         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 ' +
         'second delegated capability that expires after first', async () => {
         // bob's capability specifies a date in the future, but
@@ -2893,12 +3126,11 @@ describe('zcap', () => {
         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(
-          'A capability in the delegation chain has expired.');
+        error.message.should.equal('The invoked capability has expired.');
       });
     }); // end Expiration date
 
     describe('Chain depth of 4', () => {
       it('should verify chain', async () => {

Vulnerable code:  * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
   * @returns {object} {verified, error}.
            t2: currentCapabilityExpirationTime,
 * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
 * @returns {Promise<object>} {dereferencedChain}.
      throw new Error(
        'Root capability must not have an "expires" field.');
 * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved.
      it('should fail invoking a capability with `expires` ' +
        // the capability was also expired 20 hours ago
      it('should fail invoking a capability with `expires` ' +
        'and `date` parameter in the future', async () => {
        error.message.should.equal(
          'A capability in the delegation chain has expired.');
        error.message.should.contain(
          'capability in the delegation chain has expired');
        error.message.should.equal(
          'A capability in the delegation chain has expired.');
Fixed code: ## 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.

    // 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.');
      }
    }

 * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
   * @returns {object} An object with `{verified, error}`.
            t2: parentExpirationTime,
 * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
 * @returns {Promise<object>} Resolves to `{dereferencedChain}`.
      throw new Error('Root capability must not have an "expires" field.');
 * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved.
      it('should fail invoking a chain depth 2 capability with `expires` ' +
        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};
        // pass current time as 20 hours ago
      it('should fail invoking a chain depth 3 capability with `expires` ' +
        'and `date` parameter in the chain in the future', async () => {
        error.message.should.equal('The invoked capability has expired.');
        error.message.should.equal('The invoked capability has expired.');
        error.message.should.equal('The invoked capability has expired.');