iso-ucan 0.0.1
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/capability.d.ts +49 -0
- package/dist/src/capability.d.ts.map +1 -0
- package/dist/src/client.d.ts +47 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/delegation.d.ts +61 -0
- package/dist/src/delegation.d.ts.map +1 -0
- package/dist/src/envelope.d.ts +39 -0
- package/dist/src/envelope.d.ts.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/invocation.d.ts +46 -0
- package/dist/src/invocation.d.ts.map +1 -0
- package/dist/src/policy.d.ts +10 -0
- package/dist/src/policy.d.ts.map +1 -0
- package/dist/src/server.d.ts +5 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/store.d.ts +49 -0
- package/dist/src/store.d.ts.map +1 -0
- package/dist/src/types.d.ts +187 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +136 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/varsig.d.ts +24 -0
- package/dist/src/varsig.d.ts.map +1 -0
- package/package.json +140 -0
- package/readme.md +81 -0
- package/src/capability.js +77 -0
- package/src/client.js +52 -0
- package/src/delegation.js +183 -0
- package/src/envelope.js +88 -0
- package/src/index.js +1 -0
- package/src/invocation.js +227 -0
- package/src/policy.js +272 -0
- package/src/server.ts +24 -0
- package/src/store.js +187 -0
- package/src/types/envelope.ts +131 -0
- package/src/types/policy.ts +100 -0
- package/src/types/varsig.ts +22 -0
- package/src/types.ts +243 -0
- package/src/utils.js +375 -0
- package/src/varsig.js +157 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import * as dagCbor from '@ipld/dag-cbor'
|
|
2
|
+
import { DID } from 'iso-did'
|
|
3
|
+
import { CID } from 'multiformats/cid'
|
|
4
|
+
import { sha256 } from 'multiformats/hashes/sha2'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import * as Envelope from './envelope.js'
|
|
8
|
+
import * as varsig from './varsig.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import {PayloadSpec} from './types.js'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create the signature payload for a given envelope
|
|
16
|
+
*
|
|
17
|
+
* @param {import('./types.js').DecodedEnvelope<PayloadSpec>} envelope
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
export function signaturePayload(envelope) {
|
|
21
|
+
const payloadTag = /** @type {import('./types.js').PayloadTag} */ (
|
|
22
|
+
`ucan/${envelope.spec}@${envelope.version}`
|
|
23
|
+
)
|
|
24
|
+
const payload = {
|
|
25
|
+
h: varsig.encode({
|
|
26
|
+
enc: envelope.enc,
|
|
27
|
+
alg: envelope.alg,
|
|
28
|
+
}),
|
|
29
|
+
[payloadTag]: envelope.payload,
|
|
30
|
+
}
|
|
31
|
+
return payload
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function nowInSeconds() {
|
|
35
|
+
return Math.floor(Date.now() / 1000)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
*
|
|
40
|
+
* @param {import('./types.js').DecodedEnvelope<PayloadSpec>} envelope
|
|
41
|
+
*/
|
|
42
|
+
export async function cid(envelope) {
|
|
43
|
+
const bytes = Envelope.encode({
|
|
44
|
+
signature: envelope.signature,
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
signaturePayload: signaturePayload(envelope),
|
|
47
|
+
})
|
|
48
|
+
const hash = await sha256.digest(bytes)
|
|
49
|
+
|
|
50
|
+
return CID.create(1, dagCbor.code, hash)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a DID and signature type are compatible
|
|
55
|
+
*
|
|
56
|
+
* @param {import('iso-did/types').VerifiableDID} did
|
|
57
|
+
* @param {import('iso-signatures/types').SignatureType} sigType
|
|
58
|
+
*/
|
|
59
|
+
export function isSigAndDidCompatible(did, sigType) {
|
|
60
|
+
if (did.verifiableDid.type === 'Ed25519' && sigType === 'Ed25519') {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (did.verifiableDid.type === 'secp256k1' && sigType === 'ES256K') {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (did.verifiableDid.type === 'P-256' && sigType === 'ES256') {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (did.verifiableDid.type === 'P-384' && sigType === 'ES384') {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (did.verifiableDid.type === 'P-521' && sigType === 'ES512') {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (did.verifiableDid.type === 'RSA' && sigType === 'RS256') {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
if (did.verifiableDid.type === 'secp256k1' && sigType === 'EIP191') {
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate the expiration of a UCAN
|
|
92
|
+
*
|
|
93
|
+
* @param {number | null} exp
|
|
94
|
+
*/
|
|
95
|
+
export function assertExpiration(exp) {
|
|
96
|
+
const now = nowInSeconds()
|
|
97
|
+
|
|
98
|
+
if (exp !== null && !Number.isSafeInteger(exp)) {
|
|
99
|
+
throw new TypeError(
|
|
100
|
+
`UCAN expiration must be null or a safe integer. Received: ${exp}`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (exp !== null && exp < now) {
|
|
105
|
+
throw new TypeError(
|
|
106
|
+
`UCAN expiration must be in the future. Received: ${exp} but current time is ${now}`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Validate the not before time of a UCAN
|
|
113
|
+
*
|
|
114
|
+
* @param {number} [nbf]
|
|
115
|
+
*/
|
|
116
|
+
export function assertNotBefore(nbf) {
|
|
117
|
+
if (nbf && nbf > nowInSeconds()) {
|
|
118
|
+
throw new Error('UCAN not valid yet')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate the issuer and signature of a UCAN
|
|
124
|
+
*
|
|
125
|
+
* @param {import('./types.js').DecodedEnvelope<PayloadSpec>} envelope
|
|
126
|
+
* @param {import('iso-did').Resolver} [didResolver]
|
|
127
|
+
*/
|
|
128
|
+
export async function validateIssuerAndSignature(envelope, didResolver) {
|
|
129
|
+
const issuer = await DID.fromString(envelope.payload.iss, didResolver)
|
|
130
|
+
if (!isSigAndDidCompatible(issuer, envelope.alg)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`UCAN issuer type mismatch: DID ${issuer.verifiableDid.type} and Signature ${envelope.alg} are not compatible`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return issuer
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Verify the signature of a UCAN and that issuer is compatible with the signature
|
|
141
|
+
*
|
|
142
|
+
* @param {import('./types.js').DecodedEnvelope<PayloadSpec>} envelope
|
|
143
|
+
* @param {import('iso-signatures/verifiers/resolver.js').Resolver} signatureVerifierResolver
|
|
144
|
+
* @param {import('iso-did').Resolver} [didResolver]
|
|
145
|
+
*/
|
|
146
|
+
export async function verifySignature(
|
|
147
|
+
envelope,
|
|
148
|
+
signatureVerifierResolver,
|
|
149
|
+
didResolver
|
|
150
|
+
) {
|
|
151
|
+
const issuer = await validateIssuerAndSignature(envelope, didResolver)
|
|
152
|
+
const isVerified = await signatureVerifierResolver.verify({
|
|
153
|
+
signature: envelope.signature,
|
|
154
|
+
message: dagCbor.encode(signaturePayload(envelope)),
|
|
155
|
+
did: issuer,
|
|
156
|
+
type: envelope.alg,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (!isVerified) {
|
|
160
|
+
throw new Error('UCAN signature verification failed')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Asserts that a UCAN command string is syntactically valid.
|
|
168
|
+
* If the command is invalid, it throws a descriptive error.
|
|
169
|
+
*
|
|
170
|
+
* Rules:
|
|
171
|
+
* - Commands MUST begin with a slash (/).
|
|
172
|
+
* - Commands MUST be lowercase.
|
|
173
|
+
* - A trailing slash MUST NOT be present, unless the command is exactly "/".
|
|
174
|
+
* - Segments MUST be separated by a slash (e.g., no empty segments like "//").
|
|
175
|
+
*
|
|
176
|
+
* @param {string} command The command string to validate.
|
|
177
|
+
* @throws {Error} If the command is syntactically invalid.
|
|
178
|
+
* @returns {void} Does not return a value on success.
|
|
179
|
+
*/
|
|
180
|
+
export function assertIsValidCommand(command) {
|
|
181
|
+
// Rule: Must be a string
|
|
182
|
+
if (typeof command !== 'string') {
|
|
183
|
+
throw new TypeError(
|
|
184
|
+
`Invalid command: Input must be a string, but received type ${typeof command}`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Rule: Must begin with a slash
|
|
189
|
+
if (!command.startsWith('/')) {
|
|
190
|
+
throw new TypeError(
|
|
191
|
+
`Invalid command: Must begin with a slash (/). Received: "${command}"`
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Rule: No trailing slash, unless the command is exactly "/"
|
|
196
|
+
if (command.length > 1 && command.endsWith('/')) {
|
|
197
|
+
throw new TypeError(
|
|
198
|
+
`Invalid command: Must not have a trailing slash. Received: "${command}"`
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Rule: Must be lowercase
|
|
203
|
+
if (command !== command.toLowerCase()) {
|
|
204
|
+
throw new TypeError(
|
|
205
|
+
`Invalid command: Must be lowercase. Received: "${command}"`
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Rule: No empty segments
|
|
210
|
+
if (command.includes('//')) {
|
|
211
|
+
throw new TypeError(
|
|
212
|
+
`Invalid command: Must not contain empty segments (e.g., "//"). Received: "${command}"`
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// If no error was thrown, the command is valid.
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Assert that the input is a Uint8Array.
|
|
221
|
+
*
|
|
222
|
+
* @param {unknown} nonce The value to check.
|
|
223
|
+
* @throws {Error} If nonce is not a Uint8Array.
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts twoslash
|
|
228
|
+
* import { assertNonce } from 'iso-ucan/utils'
|
|
229
|
+
* assertNonce(new Uint8Array([1,2,3])) // ok
|
|
230
|
+
* assertNonce('foo') // throws
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export function assertNonce(nonce) {
|
|
234
|
+
if (!(nonce instanceof Uint8Array)) {
|
|
235
|
+
throw new TypeError(
|
|
236
|
+
`Invalid nonce: Expected Uint8Array, got ${Object.prototype.toString.call(nonce)}`
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Assert that args is a JSON serializable object.
|
|
242
|
+
*
|
|
243
|
+
* @param {unknown} args
|
|
244
|
+
*/
|
|
245
|
+
export function assertArgs(args) {
|
|
246
|
+
const parsed = cborObject.safeParse(args)
|
|
247
|
+
if (parsed.error) {
|
|
248
|
+
const pretty = z.prettifyError(parsed.error)
|
|
249
|
+
|
|
250
|
+
throw new TypeError(`Invalid args: ${pretty}`, { cause: parsed.error })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Assert that the input is a Record<PropertyKey, unknown>.
|
|
256
|
+
*
|
|
257
|
+
* @param {unknown} meta - The value to check.
|
|
258
|
+
* @throws {Error} If meta is not a Record<PropertyKey, unknown>.
|
|
259
|
+
* @returns {void}
|
|
260
|
+
*/
|
|
261
|
+
export function assertMeta(meta) {
|
|
262
|
+
if (meta) {
|
|
263
|
+
const parsed = cborObject.safeParse(meta)
|
|
264
|
+
if (parsed.error) {
|
|
265
|
+
const pretty = z.prettifyError(parsed.error)
|
|
266
|
+
|
|
267
|
+
throw new TypeError(`Invalid meta: ${pretty}`, { cause: parsed.error })
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Assert that the input is not revoked.
|
|
274
|
+
*
|
|
275
|
+
* @param {import('multiformats/cid').CID} cid
|
|
276
|
+
* @param {(cid: CID) => Promise<boolean>} [isRevokedFn]
|
|
277
|
+
*/
|
|
278
|
+
export async function assertNotRevoked(cid, isRevokedFn) {
|
|
279
|
+
isRevokedFn = isRevokedFn ?? (async () => false)
|
|
280
|
+
const isRevoked = await isRevokedFn(cid)
|
|
281
|
+
if (isRevoked) {
|
|
282
|
+
throw new Error('UCAN revoked')
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Expiration or TTL (default 300 seconds)
|
|
288
|
+
*
|
|
289
|
+
* @param {object} options
|
|
290
|
+
* @param {number | null} [options.exp]
|
|
291
|
+
* @param {number} [options.ttl]
|
|
292
|
+
*/
|
|
293
|
+
export function expOrTtl({ exp, ttl }) {
|
|
294
|
+
if (exp === null) {
|
|
295
|
+
return exp
|
|
296
|
+
}
|
|
297
|
+
const currentTimeInSeconds = Math.floor(Date.now() / 1000)
|
|
298
|
+
const expiration = exp ?? currentTimeInSeconds + (ttl ?? 300)
|
|
299
|
+
|
|
300
|
+
return expiration
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export const cborValue =
|
|
304
|
+
/** @type {typeof z.lazy<z.ZodType<import('../src/types.js').CborValue>>} */ (
|
|
305
|
+
z.lazy
|
|
306
|
+
)(() => {
|
|
307
|
+
return z.union([
|
|
308
|
+
z.string(),
|
|
309
|
+
z.number(),
|
|
310
|
+
z.boolean(),
|
|
311
|
+
z.null(),
|
|
312
|
+
z.boolean(),
|
|
313
|
+
z.bigint(),
|
|
314
|
+
z.instanceof(Uint8Array),
|
|
315
|
+
z.array(cborValue),
|
|
316
|
+
z.record(z.string(), cborValue),
|
|
317
|
+
])
|
|
318
|
+
})
|
|
319
|
+
export const cborObject = z.record(z.string(), cborValue)
|
|
320
|
+
|
|
321
|
+
export const selector = z.union([
|
|
322
|
+
z.literal('.'),
|
|
323
|
+
z.templateLiteral([z.literal('.'), z.string()]),
|
|
324
|
+
])
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @typedef {z.infer<typeof selector>} Selector
|
|
328
|
+
*/
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Statement
|
|
332
|
+
*/
|
|
333
|
+
export const statement =
|
|
334
|
+
/** @type {typeof z.lazy<z.ZodType<import('../src/types.js').Statement<unknown>>>} */ (
|
|
335
|
+
z.lazy
|
|
336
|
+
// @ts-ignore
|
|
337
|
+
)(() => {
|
|
338
|
+
return z.union([
|
|
339
|
+
z.tuple([
|
|
340
|
+
z.union([z.literal('=='), z.literal('!=')]),
|
|
341
|
+
selector,
|
|
342
|
+
z.unknown(),
|
|
343
|
+
]), // Equality
|
|
344
|
+
z.tuple([
|
|
345
|
+
z.union([
|
|
346
|
+
z.literal('<'),
|
|
347
|
+
z.literal('<='),
|
|
348
|
+
z.literal('>'),
|
|
349
|
+
z.literal('>='),
|
|
350
|
+
]),
|
|
351
|
+
selector,
|
|
352
|
+
z.number(),
|
|
353
|
+
]), // Inequality
|
|
354
|
+
z.tuple([z.literal('like'), selector, z.string()]), // Like
|
|
355
|
+
z.tuple([z.literal('not'), statement]),
|
|
356
|
+
z.tuple([z.literal('and'), z.array(statement)]),
|
|
357
|
+
z.tuple([z.literal('or'), z.array(statement)]),
|
|
358
|
+
z.tuple([z.literal('all'), z.string(), statement]),
|
|
359
|
+
z.tuple([z.literal('any'), z.string(), statement]),
|
|
360
|
+
])
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
export const policySchema = z.array(statement)
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* @param {unknown} policy
|
|
367
|
+
*/
|
|
368
|
+
export function assertPolicy(policy) {
|
|
369
|
+
const parsed = policySchema.safeParse(policy)
|
|
370
|
+
if (parsed.error) {
|
|
371
|
+
const pretty = z.prettifyError(parsed.error)
|
|
372
|
+
|
|
373
|
+
throw new TypeError(`Invalid policy: ${pretty}`, { cause: parsed.error })
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/varsig.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { IOBuffer } from 'iobuffer'
|
|
2
|
+
import { hex } from 'iso-base/rfc4648'
|
|
3
|
+
import { equals } from 'iso-base/utils'
|
|
4
|
+
import { varint } from 'iso-base/varint'
|
|
5
|
+
|
|
6
|
+
export const VARSIG = 0x34
|
|
7
|
+
export const VERSION = 0x01
|
|
8
|
+
export const EIP191_ENCODING = 0xe191
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @type {Record<number, import('./types.js').VarsigEncoding>}
|
|
12
|
+
*/
|
|
13
|
+
export const CODE_ENCODING = /** @types {const} */ {
|
|
14
|
+
0x5f: 'RAW',
|
|
15
|
+
0x71: 'DAG-CBOR',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @type {Record<number, string>}
|
|
20
|
+
*/
|
|
21
|
+
export const SIG_PREFIX = /** @type {const} */ ({
|
|
22
|
+
0x12_05: 'RSA',
|
|
23
|
+
0xed: 'EdDSA',
|
|
24
|
+
0xec: 'ECDSA',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Varint encoding for signature algorithms and encodings.
|
|
29
|
+
*
|
|
30
|
+
* @type {Record<'VARSIG' | 'VERSION' | import('./types.js').VarsigEncoding | import('./types.js').VarsigAlgorithm, number[] >}
|
|
31
|
+
*/
|
|
32
|
+
const VARINT = {
|
|
33
|
+
VARSIG: [52],
|
|
34
|
+
VERSION: [1],
|
|
35
|
+
|
|
36
|
+
RAW: [95],
|
|
37
|
+
'DAG-CBOR': [113],
|
|
38
|
+
|
|
39
|
+
/** Presets */
|
|
40
|
+
// rsa(2b)+SHA2-256(1b)+256(1b)
|
|
41
|
+
RS256: [133, 36, 18, 128, 2],
|
|
42
|
+
// eddsa(2b)+edwards25519(2b)+SHA2-512(1b)
|
|
43
|
+
Ed25519: [237, 1, 237, 1, 19],
|
|
44
|
+
// ECDSA(2b) + p256(2b) + SHA2-256(1b)
|
|
45
|
+
ES256: [236, 1, 128, 36, 18],
|
|
46
|
+
// ECDSA(2b) + p384(2b) + SHA2-384(1b)
|
|
47
|
+
ES384: [236, 1, 129, 36, 32],
|
|
48
|
+
// ECDSA(2b) + p512(2b) + SHA2-512(1b)
|
|
49
|
+
ES512: [236, 1, 130, 36, 19],
|
|
50
|
+
// ECDSA(2b) + secp256k1(2b) + SHA2-256(1b)
|
|
51
|
+
ES256K: [236, 1, 231, 1, 18],
|
|
52
|
+
// ECDSA(2b) + secp256k1(2b) + keccak-256(1b) + eip191-encoding(3b)
|
|
53
|
+
EIP191: [236, 1, 231, 1, 27, 145, 195, 3],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('./types.js').VarsigOptions} options
|
|
58
|
+
*/
|
|
59
|
+
export function encode(options) {
|
|
60
|
+
const enc = VARINT[options.enc]
|
|
61
|
+
if (!enc) {
|
|
62
|
+
throw new TypeError(`Unsupported encoding ${options.enc}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const alg = VARINT[options.alg]
|
|
66
|
+
if (!alg) {
|
|
67
|
+
throw new TypeError(`Unsupported algorithm ${options.alg}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return Uint8Array.from([...VARINT.VARSIG, ...VARINT.VERSION, ...alg, ...enc])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {Uint8Array<ArrayBufferLike>} bytes
|
|
75
|
+
*/
|
|
76
|
+
function getEncoding(bytes) {
|
|
77
|
+
const [code] = varint.decode(bytes)
|
|
78
|
+
const enc = CODE_ENCODING[code]
|
|
79
|
+
if (!enc) {
|
|
80
|
+
throw new TypeError(`Unsupported encoding 0x${code.toString(16)}`)
|
|
81
|
+
}
|
|
82
|
+
return enc
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {Uint8Array<ArrayBufferLike>} bytes
|
|
87
|
+
* @param {import('./types.js').VarsigAlgorithm[]} presets
|
|
88
|
+
*/
|
|
89
|
+
function getPreset(bytes, presets) {
|
|
90
|
+
for (const preset of presets) {
|
|
91
|
+
if (equals(bytes, Uint8Array.from(VARINT[preset]))) {
|
|
92
|
+
return preset
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new TypeError(`Unsupported preset ${bytes}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
/**
|
|
100
|
+
* Decode varsig header
|
|
101
|
+
*
|
|
102
|
+
* @param {Uint8Array} buf
|
|
103
|
+
* @returns {import('./types.js').DecodeOutput}
|
|
104
|
+
*/
|
|
105
|
+
export function decode(buf) {
|
|
106
|
+
const bytes = new IOBuffer(buf)
|
|
107
|
+
const varsig = bytes.readBytes(1)
|
|
108
|
+
if (varsig[0] !== VARINT.VARSIG[0]) {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
`Invalid varsig sigil expected 0x${VARSIG.toString(16)} got 0x${hex.encode(varsig)}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const version = bytes.readBytes(1)
|
|
115
|
+
if (version[0] !== VARINT.VERSION[0]) {
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
`Invalid version sigil expected 0x${VERSION.toString(16)} got 0x${hex.encode(version)}`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
const [code] = varint.decode(buf, 2)
|
|
121
|
+
|
|
122
|
+
switch (SIG_PREFIX[code]) {
|
|
123
|
+
case 'RSA': {
|
|
124
|
+
return {
|
|
125
|
+
alg: getPreset(bytes.readBytes(5), ['RS256']),
|
|
126
|
+
enc: getEncoding(bytes.readBytes(1)),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
case 'EdDSA': {
|
|
130
|
+
return {
|
|
131
|
+
alg: getPreset(bytes.readBytes(5), ['Ed25519']),
|
|
132
|
+
enc: getEncoding(bytes.readBytes(1)),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
case 'ECDSA': {
|
|
136
|
+
const isEIP191 = varint.decode(buf, 2 + 5)[0] === EIP191_ENCODING
|
|
137
|
+
if (isEIP191) {
|
|
138
|
+
return {
|
|
139
|
+
alg: getPreset(bytes.readBytes(8), ['EIP191']),
|
|
140
|
+
enc: getEncoding(bytes.readBytes(1)),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
alg: getPreset(bytes.readBytes(5), [
|
|
146
|
+
'ES256',
|
|
147
|
+
'ES256K',
|
|
148
|
+
'ES384',
|
|
149
|
+
'ES512',
|
|
150
|
+
]),
|
|
151
|
+
enc: getEncoding(bytes.readBytes(1)),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
default:
|
|
155
|
+
throw new TypeError(`Unsupported algorithm ${code}`)
|
|
156
|
+
}
|
|
157
|
+
}
|