@interop/zcap 9.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
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 {import('./utils.js').InspectCapabilityChain} InspectCapabilityChain
10
+ * @typedef {import('./utils.js').DelegatedZcap} DelegatedZcap
11
+ */
12
+
13
+ export class CapabilityInvocation extends CapabilityProofPurpose {
14
+ /**
15
+ * @param {object} options - The options.
16
+ * @param {string|DelegatedZcap} [options.capability] - The capability to
17
+ * add/reference in a created proof. A root zcap MUST be passed as its ID
18
+ * string; a delegated zcap must be passed as the full object.
19
+ * @param {string} [options.capabilityAction] - The capability action that is
20
+ * to be added to a proof.
21
+ * @param {string} [options.invocationTarget] - The invocation target to
22
+ * use; this is required and can be used to attenuate the capability's
23
+ * invocation target if the verifier supports target attenuation.
24
+ * @param {boolean} [options.allowTargetAttenuation=false] - Allow the
25
+ * invocationTarget of a delegation chain to be increasingly restrictive
26
+ * based on a hierarchical RESTful URL structure.
27
+ * @param {object} [options.controller] - The description of the controller,
28
+ * if it is not to be dereferenced via a `documentLoader`.
29
+ * @param {string|Date|number} [options.date] - Used during proof
30
+ * verification as the expected date for the creation of the proof
31
+ * (within a maximum timestamp delta) and for checking to see if a
32
+ * capability has expired; if not passed the current date will be used.
33
+ * @param {string} [options.expectedAction] - The capability action that is
34
+ * expected when validating a proof.
35
+ * @param {string|string[]} [options.expectedRootCapability] - The expected
36
+ * root capability for the delegation chain (a single root capability ID
37
+ * string, or an array of acceptable root capability ID strings).
38
+ * @param {string|string[]} [options.expectedTarget] - The target(s) we
39
+ * expect a capability to apply to (absolute URI, or array of URIs).
40
+ * @param {InspectCapabilityChain} [options.inspectCapabilityChain] - An
41
+ * async function that can be used to check for revocations related to any
42
+ * of 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 can
54
+ * deviate from `date`, defaults to `Infinity`.
55
+ * @param {object|object[]} [options.suite] - The jsonld-signature suite(s) to
56
+ * use to verify the capability chain. Required only in verify-proof mode;
57
+ * unused (and omitted) when creating an invocation proof.
58
+ */
59
+ constructor({
60
+ // proof creation params
61
+ capability,
62
+ capabilityAction,
63
+ invocationTarget,
64
+ // proof verification params
65
+ allowTargetAttenuation,
66
+ controller,
67
+ date,
68
+ expectedAction,
69
+ expectedRootCapability,
70
+ expectedTarget,
71
+ inspectCapabilityChain,
72
+ maxChainLength,
73
+ maxClockSkew,
74
+ maxDelegationTtl,
75
+ maxTimestampDelta,
76
+ suite
77
+ } = {}) {
78
+ // parameters used to create a proof
79
+ const hasCreateProofParams = capability || capabilityAction ||
80
+ invocationTarget;
81
+ // params used to verify a proof
82
+ const hasVerifyProofParams = controller || date ||
83
+ expectedAction || expectedRootCapability || expectedTarget ||
84
+ inspectCapabilityChain || suite;
85
+
86
+ if(hasCreateProofParams && hasVerifyProofParams) {
87
+ // cannot provide both create and verify params
88
+ throw new Error(
89
+ 'Parameters for both creating and verifying a proof must not be ' +
90
+ 'provided together.');
91
+ }
92
+
93
+ super({
94
+ allowTargetAttenuation,
95
+ controller, date,
96
+ expectedRootCapability, inspectCapabilityChain,
97
+ maxChainLength, maxClockSkew, maxDelegationTtl, maxTimestampDelta,
98
+ suite,
99
+ term: 'capabilityInvocation'
100
+ });
101
+
102
+ // validate `CapabilityInvocation` specific params, the base class will
103
+ // have already handled validating common ones...
104
+
105
+ // use negative conditional to cover case where neither create nor
106
+ // verify params were provided and default to proof creation case to
107
+ // avoid creating bad proofs
108
+ if(!hasVerifyProofParams) {
109
+ if(typeof capability === 'object') {
110
+ // root capabilities MUST be passed as strings
111
+ if(!(capability && capability.parentCapability)) {
112
+ throw new Error(
113
+ '"capability" must be a string if it is a root capability.');
114
+ }
115
+ } else if(typeof capability !== 'string') {
116
+ throw new TypeError('"capability" must be a string or object.');
117
+ }
118
+ if(typeof capabilityAction !== 'string') {
119
+ throw new TypeError('"capabilityAction" must be a string.');
120
+ }
121
+ if(!(typeof invocationTarget === 'string' &&
122
+ invocationTarget.includes(':'))) {
123
+ throw new TypeError(
124
+ '"invocationTarget" must be a string that expresses an absolute ' +
125
+ 'URI.');
126
+ }
127
+
128
+ this.capability = capability;
129
+ this.capabilityAction = capabilityAction;
130
+ this.invocationTarget = invocationTarget;
131
+ } else {
132
+ if(typeof expectedAction !== 'string') {
133
+ throw new TypeError('"expectedAction" must be a string.');
134
+ }
135
+ if(!(typeof expectedTarget === 'string' ||
136
+ Array.isArray(expectedTarget))) {
137
+ throw new TypeError('"expectedTarget" must be a string or array.');
138
+ }
139
+ // expected target values must be absolute URIs
140
+ const expectedTargets = Array.isArray(expectedTarget) ?
141
+ expectedTarget : [expectedTarget];
142
+ for(const et of expectedTargets) {
143
+ if(!(typeof et === 'string' && et.includes(':'))) {
144
+ throw new Error(
145
+ '"expectedTargets" values must be absolute URI strings.');
146
+ }
147
+ }
148
+
149
+ this.expectedTarget = expectedTarget;
150
+ this.expectedAction = expectedAction;
151
+ }
152
+ }
153
+
154
+ async update(proof) {
155
+ const {capability, capabilityAction, invocationTarget} = this;
156
+ proof.proofPurpose = this.term;
157
+ proof.capability = capability;
158
+ proof.invocationTarget = invocationTarget;
159
+ proof.capabilityAction = capabilityAction;
160
+ return proof;
161
+ }
162
+
163
+ async match(proof, {document, documentLoader}) {
164
+ const {expectedAction, expectedTarget} = this;
165
+
166
+ try {
167
+ // check the `proof` context before using its terms
168
+ utils.checkProofContext({proof});
169
+ } catch(e) {
170
+ // context does not match, so proof does not match
171
+ return false;
172
+ }
173
+
174
+ if(!proof.capability) {
175
+ // capability not in the proof, not a match
176
+ return false;
177
+ }
178
+
179
+ // ensure basic purpose and expected action match the proof
180
+ if(!(await super.match(proof, {document, documentLoader}) &&
181
+ (expectedAction === proof.capabilityAction))) {
182
+ return false;
183
+ }
184
+
185
+ // ensure the proof's declared invocation target matches an expected one
186
+ if(Array.isArray(expectedTarget)) {
187
+ return expectedTarget.includes(proof.invocationTarget);
188
+ }
189
+ return expectedTarget === proof.invocationTarget;
190
+ }
191
+
192
+ _getCapabilityDelegationClass() {
193
+ return CapabilityDelegation;
194
+ }
195
+
196
+ _getTailCapability({proof}) {
197
+ return {capability: proof.capability};
198
+ }
199
+
200
+ async _runChecksBeforeChainVerification({dereferencedChain, proof}) {
201
+ const {
202
+ allowTargetAttenuation,
203
+ expectedAction,
204
+ expectedTarget
205
+ } = this;
206
+
207
+ /* 1. Ensure that `capabilityAction` is an allowed action and that
208
+ it matches `expectedAction`. Note that if it doesn't match and `match`
209
+ was called to gate calling `validate`, then this code will not execute.
210
+ However, if `validate` is called directly, this check MUST run here.
211
+
212
+ If the capability restricts the actions via `allowedAction` then
213
+ `capabilityAction` must be in its set. */
214
+ const capability = dereferencedChain[dereferencedChain.length - 1];
215
+ const {capabilityAction} = proof;
216
+ const allowedActions = utils.getAllowedActions({capability});
217
+ if(allowedActions.length > 0 &&
218
+ !allowedActions.includes(capabilityAction)) {
219
+ throw new Error(
220
+ `Capability action "${capabilityAction}" is not allowed by the ` +
221
+ 'capability; allowed actions are: ' +
222
+ allowedActions.map(x => `"${x}"`).join(', '));
223
+ }
224
+ if(capabilityAction !== expectedAction) {
225
+ throw new Error(
226
+ `Capability action "${capabilityAction}" does not match the ` +
227
+ `expected action of "${expectedAction}".`);
228
+ }
229
+
230
+ /* 2. Ensure `expectedTarget` is as expected. The invocation target
231
+ will also be checked to ensure it hasn't changed from previous zcaps
232
+ in the chain (unless attenuation is permitted) later. */
233
+
234
+ /* 3. Verify the invocation target in the proof is as expected. The
235
+ `invocationTarget` specified in the capability invocation proof must
236
+ match exactly (or follow acceptable target attenuation rules) the
237
+ `invocationTarget` specified in the invoked capability. */
238
+ const capabilityTarget = utils.getTarget({capability});
239
+ const {invocationTarget} = proof;
240
+ if(!(typeof invocationTarget === 'string' &&
241
+ invocationTarget.includes(':'))) {
242
+ throw new TypeError(
243
+ `Invocation target (${invocationTarget}) must be a string that ` +
244
+ 'expresses an absolute URI.');
245
+ }
246
+ if(!utils.isValidTarget({
247
+ invocationTarget,
248
+ baseInvocationTarget: capabilityTarget,
249
+ allowTargetAttenuation
250
+ })) {
251
+ throw new Error(
252
+ `Invocation target (${invocationTarget}) does not match ` +
253
+ `capability target (${capabilityTarget}).`);
254
+ }
255
+
256
+ /* 4. Verify the invocation target is an expected target. Prior to this
257
+ step we ensured that the invocation target used matched th capability
258
+ that was invoked, but this check ensures that the invocation target used
259
+ matches the endpoint (the `expectedTarget`) where the capability was
260
+ actually invoked. */
261
+ if(!((Array.isArray(expectedTarget) &&
262
+ expectedTarget.includes(invocationTarget)) ||
263
+ (typeof expectedTarget === 'string' &&
264
+ invocationTarget === expectedTarget))) {
265
+ throw new Error(
266
+ `Expected target (${expectedTarget}) does not match ` +
267
+ `invocation target (${invocationTarget}).`);
268
+ }
269
+
270
+ /* 5. If capability is delegated (not root), then ensure the capability
271
+ invocation proof `created` date is not before the capability delegation
272
+ proof creation date. */
273
+ if(capability.parentCapability) {
274
+ const invoked = Date.parse(proof.created);
275
+ const [delegationProof] = utils.getDelegationProofs({capability});
276
+ const delegated = Date.parse(delegationProof.created);
277
+ const {maxClockSkew} = this;
278
+ // use `utils.compareTime` to allow for clock drift from the machine
279
+ // that created the delegation proof and the machine that created
280
+ // the invocation proof
281
+ if(utils.compareTime({t1: invoked, t2: delegated, maxClockSkew}) < 0) {
282
+ throw new Error(
283
+ 'A delegated capability must not be invoked before the "created" ' +
284
+ 'date in its delegation proof.');
285
+ }
286
+ }
287
+
288
+ // return no capability delegation verify results yet; the tail's
289
+ // capability delegation proof must be verified via
290
+ // `_verifyCapabilityChain`
291
+ return {capabilityChainMeta: []};
292
+ }
293
+
294
+ async _runChecksAfterChainVerification({
295
+ dereferencedChain, proof, validateOptions
296
+ }) {
297
+ /* Verify the controller of the capability. The zcap controller must
298
+ match the invoking verification method (or its controller). */
299
+ const capability = dereferencedChain[dereferencedChain.length - 1];
300
+ const {verificationMethod} = validateOptions;
301
+ if(!utils.isController({capability, verificationMethod})) {
302
+ const error = new Error(
303
+ 'The capability controller does not match the verification method ' +
304
+ '(or its controller) used to invoke.');
305
+ error.details = {
306
+ capability,
307
+ verificationMethod
308
+ };
309
+ throw error;
310
+ }
311
+
312
+ // if capability is delegated, verify that it has not expired
313
+ if(capability.parentCapability) {
314
+ // verify expiration dates
315
+ // expires date has been previously validated, so just parse it
316
+ const currentCapabilityExpirationTime = Date.parse(capability.expires);
317
+
318
+ // use `utils.compareTime` to allow for allow for clock drift because
319
+ // we are comparing against `currentDate`
320
+ const {date, maxClockSkew} = this;
321
+ const currentDate = (date && new Date(date)) || new Date();
322
+ if(utils.compareTime({
323
+ t1: currentDate.getTime(),
324
+ t2: currentCapabilityExpirationTime,
325
+ maxClockSkew
326
+ }) > 0) {
327
+ throw new Error('The invoked capability has expired.');
328
+ }
329
+ }
330
+
331
+ // run base level validation checks
332
+ const result = await this._runBaseProofValidation({proof, validateOptions});
333
+ if(!result.valid) {
334
+ throw result.error;
335
+ }
336
+
337
+ // the controller of the verification method from the proof is the
338
+ // invoker of the capability
339
+ result.invoker = result.controller;
340
+
341
+ return result;
342
+ }
343
+ }