@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.
- package/LICENSE +27 -0
- package/README.md +65 -0
- package/lib/CapabilityDelegation.js +312 -0
- package/lib/CapabilityInvocation.js +343 -0
- package/lib/CapabilityProofPurpose.js +538 -0
- package/lib/constants.js +32 -0
- package/lib/index.js +60 -0
- package/lib/utils.js +674 -0
- package/lib/zcap-types.d.ts +72 -0
- package/package.json +81 -0
- package/types/lib/CapabilityDelegation.d.ts +101 -0
- package/types/lib/CapabilityDelegation.d.ts.map +1 -0
- package/types/lib/CapabilityInvocation.d.ts +100 -0
- package/types/lib/CapabilityInvocation.d.ts.map +1 -0
- package/types/lib/CapabilityProofPurpose.d.ts +126 -0
- package/types/lib/CapabilityProofPurpose.d.ts.map +1 -0
- package/types/lib/constants.d.ts +15 -0
- package/types/lib/constants.d.ts.map +1 -0
- package/types/lib/index.d.ts +50 -0
- package/types/lib/index.d.ts.map +1 -0
- package/types/lib/utils.d.ts +312 -0
- package/types/lib/utils.d.ts.map +1 -0
package/lib/utils.js
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
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|string[]} options.controller - The root controller.
|
|
14
|
+
* @param {string} options.invocationTarget - The root invocation target.
|
|
15
|
+
*
|
|
16
|
+
* @returns {RootZcap} 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 {Zcap} options.capability - The authorization capability (zcap).
|
|
32
|
+
*
|
|
33
|
+
* @returns {string[]} 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 {Zcap} 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 {Zcap} options.capability - The authorization capability (zcap).
|
|
65
|
+
*
|
|
66
|
+
* @returns {string[]} 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 {Zcap} 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 {Zcap} options.capability - The authorization capability.
|
|
99
|
+
*
|
|
100
|
+
* @returns {CapabilityDelegationProof[]} Any `capabilityDelegation` proof
|
|
101
|
+
* objects attached to the 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 {Zcap} options.capability - The authorization capability.
|
|
121
|
+
*
|
|
122
|
+
* @returns {Array<string|DelegatedZcap>} The capability chain entries
|
|
123
|
+
* (root to parent), as stored in the delegation proof.
|
|
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 {string|Zcap} options.parentCapability - The parent capability from
|
|
208
|
+
* which to compute the capability chain (a root zcap ID string, or a full
|
|
209
|
+
* root or delegated zcap object).
|
|
210
|
+
* @param {boolean} options._skipLocalValidationForTesting - Private.
|
|
211
|
+
*
|
|
212
|
+
* @returns {Array<string|DelegatedZcap>} The computed capability chain to be
|
|
213
|
+
* included in a capability delegation proof.
|
|
214
|
+
*/
|
|
215
|
+
export function computeCapabilityChain({
|
|
216
|
+
parentCapability, _skipLocalValidationForTesting
|
|
217
|
+
}) {
|
|
218
|
+
// if parent capability is root (string or no parent of its own)
|
|
219
|
+
const type = typeof parentCapability;
|
|
220
|
+
if(type === 'string') {
|
|
221
|
+
return [parentCapability];
|
|
222
|
+
}
|
|
223
|
+
if(!parentCapability.parentCapability) {
|
|
224
|
+
// capability must be a root zcap
|
|
225
|
+
checkCapability({capability: parentCapability, expectRoot: true});
|
|
226
|
+
return [parentCapability.id];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// capability must be a delegated zcap, check it and get its chain
|
|
230
|
+
checkCapability({capability: parentCapability, expectRoot: false});
|
|
231
|
+
const proofs = getDelegationProofs({capability: parentCapability});
|
|
232
|
+
if(proofs.length !== 1) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
'Cannot compute capability chain; parent capability is invalid; it is ' +
|
|
235
|
+
'not the root capability yet it does not have exactly one delegation ' +
|
|
236
|
+
'proof.');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const {capabilityChain} = proofs[0];
|
|
240
|
+
if(!(capabilityChain && Array.isArray(capabilityChain))) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
'Cannot compute capability chain; parent capability is invalid; it ' +
|
|
243
|
+
'does not have a "capabilityChain" array in its delegation proof.');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// validate parent capability chain to help prevent bad delegations
|
|
247
|
+
if(!_skipLocalValidationForTesting) {
|
|
248
|
+
// ensure that all `capabilityChain` entries except the last are strings
|
|
249
|
+
const lastRequiredType = capabilityChain.length > 1 ?
|
|
250
|
+
'object' : 'string';
|
|
251
|
+
const lastIndex = capabilityChain.length - 1;
|
|
252
|
+
for(const [i, entry] of capabilityChain.entries()) {
|
|
253
|
+
const entryType = typeof entry;
|
|
254
|
+
if(!((i === lastIndex && entryType === lastRequiredType) ||
|
|
255
|
+
i !== lastIndex && entryType === 'string')) {
|
|
256
|
+
throw new TypeError(
|
|
257
|
+
'Cannot compute capability chain; parent capability chain is ' +
|
|
258
|
+
'invalid; it must consist of strings of capability IDs except ' +
|
|
259
|
+
'the last capability if it is delegated, in which case it must ' +
|
|
260
|
+
'be an object with an "id" property that is a string.');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// if last zcap is embedded, change it to a reference
|
|
266
|
+
const newChain = capabilityChain.slice(0, capabilityChain.length - 1);
|
|
267
|
+
const last = capabilityChain[capabilityChain.length - 1];
|
|
268
|
+
if(typeof last === 'string') {
|
|
269
|
+
newChain.push(last);
|
|
270
|
+
} else {
|
|
271
|
+
newChain.push(last.id);
|
|
272
|
+
}
|
|
273
|
+
newChain.push(parentCapability);
|
|
274
|
+
|
|
275
|
+
// ensure new chain uses absolute URLs
|
|
276
|
+
for(const entry of newChain) {
|
|
277
|
+
if((typeof entry === 'string' && !entry.includes(':')) ||
|
|
278
|
+
typeof entry === 'object' && !entry.id.includes(':')) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'Cannot compute capability chain; parent capability chain is ' +
|
|
281
|
+
'invalid because uses relative URL(s) in its capability chain.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return newChain;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Dereferences the capability chain associated with the given capability,
|
|
290
|
+
* ensuring it passes a number of validation checks.
|
|
291
|
+
*
|
|
292
|
+
* A delegated zcap's chain has a reference to a root zcap. A verifier must
|
|
293
|
+
* provide a hook (`getRootCapability`) to dereference this root zcap since
|
|
294
|
+
* the root zcap has no delegation proof and must therefore be trusted by
|
|
295
|
+
* the verifier. If the root zcap can't be dereferenced by the trusted hook,
|
|
296
|
+
* then an authorization error must be thrown by that hook.
|
|
297
|
+
*
|
|
298
|
+
* This function will dereference the root zcap and then dereference all of
|
|
299
|
+
* the embedded delegated zcaps from the chain, combining them into a single
|
|
300
|
+
* array containing full zcaps ordered from root => tail.
|
|
301
|
+
*
|
|
302
|
+
* The dereferenced chain (result of this function) should then compare the
|
|
303
|
+
* root zcap's ID against a list of expected root capabilities, throwing
|
|
304
|
+
* an error if none of them match. Otherwise, the dereferenced chain should
|
|
305
|
+
* then be processed to ensure that all delegation rules have been followed.
|
|
306
|
+
* If checking an invocation, it should also be ensured that a combination of
|
|
307
|
+
* an expected target and a root zcap is permitted (note it is conceivable that
|
|
308
|
+
* a verifier may accept more than one combination, e.g., a target of `x` could
|
|
309
|
+
* work with both root zcap `a` and `b`).
|
|
310
|
+
*
|
|
311
|
+
* @param {object} options - The options.
|
|
312
|
+
* @param {string|DelegatedZcap} options.capability - The authorization
|
|
313
|
+
* capability to dereference the chain for. Pass a string (the root zcap ID)
|
|
314
|
+
* to dereference a root zcap directly, or a delegated zcap object.
|
|
315
|
+
* @param {Function} options.getRootCapability - A function for dereferencing
|
|
316
|
+
* the root capability (the root zcap must be deref'd in a trusted way by the
|
|
317
|
+
* verifier, it must not be untrusted input).
|
|
318
|
+
* @param {number} [options.maxChainLength=10] - The maximum length of the
|
|
319
|
+
* capability delegation chain (this is inclusive of `capability` itself).
|
|
320
|
+
*
|
|
321
|
+
* @returns {Promise<{dereferencedChain: Zcap[]}>} Resolves to an object
|
|
322
|
+
* containing the full dereferenced chain ordered root to tail.
|
|
323
|
+
*/
|
|
324
|
+
export async function dereferenceCapabilityChain({
|
|
325
|
+
capability, getRootCapability, maxChainLength = MAX_CHAIN_LENGTH
|
|
326
|
+
}) {
|
|
327
|
+
// capability MUST be a string if it is root; root zcaps MUST always be
|
|
328
|
+
// dereferenced via a trusted mechanism provided by the verifier as they
|
|
329
|
+
// do not have delegation proofs
|
|
330
|
+
if(typeof capability === 'string') {
|
|
331
|
+
const id = capability;
|
|
332
|
+
const {rootCapability} = await getRootCapability({id});
|
|
333
|
+
checkCapability({capability: rootCapability, expectRoot: true});
|
|
334
|
+
if(rootCapability.id !== id) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Dereferenced root capability ID "${rootCapability.id}" does not ` +
|
|
337
|
+
`match reference ID "${id}".`);
|
|
338
|
+
}
|
|
339
|
+
capability = rootCapability;
|
|
340
|
+
} else {
|
|
341
|
+
// ensure capability itself is valid
|
|
342
|
+
checkCapability({capability, expectRoot: false});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// get a mapping of IDs to full zcaps as the chain is validated
|
|
346
|
+
const dereferencedChainMap = new Map();
|
|
347
|
+
|
|
348
|
+
// get the underef'd capability chain for the capability
|
|
349
|
+
const capabilityChain = getCapabilityChain({capability});
|
|
350
|
+
|
|
351
|
+
// ensure capability chain length (add 1 to be inclusive of `capability`)
|
|
352
|
+
// does not exceed max chain length; only check this once at the start
|
|
353
|
+
// as it produces the most sensible error -- it is true that an embedded
|
|
354
|
+
// zcap could go over the limit but this will be caught via a congruency
|
|
355
|
+
// check on the length instead
|
|
356
|
+
if((capabilityChain.length + 1) > maxChainLength) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
'The capability chain exceeds the maximum allowed length ' +
|
|
359
|
+
`of ${maxChainLength}.`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// subtract one from the max chain length to start to account for
|
|
363
|
+
// `capability` which is not present in `capabilityChain`
|
|
364
|
+
let firstPass = true;
|
|
365
|
+
let requiredLength = capabilityChain.length;
|
|
366
|
+
let currentCapability = capability;
|
|
367
|
+
let currentCapabilityChain = capabilityChain;
|
|
368
|
+
while(currentCapabilityChain.length > 0) {
|
|
369
|
+
if(currentCapabilityChain.length !== requiredLength) {
|
|
370
|
+
throw new Error('The capability chain length is incongruent.');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// if `next.length > 1`, then its last entry is a delegated
|
|
374
|
+
// capability and it MUST be fully embedded as an object; all other
|
|
375
|
+
// entries MUST be strings
|
|
376
|
+
const lastRequiredType = currentCapabilityChain.length > 1 ?
|
|
377
|
+
'object' : 'string';
|
|
378
|
+
|
|
379
|
+
// validate entries and dereference delegated zcaps
|
|
380
|
+
const lastIndex = currentCapabilityChain.length - 1;
|
|
381
|
+
for(const [i, entry] of currentCapabilityChain.entries()) {
|
|
382
|
+
const entryType = typeof entry;
|
|
383
|
+
const entryIsString = entryType === 'string';
|
|
384
|
+
const requiredType = i === lastIndex ? lastRequiredType : 'string';
|
|
385
|
+
|
|
386
|
+
// ensure entry is the required type and, if it is an object, its `id`
|
|
387
|
+
// is a string
|
|
388
|
+
if(!(entryType === requiredType &&
|
|
389
|
+
(entryIsString || typeof entry.id === 'string'))) {
|
|
390
|
+
throw new TypeError(
|
|
391
|
+
'Capability chain is invalid; it must consist of strings ' +
|
|
392
|
+
'of capability IDs except the last capability if it is ' +
|
|
393
|
+
'delegated, in which case it must be an object with an "id" ' +
|
|
394
|
+
'property that is a string.');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ensure capability ID expresses an absolute URI (i.e., it has `:`)
|
|
398
|
+
const id = entryIsString ? entry : entry.id;
|
|
399
|
+
if(!id.includes(':')) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
'Capability chain is invalid; it contains a capability ID ' +
|
|
402
|
+
'that is not an absolute URI.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ensure last entry in chain matches parent capability
|
|
406
|
+
if(i === lastIndex && currentCapability.parentCapability &&
|
|
407
|
+
currentCapability.parentCapability !== id) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
'Capability chain is invalid; the last entry does not ' +
|
|
410
|
+
'match the parent capability.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if(!entryIsString) {
|
|
414
|
+
// check zcap data model
|
|
415
|
+
checkCapability({capability: entry, expectRoot: i === 0});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ensure no cycles in the capability chain
|
|
419
|
+
if(firstPass) {
|
|
420
|
+
// on the first pass, the zcap must not have been seen yet
|
|
421
|
+
if(id === capability.id || dereferencedChainMap.has(id)) {
|
|
422
|
+
throw new Error('The capability chain contains a cycle.');
|
|
423
|
+
}
|
|
424
|
+
// add zcap to the map whether it is only a reference (an ID) or
|
|
425
|
+
// a fully embedded zcap; this will be used to ensure no additional
|
|
426
|
+
// zcaps are added to the chain
|
|
427
|
+
dereferencedChainMap.set(id, entry);
|
|
428
|
+
} else {
|
|
429
|
+
// on non-first pass, every ID should already be in the zcap map
|
|
430
|
+
// and they should all be strings, not objects
|
|
431
|
+
const existing = dereferencedChainMap.get(id);
|
|
432
|
+
if(!existing) {
|
|
433
|
+
// the chain is inconsistent across delegated zcaps
|
|
434
|
+
throw new Error('The capability chain is inconsistent.');
|
|
435
|
+
}
|
|
436
|
+
if(id === capability.id || typeof existing === 'object') {
|
|
437
|
+
// the zcap has been deferenced before, there's a cycle
|
|
438
|
+
throw new Error('The capability chain contains a cycle.');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// only update the zcaps map using a fully embedded zcap
|
|
442
|
+
if(!entryIsString) {
|
|
443
|
+
dereferencedChainMap.set(id, entry);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// if the chain has more than the root zcap, loop to process the
|
|
449
|
+
// next chain from the last delegated zcap
|
|
450
|
+
if(currentCapabilityChain.length > 1) {
|
|
451
|
+
// next chain must be 1 shorter than the current one
|
|
452
|
+
requiredLength--;
|
|
453
|
+
currentCapability = currentCapabilityChain[
|
|
454
|
+
currentCapabilityChain.length - 1];
|
|
455
|
+
currentCapabilityChain = getCapabilityChain(
|
|
456
|
+
{capability: currentCapability});
|
|
457
|
+
} else {
|
|
458
|
+
// no more chains to check
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
firstPass = false;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// dereference root zcap via provided trusted `getRootCapability` function
|
|
466
|
+
if(capabilityChain.length > 0) {
|
|
467
|
+
const [id] = capabilityChain;
|
|
468
|
+
const {rootCapability} = await getRootCapability({id});
|
|
469
|
+
checkCapability({capability: rootCapability, expectRoot: true});
|
|
470
|
+
if(rootCapability.id !== id) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Dereferenced root capability ID "${rootCapability.id}" does not ` +
|
|
473
|
+
`match reference ID "${id}" from capability chain.`);
|
|
474
|
+
}
|
|
475
|
+
dereferencedChainMap.set(id, rootCapability);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// include `capability` in dereferenced map
|
|
479
|
+
dereferencedChainMap.set(capability.id, capability);
|
|
480
|
+
const dereferencedChain = [...dereferencedChainMap.values()];
|
|
481
|
+
|
|
482
|
+
return {dereferencedChain};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function checkProofContext({proof}) {
|
|
486
|
+
// zcap context can appear anywhere in the array as it *is* protected
|
|
487
|
+
const {'@context': ctx} = proof;
|
|
488
|
+
if(!((Array.isArray(ctx) && ctx.includes(ZCAP_CONTEXT_URL)) ||
|
|
489
|
+
ctx === ZCAP_CONTEXT_URL)) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`Missing required capability proof context ("${ZCAP_CONTEXT_URL}").`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function hasValidAllowedAction({allowedAction, parentAllowedAction}) {
|
|
496
|
+
// if the parent's `allowedAction` is `undefined`, then any more restrictive
|
|
497
|
+
// action is allowed in the child
|
|
498
|
+
if(!parentAllowedAction) {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if(Array.isArray(parentAllowedAction)) {
|
|
503
|
+
// parent's `allowedAction` must include every one from child's
|
|
504
|
+
if(Array.isArray(allowedAction)) {
|
|
505
|
+
return allowedAction.every(a => parentAllowedAction.includes(a));
|
|
506
|
+
}
|
|
507
|
+
return parentAllowedAction.includes(allowedAction);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// require exact match
|
|
511
|
+
return (parentAllowedAction === allowedAction);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function checkCapability({capability, expectRoot}) {
|
|
515
|
+
const {
|
|
516
|
+
'@context': context,
|
|
517
|
+
id, parentCapability, invocationTarget, allowedAction, expires
|
|
518
|
+
} = capability;
|
|
519
|
+
|
|
520
|
+
const isRoot = parentCapability === undefined;
|
|
521
|
+
if(isRoot) {
|
|
522
|
+
if(context !== ZCAP_CONTEXT_URL) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
'Root capability must have an "@context" value of ' +
|
|
525
|
+
`"${ZCAP_CONTEXT_URL}".`);
|
|
526
|
+
}
|
|
527
|
+
if(capability.expires !== undefined) {
|
|
528
|
+
throw new Error('Root capability must not have an "expires" field.');
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
if(!((Array.isArray(context) && context[0] === ZCAP_CONTEXT_URL))) {
|
|
532
|
+
throw new Error(
|
|
533
|
+
'Delegated capability must have an "@context" array ' +
|
|
534
|
+
`with "${ZCAP_CONTEXT_URL}" in its first position.`);
|
|
535
|
+
}
|
|
536
|
+
if(!(typeof parentCapability === 'string' &&
|
|
537
|
+
parentCapability.includes(':'))) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
'Delegated capability must have a "parentCapability" with a string ' +
|
|
540
|
+
'value that expresses an absolute URI.');
|
|
541
|
+
}
|
|
542
|
+
const [proof] = getDelegationProofs({capability});
|
|
543
|
+
if(!proof) {
|
|
544
|
+
throw new Error('Delegated capability must have a "proof".');
|
|
545
|
+
}
|
|
546
|
+
if(isNaN(Date.parse(proof.created))) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
'Delegated capability must have a valid proof "created" date.');
|
|
549
|
+
}
|
|
550
|
+
if(isNaN(Date.parse(expires))) {
|
|
551
|
+
throw new Error('Delegated capability must have a valid expires date.');
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if(!(typeof id === 'string' && id.includes(':'))) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
'Capability must have an "id" with a string value that expresses an ' +
|
|
558
|
+
'absolute URI.');
|
|
559
|
+
}
|
|
560
|
+
if(!(typeof invocationTarget === 'string' &&
|
|
561
|
+
invocationTarget.includes(':'))) {
|
|
562
|
+
throw new Error(
|
|
563
|
+
'Capability must have an "invocationTarget" with a string value that ' +
|
|
564
|
+
'expresses an absolute URI.');
|
|
565
|
+
}
|
|
566
|
+
if(allowedAction !== undefined && !(
|
|
567
|
+
typeof allowedAction === 'string' ||
|
|
568
|
+
(Array.isArray(allowedAction) && allowedAction.length > 0))) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
'If present on a capability, "allowedAction" must be a string or a ' +
|
|
571
|
+
'non-empty array.');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if(isRoot !== expectRoot) {
|
|
575
|
+
if(expectRoot) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Expected capability "${capability.id}" to be root ` +
|
|
578
|
+
'but it is delegated.');
|
|
579
|
+
}
|
|
580
|
+
throw new Error(
|
|
581
|
+
`Expected capability "${capability.id}" to be delegated but it is root.`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function compareTime({t1, t2, maxClockSkew}) {
|
|
586
|
+
// `maxClockSkew` is in seconds, so transform to milliseconds
|
|
587
|
+
if(Math.abs(t1 - t2) < (maxClockSkew * 1000)) {
|
|
588
|
+
// times are equal within the max clock skew
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
return t1 < t2 ? -1 : 1;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The zcap object shapes (`RootZcap`, `DelegatedZcap`,
|
|
596
|
+
* `CapabilityDelegationProof`, `Zcap`) are defined in `./zcap-types.d.ts` and
|
|
597
|
+
* re-exported here as typedefs. They cannot be expressed as JSDoc `@typedef`s
|
|
598
|
+
* because TypeScript's JSDoc parser mangles the `@context` property name (the
|
|
599
|
+
* leading `@` becomes an empty-string key). See `./zcap-types.d.ts`.
|
|
600
|
+
*
|
|
601
|
+
* @typedef {import('./zcap-types.js').RootZcap} RootZcap
|
|
602
|
+
* @typedef {import('./zcap-types.js').CapabilityDelegationProof} CapabilityDelegationProof
|
|
603
|
+
* @typedef {import('./zcap-types.js').DelegatedZcap} DelegatedZcap
|
|
604
|
+
* @typedef {import('./zcap-types.js').Zcap} Zcap
|
|
605
|
+
*/
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* An inspection function result.
|
|
609
|
+
*
|
|
610
|
+
* @typedef {object} InspectResult
|
|
611
|
+
* @property {boolean} [valid] - `true` if the chain passed inspection.
|
|
612
|
+
* @property {Error} [error] - Set if inspection failed.
|
|
613
|
+
*/
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* A capability chain inspection function.
|
|
617
|
+
*
|
|
618
|
+
* @typedef {Function} InspectCapabilityChain
|
|
619
|
+
* @param {CapabilityChainDetails} options - The chain details to inspect.
|
|
620
|
+
* @returns {Promise<InspectResult>} Resolves to an inspection result.
|
|
621
|
+
*/
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* A capability. The capability is compacted into the security
|
|
625
|
+
* context. Only the required fields are shown here, a capability will contain
|
|
626
|
+
* additional properties.
|
|
627
|
+
*
|
|
628
|
+
* @typedef {object} Capability
|
|
629
|
+
* @property {string} id - The ID of the capability.
|
|
630
|
+
* @property {string} controller - The controller of the capability.
|
|
631
|
+
*/
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* @typedef {object} CapabilityChainDetails
|
|
635
|
+
* @property {Capability[]} capabilityChain - The capabilities in the chain.
|
|
636
|
+
* @property {CapabilityMeta[]} capabilityChainMeta - The results returned
|
|
637
|
+
* from jsonld-signatures verify for each capability in the chain. Each
|
|
638
|
+
* object contains `{verifyResult}` where each `verifyResult` is an
|
|
639
|
+
* `InspectChainResult`.
|
|
640
|
+
*/
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* The metadata resulting from the verification of a delegated capability.
|
|
644
|
+
*
|
|
645
|
+
* @typedef {object} CapabilityMeta
|
|
646
|
+
* @property {VerifyResult} verifyResult - The capability verify result, which
|
|
647
|
+
* is `null` for the root capability.
|
|
648
|
+
*/
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The result of running jsonld-signature's verify method.
|
|
652
|
+
*
|
|
653
|
+
* @typedef {object} VerifyResult
|
|
654
|
+
* @property {boolean} verified - `true` if all the checked proofs were
|
|
655
|
+
* successfully verified.
|
|
656
|
+
* @property {VerifyProofResult[]} results - The verify results for each
|
|
657
|
+
* delegation proof.
|
|
658
|
+
*/
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The result of verifying a capability delegation proof.
|
|
662
|
+
*
|
|
663
|
+
* @typedef {object} VerifyProofResult
|
|
664
|
+
* @property {VerifyProofPurposeResult} proofPurposeResult - The result from
|
|
665
|
+
* verifying the capability delegation proof purpose.
|
|
666
|
+
*/
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* The result of verifying a capability delegation proof purpose.
|
|
670
|
+
*
|
|
671
|
+
* @typedef {object} VerifyProofPurposeResult
|
|
672
|
+
* @property {string} delegator - The party that created the capability
|
|
673
|
+
* delegation proof, i.e., the party that delegated the capability.
|
|
674
|
+
*/
|