CVE-2024-31995 (JavaScript, published 2024-04-10T22:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/digitalbazaar/zcap
Files changed:
CHANGELOG.md
lib/CapabilityInvocation.js
lib/CapabilityProofPurpose.js
lib/utils.js
tests/test-common.js
Commit message: Fix chain-depth-2 zcap invocation expiry check.


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