VYPR
Medium severity4.3NVD Advisory· Published Apr 10, 2024· Updated Apr 15, 2026

CVE-2024-31995

CVE-2024-31995

Description

@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.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@digitalbazaar/zcapnpm
< 9.0.19.0.1

Patches

2
55f8549c8012

Express expectation of node >= 18.

https://github.com/digitalbazaar/zcapDave LongleyMar 29, 2024via ghsa
1 file changed · +1 1
  • package.json+1 1 modified
    @@ -63,7 +63,7 @@
         ]
       },
       "engines": {
    -    "node": ">=14"
    +    "node": ">=18"
       },
       "keywords": [
         "Authorization Capability",
    
261eea040109

Fix chain-depth-2 zcap invocation expiry check.

https://github.com/digitalbazaar/zcapDave LongleyMar 29, 2024via ghsa
5 files changed · +275 18
  • CHANGELOG.md+7 0 modified
    @@ -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
    
  • lib/CapabilityInvocation.js+19 0 modified
    @@ -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) {
    
  • lib/CapabilityProofPurpose.js+3 3 modified
    @@ -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(
    
  • lib/utils.js+3 4 modified
    @@ -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))) {
    
  • tests/test-common.js+243 11 modified
    @@ -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
     
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

6

News mentions

0

No linked articles in our index yet.