iso-ucan 0.5.2 → 2.0.0
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/dist/src/delegation.d.ts.map +1 -1
- package/dist/src/envelope.d.ts +40 -9
- package/dist/src/envelope.d.ts.map +1 -1
- package/dist/src/invocation.d.ts +1 -2
- package/dist/src/invocation.d.ts.map +1 -1
- package/dist/src/policy.d.ts +114 -0
- package/dist/src/policy.d.ts.map +1 -1
- package/dist/src/types/envelope.d.ts +4 -4
- package/dist/src/types/envelope.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +6 -2
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types.d.ts +9 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +30 -2
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/varsig.d.ts +0 -1
- package/dist/src/varsig.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/delegation.js +7 -1
- package/src/envelope.js +113 -36
- package/src/invocation.js +53 -18
- package/src/policy.js +597 -195
- package/src/types/envelope.ts +4 -4
- package/src/types/policy.ts +5 -2
- package/src/types.ts +9 -1
- package/src/utils.js +64 -10
- package/src/varsig.js +30 -19
package/src/envelope.js
CHANGED
|
@@ -3,12 +3,63 @@ import { isObject } from './utils.js'
|
|
|
3
3
|
import * as varsig from './varsig.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @import {
|
|
7
|
-
* @import { ByteView } from 'multiformats'
|
|
6
|
+
* @import { PayloadTag, EnvelopeDecodeOptions, EnvelopeEncodeOptions, EnvelopeSignOptions, SignaturePayload, PayloadSpec, DecodedEnvelope, DelegationPayload, InvocationPayload, Payload} from './types.js'
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Payload tag version used for new tokens (`ucan/dlg@1.0.0`, `ucan/inv@1.0.0`)
|
|
11
|
+
*
|
|
12
|
+
* @see https://github.com/ucan-wg/delegation#type-tag
|
|
13
|
+
* @see https://github.com/ucan-wg/invocation#type-tag
|
|
14
|
+
*/
|
|
15
|
+
export const VERSION = '1.0.0'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Payload tag versions accepted on decode.
|
|
19
|
+
*
|
|
20
|
+
* `1.0.0-rc.1` is still accepted so tokens issued by iso-ucan <= 1.0.0 keep
|
|
21
|
+
* verifying. New tokens are always encoded with {@link VERSION}.
|
|
22
|
+
*
|
|
23
|
+
* @type {readonly string[]}
|
|
24
|
+
*/
|
|
25
|
+
export const SUPPORTED_VERSIONS = Object.freeze([VERSION, '1.0.0-rc.1'])
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Payload specs
|
|
29
|
+
*
|
|
30
|
+
* @type {readonly PayloadSpec[]}
|
|
31
|
+
*/
|
|
32
|
+
export const SPECS = Object.freeze(/** @type {const} */ (['dlg', 'inv']))
|
|
33
|
+
|
|
34
|
+
const PAYLOAD_TAG_REGEX = /^ucan\/([^@/]+)@(.+)$/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse and validate a payload tag (`ucan/<spec>@<version>`)
|
|
38
|
+
*
|
|
39
|
+
* @param {string} tag
|
|
40
|
+
*/
|
|
41
|
+
export function parsePayloadTag(tag) {
|
|
42
|
+
const match = PAYLOAD_TAG_REGEX.exec(tag)
|
|
43
|
+
if (!match) {
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
`Invalid payload tag "${tag}" expected "ucan/<spec>@<version>"`
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
const spec = /** @type {PayloadSpec} */ (match[1])
|
|
49
|
+
const version = match[2]
|
|
50
|
+
if (!SPECS.includes(spec)) {
|
|
51
|
+
throw new TypeError(
|
|
52
|
+
`Unsupported payload tag spec "${spec}" in "${tag}" expected one of: ${SPECS.join(', ')}`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
if (!SUPPORTED_VERSIONS.includes(version)) {
|
|
56
|
+
throw new TypeError(
|
|
57
|
+
`Unsupported payload tag version "${version}" in "${tag}" expected one of: ${SUPPORTED_VERSIONS.join(', ')}`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { tag: /** @type {PayloadTag} */ (tag), spec, version }
|
|
62
|
+
}
|
|
12
63
|
|
|
13
64
|
/**
|
|
14
65
|
* Get the signature payload for a given payload
|
|
@@ -22,7 +73,7 @@ export const VERSION = '1.0.0-rc.1'
|
|
|
22
73
|
export function getSignaturePayload(options) {
|
|
23
74
|
const { spec, version, signatureType, payload } = options
|
|
24
75
|
|
|
25
|
-
const
|
|
76
|
+
const { tag: payloadTag } = parsePayloadTag(
|
|
26
77
|
`ucan/${spec}@${version ?? VERSION}`
|
|
27
78
|
)
|
|
28
79
|
|
|
@@ -37,44 +88,70 @@ export function getSignaturePayload(options) {
|
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
/**
|
|
40
|
-
*
|
|
91
|
+
* Validate a decoded signature payload (`{ h, "ucan/<spec>@<version>": payload }`)
|
|
41
92
|
*
|
|
42
|
-
* @
|
|
93
|
+
* @see https://github.com/ucan-wg/spec#envelope
|
|
94
|
+
* @param {unknown} sigPayload
|
|
43
95
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cbor.decode
|
|
47
|
-
)
|
|
48
|
-
const decoded = decode(bytes)
|
|
49
|
-
|
|
50
|
-
if (!isObject(decoded)) {
|
|
96
|
+
function parseSignaturePayload(sigPayload) {
|
|
97
|
+
if (!isObject(sigPayload)) {
|
|
51
98
|
throw new TypeError(
|
|
52
|
-
`Invalid signature payload expected object got ${typeof
|
|
99
|
+
`Invalid signature payload expected object got ${Array.isArray(sigPayload) ? 'array' : typeof sigPayload}`
|
|
53
100
|
)
|
|
54
101
|
}
|
|
55
|
-
|
|
102
|
+
const keys = Object.keys(sigPayload)
|
|
103
|
+
if (keys.length !== 2) {
|
|
104
|
+
throw new TypeError(
|
|
105
|
+
`Invalid signature payload expected 2 keys (h and payload tag) got ${keys.length}: ${keys.join(', ')}`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
if (!keys.includes('h')) {
|
|
56
109
|
throw new TypeError('Invalid signature payload missing h')
|
|
57
110
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
111
|
+
const { tag, spec, version } = parsePayloadTag(
|
|
112
|
+
/** @type {string} */ (keys.find((key) => key !== 'h'))
|
|
113
|
+
)
|
|
114
|
+
// DAG-CBOR sorts map keys by length first, so `h` must be the first key.
|
|
115
|
+
if (keys[0] !== 'h') {
|
|
60
116
|
throw new TypeError(
|
|
61
|
-
|
|
117
|
+
'Invalid signature payload keys are not in canonical DAG-CBOR order'
|
|
62
118
|
)
|
|
63
119
|
}
|
|
64
|
-
|
|
65
|
-
|
|
120
|
+
|
|
121
|
+
const header = sigPayload.h
|
|
122
|
+
if (!(header instanceof Uint8Array)) {
|
|
123
|
+
throw new TypeError('Invalid signature payload h expected bytes')
|
|
124
|
+
}
|
|
125
|
+
const { alg, enc } = varsig.decode(header)
|
|
126
|
+
if (enc !== 'DAG-CBOR') {
|
|
127
|
+
throw new TypeError(
|
|
128
|
+
`Unsupported varsig payload encoding ${enc} expected DAG-CBOR`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const payload = sigPayload[tag]
|
|
133
|
+
if (!isObject(payload)) {
|
|
134
|
+
throw new TypeError(`Invalid signature payload ${tag} expected object`)
|
|
66
135
|
}
|
|
67
136
|
|
|
68
|
-
const payloadTag = /** @type {PayloadTag} */ (keys[1])
|
|
69
|
-
const { alg, enc } = varsig.decode(decoded.h)
|
|
70
|
-
const payload = decoded[payloadTag]
|
|
71
137
|
return {
|
|
72
138
|
alg,
|
|
73
139
|
enc,
|
|
74
|
-
|
|
140
|
+
spec,
|
|
141
|
+
version,
|
|
142
|
+
payload: /** @type {Payload} */ (/** @type {unknown} */ (payload)),
|
|
75
143
|
}
|
|
76
144
|
}
|
|
77
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Decode and validate a signature payload
|
|
148
|
+
*
|
|
149
|
+
* @param {Uint8Array} bytes
|
|
150
|
+
*/
|
|
151
|
+
export function decodeSignaturePayload(bytes) {
|
|
152
|
+
return parseSignaturePayload(cbor.decode(bytes))
|
|
153
|
+
}
|
|
154
|
+
|
|
78
155
|
/**
|
|
79
156
|
* Check if a payload is an invocation payload
|
|
80
157
|
*
|
|
@@ -141,20 +218,20 @@ export function encode(options) {
|
|
|
141
218
|
export function decode(options) {
|
|
142
219
|
const { envelope } = options
|
|
143
220
|
|
|
144
|
-
const decoded =
|
|
145
|
-
/** @type {ByteView<Envelope>} */ (envelope)
|
|
146
|
-
)
|
|
221
|
+
const decoded = cbor.decode(envelope)
|
|
147
222
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
223
|
+
if (!Array.isArray(decoded) || decoded.length !== 2) {
|
|
224
|
+
throw new TypeError(
|
|
225
|
+
`Invalid envelope expected array of 2 elements [signature, signature payload] got ${Array.isArray(decoded) ? `array of ${decoded.length} elements` : typeof decoded}`
|
|
226
|
+
)
|
|
227
|
+
}
|
|
153
228
|
|
|
154
|
-
const [
|
|
155
|
-
|
|
229
|
+
const [signature, sigPayload] = decoded
|
|
230
|
+
if (!(signature instanceof Uint8Array)) {
|
|
231
|
+
throw new TypeError('Invalid envelope signature expected bytes')
|
|
232
|
+
}
|
|
156
233
|
|
|
157
|
-
const { alg, enc } =
|
|
234
|
+
const { alg, enc, spec, version, payload } = parseSignaturePayload(sigPayload)
|
|
158
235
|
|
|
159
236
|
return {
|
|
160
237
|
alg,
|
package/src/invocation.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse as didParse } from 'iso-did'
|
|
2
2
|
import { randomBytes } from 'iso-web/crypto'
|
|
3
|
+
import { CID } from 'multiformats/cid'
|
|
3
4
|
import * as Envelope from './envelope.js'
|
|
4
5
|
import { validate } from './policy.js'
|
|
5
6
|
import {
|
|
@@ -8,9 +9,11 @@ import {
|
|
|
8
9
|
assertIsValidCommand,
|
|
9
10
|
assertMeta,
|
|
10
11
|
assertNonce,
|
|
12
|
+
assertTimestamp,
|
|
11
13
|
cid,
|
|
12
14
|
commandCovers,
|
|
13
15
|
expOrTtl,
|
|
16
|
+
isObject,
|
|
14
17
|
nowInSeconds,
|
|
15
18
|
replayKey,
|
|
16
19
|
verifySignature,
|
|
@@ -18,7 +21,6 @@ import {
|
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* @import {Delegation} from './delegation.js'
|
|
21
|
-
* @import {CID} from 'multiformats'
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -79,18 +81,15 @@ export class Invocation {
|
|
|
79
81
|
`Invalid envelope type. Expected "inv" but got "${envelope.spec}"`
|
|
80
82
|
)
|
|
81
83
|
}
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
envelope.payload.aud !== audience.did &&
|
|
86
|
-
envelope.payload.sub !== audience.did
|
|
87
|
-
) {
|
|
84
|
+
// The executor is `aud` when present, otherwise `sub`
|
|
85
|
+
const executor = envelope.payload.aud ?? envelope.payload.sub
|
|
86
|
+
if (audience && executor !== audience.did) {
|
|
88
87
|
throw new TypeError(
|
|
89
|
-
`UCAN Invocation audience
|
|
88
|
+
`UCAN Invocation audience does not match receiver. Expected: ${audience.did} but got: ${executor ?? 'null'}`
|
|
90
89
|
)
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
assertStructure(envelope.payload)
|
|
92
|
+
assertStructure(envelope.payload, options.now)
|
|
94
93
|
await verifySignature(
|
|
95
94
|
envelope,
|
|
96
95
|
options.verifierResolver,
|
|
@@ -104,6 +103,13 @@ export class Invocation {
|
|
|
104
103
|
if (iss.did !== envelope.payload.sub) {
|
|
105
104
|
for (const proof of envelope.payload.prf) {
|
|
106
105
|
const delegation = await options.resolveProof(proof)
|
|
106
|
+
// The signature covers the proof CIDs, so the resolved delegation
|
|
107
|
+
// must be exactly that one (not an ECDSA-equivalent re-encoding)
|
|
108
|
+
if (!delegation.cid.equals(proof)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`UCAN Invocation proof CID mismatch, expected ${proof} but resolver returned ${delegation.cid}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
107
113
|
await delegation.validate(options)
|
|
108
114
|
proofs.push(delegation)
|
|
109
115
|
}
|
|
@@ -140,7 +146,6 @@ export class Invocation {
|
|
|
140
146
|
/** @type {import("./types.js").InvocationPayload} */
|
|
141
147
|
const payload = {
|
|
142
148
|
iss: options.iss.toString(),
|
|
143
|
-
aud: options.aud ? options.aud : options.sub,
|
|
144
149
|
sub: options.sub,
|
|
145
150
|
cmd: options.cmd,
|
|
146
151
|
nonce,
|
|
@@ -148,20 +153,22 @@ export class Invocation {
|
|
|
148
153
|
args: options.args,
|
|
149
154
|
prf: options.prf.map((p) => p.cid),
|
|
150
155
|
}
|
|
156
|
+
// `aud` MUST be omitted when the executor is the subject
|
|
157
|
+
if (options.aud && options.aud !== options.sub) {
|
|
158
|
+
payload.aud = options.aud
|
|
159
|
+
}
|
|
151
160
|
if (options.cause) {
|
|
152
161
|
payload.cause = options.cause
|
|
153
162
|
}
|
|
154
|
-
|
|
163
|
+
// `meta` MUST be omitted when empty
|
|
164
|
+
if (options.meta && Object.keys(options.meta).length > 0) {
|
|
155
165
|
payload.meta = options.meta
|
|
156
166
|
}
|
|
157
|
-
if (options.iat) {
|
|
167
|
+
if (options.iat !== undefined) {
|
|
158
168
|
payload.iat = options.iat
|
|
159
169
|
}
|
|
160
|
-
if (options.nbf) {
|
|
161
|
-
payload.nbf = options.nbf
|
|
162
|
-
}
|
|
163
170
|
|
|
164
|
-
assertStructure(payload)
|
|
171
|
+
assertStructure(payload, options.now)
|
|
165
172
|
|
|
166
173
|
if (options.iss.did !== options.sub) {
|
|
167
174
|
for (const proof of options.prf) {
|
|
@@ -219,20 +226,48 @@ function assertMaxTtl(exp, maxTtl, now = nowInSeconds()) {
|
|
|
219
226
|
|
|
220
227
|
/**
|
|
221
228
|
* @param {import('./types.js').InvocationPayload} payload
|
|
229
|
+
* @param {number} [now]
|
|
222
230
|
*/
|
|
223
|
-
function assertStructure(payload) {
|
|
231
|
+
function assertStructure(payload, now) {
|
|
224
232
|
didParse(payload.iss)
|
|
225
233
|
didParse(payload.sub)
|
|
226
234
|
|
|
227
235
|
if (payload.aud) {
|
|
228
236
|
didParse(payload.aud)
|
|
237
|
+
// Spec: `aud` MUST be omitted when the executor is the subject
|
|
238
|
+
if (payload.aud === payload.sub) {
|
|
239
|
+
throw new TypeError(
|
|
240
|
+
'UCAN Invocation audience must be omitted when it equals the subject'
|
|
241
|
+
)
|
|
242
|
+
}
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
assertIsValidCommand(payload.cmd)
|
|
232
246
|
assertArgs(payload.args)
|
|
233
247
|
assertMeta(payload.meta)
|
|
248
|
+
// Spec: `meta` MUST be a non-empty map, and omitted when empty
|
|
249
|
+
if (
|
|
250
|
+
payload.meta !== undefined &&
|
|
251
|
+
(!isObject(payload.meta) || Object.keys(payload.meta).length === 0)
|
|
252
|
+
) {
|
|
253
|
+
throw new TypeError(
|
|
254
|
+
'UCAN Invocation meta must be a non-empty map, omit it when empty'
|
|
255
|
+
)
|
|
256
|
+
}
|
|
234
257
|
assertNonce(payload.nonce)
|
|
235
|
-
assertExpiration(payload.exp)
|
|
258
|
+
assertExpiration(payload.exp, now)
|
|
259
|
+
assertTimestamp(payload.iat, 'iat')
|
|
260
|
+
// `nbf` is a delegation field, an invoker could wrongly expect it to delay execution
|
|
261
|
+
if ('nbf' in payload) {
|
|
262
|
+
throw new TypeError('UCAN Invocation must not have nbf')
|
|
263
|
+
}
|
|
264
|
+
// `prf` is required, even when empty for a self-issued invocation
|
|
265
|
+
if (
|
|
266
|
+
!Array.isArray(payload.prf) ||
|
|
267
|
+
!payload.prf.every((proof) => CID.asCID(proof) !== null)
|
|
268
|
+
) {
|
|
269
|
+
throw new TypeError('UCAN Invocation prf must be an array of CIDs')
|
|
270
|
+
}
|
|
236
271
|
}
|
|
237
272
|
|
|
238
273
|
/**
|