├── .eslintrc.cjs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.cjs ├── lib ├── CapabilityDelegation.js ├── CapabilityInvocation.js ├── CapabilityProofPurpose.js ├── constants.js ├── index.js └── utils.js ├── package.json └── tests ├── .eslintrc.cjs ├── helpers.js ├── mock-data.js ├── mock-documents ├── delegated-zcap-alpha.js ├── delegated-zcap-beta.js ├── did-context.js ├── did-doc-alpha.js ├── did-doc-beta.js ├── did-doc-delta.js ├── did-doc-gamma.js ├── ed25519-alice-keys.js ├── ed25519-bob-keys.js ├── ed25519-carol-keys.js ├── ed25519-diana-keys.js ├── example-doc-with-alpha-invocation.js ├── example-doc-with-beta-invocation.js ├── example-doc.js └── veres-one-context.js ├── test-common.js ├── test-karma.js └── test.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | node: true 7 | }, 8 | extends: [ 9 | 'digitalbazaar', 10 | 'digitalbazaar/jsdoc', 11 | 'digitalbazaar/module' 12 | ], 13 | ignorePatterns: [ 14 | 'mock-documents/' 15 | ], 16 | rules: { 17 | 'unicorn/prefer-node-protocol': 'error' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | strategy: 10 | matrix: 11 | node-version: [22.x] 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - run: npm install 19 | - name: Run eslint 20 | run: npm run lint 21 | test-node: 22 | needs: [lint] 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 10 25 | strategy: 26 | matrix: 27 | node-version: [20.x, 22.x] 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - run: npm install 35 | - name: Run tests with Node.js ${{ matrix.node-version }} 36 | run: npm run test-node 37 | test-karma: 38 | needs: [lint] 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | strategy: 42 | matrix: 43 | node-version: [22.x] 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Use Node.js ${{ matrix.node-version }} 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | - run: npm install 51 | - name: Run karma tests 52 | run: npm run test-karma 53 | coverage: 54 | needs: [test-node, test-karma] 55 | timeout-minutes: 10 56 | runs-on: ubuntu-latest 57 | strategy: 58 | matrix: 59 | node-version: [22.x] 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Use Node.js ${{ matrix.node-version }} 63 | uses: actions/setup-node@v4 64 | with: 65 | node-version: ${{ matrix.node-version }} 66 | - run: npm install 67 | - name: Generate coverage report 68 | run: npm run coverage-ci 69 | - name: Upload coverage to Codecov 70 | uses: codecov/codecov-action@v4 71 | with: 72 | file: ./coverage/lcov.info 73 | fail_ci_if_error: true 74 | token: ${{ secrets.CODECOV_TOKEN }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lcov 2 | *.sublime-project 3 | *.sublime-workspace 4 | *.sw[nop] 5 | *~ 6 | .DS_Store 7 | .cproject 8 | .nyc_output 9 | .project 10 | .settings 11 | .vscode 12 | TAGS 13 | coverage 14 | dist 15 | node_modules 16 | npm-debug.log 17 | v8.log 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (http://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # Typescript v1 declaration files 55 | typings/ 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | 75 | # distribution 76 | dist 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @digitalbazaar/zcap ChangeLog 2 | 3 | ## 9.0.1 - 2024-03-29 4 | 5 | ### Fixed 6 | - Ensure that when invoking a capability with a chain depth of 2, i.e., 7 | it is delegated directly from the root capability, that `expires` 8 | is properly checked against the current date or other `date` param. 9 | 10 | ## 9.0.0 - 2022-10-25 11 | 12 | ### Changed 13 | - **BREAKING**: Use `jsonld-signatures@11` to get better safe mode 14 | protections when canonizing. 15 | 16 | ## 8.0.0 - 2022-06-06 17 | 18 | ### Changed 19 | - **BREAKING**: Convert to module (ESM). 20 | - **BREAKING**: Require Node.js >=14. 21 | - Update dependencies. 22 | - Lint module. 23 | 24 | ## 7.2.2 - 2022-02-23 25 | 26 | ### Fixed 27 | - Ensure a zcap is not delegated after its parent (during 28 | delegation proof generation). 29 | 30 | ## 7.2.1 - 2022-02-22 31 | 32 | ### Fixed 33 | - Ensure `maxClockSkew` is considered when checking `maxDelegationTtl` 34 | against current time. 35 | - Use `karma@6.3.16` to address reported vulnerability with dev 36 | dependency `karma`. 37 | 38 | ## 7.2.0 - 2022-01-20 39 | 40 | ### Added 41 | - Include `capability` and `verificationMethod` as details when a zcap 42 | invocation/delegation fails verification because the capability 43 | controller does not match the verification method (or its controller). If 44 | this information causes undesirable correlation, i.e., the controller 45 | of a root zcap is private in some way, do not include it when transmitting 46 | errors to a client. This information can be omitted by deleting `details` 47 | from the error or constructing a new error that omits `details`. 48 | 49 | ## 7.1.0 - 2022-01-14 50 | 51 | ### Added 52 | - Include `dereferencedChain` in purpose result when verifying a 53 | capability invocation or delegation proof. 54 | 55 | ## 7.0.1 - 2022-01-11 56 | 57 | ### Changed 58 | - Update dependencies. 59 | 60 | ## 7.0.0 - 2022-01-11 61 | 62 | ### Changed 63 | - **BREAKING**: Rename package to `@digitalbazaar/zcap`. 64 | 65 | ## 6.0.0 - 2022-01-11 66 | 67 | ### Added 68 | - Add `createRootCapability` helper function to construct root zcaps from 69 | a root invocation target and a root controller. 70 | - Add local validation during delegation to prevent accidental delegation of 71 | zcaps that violate delegation rules that a verifier would always reject. 72 | - Add `maxClockSkew` param that defaults to `300` seconds. This parameter 73 | defines the maximum clock skew that will be accepted when comparing 74 | capability expiration date-times against the current date (or other 75 | specified date) and when comparing a capability invocation proof against 76 | the capability's delegation proof. 77 | 78 | ### Changed 79 | - **BREAKING**: Root zcaps MUST specify an `invocationTarget`. This eliminates 80 | optionality, simplifying implementations. 81 | - **BREAKING**: Root zcaps MUST be passed by reference to their ID when invoking 82 | and they will be expressed by reference (just their ID) in a capability 83 | invocation proof. Delegated zcaps MUST be fully embedded (pass full object) 84 | when invoking and they will be fully embedded in a capability invocation proof. 85 | - **BREAKING**: When creating a capability delegation proof, a new parameter 86 | `parentCapability` MUST be passed so that the chain can be auto-computed. 87 | Passing `capabilityChain` is no longer permitted. 88 | - **BREAKING**: Require `capabilityAction` when creating capability invocation 89 | proofs and `expectedAction` when verifying proofs; removing previous 90 | optionality simplifies implementations. 91 | - **BREAKING**: Changed default to check for chain date monotonicity and 92 | removed the option to do otherwise. This was an expected change for the 93 | next major breaking release. 94 | - **BREAKING**: `expires` is not permitted on root capabilities and is 95 | required on delegated capabilities. Removing optionality here simplifies 96 | implementations and improves security by reducing surface and providing 97 | an "out" for zcaps that can not be easily revoked by causing them to 98 | always expire eventually. 99 | - **BREAKING**: Combine `currentDate` and `date` parameters that were serving 100 | the same purpose. These params are only used for verification and the `date` 101 | parameter is used by the base class provided by jsonld-signatures, so the 102 | `currentDate` parameter has been removed; use `date` instead, it is only 103 | used for verification of proofs, not creation of proofs. 104 | - **BREAKING**: `invocationTarget` MUST be specified in capability invocation 105 | proofs, it will not default to the `invocationTarget` specified in the 106 | capability. Removing this optionality removes complexity in implementations. 107 | - **BREAKING**: `capabilityChain` and `capabilityChainMeta` that are passed 108 | to `inspectCapabilityChain` include entries for the root capability. The 109 | `verifyResult` is `null` for the root zcap. 110 | - **BREAKING**: `allowTargetAttenuation=true` allows both path- or query-based 111 | invocation target attenuation. Turning this on means a verifier will allow 112 | accept delegations (and invocations) where a suffix has been added to the 113 | parent zcap's invocation target (invoked zcap's invocation target). The 114 | suffix must starts with `/` or `?` if the invocation target prefix has no `?` 115 | and `&` otherwise. 116 | 117 | ### Removed 118 | - **BREAKING**: Removed support for using `invoker` and `delegator` properties. 119 | Only `controller` is now permitted and it is `required`, i.e., a ZCAP MUST 120 | have a `controller` property, the value of the ZCAP's `id` property is not 121 | considered a default controller value for the ZCAP. This change simplifies 122 | ZCAP implementations and better reflects the fact that a delegation cannot 123 | actually be restricted -- a system can only force users to use data model 124 | and protocol-external mechanisms to delegate. This change keeps all 125 | delegation within the data model/protocol for improved auditability. 126 | - **BREAKING**: Removed support for vocab-modeled custom caveats. Custom 127 | caveats should instead be modeled a combination of capability actions 128 | and path- or query-based attenuation of invocation targets. This approach 129 | provides the flexibility required to model custom caveats without the 130 | overhead of building and maintaining custom contexts and vocabularies. The 131 | common case is that custom caveats are specific to particular APIs rather 132 | than shared commonly across many different standardized APIs -- so it is 133 | unnecessarily burdensome to require the creation ofLinked Data vocabularies 134 | and contexts to represent them when using localized API-specific capability 135 | actions and invocation target paths will suffice. Common caveats such as 136 | expiration dates are provided as a part of the core model -- and should any 137 | other common caveats become evident, they can be added to the core model over 138 | time. 139 | - **BREAKING**: Removed support for allowing the last delegated zcap in a 140 | capability chain to be expressed by reference. Instead, if the last zcap in 141 | the chain is delegated, it MUST be fully embedded. All other zcaps MUST be 142 | expressed via reference to their ID. Therefore, a capability chain MUST 143 | always consist of: the root zcap ID, any non-last delegated zcap IDs, and, 144 | for chains longer than 1 (excluding the final zcap or 2 if inclusive), the 145 | fully embedded last delegated zcap. This simplifies implementations, removes 146 | any concerns around mutability in dereferenced zcaps, and guarantees that all 147 | zcaps in a chain are available in an invocation. It does require that the 148 | invoker send the entire chain, however, this considered the best trade off. 149 | - **BREAKING**: Removed ability to expire a root capability. There is no 150 | use case for this, so the complexity has been removed. 151 | - **BREAKING**: Removed support for zcaps expressed using contexts other than 152 | the zcap-ld v1 context. The zcap spec will be updated to describe zcaps as 153 | JSON in a way that JSON-LD compatible, eliminating the need for supporting 154 | and JSON-LD context transformations beyond those used to create and verify 155 | proofs. This approach will not prohibit the future use of CBOR-LD to 156 | represent zcaps over the wire to greatly reduce size. 157 | 158 | ## 5.2.0 - 2021-12-20 159 | 160 | ### Added 161 | - Add optional `maxDelegationTtl` to enable checking that all zcaps in a 162 | delegation chain have a time-to-live that is not greater than a certain 163 | value. This check will have a default value shorter than `Infinity` in 164 | a future breaking version. 165 | - Add optional `requireChainDateMonotonicity` to enable checking that all 166 | zcaps in a delegation chain have delegation proofs that were created using 167 | dates that monotonically increase (i.e., no delegated zcap was delegated 168 | any later than its parent). This check will be required in a future breaking 169 | version. 170 | 171 | ## 5.1.3 - 2021-11-15 172 | 173 | ### Fixed 174 | - Ensure `invocationTarget` from an invocation proof is checked against the 175 | capability used and the `expectedTarget`. The `invocationTarget` from the 176 | proof must both be in the `expectedTarget` list (or a direct match if a 177 | string value is used for `expectedTarget` vs. an array) and it must also 178 | match the `invocationTarget` in the capability used (if 179 | `allowTargetAttenuation=true` then the capability's `invocationTarget` may 180 | be a path prefix for the `invocationTarget` from the proof). 181 | 182 | ## 5.1.2 - 2021-07-21 183 | 184 | ### Fixed 185 | - Enable zcap context to appear anywhere in a context array when 186 | checking proof context because it is a protected context. 187 | 188 | ## 5.1.1 - 2021-07-21 189 | 190 | ### Fixed 191 | - Ensure `proof` uses an expected context during proof validation. 192 | 193 | ## 5.1.0 - 2021-07-11 194 | 195 | ### Changed 196 | - Updated jsonld-signatures to 9.3.x. This brings in an optimization for 197 | controller documents that are JSON-LD DID documents. 198 | 199 | ## 5.0.0 - 2021-07-02 200 | 201 | ### Added 202 | - Expose `ZCAP_CONTEXT` in `constants` as a convenience. 203 | - Add `documentLoader` to expose a convenience document loader that will load 204 | `ZCAP_CONTEXT`. 205 | - Add `extendDocumentLoader` for adding a custom document loader that extend 206 | `documentLoader` to load other documents. 207 | 208 | ### Changed 209 | - **BREAKING**: LD capability invocation proofs now require `invocationTarget` 210 | to be set in order for `match()` to find proofs based on `expectedTarget`. 211 | This helps ensure that the proof creator's intended `invocationTarget` is 212 | declared (important for systems that support RESTful attenuation) and it 213 | enables more efficient proof verification when documents include multiple 214 | capability invocation proofs that may have different invocation targets. 215 | 216 | ### Fixed 217 | - Ensure `expectedAction` is checked when looking for a matching proof, 218 | not `capabilityAction`. 219 | 220 | ## 4.0.0 - 2021-04-26 221 | 222 | ### Fixed 223 | - **BREAKING**: Use [`zcap-context@1.1.0`](https://github.com/digitalbazaar/zcap-context/blob/main/CHANGELOG.md) 224 | and refactor `fetchInSecurityContext` API. 225 | - Use [`@digitalbazaar/security-context@1.0.0`](https://github.com/digitalbazaar/security-context/blob/main/CHANGELOG.md). 226 | 227 | ## 3.1.1 - 2021-04-15 228 | 229 | ### Fixed 230 | - Use `jsonld-signatures@9`. 231 | - Update test dependencies and fix tests. 232 | 233 | ## 3.1.0 - 2021-04-08 234 | 235 | ### Added 236 | - Skip `jsonld.compact` step when a JSON-LD document has specific contexts. 237 | This is a temporary measure until a zcap context is created. 238 | 239 | ## 3.0.0 - 2021-03-19 240 | 241 | ### Changed 242 | - **BREAKING**: Changed package name from `ocapld` to `@digitalbazaar/zcapld`. 243 | - **BREAKING**: Only support Node.js >=12. 244 | - Update dependencies. 245 | - Use new `@digitalbazaar/ed25519-signature-2018` and 246 | `@digitalbazaar/ed25519-verification-key-2018` dependencies for testing. 247 | 248 | ### Removed 249 | - **BREAKING**: Remove `bitcore-message` dependency. It's for a specialized use 250 | case. 251 | - **BREAKING**: Remove browser bundles. 252 | 253 | ## 2.0.0 - 2020-04-02 254 | 255 | ### Changed 256 | - **BREAKING**: An `invocationTarget` must be specified in all delegations. 257 | - Improve test coverage. 258 | 259 | ### Fixed 260 | - Properly validate `allowedAction` in capabilities. 261 | 262 | ### Added 263 | - Add verification of `expires` as a core feature. 264 | - Add the ability to specificy a `maxChainLength` when verifying capability 265 | delegations. 266 | - Add an optional `allowTargetAttenuation` flag which allows the 267 | `invocationTarget` of a delegation chain to be increasingly restrictive 268 | based on a hierarchical RESTful URL structure. 269 | 270 | ## 1.8.0 - 2020-02-14 271 | 272 | ### Changed 273 | 274 | - Use jsonld-signatures@5. 275 | 276 | ## 1.7.0 - 2020-02-07 277 | 278 | ### Added 279 | - Implement validation for embedded capabilities in `capabilityChain`. 280 | 281 | ## 1.6.1 - 2020-01-30 282 | 283 | ### Fixed 284 | - Adjust the parameters to `inspectCapabilityChain` to support more general 285 | use cases. See in-line documentation for parameter details. 286 | 287 | ## 1.6.0 - 2020-01-29 288 | 289 | ### Added 290 | - Add an optional `inspectCapabilityChain` parameter to `CapabilityDelegation` 291 | and `CapabilityInvocation`. `inspectCapabilityChain` must be an async 292 | function used to check the capability chain. It can, for instance, be used 293 | to find revocations related to any of the capabilities in the chain. 294 | 295 | ## 1.5.1 - 2020-01-29 296 | 297 | ### Fixed 298 | - Address issues in `verifyCapabilityChain` helper that resulted in some 299 | proofs not being properly verified. 300 | 301 | ## 1.5.0 - 2020-01-09 302 | 303 | ### Added 304 | - Support multiple values for `expectedTarget` and `expectedRootCapability` 305 | for use cases such as where capabilities are given for reading/writing 306 | any item in a collection instead of only individual items. 307 | 308 | ## 1.4.0 - 2019-10-08 309 | 310 | ### Changed 311 | - Use jsonld-signatures@4.4.0 Use with support for Node 12 native Ed25519 312 | crypto. 313 | 314 | ## 1.3.1 - 2019-08-11 315 | 316 | ### Fixed 317 | - Handle case where a capability lists list multiple 318 | controllers/invokers/delegators. 319 | 320 | ## 1.3.0 - 2019-07-17 321 | 322 | ### Added 323 | - Add `expectedRootCapability` to allow a root capability to 324 | specify an `invocationTarget` different from its `id`. This 325 | allows zcaps to be used to manage authority for resources 326 | that cannot express their own zcap authority information 327 | such as binary files or resources that use JSON or JSON-LD 328 | but, for whatever reason, cannot express `controller`, 329 | `invoker`, `delegator`, or key information. 330 | 331 | ## 1.2.1 - 2019-06-29 332 | 333 | ### Fixed 334 | - Check `allowedAction` against expected `capabilityAction`. 335 | - Fix expected action check. 336 | - Fix capability chain check. 337 | - Ensure root caps are dereferenced and have a valid target. 338 | - Handle case where `invocationTarget` is an object. 339 | 340 | ## 1.2.0 - 2019-05-17 341 | 342 | ### Added 343 | - Support `controller` on capabilities as `delegator` and `invoker`. 344 | 345 | ### Changed 346 | - Update webpack and babel. 347 | - Switch to eslint. 348 | 349 | ## 1.1.0 - 2019-03-27 350 | 351 | ### Changed 352 | - Upgrade jsonld-signatures to version 4. 353 | 354 | ## 1.0.2 - 2019-01-03 355 | 356 | ### Fixed 357 | - Change jsonld-signatures to a regular dependency. 358 | 359 | ## 1.0.1 - 2019-01-03 360 | 361 | ### Fixed 362 | - Distribute webpack built dist files. 363 | 364 | ## 1.0.0 - 2019-01-03 365 | 366 | ### Changed 367 | - Use webpack 'externals' for jsonld and jsonld-signatures. 368 | 369 | ## 0.1.0 - 2019-01-02 370 | - Initial release. 371 | - See git history for changes. 372 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2021, Digital Bazaar, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zcap _(@digitalbazaar/zcap)_ 2 | 3 | [![Build status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/zcap/main.yml?branch=main)](https://github.com/digitalbazaar/zcap/actions?query=workflow%3A%22Node.js+CI%22) 4 | [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/zcap)](https://codecov.io/gh/digitalbazaar/zcap) 5 | [![NPM Version](https://img.shields.io/npm/v/@digitalbazaar/zcap.svg)](https://npm.im/@digitalbazaar/zcap) 6 | 7 | > JavaScript reference implementation for 8 | [Authorization Capabilities](https://w3c-ccg.github.io/zcap-spec/). 9 | 10 | ## Table of Contents 11 | 12 | - [Background](#background) 13 | - [Security](#security) 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [Contribute](#contribute) 17 | - [Commercial Support](#commercial-support) 18 | - [License](#license) 19 | 20 | ## Background 21 | 22 | TODO 23 | 24 | ## Security 25 | 26 | TBD 27 | 28 | ## Install 29 | 30 | - Browsers and Node.js 14+ are supported. 31 | 32 | To install from NPM: 33 | 34 | ```sh 35 | npm install @digitalbazaar/zcap 36 | ``` 37 | 38 | To install locally (for development): 39 | 40 | ```sh 41 | git clone https://github.com/digitalbazaar/zcap.git 42 | cd zcap 43 | npm install 44 | ``` 45 | 46 | ## Usage 47 | 48 | TBD 49 | 50 | ## Contribute 51 | 52 | See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)! 53 | 54 | PRs accepted. 55 | 56 | If editing the Readme, please conform to the 57 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 58 | 59 | ## Commercial Support 60 | 61 | Commercial support for this library is available upon request from 62 | Digital Bazaar: support@digitalbazaar.com 63 | 64 | ## License 65 | 66 | [New BSD License (3-clause)](LICENSE) © Digital Bazaar 67 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Karma configuration for zcap. 3 | * 4 | * Copyright (c) 2011-2022 Digital Bazaar, Inc. All rights reserved. 5 | */ 6 | module.exports = function(config) { 7 | config.set({ 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: '', 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['mocha'], 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | { 18 | pattern: 'tests/test-karma.js', 19 | watched: false, served: true, included: true 20 | } 21 | ], 22 | 23 | // list of files to exclude 24 | exclude: [], 25 | 26 | // preprocess matching files before serving them to the browser 27 | // available preprocessors: 28 | // https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | 'tests/*.js': ['webpack', 'sourcemap'] 31 | }, 32 | 33 | webpack: { 34 | mode: 'development', 35 | devtool: 'inline-source-map' 36 | }, 37 | 38 | // test results reporter to use 39 | // possible values: 'dots', 'progress' 40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 41 | //reporters: ['progress'], 42 | reporters: ['mocha'], 43 | 44 | // web server port 45 | port: 9876, 46 | 47 | // enable / disable colors in the output (reporters and logs) 48 | colors: true, 49 | 50 | // level of logging 51 | // possible values: 52 | // config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || 53 | // config.LOG_INFO || config.LOG_DEBUG 54 | logLevel: config.LOG_INFO, 55 | 56 | // enable / disable watching file and executing tests whenever any file 57 | // changes 58 | autoWatch: false, 59 | 60 | // start these browsers 61 | // available browser launchers: 62 | // https://npmjs.org/browse/keyword/karma-launcher 63 | //browsers: ['ChromeHeadless', 'Chrome', 'Firefox', 'Safari'], 64 | browsers: ['ChromeHeadless'], 65 | 66 | // Continuous Integration mode 67 | // if true, Karma captures browsers, runs the tests and exits 68 | singleRun: true, 69 | 70 | // Concurrency level 71 | // how many browser should be started simultaneous 72 | concurrency: Infinity, 73 | 74 | // Mocha 75 | client: { 76 | mocha: { 77 | // increase from default 2s 78 | timeout: 10000, 79 | reporter: 'html' 80 | //delay: true 81 | } 82 | } 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /lib/CapabilityDelegation.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as utils from './utils.js'; 5 | import {CapabilityProofPurpose} from './CapabilityProofPurpose.js'; 6 | 7 | /** 8 | * @typedef InspectCapabilityChain 9 | */ 10 | 11 | export class CapabilityDelegation extends CapabilityProofPurpose { 12 | /** 13 | * @param {object} options - The options. 14 | * @param {object} [options.parentCapability] - An alternative to passing 15 | * `capabilityChain` when creating a proof; passing `parentCapability` will 16 | * enable the capability chain to be auto-computed. 17 | * @param {boolean} [options.allowTargetAttenuation=false] - Allow the 18 | * invocationTarget of a delegation chain to be increasingly restrictive 19 | * based on a hierarchical RESTful URL structure. 20 | * @param {string|Date|number} [options.date] - Used during proof 21 | * verification as the expected date for the creation of the proof 22 | * (within a maximum timestamp delta) and for checking to see if a 23 | * capability has expired; if not passed the current date will be used. 24 | * @param {string|Array} [options.expectedRootCapability] - The expected root 25 | * capability for the delegation chain (this can be a single root 26 | * capability ID expressed as a string or, if there is more than one 27 | * acceptable root capability, several root capability IDs in an array. 28 | * @param {object} [options.controller] - The description of the controller, 29 | * if it is not to be dereferenced via a `documentLoader`. 30 | * @param {InspectCapabilityChain} [options.inspectCapabilityChain] - An 31 | * async function that can be used to check for revocations related to any 32 | * of verified capabilities. 33 | * @param {number} [options.maxChainLength=10] - The maximum length of the 34 | * capability delegation chain. 35 | * @param {number} [options.maxClockSkew=300] - A maximum number of seconds 36 | * that clocks may be skewed when checking capability expiration date-times 37 | * against `date`. 38 | * @param {number} [options.maxDelegationTtl=Infinity] - The maximum 39 | * milliseconds to live for a delegated zcap as measured by the time 40 | * difference between * `expires` and `created` on the delegation proof. 41 | * @param {object|Array} options.suite - The jsonld-signature suite(s) to 42 | * use to verify the capability chain. 43 | * @param {object} options._verifiedParentCapability - Private. 44 | * @param {object} options._capabilityChain - Private. 45 | * @param {boolean} options._skipLocalValidationForTesting - Private. 46 | */ 47 | constructor({ 48 | // proof creation params 49 | parentCapability, 50 | // proof verification params 51 | allowTargetAttenuation, 52 | controller, 53 | date, 54 | expectedRootCapability, 55 | inspectCapabilityChain, 56 | maxChainLength, 57 | maxClockSkew, 58 | maxDelegationTtl, 59 | suite, 60 | _verifiedParentCapability, 61 | // for testing purposes only, not documented intentionally 62 | _capabilityChain, 63 | _skipLocalValidationForTesting = false 64 | } = {}) { 65 | // parameters used to create a proof 66 | const hasCreateProofParams = parentCapability || _capabilityChain; 67 | // params used to verify a proof 68 | const hasVerifyProofParams = controller || date || 69 | expectedRootCapability || 70 | inspectCapabilityChain || suite || 71 | _verifiedParentCapability; 72 | 73 | if(hasCreateProofParams && hasVerifyProofParams) { 74 | // cannot provide both create and verify params 75 | throw new Error( 76 | 'Parameters for both creating and verifying a proof must not be ' + 77 | 'provided together.'); 78 | } 79 | 80 | super({ 81 | allowTargetAttenuation, 82 | controller, date, 83 | expectedRootCapability, inspectCapabilityChain, 84 | maxChainLength, maxClockSkew, maxDelegationTtl, 85 | // always `Infinity` for capability delegation proofs, as their "created" 86 | // values are not checked for liveness, rather "expires" is used instead 87 | maxTimestampDelta: Infinity, 88 | suite, 89 | term: 'capabilityDelegation' 90 | }); 91 | 92 | // validate `CapabilityDelegation` specific params, the base class will 93 | // have already handled validating common ones... 94 | 95 | // use negative conditional to cover case where neither create nor 96 | // verify params were provided and default to proof creation case to 97 | // avoid creating bad proofs 98 | if(!hasVerifyProofParams) { 99 | if(!(typeof parentCapability === 'string' || 100 | (typeof parentCapability === 'object' && 101 | typeof parentCapability.id === 'string'))) { 102 | throw new TypeError( 103 | '"parentCapability" must be a string expressing the ID of a root ' + 104 | 'capability or an object expressing the full parent capability.'); 105 | } 106 | 107 | this.parentCapability = parentCapability; 108 | if(_capabilityChain) { 109 | if(!Array.isArray(_capabilityChain)) { 110 | throw new TypeError('"_capabilityChain" must be an array.'); 111 | } 112 | this._capabilityChain = _capabilityChain; 113 | } 114 | if(_skipLocalValidationForTesting !== undefined) { 115 | this._skipLocalValidationForTesting = _skipLocalValidationForTesting; 116 | } 117 | } else { 118 | this._verifiedParentCapability = _verifiedParentCapability; 119 | } 120 | } 121 | 122 | async update(proof, {document}) { 123 | // if no capability chain given (*for testing purposes only*), then 124 | // compute from parent 125 | let capabilityChain; 126 | const { 127 | parentCapability, term, 128 | _capabilityChain, _skipLocalValidationForTesting 129 | } = this; 130 | if(_capabilityChain) { 131 | // use chain override from tests 132 | capabilityChain = _capabilityChain; 133 | } else { 134 | capabilityChain = utils.computeCapabilityChain({ 135 | parentCapability, _skipLocalValidationForTesting 136 | }); 137 | } 138 | 139 | proof.proofPurpose = term; 140 | proof.capabilityChain = capabilityChain; 141 | 142 | if(!_skipLocalValidationForTesting) { 143 | // check capability data model 144 | const capability = {...document, proof}; 145 | utils.checkCapability({capability, expectRoot: false}); 146 | 147 | // ensure proof will not be created after it expires 148 | const created = Date.parse(proof.created); 149 | const expires = Date.parse(capability.expires); 150 | /* Note: Intentionally do not use `utils.compareTime` as there is no 151 | clock drift issue here. We are not comparing against any live values 152 | but against date-time values expressed in the chain. */ 153 | if(created > expires) { 154 | throw new Error('Cannot delegate an expired capability.'); 155 | } 156 | 157 | // ensure `allowedAction`, if present, is not less restrictive 158 | const {allowedAction: parentAllowedAction} = parentCapability; 159 | const {allowedAction} = document; 160 | if(!utils.hasValidAllowedAction({allowedAction, parentAllowedAction})) { 161 | throw new Error( 162 | 'The "allowedAction" in a delegated capability ' + 163 | 'must not be less restrictive than its parent.'); 164 | } 165 | 166 | // ensure `expires` is not less restrictive 167 | const {expires: parentExpires} = parentCapability; 168 | if(parentExpires !== undefined) { 169 | // handle case where `expires` is set in the parent, but the child 170 | // has an expiration date greater than the parent; 171 | /* Note: Intentionally do not use `utils.compareTime` as there is no 172 | clock drift issue here. We are not comparing against any live values 173 | but against date-time values expressed in the chain. Additionally, 174 | allowing skew here could introduce vulnerabilities where the expires 175 | time drift could aggregate with each new capability in the chain. */ 176 | if(expires > Date.parse(parentExpires)) { 177 | throw new Error( 178 | 'The `expires` property in a delegated capability must not be ' + 179 | 'less restrictive than its parent.'); 180 | } 181 | } 182 | 183 | // ensure capability won't be delegated before its parent was delegated 184 | // (if that parent is non-root) 185 | if(capabilityChain.length > 1) { 186 | // get delegated date-time (note: `computeCapabilityChain` has already 187 | // validated that there is a single delegation proof in 188 | // `parentCapability`) 189 | const [parentProof] = utils.getDelegationProofs( 190 | {capability: parentCapability}); 191 | const parentDelegationTime = Date.parse(parentProof.created); 192 | const childDelegationTime = Date.parse(proof.created); 193 | // verify parent capability was not delegated after child 194 | if(parentDelegationTime > childDelegationTime) { 195 | throw new Error( 196 | 'A capability in the delegation chain was delegated before ' + 197 | 'its parent.'); 198 | } 199 | } 200 | } 201 | 202 | return proof; 203 | } 204 | 205 | async match(proof, {document, documentLoader}) { 206 | try { 207 | // check the `proof` context before using its terms 208 | utils.checkProofContext({proof}); 209 | } catch(e) { 210 | // context does not match, so proof does not match 211 | return false; 212 | } 213 | 214 | return super.match(proof, {document, documentLoader}); 215 | } 216 | 217 | _getCapabilityDelegationClass() { 218 | return CapabilityDelegation; 219 | } 220 | 221 | _getTailCapability({document, proof}) { 222 | // `proof` must be reattached to the capability because it contains 223 | // the `capabilityChain` that must be dereferenced and verified 224 | return {capability: {...document, proof}}; 225 | } 226 | 227 | async _runChecksBeforeChainVerification() { 228 | /* Note: Here we create a signal to be sent to `_verifyCapabilityChain` 229 | that the capability delegation proof for the tail has already been 230 | verified (to avoid it being reverified). We will compute the full 231 | `verifyResult` in `_runChecksAfterChainVerification` once we have verified 232 | the parent capability. */ 233 | return {capabilityChainMeta: [{verifyResult: {}}]}; 234 | } 235 | 236 | async _runChecksAfterChainVerification({ 237 | capabilityChainMeta, dereferencedChain, proof, validateOptions 238 | }) { 239 | // verified parent is second to last in the chain (i.e., it is the parent 240 | // of the last in the chain) 241 | const verifiedParentCapability = dereferencedChain[ 242 | dereferencedChain.length - 2]; 243 | 244 | // get purpose result which needs to be used to build `verifyResult` 245 | const purposeResult = await this._validateAgainstParent({ 246 | proof, verifiedParentCapability, validateOptions 247 | }); 248 | 249 | // build verify result 250 | const {verificationMethod} = validateOptions; 251 | const {verifyResult} = capabilityChainMeta[capabilityChainMeta.length - 1]; 252 | verifyResult.verified = purposeResult.valid; 253 | verifyResult.results = [{ 254 | proof, verified: true, verificationMethod, purposeResult 255 | }]; 256 | 257 | return purposeResult; 258 | } 259 | 260 | async _shortCircuitValidate({proof, validateOptions}) { 261 | // see if the parent capability has already been verified 262 | const { 263 | _verifiedParentCapability: verifiedParentCapability 264 | } = this; 265 | if(verifiedParentCapability) { 266 | // simple case, just validate against parent and return, we have been 267 | // called from within a chain verification and can short circuit proof 268 | // validation 269 | return this._validateAgainstParent({ 270 | proof, verifiedParentCapability, validateOptions 271 | }); 272 | } 273 | 274 | // no short-circuit possible, we've just started validating the proof 275 | // from root => tail 276 | } 277 | 278 | async _validateAgainstParent({ 279 | proof, verifiedParentCapability, validateOptions 280 | }) { 281 | // ensure proof created by authorized delegator... 282 | // parent zcap controller must match the delegating verification method 283 | // (or its controller) 284 | const {verificationMethod} = validateOptions; 285 | if(!utils.isController( 286 | {capability: verifiedParentCapability, verificationMethod})) { 287 | const error = new Error( 288 | 'The capability controller does not match the verification ' + 289 | 'method (or its controller) used to delegate.'); 290 | error.details = { 291 | capability: verifiedParentCapability, 292 | verificationMethod 293 | }; 294 | throw error; 295 | } 296 | 297 | // run base level validation checks 298 | const result = await this._runBaseProofValidation({proof, validateOptions}); 299 | if(!result.valid) { 300 | throw result.error; 301 | } 302 | 303 | // the controller of the proof is the delegator of the capability 304 | result.delegator = result.controller; 305 | 306 | // `result` includes meta data about the proof controller 307 | return result; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /lib/CapabilityInvocation.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as utils from './utils.js'; 5 | import {CapabilityDelegation} from './CapabilityDelegation.js'; 6 | import {CapabilityProofPurpose} from './CapabilityProofPurpose.js'; 7 | 8 | /** 9 | * @typedef InspectCapabilityChain 10 | */ 11 | 12 | export class CapabilityInvocation extends CapabilityProofPurpose { 13 | /** 14 | * @param {object} options - The options. 15 | * @param {string|object} [options.capability] - The capability that is to be 16 | * added/referenced in a created proof (a root zcap MUST be passed as 17 | * a string and a delegated zcap as an object). 18 | * @param {string} [options.capabilityAction] - The capability action that is 19 | * to be added to a proof. 20 | * @param {string} [options.invocationTarget] - The invocation target to 21 | * use; this is required and can be used to attenuate the capability's 22 | * invocation target if the verifier supports target attentuation. 23 | * @param {boolean} [options.allowTargetAttenuation=false] - Allow the 24 | * invocationTarget of a delegation chain to be increasingly restrictive 25 | * based on a hierarchical RESTful URL structure. 26 | * @param {object} [options.controller] - The description of the controller, 27 | * if it is not to be dereferenced via a `documentLoader`. 28 | * @param {string|Date|number} [options.date] - Used during proof 29 | * verification as the expected date for the creation of the proof 30 | * (within a maximum timestamp delta) and for checking to see if a 31 | * capability has expired; if not passed the current date will be used. 32 | * @param {string} [options.expectedAction] - The capability action that is 33 | * expected when validating a proof. 34 | * @param {string|Array} [options.expectedRootCapability] - The expected root 35 | * capability for the delegation chain (this can be a single root 36 | * capability ID expressed as a string or, if there is more than one 37 | * acceptable root capability, several root capability IDs in an array. 38 | * @param {string} [options.expectedTarget] - The target we expect a 39 | * capability to apply to (URI). 40 | * @param {InspectCapabilityChain} [options.inspectCapabilityChain] - An async 41 | * function that can be used to check for revocations related to any of 42 | * verified capabilities. 43 | * @param {number} [options.maxChainLength=10] - The maximum length of the 44 | * capability delegation chain. 45 | * @param {number} [options.maxClockSkew=300] - A maximum number of seconds 46 | * that clocks may be skewed when checking capability expiration date-times 47 | * against `date` and when comparing invocation proof creation time against 48 | * delegation proof creation time. 49 | * @param {number} [options.maxDelegationTtl=Infinity] - The maximum 50 | * milliseconds to live for a delegated zcap as measured by the time 51 | * difference between `expires` and `created` on the delegation proof. 52 | * @param {number} [options.maxTimestampDelta=Infinity] - A maximum number 53 | * of seconds that "created" date on the capability invocation proof 54 | * can deviate from * `date`, defaults to `Infinity`. 55 | * @param {object|Array} options.suite - The jsonld-signature suite(s) to use 56 | * to verify the capability chain. 57 | */ 58 | constructor({ 59 | // proof creation params 60 | capability, 61 | capabilityAction, 62 | invocationTarget, 63 | // proof verification params 64 | allowTargetAttenuation, 65 | controller, 66 | date, 67 | expectedAction, 68 | expectedRootCapability, 69 | expectedTarget, 70 | inspectCapabilityChain, 71 | maxChainLength, 72 | maxClockSkew, 73 | maxDelegationTtl, 74 | maxTimestampDelta, 75 | suite 76 | } = {}) { 77 | // parameters used to create a proof 78 | const hasCreateProofParams = capability || capabilityAction || 79 | invocationTarget; 80 | // params used to verify a proof 81 | const hasVerifyProofParams = controller || date || 82 | expectedAction || expectedRootCapability || expectedTarget || 83 | inspectCapabilityChain || suite; 84 | 85 | if(hasCreateProofParams && hasVerifyProofParams) { 86 | // cannot provide both create and verify params 87 | throw new Error( 88 | 'Parameters for both creating and verifying a proof must not be ' + 89 | 'provided together.'); 90 | } 91 | 92 | super({ 93 | allowTargetAttenuation, 94 | controller, date, 95 | expectedRootCapability, inspectCapabilityChain, 96 | maxChainLength, maxClockSkew, maxDelegationTtl, maxTimestampDelta, 97 | suite, 98 | term: 'capabilityInvocation' 99 | }); 100 | 101 | // validate `CapabilityInvocation` specific params, the base class will 102 | // have already handled validating common ones... 103 | 104 | // use negative conditional to cover case where neither create nor 105 | // verify params were provided and default to proof creation case to 106 | // avoid creating bad proofs 107 | if(!hasVerifyProofParams) { 108 | if(typeof capability === 'object') { 109 | // root capabilities MUST be passed as strings 110 | if(!(capability && capability.parentCapability)) { 111 | throw new Error( 112 | '"capability" must be a string if it is a root capability.'); 113 | } 114 | } else if(typeof capability !== 'string') { 115 | throw new TypeError('"capability" must be a string or object.'); 116 | } 117 | if(typeof capabilityAction !== 'string') { 118 | throw new TypeError('"capabilityAction" must be a string.'); 119 | } 120 | if(!(typeof invocationTarget === 'string' && 121 | invocationTarget.includes(':'))) { 122 | throw new TypeError( 123 | '"invocationTarget" must be a string that expresses an absolute ' + 124 | 'URI.'); 125 | } 126 | 127 | this.capability = capability; 128 | this.capabilityAction = capabilityAction; 129 | this.invocationTarget = invocationTarget; 130 | } else { 131 | if(typeof expectedAction !== 'string') { 132 | throw new TypeError('"expectedAction" must be a string.'); 133 | } 134 | if(!(typeof expectedTarget === 'string' || 135 | Array.isArray(expectedTarget))) { 136 | throw new TypeError('"expectedTarget" must be a string or array.'); 137 | } 138 | // expected target values must be absolute URIs 139 | const expectedTargets = Array.isArray(expectedTarget) ? 140 | expectedTarget : [expectedTarget]; 141 | for(const et of expectedTargets) { 142 | if(!(typeof et === 'string' && et.includes(':'))) { 143 | throw new Error( 144 | '"expectedTargets" values must be absolute URI strings.'); 145 | } 146 | } 147 | 148 | this.expectedTarget = expectedTarget; 149 | this.expectedAction = expectedAction; 150 | } 151 | } 152 | 153 | async update(proof) { 154 | const {capability, capabilityAction, invocationTarget} = this; 155 | proof.proofPurpose = this.term; 156 | proof.capability = capability; 157 | proof.invocationTarget = invocationTarget; 158 | proof.capabilityAction = capabilityAction; 159 | return proof; 160 | } 161 | 162 | async match(proof, {document, documentLoader}) { 163 | const {expectedAction, expectedTarget} = this; 164 | 165 | try { 166 | // check the `proof` context before using its terms 167 | utils.checkProofContext({proof}); 168 | } catch(e) { 169 | // context does not match, so proof does not match 170 | return false; 171 | } 172 | 173 | if(!proof.capability) { 174 | // capability not in the proof, not a match 175 | return false; 176 | } 177 | 178 | // ensure basic purpose and expected action match the proof 179 | if(!(await super.match(proof, {document, documentLoader}) && 180 | (expectedAction === proof.capabilityAction))) { 181 | return false; 182 | } 183 | 184 | // ensure the proof's declared invocation target matches an expected one 185 | if(Array.isArray(expectedTarget)) { 186 | return expectedTarget.includes(proof.invocationTarget); 187 | } 188 | return expectedTarget === proof.invocationTarget; 189 | } 190 | 191 | _getCapabilityDelegationClass() { 192 | return CapabilityDelegation; 193 | } 194 | 195 | _getTailCapability({proof}) { 196 | return {capability: proof.capability}; 197 | } 198 | 199 | async _runChecksBeforeChainVerification({dereferencedChain, proof}) { 200 | const { 201 | allowTargetAttenuation, 202 | expectedAction, 203 | expectedTarget 204 | } = this; 205 | 206 | /* 1. Ensure that `capabilityAction` is an allowed action and that 207 | it matches `expectedAction`. Note that if it doesn't match and `match` 208 | was called to gate calling `validate`, then this code will not execute. 209 | However, if `validate` is called directly, this check MUST run here. 210 | 211 | If the capability restricts the actions via `allowedAction` then 212 | `capabilityAction` must be in its set. */ 213 | const capability = dereferencedChain[dereferencedChain.length - 1]; 214 | const {capabilityAction} = proof; 215 | const allowedActions = utils.getAllowedActions({capability}); 216 | if(allowedActions.length > 0 && 217 | !allowedActions.includes(capabilityAction)) { 218 | throw new Error( 219 | `Capability action "${capabilityAction}" is not allowed by the ` + 220 | 'capability; allowed actions are: ' + 221 | allowedActions.map(x => `"${x}"`).join(', ')); 222 | } 223 | if(capabilityAction !== expectedAction) { 224 | throw new Error( 225 | `Capability action "${capabilityAction}" does not match the ` + 226 | `expected action of "${expectedAction}".`); 227 | } 228 | 229 | /* 2. Ensure `expectedTarget` is as expected. The invocation target 230 | will also be checked to ensure it hasn't changed from previous zcaps 231 | in the chain (unless attenuation is permitted) later. */ 232 | 233 | /* 3. Verify the invocation target in the proof is as expected. The 234 | `invocationTarget` specified in the capability invocation proof must 235 | match exactly (or follow acceptable target attenuation rules) the 236 | `invocationTarget` specified in the invoked capability. */ 237 | const capabilityTarget = utils.getTarget({capability}); 238 | const {invocationTarget} = proof; 239 | if(!(typeof invocationTarget === 'string' && 240 | invocationTarget.includes(':'))) { 241 | throw new TypeError( 242 | `Invocation target (${invocationTarget}) must be a string that ` + 243 | 'expresses an absolute URI.'); 244 | } 245 | if(!utils.isValidTarget({ 246 | invocationTarget, 247 | baseInvocationTarget: capabilityTarget, 248 | allowTargetAttenuation 249 | })) { 250 | throw new Error( 251 | `Invocation target (${invocationTarget}) does not match ` + 252 | `capability target (${capabilityTarget}).`); 253 | } 254 | 255 | /* 4. Verify the invocation target is an expected target. Prior to this 256 | step we ensured that the invocation target used matched th capability 257 | that was invoked, but this check ensures that the invocation target used 258 | matches the endpoint (the `expectedTarget`) where the capability was 259 | actually invoked. */ 260 | if(!((Array.isArray(expectedTarget) && 261 | expectedTarget.includes(invocationTarget)) || 262 | (typeof expectedTarget === 'string' && 263 | invocationTarget === expectedTarget))) { 264 | throw new Error( 265 | `Expected target (${expectedTarget}) does not match ` + 266 | `invocation target (${invocationTarget}).`); 267 | } 268 | 269 | /* 5. If capability is delegated (not root), then ensure the capability 270 | invocation proof `created` date is not before the capability delegation 271 | proof creation date. */ 272 | if(capability.parentCapability) { 273 | const invoked = Date.parse(proof.created); 274 | const [delegationProof] = utils.getDelegationProofs({capability}); 275 | const delegated = Date.parse(delegationProof.created); 276 | const {maxClockSkew} = this; 277 | // use `utils.compareTime` to allow for clock drift from the machine 278 | // that created the delegation proof and the machine that created 279 | // the invocation proof 280 | if(utils.compareTime({t1: invoked, t2: delegated, maxClockSkew}) < 0) { 281 | throw new Error( 282 | 'A delegated capability must not be invoked before the "created" ' + 283 | 'date in its delegation proof.'); 284 | } 285 | } 286 | 287 | // return no capability delegation verify results yet; the tail's 288 | // capability delegation proof must be verified via 289 | // `_verifyCapabilityChain` 290 | return {capabilityChainMeta: []}; 291 | } 292 | 293 | async _runChecksAfterChainVerification({ 294 | dereferencedChain, proof, validateOptions 295 | }) { 296 | /* Verify the controller of the capability. The zcap controller must 297 | match the invoking verification method (or its controller). */ 298 | const capability = dereferencedChain[dereferencedChain.length - 1]; 299 | const {verificationMethod} = validateOptions; 300 | if(!utils.isController({capability, verificationMethod})) { 301 | const error = new Error( 302 | 'The capability controller does not match the verification method ' + 303 | '(or its controller) used to invoke.'); 304 | error.details = { 305 | capability, 306 | verificationMethod 307 | }; 308 | throw error; 309 | } 310 | 311 | // if capability is delegated, verify that it has not expired 312 | if(capability.parentCapability) { 313 | // verify expiration dates 314 | // expires date has been previously validated, so just parse it 315 | const currentCapabilityExpirationTime = Date.parse(capability.expires); 316 | 317 | // use `utils.compareTime` to allow for allow for clock drift because 318 | // we are comparing against `currentDate` 319 | const {date, maxClockSkew} = this; 320 | const currentDate = (date && new Date(date)) || new Date(); 321 | if(utils.compareTime({ 322 | t1: currentDate.getTime(), 323 | t2: currentCapabilityExpirationTime, 324 | maxClockSkew 325 | }) > 0) { 326 | throw new Error('The invoked capability has expired.'); 327 | } 328 | } 329 | 330 | // run base level validation checks 331 | const result = await this._runBaseProofValidation({proof, validateOptions}); 332 | if(!result.valid) { 333 | throw result.error; 334 | } 335 | 336 | // the controller of the verification method from the proof is the 337 | // invoker of the capability 338 | result.invoker = result.controller; 339 | 340 | return result; 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /lib/CapabilityProofPurpose.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as utils from './utils.js'; 5 | import jsigs from 'jsonld-signatures'; 6 | const {ControllerProofPurpose} = jsigs.purposes; 7 | 8 | /* Note: This class is just an abstract base class for the 9 | `CapabilityInvocation` and `CapabilityDelegation` proof purposes. */ 10 | 11 | /** 12 | * @typedef InspectCapabilityChain 13 | */ 14 | 15 | export class CapabilityProofPurpose extends ControllerProofPurpose { 16 | /** 17 | * @param {object} options - The options. 18 | * @param {boolean} [options.allowTargetAttenuation=false] - Allow the 19 | * invocationTarget of a delegation chain to be increasingly restrictive 20 | * based on a hierarchical RESTful URL structure. 21 | * @param {object} [options.controller] - The description of the controller, 22 | * if it is not to be dereferenced via a `documentLoader`. 23 | * @param {string|Date|number} [options.date] - Used during proof 24 | * verification as the expected date for the creation of the proof 25 | * (within a maximum timestamp delta) and for checking to see if a 26 | * capability has expired; if not passed the current date will be used. 27 | * @param {string|Array} [options.expectedRootCapability] - The expected root 28 | * capability for the delegation chain (this can be a single root 29 | * capability ID expressed as a string or, if there is more than one 30 | * acceptable root capability, several root capability IDs in an array. 31 | * @param {InspectCapabilityChain} [options.inspectCapabilityChain] - An 32 | * async function that can be used to check for revocations related to any 33 | * of verified capabilities. 34 | * @param {number} [options.maxChainLength=10] - The maximum length of the 35 | * capability 36 | * delegation chain. 37 | * @param {number} [options.maxClockSkew=300] - A maximum number of seconds 38 | * that clocks may be skewed checking capability expiration date-times 39 | * against `date` and when comparing invocation proof creation time against 40 | * delegation proof creation time. 41 | * @param {number} [options.maxDelegationTtl=Infinity] - The maximum 42 | * milliseconds to live for a delegated zcap as measured by the time 43 | * difference between `expires` and `created` on the delegation proof. 44 | * @param {number} [options.maxTimestampDelta=Infinity] - A maximum number 45 | * of seconds that a capability invocation proof (only used by this proof 46 | * type) "created" date can deviate from `date`, defaults to `Infinity`. 47 | * @param {object|Array} options.suite - The jsonld-signature suites to use 48 | * to verify the capability chain. 49 | * @param {string} options.term - The term `capabilityInvocation` or 50 | * `capabilityDelegation` to look for in an LD proof. 51 | */ 52 | constructor({ 53 | // proof verification params (and common to all derived classes) 54 | allowTargetAttenuation = false, 55 | controller, 56 | date, 57 | expectedRootCapability, 58 | inspectCapabilityChain, 59 | maxChainLength, 60 | maxDelegationTtl = Infinity, 61 | maxTimestampDelta = Infinity, 62 | maxClockSkew = 300, 63 | suite, 64 | term 65 | } = {}) { 66 | super({term, controller, date, maxTimestampDelta}); 67 | 68 | // params used to verify a proof 69 | const hasVerifyProofParams = controller || date || 70 | expectedRootCapability || inspectCapabilityChain || suite; 71 | if(hasVerifyProofParams) { 72 | if(!(typeof expectedRootCapability === 'string' || 73 | Array.isArray(expectedRootCapability))) { 74 | throw new TypeError( 75 | '"expectedRootCapability" must be a string or array.'); 76 | } 77 | 78 | // expected root capability values must be absolute URIs 79 | const expectedRootCapabilities = Array.isArray(expectedRootCapability) ? 80 | expectedRootCapability : [expectedRootCapability]; 81 | for(const erc of expectedRootCapabilities) { 82 | if(!(typeof erc === 'string' && erc.includes(':'))) { 83 | throw new Error( 84 | '"expectedRootCapability" values must be absolute URI strings.'); 85 | } 86 | } 87 | 88 | if(typeof maxClockSkew !== 'number') { 89 | throw new TypeError('"maxClockSkew" must be a number.'); 90 | } 91 | 92 | this.allowTargetAttenuation = allowTargetAttenuation; 93 | this.expectedRootCapability = expectedRootCapability; 94 | this.inspectCapabilityChain = inspectCapabilityChain; 95 | this.maxChainLength = maxChainLength; 96 | this.maxClockSkew = maxClockSkew; 97 | this.maxDelegationTtl = maxDelegationTtl; 98 | this.suite = suite; 99 | } 100 | } 101 | 102 | async validate(proof, validateOptions) { 103 | /* Note: Trust begins at the root zcap, so we start chain validation at 104 | the root and move forward toward the tail from there. This also helps 105 | prevent an attacker from wasting time when they submit long zcap chains 106 | that are extensions of otherwise valid chains. 107 | 108 | So, each parent zcap must be verified before its child is. This also means 109 | that we can't simply recursively unwind the chain in reverse; therefore, 110 | the code is a bit more complex. 111 | 112 | Note that if a chain is being checked without an invocation, i.e., without 113 | invoking the tail capability, then the tail's capability delegation *proof* 114 | will have been cryptographically verified prior to this call. Otherwise, 115 | it will need to be cryptographically verified. There is a signal described 116 | below to indicate whether this verification needs to occur. Regardless, the 117 | tail has not yet been validated as a tail for the chain and won't be until 118 | the rest of the chain, starting at the root, is validated. 119 | 120 | The validation process is: 121 | 122 | 0. Run a short-circuit check to ensure that we only verify the capability 123 | chain once; that is, we only start checking the chain when we haven't 124 | verified any parent zcaps yet. Whether we've started checking the chain 125 | yet or not is handled by a derived class that implements 126 | `_shortCircuitValidate`, returning the short-circuit validation result 127 | if the chain check has already started and `undefined` if it hasn't. 128 | 1. If we haven't been short-circuited, then dereference the capability 129 | chain referenced in the tail proof to get all zcaps in the chain. 130 | 2. Run any proof-purpose specific checks prior to checking the rest of 131 | the chain. This allows shortcuts when checking a capability invocation 132 | proof, e.g., if an invocation is immediately invalid for some reason, 133 | there is no need to check that the delegation rules were followed along 134 | the entire chain. This method also returns the `capabilityChainMeta` 135 | array to use to hold the capability delegation proof verify results. If 136 | a capability delegation proof for the tail has already been verified, 137 | this array will have a placeholder for its full proof validation result 138 | as a signal to avoid duplicating this work later. 139 | 3. Verify the chain from root => tail by calling `verifyCapabilityChain` 140 | just once -- when validating the tail. The short-circuit check above 141 | ensures we don't call this more than once. Additionally, the 142 | `capabilityChainMeta` array signals whether we need to cryptographically 143 | verify the capability delegation proof on the tail or if we must skip 144 | this to avoid duplicating that work. 145 | 4. Run any purpose-specific checks after chain verification. This allows 146 | capability delegation proof checks to be run on the tail against the now 147 | verified parent, allowing its proof validation result to be fully 148 | constructed and updated in the `capabilityChainMeta` array (as well 149 | as the return value for this function). 150 | 5. Run the `inspectCapabilityChain` hook, if given, to allow for custom 151 | implementations to check for revoked zcaps in databases or whatever other 152 | behavior is desired. */ 153 | 154 | try { 155 | // ensure proof has expected context (even though this is called in 156 | // `match`, it is possible to call `validate` separately without calling 157 | // `match`, so check here too) 158 | utils.checkProofContext({proof}); 159 | 160 | const {document, documentLoader} = validateOptions; 161 | 162 | // 0. Run any proof-purpose-specific short-circuit check. 163 | const shortcircuit = await this._shortCircuitValidate({ 164 | proof, validateOptions 165 | }); 166 | if(shortcircuit) { 167 | return shortcircuit; 168 | } 169 | 170 | /* 1. Dereference the capability chain. This involves finding all 171 | embedded delegated zcaps, using a verifier-trusted hook to dereference 172 | the root zcap, and putting the full zcaps in order (root => tail) in an 173 | array. The `tail` is the zcap that was invoked. */ 174 | const {dereferencedChain} = await this._dereferenceChain({ 175 | document, documentLoader, proof 176 | }); 177 | 178 | /* 2. Run any proof-purpose-specific early checks prior to chain 179 | verification. */ 180 | const { 181 | capabilityChainMeta 182 | } = await this._runChecksBeforeChainVerification({ 183 | dereferencedChain, proof, validateOptions 184 | }); 185 | 186 | /* 3. Verify the capability delegation chain. This will make sure that 187 | the root zcap in the chain is as expected (for the endpoint where the 188 | invocation occurred) and that every other zcap in the chain (including 189 | the invoked one), has been properly delegated. */ 190 | const { 191 | verified, error 192 | } = await this._verifyCapabilityChain({ 193 | // required to avoid circular dependencies 194 | CapabilityDelegation: this._getCapabilityDelegationClass(), 195 | capabilityChainMeta, 196 | dereferencedChain, 197 | documentLoader 198 | }); 199 | if(!verified) { 200 | throw error; 201 | } 202 | 203 | /* 4. Run any proof-purpose-specific checks after chain verification 204 | to get the proof validation result. */ 205 | const validateResult = await this._runChecksAfterChainVerification({ 206 | capabilityChainMeta, dereferencedChain, proof, validateOptions 207 | }); 208 | 209 | // 5. Run `inspectCapabilityChain` hook. 210 | const {inspectCapabilityChain} = this; 211 | if(inspectCapabilityChain) { 212 | const {valid, error} = await inspectCapabilityChain({ 213 | // full chain, including root zcap 214 | capabilityChain: dereferencedChain, 215 | // capability chain meta including `null` for root zcap 216 | capabilityChainMeta: [{verifyResult: null}, ...capabilityChainMeta] 217 | }); 218 | if(!valid) { 219 | throw error; 220 | } 221 | } 222 | 223 | // include dereferenced chain result 224 | validateResult.dereferencedChain = dereferencedChain; 225 | 226 | return validateResult; 227 | } catch(error) { 228 | return {valid: false, error}; 229 | } 230 | } 231 | 232 | async _dereferenceChain({document, documentLoader, proof}) { 233 | const {expectedRootCapability, maxChainLength} = this; 234 | const {capability} = this._getTailCapability({document, proof}); 235 | const {dereferencedChain} = await utils.dereferenceCapabilityChain({ 236 | capability, 237 | async getRootCapability({id}) { 238 | // ensure root zcap in chain is as expected 239 | let match; 240 | if(typeof expectedRootCapability === 'string') { 241 | match = expectedRootCapability === id; 242 | } else { 243 | match = expectedRootCapability.includes(id); 244 | } 245 | if(!match) { 246 | const error = new Error( 247 | `Actual root capability (${id}) does not match expected root ` + 248 | `capability (${expectedRootCapability}).`); 249 | error.details = { 250 | actual: id, 251 | expected: expectedRootCapability, 252 | }; 253 | throw error; 254 | } 255 | 256 | // load root zcap 257 | const {document} = await documentLoader(id); 258 | return {rootCapability: document}; 259 | }, 260 | maxChainLength 261 | }); 262 | return {dereferencedChain}; 263 | } 264 | 265 | _getCapabilityDelegationClass() { 266 | throw new Error('Not implemented.'); 267 | } 268 | 269 | async _getTailCapability(/*{document, proof}*/) { 270 | throw new Error('Not implemented.'); 271 | } 272 | 273 | // no-op by default 274 | async _runChecksBeforeChainVerification() {} 275 | 276 | // no-op by default 277 | async _runChecksAfterChainVerification() {} 278 | 279 | async _runBaseProofValidation({proof, validateOptions}) { 280 | // run super class's validation checks 281 | const result = await super.validate(proof, validateOptions); 282 | if(!result.valid) { 283 | throw result.error; 284 | } 285 | return result; 286 | } 287 | 288 | // no-op by default 289 | async _shortCircuitValidate() {} 290 | 291 | /** 292 | * @typedef class 293 | */ 294 | /** 295 | * @typedef CapabilityMeta 296 | */ 297 | 298 | /** 299 | * Verifies the given dereferenced capability chain. This involves ensuring 300 | * that the root zcap in the chain is as expected (for the endpoint where an 301 | * invocation or a simple chain chain is occurring) and that every other zcap 302 | * in the chain (including any invoked one), has been properly delegated. 303 | * 304 | * @param {object} options - The options. 305 | * @param {class} options.CapabilityDelegation - The CapabilityDelegation 306 | * class; this must be passed to avoid circular references in this module. 307 | * @param {CapabilityMeta[]} options.capabilityChainMeta - The array of 308 | * results for inspecting the capability chain; if this has a value when 309 | * passed, then it is presumed to be the verify result for the tail 310 | * capability and that tail capability will not be verified internally by 311 | * this function to avoid duplicating work; all verification results 312 | * (including the tail's -- either computed locally or reused from what 313 | * was passed) will be added to this array in order from root => tail. 314 | * @param {Array} options.dereferencedChain - The dereferenced capability 315 | * chain for `capability`, starting at the root capability and ending at 316 | * `capability`. 317 | * @param {Function} options.documentLoader - A configured jsonld 318 | * documentLoader. 319 | * 320 | * @returns {object} An object with `{verified, error}`. 321 | */ 322 | async _verifyCapabilityChain({ 323 | CapabilityDelegation, 324 | capabilityChainMeta, 325 | dereferencedChain, 326 | documentLoader 327 | }) { 328 | /* Note: We start verifying a capability chain at its root of trust (the 329 | root capability) and then move toward the tail. To prevent recursively 330 | repeating checks, we pass a `verifiedParentCapability` each time we start 331 | verifying another capability delegation proof in the capability chain. 332 | 333 | Verification process is: 334 | 335 | 1. If the chain only as the root capability, exit early. 336 | 2. For each capability `zcap` in the chain, verify the capability delegation 337 | proof on `zcap` (if `capabilityChainMeta` has no precomputed result) and 338 | that all of the delegation rules have been followed. */ 339 | 340 | try { 341 | // 1. If the chain only has the root, exit early. 342 | if(dereferencedChain.length === 1) { 343 | return {verified: true}; 344 | } 345 | 346 | // 2. For each capability `zcap` in the chain, verify the capability 347 | // delegation proof on `zcap` and that the delegation rules have been 348 | // followed. 349 | let parentAllowedAction; 350 | let parentDelegationTime; 351 | let parentExpirationTime; 352 | const [root] = dereferencedChain; 353 | let {invocationTarget: parentInvocationTarget} = root; 354 | 355 | // track whether `capabilityChainMeta` needs its first result shifted to 356 | // the end (if a result was present, it is for the last or "tail" zcap, 357 | // so we set a flag to remember to move it to the end when we're done 358 | // checking zcaps below) 359 | const mustShift = capabilityChainMeta.length > 0; 360 | 361 | // get all delegated capabilities (no root zcap since it has no delegation 362 | // proof to check) 363 | const delegatedCapabilities = dereferencedChain.slice(1); 364 | const { 365 | allowTargetAttenuation, 366 | expectedRootCapability, 367 | date, 368 | maxClockSkew, 369 | maxDelegationTtl, 370 | suite 371 | } = this; 372 | const currentDate = (date && new Date(date)) || new Date(); 373 | for(let i = 0; i < delegatedCapabilities.length; ++i) { 374 | const zcap = delegatedCapabilities[i]; 375 | /* Note: Passing `_verifiedParentCapability` will prevent repetitive 376 | checking of the same segments of the chain (once a parent is verified, 377 | its chain is not checked again when checking its children). */ 378 | const _verifiedParentCapability = delegatedCapabilities[i - 1] || root; 379 | 380 | // verify proof on zcap if no result has been computed yet (one 381 | // verify result will be present in `capabilityChainMeta` per 382 | // delegated capability) 383 | if(capabilityChainMeta.length < delegatedCapabilities.length) { 384 | const verifyResult = await jsigs.verify(zcap, { 385 | suite, 386 | purpose: new CapabilityDelegation({ 387 | allowTargetAttenuation, 388 | date: currentDate, 389 | expectedRootCapability, 390 | maxDelegationTtl, 391 | _verifiedParentCapability 392 | }), 393 | documentLoader 394 | }); 395 | if(!verifyResult.verified) { 396 | throw verifyResult.error; 397 | } 398 | // delegation proof verified; save meta data for later inspection 399 | capabilityChainMeta.push({verifyResult}); 400 | } 401 | 402 | // ensure `allowedAction` is valid (compared against parent) 403 | const {allowedAction} = zcap; 404 | if(!utils.hasValidAllowedAction({allowedAction, parentAllowedAction})) { 405 | throw new Error( 406 | 'The "allowedAction" in a delegated capability ' + 407 | 'must not be less restrictive than its parent.'); 408 | } 409 | 410 | // ensure `invocationTarget` delegation is acceptable 411 | const invocationTarget = utils.getTarget({capability: zcap}); 412 | if(!utils.isValidTarget({ 413 | invocationTarget, 414 | baseInvocationTarget: parentInvocationTarget, 415 | allowTargetAttenuation 416 | })) { 417 | if(allowTargetAttenuation) { 418 | throw new Error( 419 | `The "invocationTarget" in a delegated capability must not be ` + 420 | 'less restrictive than its parent.'); 421 | } else { 422 | throw new Error( 423 | 'The "invocationTarget" in a delegated capability ' + 424 | 'must be equivalent to its parent.'); 425 | } 426 | } 427 | 428 | // verify expiration dates 429 | // expires date has been previously validated, so just parse it 430 | const currentCapabilityExpirationTime = Date.parse(zcap.expires); 431 | 432 | // if the parent does not specify an expiration date, then any more 433 | // restrictive expiration date is acceptable 434 | if(parentExpirationTime !== undefined) { 435 | // handle case where `expires` is set in the parent, but the child 436 | // has an expiration date greater than the parent 437 | if(currentCapabilityExpirationTime > parentExpirationTime) { 438 | // `utils.compareTime` intentionally not used; the delegator MUST 439 | // not use an `expires` value later than what is in the parent, 440 | // which they have access to (not a decentralized clock problem) 441 | throw new Error( 442 | 'The `expires` property in a delegated capability must not ' + 443 | 'be less restrictive than its parent.'); 444 | } 445 | // use `utils.compareTime` to allow for allow for clock drift because 446 | // we are comparing against `currentDate` 447 | if(utils.compareTime({ 448 | t1: currentDate.getTime(), 449 | t2: parentExpirationTime, 450 | maxClockSkew 451 | }) > 0) { 452 | throw new Error( 453 | 'A capability in the delegation chain has expired.'); 454 | } 455 | } 456 | 457 | // get delegated date-time 458 | // note: there can be only one proof here and this has already been 459 | // validated to be the case during `dereferenceCapabilityChain` 460 | const [proof] = utils.getDelegationProofs({capability: zcap}); 461 | const currentCapabilityDelegationTime = Date.parse(proof.created); 462 | 463 | // verify parent capability was not delegated after child 464 | if(parentDelegationTime !== undefined && 465 | parentDelegationTime > currentCapabilityDelegationTime) { 466 | throw new Error( 467 | 'A capability in the delegation chain was delegated before ' + 468 | 'its parent.'); 469 | } 470 | 471 | // some systems may require historical verification of zcaps, so 472 | // allow `maxDelegationTtl` of `Infinity` 473 | if(maxDelegationTtl < Infinity) { 474 | /* Note: Here we ensure zcap has a time-to-live (TTL) that is 475 | sufficiently short. This is to prevent the use of zcaps that, when 476 | revoked, will have to be stored for long periods of time. We have to 477 | ensure: 478 | 479 | 1. The zcap's delegation date is not in the future (this also ensures 480 | that the zcap's expiration date is not before its delegation date 481 | as it would have triggered an expiration error in a previous check). 482 | 2. The zcap's current TTL is <= `maxDelegationTtl` 483 | 3. The zcap's TTL was never > `maxDelegationTtl`. */ 484 | 485 | // use `utils.compareTime` to allow for allow for clock drift because 486 | // we are comparing against `currentDate` 487 | if(utils.compareTime({ 488 | t1: currentCapabilityDelegationTime, 489 | t2: currentDate.getTime(), 490 | maxClockSkew 491 | }) > 0) { 492 | throw new Error( 493 | 'A delegated capability in the delegation chain was delegated ' + 494 | 'in the future.'); 495 | } 496 | const currentTtl = currentCapabilityExpirationTime - 497 | currentDate.getTime(); 498 | const maxTtl = currentCapabilityExpirationTime - 499 | currentCapabilityDelegationTime; 500 | // use `utils.compareTime` to allow for allow for clock drift because 501 | // we are comparing against `currentDate` 502 | const currentTtlComparison = utils.compareTime({ 503 | t1: currentTtl, 504 | t2: maxDelegationTtl, 505 | maxClockSkew 506 | }); 507 | if(currentTtlComparison > 0 || maxTtl > maxDelegationTtl) { 508 | throw new Error( 509 | 'A delegated capability in the delegation chain has a time to ' + 510 | 'live that is too long.'); 511 | } 512 | } 513 | 514 | parentAllowedAction = allowedAction; 515 | parentExpirationTime = currentCapabilityExpirationTime; 516 | parentDelegationTime = currentCapabilityDelegationTime; 517 | parentInvocationTarget = invocationTarget; 518 | } 519 | 520 | // shift zcap verify result for last zcap to the end of meta array if 521 | // necessary 522 | if(mustShift) { 523 | capabilityChainMeta.push(capabilityChainMeta.shift()); 524 | } 525 | 526 | return {verified: true}; 527 | } catch(error) { 528 | return {verified: false, error}; 529 | } 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export { 5 | CONTEXT as ZCAP_CONTEXT, 6 | CONTEXT_URL as ZCAP_CONTEXT_URL 7 | } from '@digitalbazaar/zcap-context'; 8 | 9 | export const CAPABILITY_VOCAB_URL = 'https://w3id.org/security#'; 10 | export const ZCAP_ROOT_PREFIX = 'urn:zcap:root:'; 11 | // 6 is probably more reasonable for Kevin Bacon reasons? but picking a 12 | // power of 10 13 | export const MAX_CHAIN_LENGTH = 10; 14 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import jsigs from 'jsonld-signatures'; 5 | 6 | /* Core API */ 7 | export {CapabilityInvocation} from './CapabilityInvocation.js'; 8 | export {CapabilityDelegation} from './CapabilityDelegation.js'; 9 | export {createRootCapability} from './utils.js'; 10 | import * as constants from './constants.js'; 11 | export {constants}; 12 | 13 | // enable external document loaders to extend an internal one that loads 14 | // ZCAP context(s) 15 | export function extendDocumentLoader(documentLoader) { 16 | return async function loadZcapContexts(url) { 17 | if(url === constants.ZCAP_CONTEXT_URL) { 18 | return { 19 | contextUrl: null, 20 | documentUrl: url, 21 | document: constants.ZCAP_CONTEXT, 22 | tag: 'static' 23 | }; 24 | } 25 | return documentLoader(url); 26 | }; 27 | } 28 | 29 | // default doc loader; only loads ZCAP and jsigs contexts 30 | export const documentLoader = extendDocumentLoader( 31 | jsigs.strictDocumentLoader); 32 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | MAX_CHAIN_LENGTH, ZCAP_CONTEXT_URL, ZCAP_ROOT_PREFIX 6 | } from './constants.js'; 7 | 8 | /** 9 | * Creates a root capability from a root controller and a root invocation 10 | * target. 11 | * 12 | * @param {object} options - The options. 13 | * @param {string|Array} options.controller - The root controller. 14 | * @param {string} options.invocationTarget - The root invocation target. 15 | * 16 | * @returns {object} The root capability. 17 | */ 18 | export function createRootCapability({controller, invocationTarget}) { 19 | return { 20 | '@context': ZCAP_CONTEXT_URL, 21 | id: `${ZCAP_ROOT_PREFIX}${encodeURIComponent(invocationTarget)}`, 22 | controller, 23 | invocationTarget 24 | }; 25 | } 26 | 27 | /** 28 | * Retrieves the controller(s) from a capability. 29 | * 30 | * @param {object} options - The options. 31 | * @param {object} options.capability - The authorization capability (zcap). 32 | * 33 | * @returns {Array} The controller(s) for the capability. 34 | */ 35 | export function getControllers({capability}) { 36 | const {controller} = capability; 37 | if(!controller) { 38 | throw new Error('Capability controller not found.'); 39 | } 40 | return Array.isArray(controller) ? controller : [controller]; 41 | } 42 | 43 | /** 44 | * Returns true if the given verification method is a controller (or is 45 | * controlled by a controller) of the given capability. 46 | * 47 | * @param {object} options - The options. 48 | * @param {object} options.capability - The authorization capability (zcap). 49 | * @param {object} options.verificationMethod - The verification method to 50 | * check. 51 | * 52 | * @returns {boolean} `true` if the controller matches, `false` if not. 53 | */ 54 | export function isController({capability, verificationMethod}) { 55 | const controllers = getControllers({capability}); 56 | return controllers.includes(verificationMethod.controller) || 57 | controllers.includes(verificationMethod.id); 58 | } 59 | 60 | /** 61 | * Retrieves the allowed actions from a capability. 62 | * 63 | * @param {object} options - The options. 64 | * @param {object} options.capability - The authorization capability (zcap). 65 | * 66 | * @returns {Array} Allowed actions. 67 | */ 68 | export function getAllowedActions({capability}) { 69 | const {allowedAction} = capability; 70 | if(!allowedAction) { 71 | return []; 72 | } 73 | if(Array.isArray(allowedAction)) { 74 | return allowedAction; 75 | } 76 | return [allowedAction]; 77 | } 78 | 79 | /** 80 | * Retrieves the target from a capability. 81 | * 82 | * @param {object} options - The options. 83 | * @param {object} options.capability - The authorization capability (zcap). 84 | * 85 | * @returns {string} - Capability target. 86 | */ 87 | export function getTarget({capability}) { 88 | // zcaps MUST have an `invocationTarget` that is a string 89 | return capability.invocationTarget; 90 | } 91 | 92 | /** 93 | * Retrieves the delegation proof(s) for a capability that is associated with 94 | * its parent capability. A capability that has no parent or no associated 95 | * delegation proofs will cause this function to return an empty array. 96 | * 97 | * @param {object} options - The options. 98 | * @param {object} options.capability - The authorization capability. 99 | * 100 | * @returns {object} Any `capabilityDelegation` proof objects attached to the 101 | * given capability. 102 | */ 103 | export function getDelegationProofs({capability}) { 104 | // capability is root or capability has no `proof`, then it has no relevant 105 | // delegation proofs 106 | if(!capability.parentCapability || !capability.proof) { 107 | return []; 108 | } 109 | let {proof} = capability; 110 | if(!Array.isArray(proof)) { 111 | proof = [proof]; 112 | } 113 | return proof.filter(p => p && p.proofPurpose === 'capabilityDelegation'); 114 | } 115 | 116 | /** 117 | * Gets the `capabilityChain` associated with the given capability. 118 | * 119 | * @param {object} options - The options. 120 | * @param {object} options.capability - The authorization capability. 121 | * 122 | * @returns {object} Any `capabilityDelegation` proof objects attached to the 123 | * given capability. 124 | */ 125 | export function getCapabilityChain({capability}) { 126 | if(!capability.parentCapability) { 127 | // root capability has no chain 128 | return []; 129 | } 130 | 131 | const proofs = getDelegationProofs({capability}); 132 | if(proofs.length !== 1) { 133 | throw new Error( 134 | 'Cannot get capability chain; capability is invalid; it is not the ' + 135 | 'root capability yet it does not have exactly one delegation proof.'); 136 | } 137 | 138 | const {capabilityChain} = proofs[0]; 139 | if(!(capabilityChain && Array.isArray(capabilityChain))) { 140 | throw new Error( 141 | 'Cannot get capability chain; capability is invalid; it does not have ' + 142 | 'a "capabilityChain" array in its delegation proof.'); 143 | } 144 | 145 | return capabilityChain.slice(); 146 | } 147 | 148 | /** 149 | * Determines if the given `invocationTarget` is valid given a 150 | * `baseInvocationTarget`. 151 | * 152 | * To check for a proper delegation, `invocationTarget` must be the child 153 | * capability's `invocationTarget` and `baseInvocationTarget` must be the 154 | * parent capability's `invocationTarget`. 155 | * 156 | * To check for a proper invocation, `invocationTarget` must be the value from 157 | * the invocation proof and `baseInvocationTarget` must be the invoked 158 | * capability's `invocationTarget`. 159 | * 160 | * @param {object} options - The options. 161 | * @param {string} options.invocationTarget - The invocation target to check. 162 | * @param {string} options.baseInvocationTarget - The base invocation target. 163 | * @param {boolean} options.allowTargetAttenuation - `true` to allow target 164 | * attenuation. 165 | * 166 | * @returns {boolean} `true` if the target is valid, `false` if not. 167 | */ 168 | export function isValidTarget({ 169 | invocationTarget, baseInvocationTarget, allowTargetAttenuation 170 | }) { 171 | // direct match, valid 172 | if(baseInvocationTarget === invocationTarget) { 173 | return true; 174 | } 175 | if(allowTargetAttenuation) { 176 | /* Note: When `allowTargetAttenuation=true`, a zcap can be invoked with 177 | a more narrow target and delegated zcap can have a different invocation 178 | target from its parent. Here we must ensure that the invocation target 179 | has a proper prefix relative to the base one we're comparing against. 180 | 181 | If the `baseInvocationTarget` already has a query (has `?`) then the 182 | suffix that follows it must start with `&`. Otherwise, it may start 183 | with either `/` or `?`. */ 184 | const prefixes = []; 185 | if(baseInvocationTarget.includes('?')) { 186 | // query already present in base invocation target, so only accept new 187 | // variables in the query 188 | prefixes.push(`${baseInvocationTarget}&`); 189 | } else { 190 | // accept path-based attenuation or new query-based attenuation 191 | prefixes.push(`${baseInvocationTarget}/`); 192 | prefixes.push(`${baseInvocationTarget}?`); 193 | } 194 | if(prefixes.some(prefix => invocationTarget.startsWith(prefix))) { 195 | return true; 196 | } 197 | } 198 | // not a match 199 | return false; 200 | } 201 | 202 | /** 203 | * Creates a capability chain for delegating a capability from the 204 | * given `parentCapability`. 205 | * 206 | * @param {object} options - The options. 207 | * @param {object} options.parentCapability - The parent capability from 208 | * which to compute the capability chain. 209 | * @param {boolean} options._skipLocalValidationForTesting - Private. 210 | * 211 | * @returns {Array} The computed capability chain for the capability to be 212 | * included in a capability delegation proof. 213 | */ 214 | export function computeCapabilityChain({ 215 | parentCapability, _skipLocalValidationForTesting 216 | }) { 217 | // if parent capability is root (string or no parent of its own) 218 | const type = typeof parentCapability; 219 | if(type === 'string') { 220 | return [parentCapability]; 221 | } 222 | if(!parentCapability.parentCapability) { 223 | // capability must be a root zcap 224 | checkCapability({capability: parentCapability, expectRoot: true}); 225 | return [parentCapability.id]; 226 | } 227 | 228 | // capability must be a delegated zcap, check it and get its chain 229 | checkCapability({capability: parentCapability, expectRoot: false}); 230 | const proofs = getDelegationProofs({capability: parentCapability}); 231 | if(proofs.length !== 1) { 232 | throw new Error( 233 | 'Cannot compute capability chain; parent capability is invalid; it is ' + 234 | 'not the root capability yet it does not have exactly one delegation ' + 235 | 'proof.'); 236 | } 237 | 238 | const {capabilityChain} = proofs[0]; 239 | if(!(capabilityChain && Array.isArray(capabilityChain))) { 240 | throw new Error( 241 | 'Cannot compute capability chain; parent capability is invalid; it ' + 242 | 'does not have a "capabilityChain" array in its delegation proof.'); 243 | } 244 | 245 | // validate parent capability chain to help prevent bad delegations 246 | if(!_skipLocalValidationForTesting) { 247 | // ensure that all `capabilityChain` entries except the last are strings 248 | const lastRequiredType = capabilityChain.length > 1 ? 249 | 'object' : 'string'; 250 | const lastIndex = capabilityChain.length - 1; 251 | for(const [i, entry] of capabilityChain.entries()) { 252 | const entryType = typeof entry; 253 | if(!((i === lastIndex && entryType === lastRequiredType) || 254 | i !== lastIndex && entryType === 'string')) { 255 | throw new TypeError( 256 | 'Cannot compute capability chain; parent capability chain is ' + 257 | 'invalid; it must consist of strings of capability IDs except ' + 258 | 'the last capability if it is delegated, in which case it must ' + 259 | 'be an object with an "id" property that is a string.'); 260 | } 261 | } 262 | } 263 | 264 | // if last zcap is embedded, change it to a reference 265 | const newChain = capabilityChain.slice(0, capabilityChain.length - 1); 266 | const last = capabilityChain[capabilityChain.length - 1]; 267 | if(typeof last === 'string') { 268 | newChain.push(last); 269 | } else { 270 | newChain.push(last.id); 271 | } 272 | newChain.push(parentCapability); 273 | 274 | // ensure new chain uses absolute URLs 275 | for(const entry of newChain) { 276 | if((typeof entry === 'string' && !entry.includes(':')) || 277 | typeof entry === 'object' && !entry.id.includes(':')) { 278 | throw new Error( 279 | 'Cannot compute capability chain; parent capability chain is ' + 280 | 'invalid because uses relative URL(s) in its capability chain.'); 281 | } 282 | } 283 | 284 | return newChain; 285 | } 286 | 287 | /** 288 | * Dereferences the capability chain associated with the given capability, 289 | * ensuring it passes a number of validation checks. 290 | * 291 | * A delegated zcap's chain has a reference to a root zcap. A verifier must 292 | * provide a hook (`getRootCapability`) to dereference this root zcap since 293 | * the root zcap has no delegation proof and must therefore be trusted by 294 | * the verifier. If the root zcap can't be dereferenced by the trusted hook, 295 | * then an authorization error must be thrown by that hook. 296 | * 297 | * This function will dereference the root zcap and then dereference all of 298 | * the embedded delegated zcaps from the chain, combining them into a single 299 | * array containing full zcaps ordered from root => tail. 300 | * 301 | * The dereferenced chain (result of this function) should then compare the 302 | * root zcap's ID against a list of expected root capabilities, throwing 303 | * an error if none of them match. Otherwise, the dereferenced chain should 304 | * then be processed to ensure that all delegation rules have been followed. 305 | * If checking an invocation, it should also be ensured that a combination of 306 | * an expected target and a root zcap is permitted (note it is conceivable that 307 | * a verifier may accept more than one combination, e.g., a target of `x` could 308 | * work with both root zcap `a` and `b`). 309 | * 310 | * @param {object} options - The options. 311 | * @param {string|object} options.capability - The authorization capability 312 | * (zcap) to get the chain for. 313 | * @param {Function} options.getRootCapability - A function for dereferencing 314 | * the root capability (the root zcap must be deref'd in a trusted way by the 315 | * verifier, it must not be untrusted input). 316 | * @param {number} [options.maxChainLength=10] - The maximum length of the 317 | * capability delegation chain (this is inclusive of `capability` itself). 318 | * 319 | * @returns {Promise} Resolves to `{dereferencedChain}`. 320 | */ 321 | export async function dereferenceCapabilityChain({ 322 | capability, getRootCapability, maxChainLength = MAX_CHAIN_LENGTH 323 | }) { 324 | // capability MUST be a string if it is root; root zcaps MUST always be 325 | // dereferenced via a trusted mechanism provided by the verifier as they 326 | // do not have delegation proofs 327 | if(typeof capability === 'string') { 328 | const id = capability; 329 | const {rootCapability} = await getRootCapability({id}); 330 | checkCapability({capability: rootCapability, expectRoot: true}); 331 | if(rootCapability.id !== id) { 332 | throw new Error( 333 | `Dereferenced root capability ID "${rootCapability.id}" does not ` + 334 | `match reference ID "${id}".`); 335 | } 336 | capability = rootCapability; 337 | } else { 338 | // ensure capability itself is valid 339 | checkCapability({capability, expectRoot: false}); 340 | } 341 | 342 | // get a mapping of IDs to full zcaps as the chain is validated 343 | const dereferencedChainMap = new Map(); 344 | 345 | // get the underef'd capability chain for the capability 346 | const capabilityChain = getCapabilityChain({capability}); 347 | 348 | // ensure capability chain length (add 1 to be inclusive of `capability`) 349 | // does not exceed max chain length; only check this once at the start 350 | // as it produces the most sensible error -- it is true that an embedded 351 | // zcap could go over the limit but this will be caught via a congruency 352 | // check on the length instead 353 | if((capabilityChain.length + 1) > maxChainLength) { 354 | throw new Error( 355 | 'The capability chain exceeds the maximum allowed length ' + 356 | `of ${maxChainLength}.`); 357 | } 358 | 359 | // subtract one from the max chain length to start to account for 360 | // `capability` which is not present in `capabilityChain` 361 | let firstPass = true; 362 | let requiredLength = capabilityChain.length; 363 | let currentCapability = capability; 364 | let currentCapabilityChain = capabilityChain; 365 | while(currentCapabilityChain.length > 0) { 366 | if(currentCapabilityChain.length !== requiredLength) { 367 | throw new Error('The capability chain length is incongruent.'); 368 | } 369 | 370 | // if `next.length > 1`, then its last entry is a delegated 371 | // capability and it MUST be fully embedded as an object; all other 372 | // entries MUST be strings 373 | const lastRequiredType = currentCapabilityChain.length > 1 ? 374 | 'object' : 'string'; 375 | 376 | // validate entries and dereference delegated zcaps 377 | const lastIndex = currentCapabilityChain.length - 1; 378 | for(const [i, entry] of currentCapabilityChain.entries()) { 379 | const entryType = typeof entry; 380 | const entryIsString = entryType === 'string'; 381 | const requiredType = i === lastIndex ? lastRequiredType : 'string'; 382 | 383 | // ensure entry is the required type and, if it is an object, its `id` 384 | // is a string 385 | if(!(entryType === requiredType && 386 | (entryIsString || typeof entry.id === 'string'))) { 387 | throw new TypeError( 388 | 'Capability chain is invalid; it must consist of strings ' + 389 | 'of capability IDs except the last capability if it is ' + 390 | 'delegated, in which case it must be an object with an "id" ' + 391 | 'property that is a string.'); 392 | } 393 | 394 | // ensure capability ID expresses an absolute URI (i.e., it has `:`) 395 | const id = entryIsString ? entry : entry.id; 396 | if(!id.includes(':')) { 397 | throw new Error( 398 | 'Capability chain is invalid; it contains a capability ID ' + 399 | 'that is not an absolute URI.'); 400 | } 401 | 402 | // ensure last entry in chain matches parent capability 403 | if(i === lastIndex && currentCapability.parentCapability && 404 | currentCapability.parentCapability !== id) { 405 | throw new Error( 406 | 'Capability chain is invalid; the last entry does not ' + 407 | 'match the parent capability.'); 408 | } 409 | 410 | if(!entryIsString) { 411 | // check zcap data model 412 | checkCapability({capability: entry, expectRoot: i === 0}); 413 | } 414 | 415 | // ensure no cycles in the capability chain 416 | if(firstPass) { 417 | // on the first pass, the zcap must not have been seen yet 418 | if(id === capability.id || dereferencedChainMap.has(id)) { 419 | throw new Error('The capability chain contains a cycle.'); 420 | } 421 | // add zcap to the map whether it is only a reference (an ID) or 422 | // a fully embedded zcap; this will be used to ensure no additional 423 | // zcaps are added to the chain 424 | dereferencedChainMap.set(id, entry); 425 | } else { 426 | // on non-first pass, every ID should already be in the zcap map 427 | // and they should all be strings, not objects 428 | const existing = dereferencedChainMap.get(id); 429 | if(!existing) { 430 | // the chain is inconsistent across delegated zcaps 431 | throw new Error('The capability chain is inconsistent.'); 432 | } 433 | if(id === capability.id || typeof existing === 'object') { 434 | // the zcap has been deferenced before, there's a cycle 435 | throw new Error('The capability chain contains a cycle.'); 436 | } 437 | 438 | // only update the zcaps map using a fully embedded zcap 439 | if(!entryIsString) { 440 | dereferencedChainMap.set(id, entry); 441 | } 442 | } 443 | } 444 | 445 | // if the chain has more than the root zcap, loop to process the 446 | // next chain from the last delegated zcap 447 | if(currentCapabilityChain.length > 1) { 448 | // next chain must be 1 shorter than the current one 449 | requiredLength--; 450 | currentCapability = currentCapabilityChain[ 451 | currentCapabilityChain.length - 1]; 452 | currentCapabilityChain = getCapabilityChain( 453 | {capability: currentCapability}); 454 | } else { 455 | // no more chains to check 456 | break; 457 | } 458 | 459 | firstPass = false; 460 | } 461 | 462 | // dereference root zcap via provided trusted `getRootCapability` function 463 | if(capabilityChain.length > 0) { 464 | const [id] = capabilityChain; 465 | const {rootCapability} = await getRootCapability({id}); 466 | checkCapability({capability: rootCapability, expectRoot: true}); 467 | if(rootCapability.id !== id) { 468 | throw new Error( 469 | `Dereferenced root capability ID "${rootCapability.id}" does not ` + 470 | `match reference ID "${id}" from capability chain.`); 471 | } 472 | dereferencedChainMap.set(id, rootCapability); 473 | } 474 | 475 | // include `capability` in dereferenced map 476 | dereferencedChainMap.set(capability.id, capability); 477 | const dereferencedChain = [...dereferencedChainMap.values()]; 478 | 479 | return {dereferencedChain}; 480 | } 481 | 482 | export function checkProofContext({proof}) { 483 | // zcap context can appear anywhere in the array as it *is* protected 484 | const {'@context': ctx} = proof; 485 | if(!((Array.isArray(ctx) && ctx.includes(ZCAP_CONTEXT_URL)) || 486 | ctx === ZCAP_CONTEXT_URL)) { 487 | throw new Error( 488 | `Missing required capability proof context ("${ZCAP_CONTEXT_URL}").`); 489 | } 490 | } 491 | 492 | export function hasValidAllowedAction({allowedAction, parentAllowedAction}) { 493 | // if the parent's `allowedAction` is `undefined`, then any more restrictive 494 | // action is allowed in the child 495 | if(!parentAllowedAction) { 496 | return true; 497 | } 498 | 499 | if(Array.isArray(parentAllowedAction)) { 500 | // parent's `allowedAction` must include every one from child's 501 | if(Array.isArray(allowedAction)) { 502 | return allowedAction.every(a => parentAllowedAction.includes(a)); 503 | } 504 | return parentAllowedAction.includes(allowedAction); 505 | } 506 | 507 | // require exact match 508 | return (parentAllowedAction === allowedAction); 509 | } 510 | 511 | export function checkCapability({capability, expectRoot}) { 512 | const { 513 | '@context': context, 514 | id, parentCapability, invocationTarget, allowedAction, expires 515 | } = capability; 516 | 517 | const isRoot = parentCapability === undefined; 518 | if(isRoot) { 519 | if(context !== ZCAP_CONTEXT_URL) { 520 | throw new Error( 521 | 'Root capability must have an "@context" value of ' + 522 | `"${ZCAP_CONTEXT_URL}".`); 523 | } 524 | if(capability.expires !== undefined) { 525 | throw new Error('Root capability must not have an "expires" field.'); 526 | } 527 | } else { 528 | if(!((Array.isArray(context) && context[0] === ZCAP_CONTEXT_URL))) { 529 | throw new Error( 530 | 'Delegated capability must have an "@context" array ' + 531 | `with "${ZCAP_CONTEXT_URL}" in its first position.`); 532 | } 533 | if(!(typeof parentCapability === 'string' && 534 | parentCapability.includes(':'))) { 535 | throw new Error( 536 | 'Delegated capability must have a "parentCapability" with a string ' + 537 | 'value that expresses an absolute URI.'); 538 | } 539 | const [proof] = getDelegationProofs({capability}); 540 | if(!proof) { 541 | throw new Error('Delegated capability must have a "proof".'); 542 | } 543 | if(isNaN(Date.parse(proof.created))) { 544 | throw new Error( 545 | 'Delegated capability must have a valid proof "created" date.'); 546 | } 547 | if(isNaN(Date.parse(expires))) { 548 | throw new Error('Delegated capability must have a valid expires date.'); 549 | } 550 | } 551 | 552 | if(!(typeof id === 'string' && id.includes(':'))) { 553 | throw new Error( 554 | 'Capability must have an "id" with a string value that expresses an ' + 555 | 'absolute URI.'); 556 | } 557 | if(!(typeof invocationTarget === 'string' && 558 | invocationTarget.includes(':'))) { 559 | throw new Error( 560 | 'Capability must have an "invocationTarget" with a string value that ' + 561 | 'expresses an absolute URI.'); 562 | } 563 | if(allowedAction !== undefined && !( 564 | typeof allowedAction === 'string' || 565 | (Array.isArray(allowedAction) && allowedAction.length > 0))) { 566 | throw new Error( 567 | 'If present on a capability, "allowedAction" must be a string or a ' + 568 | 'non-empty array.'); 569 | } 570 | 571 | if(isRoot !== expectRoot) { 572 | if(expectRoot) { 573 | throw new Error( 574 | `Expected capability "${capability.id}" to be root ` + 575 | 'but it is delegated.'); 576 | } 577 | throw new Error( 578 | `Expected capability "${capability.id}" to be delegated but it is root.`); 579 | } 580 | } 581 | 582 | export function compareTime({t1, t2, maxClockSkew}) { 583 | // `maxClockSkew` is in seconds, so transform to milliseconds 584 | if(Math.abs(t1 - t2) < (maxClockSkew * 1000)) { 585 | // times are equal within the max clock skew 586 | return 0; 587 | } 588 | return t1 < t2 ? -1 : 1; 589 | } 590 | 591 | // documentation typedefs 592 | 593 | /** 594 | * A inspection function result. 595 | * 596 | * @typedef {object} InspectResult 597 | */ 598 | 599 | /** 600 | * A capability chain inspection function. 601 | * 602 | * @typedef {Function} InspectCapabilityChain 603 | * @param {CapabilityChainDetails} 604 | * @returns {InspectResult} 605 | */ 606 | 607 | /** 608 | * A capability to inspect. The capability is compacted into the security 609 | * context. Only the required fields are shown here, a capability will contain 610 | * additional properties. 611 | * 612 | * @typedef {object} Capability 613 | * @property {string} id - The ID of the capability. 614 | * @property {string} controller - The controller of the capability. 615 | */ 616 | 617 | /** 618 | * The capability to inspect. 619 | * 620 | * @typedef {object} CapabilityChainDetails 621 | * @property {Capability[]} capabilityChain - The capabilities in the chain. 622 | * @property {CapabilityMeta[]} capabilityChainMeta - The results returned 623 | * from jsonld-signatures verify for each capability in the chain. Each 624 | * object contains `{verifyResult}` where each `verifyResult` is an 625 | * `InspectChainResult`. 626 | */ 627 | 628 | /** 629 | * The meta data resulting from the verification of a delegated capability. 630 | * 631 | * @typedef {object} CapabilityMeta 632 | * @property {VerifyResult} verifyResult - The capability verify result, which 633 | * is `null` for the root capability. 634 | */ 635 | 636 | /** 637 | * The result of running jsonld-signature's verify method. 638 | * 639 | * @typedef {object} VerifyResult 640 | * @property {boolean} verified - `true` if all the checked proofs were 641 | * successfully verified. 642 | * @property {VerifyProofResult[]} results - The verify results for each 643 | * delegation proof. 644 | */ 645 | 646 | /** 647 | * The result of verifying a capability delegation proof. 648 | * 649 | * @typedef {object} VerifyProofResult 650 | * @property {VerifyProofPurposeResult} proofPurposeResult - The result from 651 | * verifying the capability delegation proof purpose. 652 | */ 653 | 654 | /** 655 | * The result of verifying a capability delegation proof purpose. 656 | * 657 | * @typedef {object} VerifyProofPurposeResult 658 | * @property {string} delegator - The party that created the capability 659 | * delegation proof, i.e., the party that delegated the capability. 660 | */ 661 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digitalbazaar/zcap", 3 | "version": "9.0.2-0", 4 | "description": "Authorization Capabilities reference implementation.", 5 | "homepage": "https://github.com/digitalbazaar/zcap", 6 | "author": { 7 | "name": "Digital Bazaar, Inc.", 8 | "email": "support@digitalbazaar.com", 9 | "url": "https://digitalbazaar.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/digitalbazaar/zcap" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/digitalbazaar/zcap/issues/" 17 | }, 18 | "license": "BSD-3-Clause", 19 | "type": "module", 20 | "exports": "./lib/index.js", 21 | "files": [ 22 | "lib/**/*.js" 23 | ], 24 | "dependencies": { 25 | "@digitalbazaar/zcap-context": "^2.0.0", 26 | "jsonld-signatures": "^11.0.0" 27 | }, 28 | "devDependencies": { 29 | "@digitalbazaar/ed25519-signature-2020": "^5.0.0", 30 | "@digitalbazaar/ed25519-verification-key-2020": "^4.0.0", 31 | "c8": "^9.1.0", 32 | "chai": "^4.3.6", 33 | "cross-env": "^7.0.3", 34 | "eslint": "^8.17.0", 35 | "eslint-config-digitalbazaar": "^5.0.1", 36 | "eslint-plugin-jsdoc": "^48.2.2", 37 | "eslint-plugin-unicorn": "^51.0.1", 38 | "karma": "^6.3.20", 39 | "karma-chrome-launcher": "^3.1.1", 40 | "karma-mocha": "^2.0.1", 41 | "karma-mocha-reporter": "^2.2.5", 42 | "karma-sourcemap-loader": "^0.4.0", 43 | "karma-webpack": "^5.0.0", 44 | "mocha": "^10.0.0", 45 | "mocha-lcov-reporter": "^1.3.0", 46 | "webpack": "^5.73.0" 47 | }, 48 | "scripts": { 49 | "test": "npm run test-node", 50 | "__test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks --delay -t 30000 -A -R ${REPORTER:-spec} tests/test.js", 51 | "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} tests/test.js", 52 | "test-karma": "cross-env NODE_ENV=test karma start karma.conf.cjs", 53 | "coverage": "cross-env NODE_ENV=test c8 npm run test-node", 54 | "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node", 55 | "coverage-report": "c8 report", 56 | "lint": "eslint ." 57 | }, 58 | "c8": { 59 | "reporter": [ 60 | "lcov", 61 | "text-summary", 62 | "text" 63 | ] 64 | }, 65 | "engines": { 66 | "node": ">=18" 67 | }, 68 | "keywords": [ 69 | "Authorization Capability", 70 | "Authorization Capabilities", 71 | "JSON", 72 | "JSON-LD", 73 | "Linked Data", 74 | "OCAP", 75 | "OCAP-LD", 76 | "Semantic Web", 77 | "ZCAP", 78 | "ZCAP-LD", 79 | "digital signatures", 80 | "object capabilities" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /tests/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export class Controller { 5 | constructor(doc) { 6 | // doc is the key controller document 7 | this.doc = doc; 8 | } 9 | 10 | id() { 11 | return this.doc.id; 12 | } 13 | 14 | get(keyType, index) { 15 | const vm = this.doc[keyType][index]; 16 | if(typeof vm === 'string') { 17 | // dereference verification method 18 | return this.doc.verificationMethod.find(({id}) => id === vm); 19 | } 20 | return vm; 21 | } 22 | } 23 | 24 | /* eslint-disable */ 25 | const b = a=>a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b); 26 | export function uuid() { 27 | return `urn:uuid:${b()}`; 28 | } 29 | /* eslint-enable */ 30 | -------------------------------------------------------------------------------- /tests/mock-data.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as zcap from '../lib/index.js'; 5 | const {constants: {ZCAP_CONTEXT_URL}} = zcap; 6 | 7 | export const capabilities = {}; 8 | export const didDocs = {}; 9 | export const privateDidDocs = {}; 10 | export const controllers = {}; 11 | const _loaderData = new Map(); 12 | 13 | const KEY_TYPES = [ 14 | 'capabilityDelegation', 'capabilityInvocation', 'verificationMethod' 15 | ]; 16 | 17 | export {default as exampleDoc} from './mock-documents/example-doc.js'; 18 | export const exampleDocWithInvocation = {}; 19 | 20 | import exampleDocWithInvocation_alpha from 21 | './mock-documents/example-doc-with-alpha-invocation.js'; 22 | exampleDocWithInvocation.alpha = exampleDocWithInvocation_alpha; 23 | import exampleDocWithInvocation_beta from 24 | './mock-documents/example-doc-with-beta-invocation.js'; 25 | exampleDocWithInvocation.beta = exampleDocWithInvocation_beta; 26 | 27 | import didContext from './mock-documents/did-context.js'; 28 | _loaderData.set('https://www.w3.org/ns/did/v1', didContext); 29 | 30 | import v1Context from './mock-documents/veres-one-context.js'; 31 | _loaderData.set('https://w3id.org/veres-one/v1', v1Context); 32 | 33 | import {suiteContext} from '@digitalbazaar/ed25519-signature-2020'; 34 | _loaderData.set(suiteContext.CONTEXT_URL, suiteContext.CONTEXT); 35 | 36 | import controllers_alice from './mock-documents/ed25519-alice-keys.js'; 37 | controllers.alice = controllers_alice; 38 | import controllers_bob from './mock-documents/ed25519-bob-keys.js'; 39 | controllers.bob = controllers_bob; 40 | import controllers_carol from './mock-documents/ed25519-carol-keys.js'; 41 | controllers.carol = controllers_carol; 42 | import controllers_diana from './mock-documents/ed25519-diana-keys.js'; 43 | controllers.diana = controllers_diana; 44 | 45 | import privateDidDocs_alpha from './mock-documents/did-doc-alpha.js'; 46 | privateDidDocs.alpha = privateDidDocs_alpha; 47 | import privateDidDocs_beta from './mock-documents/did-doc-beta.js'; 48 | privateDidDocs.beta = privateDidDocs_beta; 49 | import privateDidDocs_gamma from './mock-documents/did-doc-gamma.js'; 50 | privateDidDocs.gamma = privateDidDocs_gamma; 51 | import privateDidDocs_delta from './mock-documents/did-doc-delta.js'; 52 | privateDidDocs.delta = privateDidDocs_delta; 53 | 54 | didDocs.alpha = _stripPrivateKeys(privateDidDocs.alpha); 55 | didDocs.beta = _stripPrivateKeys(privateDidDocs.beta); 56 | didDocs.gamma = _stripPrivateKeys(privateDidDocs.gamma); 57 | didDocs.delta = _stripPrivateKeys(privateDidDocs.delta); 58 | 59 | capabilities.root = {}; 60 | 61 | // keys as controller 62 | capabilities.root.alpha = { 63 | '@context': ZCAP_CONTEXT_URL, 64 | id: 'https://example.org/alice/caps#1', 65 | controller: 'https://example.com/i/alice/keys/1', 66 | invocationTarget: 'https://example.org/alice/targets/alpha' 67 | }; 68 | capabilities.root.beta = { 69 | '@context': ZCAP_CONTEXT_URL, 70 | id: 'https://example.org/alice/caps#0', 71 | controller: controllers.alice.id, 72 | invocationTarget: 'https://example.org/alice/targets/beta' 73 | }; 74 | 75 | capabilities.root.restful = { 76 | '@context': ZCAP_CONTEXT_URL, 77 | id: `urn:zcap:root:${encodeURIComponent('https://zcap.example')}`, 78 | controller: controllers.alice.id, 79 | invocationTarget: 'https://zcap.example' 80 | }; 81 | 82 | capabilities.delegated = {}; 83 | import capabilities_delegated_alpha from 84 | './mock-documents/delegated-zcap-alpha.js'; 85 | capabilities.delegated.alpha = capabilities_delegated_alpha; 86 | import capabilities_delegated_beta from 87 | './mock-documents/delegated-zcap-beta.js'; 88 | capabilities.delegated.beta = capabilities_delegated_beta; 89 | 90 | // generate a flattened list of all keys 91 | const keyList = [].concat( 92 | ...Object.values(controllers).map(_getKeysWithContext), 93 | ...Object.values(privateDidDocs).map(_getKeysWithContext)); 94 | 95 | export function addToLoader({doc}) { 96 | if(_loaderData.has(doc.id)) { 97 | throw new Error( 98 | `ID of document has already been registered in the loader: ${doc.id}`); 99 | } 100 | _loaderData.set(doc.id, doc); 101 | } 102 | 103 | export const testLoader = zcap.extendDocumentLoader(async url => { 104 | const document = _loaderData.get(url); 105 | if(document !== undefined) { 106 | return { 107 | contextUrl: null, 108 | document, 109 | documentUrl: url 110 | }; 111 | } 112 | throw new Error(`Document "${url}" not found.`); 113 | }); 114 | 115 | function _stripPrivateKeys(privateControllerDoc) { 116 | // clone the doc 117 | const publicControllerDoc = JSON.parse(JSON.stringify(privateControllerDoc)); 118 | const verificationRelationships = [ 119 | 'verificationMethod', 120 | 'authentication', 121 | 'capabilityDelegation', 122 | 'capabilityInvocation' 123 | ]; 124 | for(const vr of verificationRelationships) { 125 | if(Array.isArray(publicControllerDoc[vr])) { 126 | for(const vm of publicControllerDoc[vr]) { 127 | if(typeof vm === 'string') { 128 | continue; 129 | } 130 | delete vm.privateKeyMultibase; 131 | } 132 | } 133 | } 134 | return publicControllerDoc; 135 | } 136 | 137 | const docsForLoader = [ 138 | _stripPrivateKeys(controllers.alice), 139 | _stripPrivateKeys(controllers.bob), 140 | _stripPrivateKeys(controllers.carol), 141 | _stripPrivateKeys(controllers.diana), 142 | didDocs.alpha, 143 | didDocs.beta, 144 | didDocs.gamma, 145 | didDocs.delta, 146 | capabilities.root.alpha, 147 | capabilities.root.beta, 148 | capabilities.root.restful, 149 | ...keyList 150 | ]; 151 | 152 | docsForLoader.map(doc => addToLoader({doc})); 153 | 154 | function _getKeysWithContext(doc) { 155 | const keys = []; 156 | for(const keyType of KEY_TYPES) { 157 | keys.push(...(doc[keyType] || []) 158 | .filter(k => typeof k !== 'string') 159 | .map(k => ({'@context': doc['@context'], ...k}))); 160 | } 161 | return keys; 162 | } 163 | -------------------------------------------------------------------------------- /tests/mock-documents/delegated-zcap-alpha.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/zcap/v1", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "urn:uuid:055f47a4-61d3-11ec-9144-10bf48838a41", 7 | "parentCapability": "https://example.org/alice/caps#1", 8 | "controller": "https://example.com/i/bob/keys/1", 9 | "invocationTarget": "https://example.org/alice/targets/alpha", 10 | "expires": "3000-01-01T00:01Z", 11 | "proof": { 12 | "type": "Ed25519Signature2020", 13 | "created": "2018-02-13T21:26:08Z", 14 | "capabilityChain": [ 15 | "https://example.org/alice/caps#1" 16 | ], 17 | "proofPurpose": "capabilityDelegation", 18 | "proofValue": "z4Hm6e5ziMoiG2eWpRyB1ozrnh65gikaVAZzkXpMUNFarzouKNYYXCc4YqLZch12JgcfCqpSmgYfV6JXL8FSyC4pW", 19 | "verificationMethod": "https://example.com/i/alice/keys/1" 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tests/mock-documents/delegated-zcap-beta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/zcap/v1", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "urn:uuid:710910c8-61e4-11ec-8739-10bf48838a41", 7 | "parentCapability": "https://example.org/alice/caps#0", 8 | "controller": "https://example.com/i/bob", 9 | "expires": "3000-01-01T00:01Z", 10 | "invocationTarget": "https://example.org/alice/targets/beta", 11 | "proof": { 12 | "type": "Ed25519Signature2020", 13 | "created": "2018-02-13T21:26:08Z", 14 | "capabilityChain": [ 15 | "https://example.org/alice/caps#0" 16 | ], 17 | "proofPurpose": "capabilityDelegation", 18 | "proofValue": "z5tBRPbzifdC69CWhF2Y9UZ3KCXDuRHG4GqVjMWf2nCZG6XCUXoiDV75Afy93wQQC8sQtYxmwfhzW5bAeaKjLJuH4", 19 | "verificationMethod": "https://example.com/i/alice/keys/1" 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /tests/mock-documents/did-context.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": { 3 | "@protected": true, 4 | "id": "@id", 5 | "type": "@type", 6 | 7 | "alsoKnownAs": { 8 | "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", 9 | "@type": "@id" 10 | }, 11 | "assertionMethod": { 12 | "@id": "https://w3id.org/security#assertionMethod", 13 | "@type": "@id", 14 | "@container": "@set" 15 | }, 16 | "authentication": { 17 | "@id": "https://w3id.org/security#authenticationMethod", 18 | "@type": "@id", 19 | "@container": "@set" 20 | }, 21 | "capabilityDelegation": { 22 | "@id": "https://w3id.org/security#capabilityDelegationMethod", 23 | "@type": "@id", 24 | "@container": "@set" 25 | }, 26 | "capabilityInvocation": { 27 | "@id": "https://w3id.org/security#capabilityInvocationMethod", 28 | "@type": "@id", 29 | "@container": "@set" 30 | }, 31 | "controller": { 32 | "@id": "https://w3id.org/security#controller", 33 | "@type": "@id" 34 | }, 35 | "keyAgreement": { 36 | "@id": "https://w3id.org/security#keyAgreementMethod", 37 | "@type": "@id", 38 | "@container": "@set" 39 | }, 40 | "service": { 41 | "@id": "https://www.w3.org/ns/did#service", 42 | "@type": "@id", 43 | "@context": { 44 | "@protected": true, 45 | "id": "@id", 46 | "type": "@type", 47 | "serviceEndpoint": { 48 | "@id": "https://www.w3.org/ns/did#serviceEndpoint", 49 | "@type": "@id" 50 | } 51 | } 52 | }, 53 | "verificationMethod": { 54 | "@id": "https://w3id.org/security#verificationMethod", 55 | "@type": "@id" 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /tests/mock-documents/did-doc-alpha.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://www.w3.org/ns/did/v1", 4 | "https://w3id.org/veres-one/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa", 8 | "authentication": [ 9 | { 10 | "id": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa#authn-key-1", 11 | "type": "Ed25519VerificationKey2020", 12 | "controller": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa", 13 | "publicKeyMultibase": "z6MkmiaTWH2pqNm3uTGvfFAYyH5rFoZzkfLksiTJM2KDRmzx", 14 | "privateKeyMultibase": "zrv2dDe2MjWQDiF8qhFk6z34MguMND8cpzP5XbUJAGSfJKTJqvNewtstT7voohUuRzEdBQo6WpUrNZhZvhJRwu4EQSt" 15 | } 16 | ], 17 | "capabilityDelegation": [ 18 | { 19 | "id": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa#ocap-grant-key-1", 20 | "type": "Ed25519VerificationKey2020", 21 | "controller": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa", 22 | "publicKeyMultibase": "z6MksYPyfcP7u6J7tTdTzRNMnFcfH21TVCip3QxUsVGifMra", 23 | "privateKeyMultibase": "zrv4PPrdSfNfPLfVgQgsWFexMG7xQDk3GqF8JUCTrVB788MSMn841mGgQCHSPT5yypkkhm2mKgUK51cQSwSBkBaaNbC" 24 | } 25 | ], 26 | "capabilityInvocation": [ 27 | { 28 | "id": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa#ocap-invoke-key-1", 29 | "type": "Ed25519VerificationKey2020", 30 | "controller": "did:v1:test:nym:8GKQv2nPVqGanxSDygCi8BXrSEJ9Ln6QBhYNWkMCWZDa", 31 | "publicKeyMultibase": "z6MkiKziMaqh1sD2akDsUirbCvY27AJYLsDMTHGiQZGb6u96", 32 | "privateKeyMultibase": "zrv4PqrmzrfMGordnQkbqjhe3MRdMhjvFzw9q7DCeJtz9aj6FyH6SfY1RYuFYi5bnTq8D2pRzdkxhJuJqpNnokAVhx8" 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tests/mock-documents/did-doc-beta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://www.w3.org/ns/did/v1", 4 | "https://w3id.org/veres-one/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo", 8 | "authentication": [ 9 | { 10 | "id": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo#authn-key-1", 11 | "type": "Ed25519VerificationKey2020", 12 | "controller": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo", 13 | "publicKeyMultibase": "z6MksbkXcLEzwTmHfA1ZDujnHJPSq9eb46Z7vewEwMeXbv1B", 14 | "privateKeyMultibase": "zrv5YnG22UftQGBRhtyf2HEv6d2ppzjvmTowUeC7gEmCYsq6YQRtrd2WtJ66rwfwC77Xx8QDUsJn77xk2kw7YHGhvQu" 15 | } 16 | ], 17 | "capabilityDelegation": [ 18 | { 19 | "id": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo#ocap-grant-key-1", 20 | "type": "Ed25519VerificationKey2020", 21 | "controller": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo", 22 | "publicKeyMultibase": "z6MkhhTUjCpwGrPDf9CjxWGPbEbY9yTFWT43dsyLrUGYJByb", 23 | "privateKeyMultibase": "zrv5VJkR36AqR3E91J8iGEV7fjA9rsbuxcCsXiqwW6xxtQsXSStDm3NsZgwePZkq9chiJWdtJyYdpPEgCEnHQ1H1oKf" 24 | } 25 | ], 26 | "capabilityInvocation": [ 27 | { 28 | "id": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo#ocap-invoke-key-1", 29 | "type": "Ed25519VerificationKey2020", 30 | "controller": "did:v1:test:nym:E9VV25zZbvGpYfArYLmwSCqT1aNjeDJmEe2K75gWghDo", 31 | "publicKeyMultibase": "z6MknMbFPAtXNiWgG3bgFHyeKv4eAQ37KVyHK1sSdskvUMzb", 32 | "privateKeyMultibase": "zrv2dZ4knZuQhKZU6vhmmVK6QimUA85zGLKkdfqfxJRPWfRnLfBv9Yp37MTe1W5HssEGsB1ou5gMhH7o6jKkkicyfRm" 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tests/mock-documents/did-doc-delta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://www.w3.org/ns/did/v1", 4 | "https://w3id.org/veres-one/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88", 8 | "authentication": [ 9 | { 10 | "id": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88#authn-key-1", 11 | "type": "Ed25519VerificationKey2020", 12 | "controller": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88", 13 | "publicKeyMultibase": "z6MkoB4hvk5Kjidf43tpkXR8T2Vfmp3DS5Dj79cNMVzvd1uW", 14 | "privateKeyMultibase": "zrv2mUWBfwDx14WijJBV5BwegzWozbktapTBouz3f8NE5mxnPsSpKeYLji2HqYHe6oTiDHgh3atgPt1GcY66Mvqqatc" 15 | } 16 | ], 17 | "capabilityDelegation": [ 18 | { 19 | "id": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88#ocap-grant-key-1", 20 | "type": "Ed25519VerificationKey2020", 21 | "controller": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88", 22 | "publicKeyMultibase": "z6MkihK2sj6aJMzTYWLpZqUJ5m8yR2gRBNZ7bPByyp6AnCWW", 23 | "privateKeyMultibase": "zrv2mw9unbzbjpVgwvJ985qSJTK8vWKTuoA3UmBYkzkHQKTnHrVaChuoQwKWNApBrnfoA5RKjtKJksBAqLdByFmCseJ" 24 | } 25 | ], 26 | "capabilityInvocation": [ 27 | { 28 | "id": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88#ocap-invoke-key-1", 29 | "type": "Ed25519VerificationKey2020", 30 | "controller": "did:v1:test:nym:9iofLVptQB9BwZ484xTHbvwfxEmN2ByNR8hSXE2uho88", 31 | "publicKeyMultibase": "z6Mkkd2BQPJswFW53GBgJFz6E1jdfp9q7UyCJDoSFNxubFVB", 32 | "privateKeyMultibase": "zrv3WPqSabzGxzDSN2KYyifcd5iXGyG4ag1Dg51cy2Co4vqJLCDyoMyNVUDcBrfETVFK1ECJJzJASWnifPSgGkpfbDB" 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tests/mock-documents/did-doc-gamma.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://www.w3.org/ns/did/v1", 4 | "https://w3id.org/veres-one/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC", 8 | "authentication": [ 9 | { 10 | "id": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC#authn-key-1", 11 | "type": "Ed25519VerificationKey2020", 12 | "controller": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC", 13 | "publicKeyMultibase": "z6MkoCzCjDsKLdyU3KJPfMA53ZTP4P5rC27PnGsMnkGw2BUa", 14 | "privateKeyMultibase": "zrv5BmMMagZRgCLRpNCDDMZo2fTeVWGQaWvMKPcbf94EFkME4pe4iHaxndihGEBdxty2wcw3ybjK78Wx1hXWqSJw6a2" 15 | } 16 | ], 17 | "capabilityDelegation": [ 18 | { 19 | "id": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC#ocap-grant-key-1", 20 | "type": "Ed25519VerificationKey2020", 21 | "controller": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC", 22 | "publicKeyMultibase": "z6Mkg8S2GjDqPuHRfWKgB4Zm98xH4Sg2WZEHuUiZtcZbmGV5", 23 | "privateKeyMultibase": "zrv1EMJ9Le5oDh8XeJFBqFfQNJmuiNMm8urPqg2WnBYyUxyZrsa77GUXEKBx9rNXQ6hL7yhXxTbGT8tkvCiBw4T87pb" 24 | } 25 | ], 26 | "capabilityInvocation": [ 27 | { 28 | "id": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC#ocap-invoke-key-1", 29 | "type": "Ed25519VerificationKey2020", 30 | "controller": "did:v1:test:nym:9kjA8yct16UzvpTgynCECTuPEoozn8s36FxRxUJv6xhC", 31 | "publicKeyMultibase": "z6MkswgjiTgmzABJu5wtqFrmnNGVJFv551y9HeF5NzmL4GQo", 32 | "privateKeyMultibase": "zrv1sM86RbW2KEbWhVHawGW56aqSM7ewBzhzEU9bq4aKvxSuuv9QEB1rL7E8cNNHYhKQapodxwRNxptiBxxKjvYbjxj" 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /tests/mock-documents/ed25519-alice-keys.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "https://example.com/i/alice", 7 | "verificationMethod": [ 8 | { 9 | "id": "https://example.com/i/alice/keys/1", 10 | "type": "Ed25519VerificationKey2020", 11 | "controller": "https://example.com/i/alice", 12 | "publicKeyMultibase": "z6MkvRsV39xVQc8HevAQwCqEw18DwrEtzVLz8NJY15NtfMmD", 13 | "privateKeyMultibase": "zrv2zfksN9F1MiYpTVoLjZKks7UArP87c1dKbdkzXkMTwtoAYadt7ozJEVZwQpcroQruoLxY7kESHpnVyJ1a6bbSVK9" 14 | } 15 | ], 16 | "capabilityInvocation": [ 17 | "https://example.com/i/alice/keys/1" 18 | ], 19 | "capabilityDelegation": [ 20 | "https://example.com/i/alice/keys/1" 21 | ] 22 | }; 23 | -------------------------------------------------------------------------------- /tests/mock-documents/ed25519-bob-keys.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "https://example.com/i/bob", 7 | "capabilityInvocation": [ 8 | { 9 | "id": "https://example.com/i/bob/keys/1", 10 | "type": "Ed25519VerificationKey2020", 11 | "controller": "https://example.com/i/bob", 12 | "publicKeyMultibase": "z6MkqyrirHAq8Acicq1FyNJGd9R7D1DW7Q8A3v1qqZfP4pdY", 13 | "privateKeyMultibase": "zrv2yZunr7Cz1cZTSQgJPZ35CQaSiJt5iN3Ah4JCND7cBAzyrfRaiE4HhFsUtcSadaf7f9qMDH9rXVutKr12LGxgXPY" 14 | } 15 | ], 16 | "capabilityDelegation": [ 17 | { 18 | "id": "https://example.com/i/bob/keys/2", 19 | "type": "Ed25519VerificationKey2020", 20 | "controller": "https://example.com/i/bob", 21 | "publicKeyMultibase": "z6MkqyrirHAq8Acicq1FyNJGd9R7D1DW7Q8A3v1qqZfP4pdY", 22 | "privateKeyMultibase": "zrv2yZunr7Cz1cZTSQgJPZ35CQaSiJt5iN3Ah4JCND7cBAzyrfRaiE4HhFsUtcSadaf7f9qMDH9rXVutKr12LGxgXPY" 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /tests/mock-documents/ed25519-carol-keys.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "https://example.com/i/carol", 7 | "capabilityInvocation": [ 8 | { 9 | "id": "https://example.com/i/carol/keys/1", 10 | "type": "Ed25519VerificationKey2020", 11 | "controller": "https://example.com/i/carol", 12 | "publicKeyMultibase": "z6Mku8G8HT5jLgprB1u8GHsc9b98NDqeQMys2i1zXboCqxrZ", 13 | "privateKeyMultibase": "zrv51RAzPCYDawUCCNR8JAGd4SNvhaTPWodCbBKQu4CjsKjyG7M3bNLLY4VFgiCmk3YbV1gxpqrvoGvTbVw8piitidK" 14 | } 15 | ], 16 | "capabilityDelegation": [ 17 | { 18 | "id": "https://example.com/i/carol/keys/2", 19 | "type": "Ed25519VerificationKey2020", 20 | "controller": "https://example.com/i/carol", 21 | "publicKeyMultibase": "z6Mku8G8HT5jLgprB1u8GHsc9b98NDqeQMys2i1zXboCqxrZ", 22 | "privateKeyMultibase": "zrv51RAzPCYDawUCCNR8JAGd4SNvhaTPWodCbBKQu4CjsKjyG7M3bNLLY4VFgiCmk3YbV1gxpqrvoGvTbVw8piitidK" 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /tests/mock-documents/ed25519-diana-keys.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/security/suites/ed25519-2020/v1" 5 | ], 6 | "id": "https://example.com/i/diana", 7 | "capabilityInvocation": [ 8 | { 9 | "id": "https://example.com/i/diana/keys/1", 10 | "type": "Ed25519VerificationKey2020", 11 | "controller": "https://example.com/i/diana", 12 | "publicKeyMultibase": "z6Mkmx1NnKQS5Nie3UkYHaVE9xxy1AgxF3RXj3rCqrQfQN63", 13 | "privateKeyMultibase": "zrv3yvMmvs3EkVyb9j2YJ9jWQ2dopxcVCHXWQFG7v6Kvf6VS9xHZ7w97ZkKK1UMxebdS8vkA2m9kpu8WXLm9oJoaMtj" 14 | } 15 | ], 16 | "capabilityDelegation": [ 17 | { 18 | "id": "https://example.com/i/diana/keys/2", 19 | "type": "Ed25519VerificationKey2020", 20 | "controller": "https://example.com/i/diana", 21 | "publicKeyMultibase": "z6Mkmx1NnKQS5Nie3UkYHaVE9xxy1AgxF3RXj3rCqrQfQN63", 22 | "privateKeyMultibase": "zrv3yvMmvs3EkVyb9j2YJ9jWQ2dopxcVCHXWQFG7v6Kvf6VS9xHZ7w97ZkKK1UMxebdS8vkA2m9kpu8WXLm9oJoaMtj" 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /tests/mock-documents/example-doc-with-alpha-invocation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/zcap/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "urn:uuid:cab83279-c695-4e66-9458-4327de49197a", 8 | "nonce": "123", 9 | "proof": { 10 | "type": "Ed25519Signature2020", 11 | "created": "2018-02-13T21:26:08Z", 12 | "capability": "https://example.org/alice/caps#1", 13 | "capabilityAction": "read", 14 | "invocationTarget": "https://example.org/alice/targets/alpha", 15 | "proofPurpose": "capabilityInvocation", 16 | "proofValue": "z58nkwkpoeLsrx37nNWDbDEAGrbCWwLCsTmr4aPztBMJvo9UPDLGUyjNzoTgsZpqFkJYq3VM8YgC3RpLn9U4ThkxD", 17 | "verificationMethod": "https://example.com/i/alice/keys/1" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /tests/mock-documents/example-doc-with-beta-invocation.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": [ 3 | "https://w3id.org/security/v2", 4 | "https://w3id.org/zcap/v1", 5 | "https://w3id.org/security/suites/ed25519-2020/v1" 6 | ], 7 | "id": "urn:uuid:cab83279-c695-4e66-9458-4327de49197a", 8 | "nonce": "123", 9 | "proof": { 10 | "type": "Ed25519Signature2020", 11 | "created": "2018-02-13T21:26:08Z", 12 | "capability": "https://example.org/alice/caps#0", 13 | "capabilityAction": "read", 14 | "invocationTarget": "https://example.org/alice/targets/beta", 15 | "proofPurpose": "capabilityInvocation", 16 | "proofValue": "zoRUfzD72MaMVShok9n5GhTSSB4ZA9iSs9kKGeKEfgAQieEtFWfpVSb8Q87thnyeoDABdYsfksTgj4jUj3J6KSrd", 17 | "verificationMethod": "https://example.com/i/alice/keys/1" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /tests/mock-documents/example-doc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | '@context': [ 3 | 'https://w3id.org/security/v2', 4 | 'https://w3id.org/zcap/v1' 5 | ], 6 | id: 'urn:uuid:cab83279-c695-4e66-9458-4327de49197a', 7 | nonce: '123' 8 | }; 9 | -------------------------------------------------------------------------------- /tests/mock-documents/veres-one-context.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "@context": { 3 | // intentionally left blank 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/test-karma.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Karma test runner for zcap. 3 | * 4 | * Copyright (c) 2011-2024 Digital Bazaar, Inc. All rights reserved. 5 | */ 6 | import * as zcap from '../lib/index.js'; 7 | import chai from 'chai'; 8 | import common from './test-common.js'; 9 | import jsigs from 'jsonld-signatures'; 10 | 11 | import * as helpers from './helpers.js'; 12 | import * as mock from './mock-data.js'; 13 | 14 | const expect = chai.expect; 15 | 16 | const options = { 17 | expect, 18 | helpers, 19 | jsigs, 20 | mock, 21 | zcap, 22 | nodejs: false 23 | }; 24 | 25 | common(options).catch(err => { 26 | console.error(err); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Node.js test runner for zcap. 3 | * 4 | * Copyright (c) 2011-2024 Digital Bazaar, Inc. All rights reserved. 5 | */ 6 | import * as zcap from '../lib/index.js'; 7 | import chai from 'chai'; 8 | import common from './test-common.js'; 9 | import jsigs from 'jsonld-signatures'; 10 | 11 | import * as helpers from './helpers.js'; 12 | import * as mock from './mock-data.js'; 13 | 14 | const expect = chai.expect; 15 | 16 | const options = { 17 | expect, 18 | helpers, 19 | jsigs, 20 | mock, 21 | zcap, 22 | nodejs: true 23 | }; 24 | 25 | common(options).then(() => { 26 | // '--delay' event loop hack for mocha 7.1.0+ 27 | return Promise.resolve(); 28 | }).then(() => { 29 | run(); 30 | }).catch(err => { 31 | console.error(err); 32 | }); 33 | 34 | process.on('unhandledRejection', (reason, p) => { 35 | console.error('Unhandled Rejection at:', p, 'reason:', reason); 36 | }); 37 | --------------------------------------------------------------------------------