iso-ucan 1.0.0 → 3.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 +1 -1
- package/dist/src/delegation.d.ts.map +1 -1
- package/dist/src/envelope.d.ts +42 -10
- package/dist/src/envelope.d.ts.map +1 -1
- package/dist/src/invocation.d.ts +2 -14
- 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/varsig.d.ts +11 -1
- package/dist/src/types/varsig.d.ts.map +1 -1
- package/dist/src/types.d.ts +10 -9
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils.d.ts +27 -38
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/varsig.d.ts +2 -3
- package/dist/src/varsig.d.ts.map +1 -1
- package/package.json +3 -3
- package/readme.md +57 -9
- package/src/delegation.js +11 -5
- package/src/envelope.js +117 -36
- package/src/invocation.js +50 -41
- package/src/policy.js +597 -195
- package/src/types/envelope.ts +4 -4
- package/src/types/policy.ts +5 -2
- package/src/types/varsig.ts +12 -1
- package/src/types.ts +10 -9
- package/src/utils.js +64 -157
- package/src/varsig.js +32 -29
package/readme.md
CHANGED
|
@@ -115,10 +115,11 @@ const invocation = await Invocation.from({
|
|
|
115
115
|
})
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
The store is keyed on `invocation.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
for
|
|
118
|
+
The store is keyed on `invocation.cid`. `iso-ucan` verifies signatures in
|
|
119
|
+
strict mode, which accepts only the
|
|
120
|
+
[canonical Varsig encoding](https://github.com/ChainAgnostic/varsig#canonical-signatures),
|
|
121
|
+
for example low-S for ECDSA. An attacker can't re-encode an intercepted
|
|
122
|
+
invocation under a different CID.
|
|
122
123
|
|
|
123
124
|
`KVReplayStore` is atomic within one process only. When several processes or
|
|
124
125
|
instances execute invocations, implement the `ReplayStore` interface on a
|
|
@@ -128,11 +129,58 @@ shared store with an atomic set-if-absent, such as Redis `SET NX` or SQL
|
|
|
128
129
|
## Revocation
|
|
129
130
|
|
|
130
131
|
Pass `isRevoked` to `Delegation.from`, `Delegation#validate` or
|
|
131
|
-
`Invocation.from` to reject revoked delegations by CID.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
`Invocation.from` to reject revoked delegations by CID. Strict verification
|
|
133
|
+
rejects non-canonical signatures, so a revoked delegation can't come back under
|
|
134
|
+
a different CID.
|
|
135
|
+
|
|
136
|
+
### Migrate revocation lists from earlier versions
|
|
137
|
+
|
|
138
|
+
Earlier versions accepted ECDSA (ES256, ES384, ES512) signatures with high S,
|
|
139
|
+
and EIP-191 signatures with `v` of 0 or 1. If your revocation list holds the
|
|
140
|
+
CID of such a delegation, anyone holding the delegation can convert it to the
|
|
141
|
+
canonical encoding. The canonical CID is not in your list, so the revoked
|
|
142
|
+
delegation passes `isRevoked`.
|
|
143
|
+
|
|
144
|
+
To keep those revocations, add the canonical CID of each revoked delegation to
|
|
145
|
+
your list. Compute it from the stored delegation bytes:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import { Delegation } from 'iso-ucan/delegation'
|
|
149
|
+
import * as Envelope from 'iso-ucan/envelope'
|
|
150
|
+
import { normalizeS } from 'iso-signatures/signers/ecdsa.js'
|
|
151
|
+
import { normalizeSignature } from 'iso-signatures/signers/eip191.js'
|
|
152
|
+
|
|
153
|
+
const CURVES = { ES256: 'P-256', ES384: 'P-384', ES512: 'P-521' }
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {Uint8Array} bytes - Revoked delegation, as stored
|
|
157
|
+
* @param {import('iso-ucan/types').VerifierResolver} verifierResolver
|
|
158
|
+
*/
|
|
159
|
+
async function canonicalCid(bytes, verifierResolver) {
|
|
160
|
+
const envelope = Envelope.decode({ envelope: bytes })
|
|
161
|
+
let signature = envelope.signature
|
|
162
|
+
if (envelope.alg === 'EIP191') {
|
|
163
|
+
signature = normalizeSignature(signature)
|
|
164
|
+
} else if (envelope.alg in CURVES) {
|
|
165
|
+
signature = normalizeS(CURVES[envelope.alg], signature)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const canonical = Envelope.encode({
|
|
169
|
+
signature,
|
|
170
|
+
signaturePayload: Envelope.getSignaturePayload({
|
|
171
|
+
spec: envelope.spec,
|
|
172
|
+
version: envelope.version,
|
|
173
|
+
signatureType: envelope.alg,
|
|
174
|
+
payload: envelope.payload,
|
|
175
|
+
}),
|
|
176
|
+
})
|
|
177
|
+
const delegation = await Delegation.from({ bytes: canonical, verifierResolver })
|
|
178
|
+
return delegation.cid
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Ed25519 and ES256K signatures were already canonical, so their CIDs don't
|
|
183
|
+
change.
|
|
136
184
|
|
|
137
185
|
## EIP-191 wallet signing
|
|
138
186
|
|
package/src/delegation.js
CHANGED
|
@@ -3,12 +3,15 @@ import { parse as didParse } from 'iso-did'
|
|
|
3
3
|
import { randomBytes } from 'iso-web/crypto'
|
|
4
4
|
import * as Envelope from './envelope.js'
|
|
5
5
|
import {
|
|
6
|
+
assertCanonical,
|
|
6
7
|
assertExpiration,
|
|
7
8
|
assertIsValidCommand,
|
|
8
9
|
assertMeta,
|
|
9
10
|
assertNonce,
|
|
10
11
|
assertNotBefore,
|
|
11
12
|
assertNotRevoked,
|
|
13
|
+
assertPolicy,
|
|
14
|
+
assertTimestamp,
|
|
12
15
|
cid,
|
|
13
16
|
expOrTtl,
|
|
14
17
|
verifySignature,
|
|
@@ -84,12 +87,13 @@ export class Delegation {
|
|
|
84
87
|
)
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
assertCanonical(envelope, bytes)
|
|
87
91
|
assertStructure(envelope.payload, options.now)
|
|
88
92
|
|
|
89
93
|
await verifySignature(envelope, verifierResolver, didResolveOptions)
|
|
90
94
|
|
|
91
95
|
const _cid = await cid(envelope)
|
|
92
|
-
await assertNotRevoked(
|
|
96
|
+
await assertNotRevoked(_cid, options.isRevoked)
|
|
93
97
|
|
|
94
98
|
return new Delegation(envelope, bytes, _cid)
|
|
95
99
|
}
|
|
@@ -112,6 +116,7 @@ export class Delegation {
|
|
|
112
116
|
)
|
|
113
117
|
}
|
|
114
118
|
|
|
119
|
+
assertCanonical(envelope, bytes)
|
|
115
120
|
assertStructure(envelope.payload, options.now)
|
|
116
121
|
const _cid = await cid(envelope)
|
|
117
122
|
return new Delegation(envelope, bytes, _cid)
|
|
@@ -146,7 +151,7 @@ export class Delegation {
|
|
|
146
151
|
|
|
147
152
|
assertStructure(payload, options.now)
|
|
148
153
|
|
|
149
|
-
const { signature, signaturePayload } = await Envelope.sign({
|
|
154
|
+
const { alg, signature, signaturePayload } = await Envelope.sign({
|
|
150
155
|
spec: 'dlg',
|
|
151
156
|
signer: options.iss,
|
|
152
157
|
payload,
|
|
@@ -156,7 +161,7 @@ export class Delegation {
|
|
|
156
161
|
|
|
157
162
|
/** @type {import("./types.js").DecodedEnvelope<'dlg'>} */
|
|
158
163
|
const envelope = {
|
|
159
|
-
alg
|
|
164
|
+
alg,
|
|
160
165
|
enc: 'DAG-CBOR',
|
|
161
166
|
signature,
|
|
162
167
|
payload,
|
|
@@ -182,7 +187,7 @@ export class Delegation {
|
|
|
182
187
|
options.verifierResolver,
|
|
183
188
|
options.didResolver
|
|
184
189
|
)
|
|
185
|
-
await assertNotRevoked(this.
|
|
190
|
+
await assertNotRevoked(this.cid, options.isRevoked)
|
|
186
191
|
|
|
187
192
|
return true
|
|
188
193
|
}
|
|
@@ -232,9 +237,10 @@ function assertStructure(payload, now) {
|
|
|
232
237
|
|
|
233
238
|
assertIsValidCommand(payload.cmd)
|
|
234
239
|
assertExpiration(payload.exp, now)
|
|
240
|
+
assertTimestamp(payload.nbf, 'nbf')
|
|
235
241
|
assertNonce(payload.nonce)
|
|
236
242
|
|
|
237
|
-
|
|
243
|
+
assertPolicy(payload.pol)
|
|
238
244
|
|
|
239
245
|
assertMeta(payload.meta)
|
|
240
246
|
}
|
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
|
*
|
|
@@ -110,6 +187,10 @@ export async function sign(options) {
|
|
|
110
187
|
})
|
|
111
188
|
const signature = await signer.sign(cbor.encode(signaturePayload))
|
|
112
189
|
return {
|
|
190
|
+
// getSignaturePayload throws for algorithms varsig does not support
|
|
191
|
+
alg: /** @type {import('./types.js').VarsigAlgorithm} */ (
|
|
192
|
+
signer.signatureType
|
|
193
|
+
),
|
|
113
194
|
signature,
|
|
114
195
|
signaturePayload,
|
|
115
196
|
}
|
|
@@ -141,20 +222,20 @@ export function encode(options) {
|
|
|
141
222
|
export function decode(options) {
|
|
142
223
|
const { envelope } = options
|
|
143
224
|
|
|
144
|
-
const decoded =
|
|
145
|
-
/** @type {ByteView<Envelope>} */ (envelope)
|
|
146
|
-
)
|
|
225
|
+
const decoded = cbor.decode(envelope)
|
|
147
226
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
227
|
+
if (!Array.isArray(decoded) || decoded.length !== 2) {
|
|
228
|
+
throw new TypeError(
|
|
229
|
+
`Invalid envelope expected array of 2 elements [signature, signature payload] got ${Array.isArray(decoded) ? `array of ${decoded.length} elements` : typeof decoded}`
|
|
230
|
+
)
|
|
231
|
+
}
|
|
153
232
|
|
|
154
|
-
const [
|
|
155
|
-
|
|
233
|
+
const [signature, sigPayload] = decoded
|
|
234
|
+
if (!(signature instanceof Uint8Array)) {
|
|
235
|
+
throw new TypeError('Invalid envelope signature expected bytes')
|
|
236
|
+
}
|
|
156
237
|
|
|
157
|
-
const { alg, enc } =
|
|
238
|
+
const { alg, enc, spec, version, payload } = parseSignaturePayload(sigPayload)
|
|
158
239
|
|
|
159
240
|
return {
|
|
160
241
|
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,17 +9,17 @@ 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
|
-
replayKey,
|
|
16
18
|
verifySignature,
|
|
17
19
|
} from './utils.js'
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* @import {Delegation} from './delegation.js'
|
|
21
|
-
* @import {CID} from 'multiformats'
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -35,32 +36,19 @@ export class Invocation {
|
|
|
35
36
|
*/
|
|
36
37
|
cid
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
* Key to deduplicate this invocation on: the CID of its signature payload.
|
|
40
|
-
*
|
|
41
|
-
* Use this instead of {@link Invocation.cid} to detect replays. The CID
|
|
42
|
-
* covers the signature bytes, and some signature algorithms (e.g. ECDSA)
|
|
43
|
-
* accept more than one valid signature for the same payload.
|
|
44
|
-
*
|
|
45
|
-
* @type {string}
|
|
46
|
-
*/
|
|
47
|
-
replayKey
|
|
48
|
-
|
|
49
39
|
/**
|
|
50
40
|
*
|
|
51
41
|
* @param {import("./types.js").DecodedEnvelope<'inv'>} envelope
|
|
52
42
|
* @param {Uint8Array} bytes
|
|
53
43
|
* @param {CID} cid
|
|
54
44
|
* @param {Delegation[]} delegations
|
|
55
|
-
* @param {string} replayKey
|
|
56
45
|
*/
|
|
57
|
-
constructor(envelope, bytes, cid, delegations
|
|
46
|
+
constructor(envelope, bytes, cid, delegations) {
|
|
58
47
|
this.envelope = envelope
|
|
59
48
|
this.payload = envelope.payload
|
|
60
49
|
this.bytes = bytes
|
|
61
50
|
this.cid = cid
|
|
62
51
|
this.delegations = delegations
|
|
63
|
-
this.replayKey = replayKey
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
/**
|
|
@@ -87,7 +75,7 @@ export class Invocation {
|
|
|
87
75
|
)
|
|
88
76
|
}
|
|
89
77
|
|
|
90
|
-
assertStructure(envelope.payload)
|
|
78
|
+
assertStructure(envelope.payload, options.now)
|
|
91
79
|
await verifySignature(
|
|
92
80
|
envelope,
|
|
93
81
|
options.verifierResolver,
|
|
@@ -101,6 +89,13 @@ export class Invocation {
|
|
|
101
89
|
if (iss.did !== envelope.payload.sub) {
|
|
102
90
|
for (const proof of envelope.payload.prf) {
|
|
103
91
|
const delegation = await options.resolveProof(proof)
|
|
92
|
+
// The signature covers the proof CIDs, so the resolved delegation
|
|
93
|
+
// must be exactly that one
|
|
94
|
+
if (!delegation.cid.equals(proof)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`UCAN Invocation proof CID mismatch, expected ${proof} but resolver returned ${delegation.cid}`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
104
99
|
await delegation.validate(options)
|
|
105
100
|
proofs.push(delegation)
|
|
106
101
|
}
|
|
@@ -112,20 +107,20 @@ export class Invocation {
|
|
|
112
107
|
assertMaxTtl(envelope.payload.exp, options.maxTtl, options.now)
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
const
|
|
110
|
+
const _cid = await cid(envelope)
|
|
116
111
|
|
|
117
112
|
// Must run last, so only fully validated invocations are recorded
|
|
118
113
|
if (
|
|
119
114
|
options.replayStore &&
|
|
120
|
-
!(await options.replayStore.checkAndSet(
|
|
115
|
+
!(await options.replayStore.checkAndSet(
|
|
116
|
+
_cid.toString(),
|
|
117
|
+
envelope.payload.exp
|
|
118
|
+
))
|
|
121
119
|
) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
`UCAN Invocation replay detected, already seen ${_replayKey}`
|
|
124
|
-
)
|
|
120
|
+
throw new Error(`UCAN Invocation replay detected, already seen ${_cid}`)
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
|
|
128
|
-
return new Invocation(envelope, bytes, _cid, proofs, _replayKey)
|
|
123
|
+
return new Invocation(envelope, bytes, _cid, proofs)
|
|
129
124
|
}
|
|
130
125
|
|
|
131
126
|
/**
|
|
@@ -151,17 +146,15 @@ export class Invocation {
|
|
|
151
146
|
if (options.cause) {
|
|
152
147
|
payload.cause = options.cause
|
|
153
148
|
}
|
|
154
|
-
|
|
149
|
+
// `meta` MUST be omitted when empty
|
|
150
|
+
if (options.meta && Object.keys(options.meta).length > 0) {
|
|
155
151
|
payload.meta = options.meta
|
|
156
152
|
}
|
|
157
|
-
if (options.iat) {
|
|
153
|
+
if (options.iat !== undefined) {
|
|
158
154
|
payload.iat = options.iat
|
|
159
155
|
}
|
|
160
|
-
if (options.nbf) {
|
|
161
|
-
payload.nbf = options.nbf
|
|
162
|
-
}
|
|
163
156
|
|
|
164
|
-
assertStructure(payload)
|
|
157
|
+
assertStructure(payload, options.now)
|
|
165
158
|
|
|
166
159
|
if (options.iss.did !== options.sub) {
|
|
167
160
|
for (const proof of options.prf) {
|
|
@@ -171,7 +164,7 @@ export class Invocation {
|
|
|
171
164
|
assertProofs(payload, options.prf)
|
|
172
165
|
}
|
|
173
166
|
|
|
174
|
-
const { signature, signaturePayload } = await Envelope.sign({
|
|
167
|
+
const { alg, signature, signaturePayload } = await Envelope.sign({
|
|
175
168
|
spec: 'inv',
|
|
176
169
|
signer: options.iss,
|
|
177
170
|
payload,
|
|
@@ -181,7 +174,7 @@ export class Invocation {
|
|
|
181
174
|
|
|
182
175
|
/** @type {import("./types.js").DecodedEnvelope<'inv'>} */
|
|
183
176
|
const envelope = {
|
|
184
|
-
alg
|
|
177
|
+
alg,
|
|
185
178
|
enc: 'DAG-CBOR',
|
|
186
179
|
signature,
|
|
187
180
|
payload,
|
|
@@ -189,13 +182,7 @@ export class Invocation {
|
|
|
189
182
|
version: Envelope.VERSION,
|
|
190
183
|
}
|
|
191
184
|
|
|
192
|
-
return new Invocation(
|
|
193
|
-
envelope,
|
|
194
|
-
bytes,
|
|
195
|
-
await cid(envelope),
|
|
196
|
-
options.prf,
|
|
197
|
-
await replayKey(envelope)
|
|
198
|
-
)
|
|
185
|
+
return new Invocation(envelope, bytes, await cid(envelope), options.prf)
|
|
199
186
|
}
|
|
200
187
|
}
|
|
201
188
|
|
|
@@ -219,8 +206,9 @@ function assertMaxTtl(exp, maxTtl, now = nowInSeconds()) {
|
|
|
219
206
|
|
|
220
207
|
/**
|
|
221
208
|
* @param {import('./types.js').InvocationPayload} payload
|
|
209
|
+
* @param {number} [now]
|
|
222
210
|
*/
|
|
223
|
-
function assertStructure(payload) {
|
|
211
|
+
function assertStructure(payload, now) {
|
|
224
212
|
didParse(payload.iss)
|
|
225
213
|
didParse(payload.sub)
|
|
226
214
|
|
|
@@ -237,8 +225,29 @@ function assertStructure(payload) {
|
|
|
237
225
|
assertIsValidCommand(payload.cmd)
|
|
238
226
|
assertArgs(payload.args)
|
|
239
227
|
assertMeta(payload.meta)
|
|
228
|
+
// Spec: `meta` MUST be a non-empty map, and omitted when empty
|
|
229
|
+
if (
|
|
230
|
+
payload.meta !== undefined &&
|
|
231
|
+
(!isObject(payload.meta) || Object.keys(payload.meta).length === 0)
|
|
232
|
+
) {
|
|
233
|
+
throw new TypeError(
|
|
234
|
+
'UCAN Invocation meta must be a non-empty map, omit it when empty'
|
|
235
|
+
)
|
|
236
|
+
}
|
|
240
237
|
assertNonce(payload.nonce)
|
|
241
|
-
assertExpiration(payload.exp)
|
|
238
|
+
assertExpiration(payload.exp, now)
|
|
239
|
+
assertTimestamp(payload.iat, 'iat')
|
|
240
|
+
// `nbf` is a delegation field, an invoker could wrongly expect it to delay execution
|
|
241
|
+
if ('nbf' in payload) {
|
|
242
|
+
throw new TypeError('UCAN Invocation must not have nbf')
|
|
243
|
+
}
|
|
244
|
+
// `prf` is required, even when empty for a self-issued invocation
|
|
245
|
+
if (
|
|
246
|
+
!Array.isArray(payload.prf) ||
|
|
247
|
+
!payload.prf.every((proof) => CID.asCID(proof) !== null)
|
|
248
|
+
) {
|
|
249
|
+
throw new TypeError('UCAN Invocation prf must be an array of CIDs')
|
|
250
|
+
}
|
|
242
251
|
}
|
|
243
252
|
|
|
244
253
|
/**
|