@sphereon/ssi-sdk.credential-vcdm2-jose-provider 0.33.1-feature.jose.vcdm.59

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,655 @@
1
+ import canonicalizeData from 'canonicalize'
2
+ import { DIDDocument, DIDResolutionResult, parse, ParsedDID, Resolvable, VerificationMethod } from 'did-resolver'
3
+ import SignerAlg from './SignerAlgorithm.js'
4
+ import { decodeBase64url, EcdsaSignature, encodeBase64url, KNOWN_JWA, SUPPORTED_PUBLIC_KEY_TYPES } from './util.js'
5
+ import VerifierAlgorithm from './VerifierAlgorithm.js'
6
+ import { JWT_ERROR } from 'did-jwt'
7
+
8
+
9
+ export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
10
+ export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
11
+
12
+ export type ProofPurposeTypes =
13
+ | 'assertionMethod'
14
+ | 'authentication'
15
+ // | 'keyAgreement' // keyAgreement VerificationMethod should not be used for signing
16
+ | 'capabilityDelegation'
17
+ | 'capabilityInvocation'
18
+
19
+ export interface JWTOptions {
20
+ issuer: string
21
+ signer: Signer
22
+ /**
23
+ * @deprecated Please use `header.alg` to specify the JWT algorithm.
24
+ */
25
+ alg?: string
26
+ expiresIn?: number
27
+ canonicalize?: boolean
28
+ }
29
+
30
+ export interface JWTVerifyOptions {
31
+ /** @deprecated Please use `proofPurpose: 'authentication' instead` */
32
+ auth?: boolean
33
+ audience?: string
34
+ callbackUrl?: string
35
+ resolver?: Resolvable
36
+ skewTime?: number
37
+ /** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */
38
+ proofPurpose?: ProofPurposeTypes
39
+ policies?: JWTVerifyPolicies
40
+ didAuthenticator?: DIDAuthenticator
41
+ }
42
+
43
+ /**
44
+ * Overrides the different types of checks performed on the JWT besides the signature check
45
+ */
46
+ export interface JWTVerifyPolicies {
47
+ // overrides the timestamp against which the validity interval is checked
48
+ now?: number
49
+ // when set to false, the timestamp checks ignore the Not Before(`nbf`) property
50
+ nbf?: boolean
51
+ // when set to false, the timestamp checks ignore the Issued At(`iat`) property
52
+ iat?: boolean
53
+ // when set to false, the timestamp checks ignore the Expires At(`exp`) property
54
+ exp?: boolean
55
+ // when set to false, the JWT audience check is skipped
56
+ aud?: boolean
57
+ }
58
+
59
+ export interface JWSCreationOptions {
60
+ canonicalize?: boolean
61
+ }
62
+
63
+ export interface DIDAuthenticator {
64
+ authenticators: VerificationMethod[]
65
+ issuer: string
66
+ didResolutionResult: DIDResolutionResult
67
+ }
68
+
69
+ export interface JWTHeader {
70
+ typ: 'JWT'
71
+ alg: string
72
+
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ [x: string]: any
75
+ }
76
+
77
+ export interface JWTPayload {
78
+ iss?: string
79
+ sub?: string
80
+ aud?: string | string[]
81
+ iat?: number
82
+ nbf?: number
83
+ exp?: number
84
+ rexp?: number
85
+
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ [x: string]: any
88
+ }
89
+
90
+ export interface JWTDecoded {
91
+ header: JWTHeader
92
+ payload: JWTPayload
93
+ signature: string
94
+ data: string
95
+ }
96
+
97
+ export interface JWSDecoded {
98
+ header: JWTHeader
99
+ payload: string
100
+ signature: string
101
+ data: string
102
+ }
103
+
104
+ /**
105
+ * Result object returned by {@link verifyJWT}
106
+ */
107
+ export interface JWTVerified {
108
+ /**
109
+ * Set to true for a JWT that passes all the required checks minus any verification overrides.
110
+ */
111
+ verified: true
112
+
113
+ /**
114
+ * The decoded JWT payload
115
+ */
116
+ payload: Partial<JWTPayload>
117
+
118
+ /**
119
+ * The result of resolving the issuer DID
120
+ */
121
+ didResolutionResult: DIDResolutionResult
122
+
123
+ /**
124
+ * the issuer DID
125
+ */
126
+ issuer: string
127
+
128
+ /**
129
+ * The public key of the issuer that matches the JWT signature
130
+ */
131
+ signer: VerificationMethod
132
+
133
+ /**
134
+ * The original JWT that was verified
135
+ */
136
+ jwt: string
137
+
138
+ /**
139
+ * Any overrides that were used during verification
140
+ */
141
+ policies?: JWTVerifyPolicies
142
+ }
143
+
144
+ export const SELF_ISSUED_V2 = 'https://self-issued.me/v2'
145
+ export const SELF_ISSUED_V2_VC_INTEROP = 'https://self-issued.me/v2/openid-vc' // https://identity.foundation/jwt-vc-presentation-profile/#id-token-validation
146
+ export const SELF_ISSUED_V0_1 = 'https://self-issued.me'
147
+
148
+ type LegacyVerificationMethod = { publicKey?: string }
149
+
150
+ const defaultAlg: KNOWN_JWA = 'ES256K'
151
+ const DID_JSON = 'application/did+json'
152
+
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ function encodeSection(data: any, shouldCanonicalize = false): string {
155
+ if (shouldCanonicalize) {
156
+ return encodeBase64url(<string>canonicalizeData(data))
157
+ } else {
158
+ return encodeBase64url(JSON.stringify(data))
159
+ }
160
+ }
161
+
162
+ export const NBF_SKEW = 300
163
+
164
+ function decodeJWS(jws: string): JWSDecoded {
165
+ const parts = jws.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
166
+ if (parts) {
167
+ return {
168
+ header: JSON.parse(decodeBase64url(parts[1])),
169
+ payload: parts[2],
170
+ signature: parts[3],
171
+ data: `${parts[1]}.${parts[2]}`,
172
+ }
173
+ }
174
+ throw new Error('invalid_argument: Incorrect format JWS')
175
+ }
176
+
177
+ /**
178
+ * Decodes a JWT and returns an object representing the payload
179
+ *
180
+ * @example
181
+ * decodeJWT('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE1...')
182
+ *
183
+ * @param {String} jwt a JSON Web Token to verify
184
+ * @param {Object} [recurse] whether to recurse into the payload to decode any nested JWTs
185
+ * @return {Object} a JS object representing the decoded JWT
186
+ */
187
+ export function decodeJWT(jwt: string, recurse = true): JWTDecoded {
188
+ if (!jwt) throw new Error('invalid_argument: no JWT passed into decodeJWT')
189
+ try {
190
+ const jws = decodeJWS(jwt)
191
+ const decodedJwt: JWTDecoded = Object.assign(jws, { payload: JSON.parse(decodeBase64url(jws.payload)) })
192
+ const iss = decodedJwt.payload.iss
193
+
194
+ if (decodedJwt.header.cty === 'JWT' && recurse) {
195
+ const innerDecodedJwt = decodeJWT(decodedJwt.payload.jwt)
196
+
197
+ if (innerDecodedJwt.payload.iss !== iss) throw new Error(`${JWT_ERROR.INVALID_JWT}: multiple issuers`)
198
+ return innerDecodedJwt
199
+ }
200
+ return decodedJwt
201
+ } catch (e) {
202
+ throw new Error(`invalid_argument: ${JWT_ERROR.INVALID_JWT}: ${e}`)
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Creates a signed JWS given a payload, a signer, and an optional header.
208
+ *
209
+ * @example
210
+ * const signer = ES256KSigner(process.env.PRIVATE_KEY)
211
+ * const jws = await createJWS({ my: 'payload' }, signer)
212
+ *
213
+ * @param {Object} payload payload object
214
+ * @param {Signer} signer a signer, see `ES256KSigner or `EdDSASigner`
215
+ * @param {Object} header optional object to specify or customize the JWS header
216
+ * @param {Object} options can be used to trigger automatic canonicalization of header and
217
+ * payload properties
218
+ * @return {Promise<string>} a Promise which resolves to a JWS string or rejects with an error
219
+ */
220
+ export async function createJWS(
221
+ payload: string | Partial<JWTPayload>,
222
+ signer: Signer,
223
+ header: Partial<JWTHeader> = {},
224
+ options: JWSCreationOptions = {}
225
+ ): Promise<string> {
226
+ if (!header.alg) header.alg = defaultAlg
227
+ const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload, options.canonicalize)
228
+ const signingInput: string = [encodeSection(header, options.canonicalize), encodedPayload].join('.')
229
+
230
+ const jwtSigner: SignerAlgorithm = SignerAlg(header.alg)
231
+ const signature: string = await jwtSigner(signingInput, signer)
232
+
233
+ // JWS Compact Serialization
234
+ // https://www.rfc-editor.org/rfc/rfc7515#section-7.1
235
+ return [signingInput, signature].join('.')
236
+ }
237
+
238
+ /**
239
+ * Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is
240
+ * over.
241
+ *
242
+ * @example
243
+ * const signer = ES256KSigner(process.env.PRIVATE_KEY)
244
+ * createJWT({address: '5A8bRWU3F7j3REx3vkJ...', signer}, {key1: 'value', key2: ..., ... }).then(jwt => {
245
+ * ...
246
+ * })
247
+ *
248
+ * @param {Object} payload payload object
249
+ * @param {Object} [options] an unsigned credential object
250
+ * @param {String} options.issuer The DID of the issuer (signer) of JWT
251
+ * @param {String} options.alg [DEPRECATED] The JWT signing algorithm to use. Supports:
252
+ * [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K. Please use `header.alg` to specify the algorithm
253
+ * @param {Signer} options.signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
254
+ * @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
255
+ * @param {Object} header optional object to specify or customize the JWT header
256
+ * @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or
257
+ * rejects with an error
258
+ */
259
+ export async function createJWT(
260
+ payload: Partial<JWTPayload>,
261
+ { issuer, signer, alg, expiresIn, canonicalize }: JWTOptions,
262
+ header: Partial<JWTHeader> = {}
263
+ ): Promise<string> {
264
+ if (!signer) throw new Error('missing_signer: No Signer functionality has been configured')
265
+ if (!issuer) throw new Error('missing_issuer: No issuing DID has been configured')
266
+ if (!header.typ) header.typ = 'JWT'
267
+ if (!header.alg) header.alg = alg
268
+ const timestamps: Partial<JWTPayload> = {
269
+ iat: Math.floor(Date.now() / 1000),
270
+ exp: undefined,
271
+ }
272
+ if (expiresIn) {
273
+ if (typeof expiresIn === 'number') {
274
+ timestamps.exp = <number>(payload.nbf || timestamps.iat) + Math.floor(expiresIn)
275
+ } else {
276
+ throw new Error('invalid_argument: JWT expiresIn is not a number')
277
+ }
278
+ }
279
+ const fullPayload = { ...timestamps, ...payload, iss: issuer }
280
+ return createJWS(fullPayload, signer, header, { canonicalize })
281
+ }
282
+
283
+ /**
284
+ * Creates a multi-signature signed JWT given multiple issuers and their corresponding signers, and a payload for
285
+ * which the signature is over.
286
+ *
287
+ * @example
288
+ * const signer = ES256KSigner(process.env.PRIVATE_KEY)
289
+ * createJWT({address: '5A8bRWU3F7j3REx3vkJ...', signer}, {key1: 'value', key2: ..., ... }).then(jwt => {
290
+ * ...
291
+ * })
292
+ *
293
+ * @param {Object} payload payload object
294
+ * @param {Object} [options] an unsigned credential object
295
+ * @param {boolean} options.expiresIn optional flag to denote the expiration time
296
+ * @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
297
+ * @param {Object[]} issuers array of the issuers, their signers and algorithms
298
+ * @param {string} issuers[].issuer The DID of the issuer (signer) of JWT
299
+ * @param {Signer} issuers[].signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
300
+ * @param {String} issuers[].alg [DEPRECATED] The JWT signing algorithm to use. Supports:
301
+ * [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K. Please use `header.alg` to specify the algorithm
302
+ * @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or
303
+ * rejects with an error
304
+ */
305
+ export async function createMultisignatureJWT(
306
+ payload: Partial<JWTPayload>,
307
+ { expiresIn, canonicalize }: Partial<JWTOptions>,
308
+ issuers: { issuer: string; signer: Signer; alg: string }[]
309
+ ): Promise<string> {
310
+ if (issuers.length === 0) throw new Error('invalid_argument: must provide one or more issuers')
311
+
312
+ let payloadResult: Partial<JWTPayload> = payload
313
+
314
+ let jwt = ''
315
+ for (let i = 0; i < issuers.length; i++) {
316
+ const issuer = issuers[i]
317
+
318
+ const header: Partial<JWTHeader> = {
319
+ typ: 'JWT',
320
+ alg: issuer.alg,
321
+ }
322
+
323
+ // Create nested JWT
324
+ // See Point 5 of https://www.rfc-editor.org/rfc/rfc7519#section-7.1
325
+ // After the first JWT is created (the first JWS), the next JWT is created by inputting the previous JWT as the
326
+ // payload
327
+ if (i !== 0) {
328
+ header.cty = 'JWT'
329
+ }
330
+
331
+ jwt = await createJWT(payloadResult, { ...issuer, canonicalize, expiresIn }, header)
332
+
333
+ payloadResult = { jwt }
334
+ }
335
+ return jwt
336
+ }
337
+
338
+ export function verifyJWTDecoded(
339
+ { header, payload, data, signature }: JWTDecoded,
340
+ pubKeys: VerificationMethod | VerificationMethod[]
341
+ ): VerificationMethod {
342
+ if (!Array.isArray(pubKeys)) pubKeys = [pubKeys]
343
+
344
+ const iss = payload.iss
345
+ let recurse = true
346
+ do {
347
+ if (iss !== payload.iss) throw new Error(`${JWT_ERROR.INVALID_JWT}: multiple issuers`)
348
+
349
+ try {
350
+ const result = VerifierAlgorithm(header.alg)(data, signature, pubKeys)
351
+
352
+ return result
353
+ } catch (e) {
354
+ if (!(e as Error).message.startsWith(JWT_ERROR.INVALID_SIGNATURE)) throw e
355
+ }
356
+
357
+ // TODO probably best to create copy objects than replace reference objects
358
+ if (header.cty !== 'JWT') {
359
+ recurse = false
360
+ } else {
361
+ ;({ payload, header, signature, data } = decodeJWT(payload.jwt, false))
362
+ }
363
+ } while (recurse)
364
+
365
+ throw new Error(`${JWT_ERROR.INVALID_SIGNATURE}: no matching public key found`)
366
+ }
367
+
368
+ export function verifyJWSDecoded(
369
+ { header, data, signature }: JWSDecoded,
370
+ pubKeys: VerificationMethod | VerificationMethod[]
371
+ ): VerificationMethod {
372
+ if (!Array.isArray(pubKeys)) pubKeys = [pubKeys]
373
+ const signer: VerificationMethod = VerifierAlgorithm(header.alg)(data, signature, pubKeys)
374
+ return signer
375
+ }
376
+
377
+ /**
378
+ * Verifies given JWS. If the JWS is valid, returns the public key that was
379
+ * used to sign the JWS, or throws an `Error` if none of the `pubKeys` match.
380
+ *
381
+ * @example
382
+ * const pubKey = verifyJWS('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', { publicKeyHex: '0x12341...' })
383
+ *
384
+ * @param {String} jws A JWS string to verify
385
+ * @param {Array<VerificationMethod> | VerificationMethod} pubKeys The public keys used to verify the JWS
386
+ * @return {VerificationMethod} The public key used to sign the JWS
387
+ */
388
+ export function verifyJWS(jws: string, pubKeys: VerificationMethod | VerificationMethod[]): VerificationMethod {
389
+ const jwsDecoded: JWSDecoded = decodeJWS(jws)
390
+ return verifyJWSDecoded(jwsDecoded, pubKeys)
391
+ }
392
+
393
+ /**
394
+ * Verifies given JWT. If the JWT is valid, the promise returns an object including the JWT, the payload of the JWT,
395
+ * and the DID document of the issuer of the JWT.
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * verifyJWT(
400
+ * 'did:uport:eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....',
401
+ * {audience: '5A8bRWU3F7j3REx3vkJ...', callbackUrl: 'https://...'}
402
+ * ).then(obj => {
403
+ * const did = obj.did // DID of signer
404
+ * const payload = obj.payload
405
+ * const doc = obj.didResolutionResult.didDocument // DID Document of issuer
406
+ * const jwt = obj.jwt
407
+ * const signerKeyId = obj.signer.id // ID of key in DID document that signed JWT
408
+ * ...
409
+ * })
410
+ * ```
411
+ *
412
+ * @param {String} jwt a JSON Web Token to verify
413
+ * @param {Object} [options] an unsigned credential object
414
+ * @param {Boolean} options.auth Require signer to be listed in the authentication section of the
415
+ * DID document (for Authentication purposes)
416
+ * @param {String} options.audience DID of the recipient of the JWT
417
+ * @param {String} options.callbackUrl callback url in JWT
418
+ * @return {Promise<Object, Error>} a promise which resolves with a response object or rejects with an
419
+ * error
420
+ */
421
+ export async function verifyJWT(
422
+ jwt: string,
423
+ options: JWTVerifyOptions = {
424
+ resolver: undefined,
425
+ auth: undefined,
426
+ audience: undefined,
427
+ callbackUrl: undefined,
428
+ skewTime: undefined,
429
+ proofPurpose: undefined,
430
+ policies: {},
431
+ didAuthenticator: undefined,
432
+ }
433
+ ): Promise<JWTVerified> {
434
+ if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
435
+ const { payload, header/*, signature, data*/ }: JWTDecoded = decodeJWT(jwt, false)
436
+ const proofPurpose: ProofPurposeTypes | undefined = Object.prototype.hasOwnProperty.call(options, 'auth')
437
+ ? options.auth
438
+ ? 'authentication'
439
+ : undefined
440
+ : options.proofPurpose
441
+
442
+ let didUrl: string | undefined
443
+
444
+ if (!payload.iss && !payload.client_id) {
445
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT iss or client_id are required`)
446
+ }
447
+
448
+ if (options.didAuthenticator) {
449
+ didUrl = options.didAuthenticator.issuer
450
+ } else if (payload.iss === SELF_ISSUED_V2 || payload.iss === SELF_ISSUED_V2_VC_INTEROP) {
451
+ if (!payload.sub) {
452
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT sub is required`)
453
+ }
454
+ if (typeof payload.sub_jwk === 'undefined') {
455
+ didUrl = payload.sub
456
+ } else {
457
+ didUrl = (header.kid || '').split('#')[0]
458
+ }
459
+ } else if (payload.iss === SELF_ISSUED_V0_1) {
460
+ if (!payload.did) {
461
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT did is required`)
462
+ }
463
+ didUrl = payload.did
464
+ } else if (!payload.iss && payload.scope === 'openid' && payload.redirect_uri) {
465
+ // SIOP Request payload
466
+ // https://identity.foundation/jwt-vc-presentation-profile/#self-issued-op-request-object
467
+ if (!payload.client_id) {
468
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT client_id is required`)
469
+ }
470
+ didUrl = payload.client_id
471
+ } else {
472
+ didUrl = payload.iss
473
+ }
474
+
475
+ if (!didUrl) {
476
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: No DID has been found in the JWT`)
477
+ }
478
+
479
+ let authenticators: VerificationMethod[]
480
+ let issuer: string
481
+ let didResolutionResult: DIDResolutionResult
482
+ if (options.didAuthenticator) {
483
+ ;({ didResolutionResult, authenticators, issuer } = options.didAuthenticator)
484
+ } else {
485
+ ;({ didResolutionResult, authenticators, issuer } = await resolveAuthenticator(
486
+ options.resolver,
487
+ header.alg,
488
+ didUrl,
489
+ proofPurpose
490
+ ))
491
+ // Add to options object for recursive reference
492
+ options.didAuthenticator = { didResolutionResult, authenticators, issuer }
493
+ }
494
+
495
+ const { did } = parse(didUrl) as ParsedDID
496
+
497
+ let signer: VerificationMethod | null = null
498
+
499
+ if (did !== didUrl) {
500
+ const authenticator = authenticators.find((auth) => auth.id === didUrl)
501
+ if (!authenticator) {
502
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: No authenticator found for did URL ${didUrl}`)
503
+ }
504
+
505
+ // signer = await verifyProof(jwt, { payload, header, signature, data }, authenticator, options)
506
+ } else {
507
+ let i = 0
508
+ while (!signer && i < authenticators.length) {
509
+ // const authenticator = authenticators[i]
510
+ try {
511
+ // signer = await verifyProof(jwt, { payload, header, signature, data }, authenticator, options)
512
+ } catch (e) {
513
+ if (!(e as Error).message.includes(JWT_ERROR.INVALID_SIGNATURE) || i === authenticators.length - 1) throw e
514
+ }
515
+
516
+ i++
517
+ }
518
+ }
519
+
520
+ if (signer) {
521
+ const now: number = typeof options.policies?.now === 'number' ? options.policies.now : Math.floor(Date.now() / 1000)
522
+ const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW
523
+
524
+ const nowSkewed = now + skewTime
525
+ if (options.policies?.nbf !== false && payload.nbf) {
526
+ if (payload.nbf > nowSkewed) {
527
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid before nbf: ${payload.nbf}`)
528
+ }
529
+ } else if (options.policies?.iat !== false && payload.iat && payload.iat > nowSkewed) {
530
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid yet (issued in the future) iat: ${payload.iat}`)
531
+ }
532
+ if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) {
533
+ throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT has expired: exp: ${payload.exp} < now: ${now}`)
534
+ }
535
+ if (options.policies?.aud !== false && payload.aud) {
536
+ if (!options.audience && !options.callbackUrl) {
537
+ throw new Error(
538
+ `${JWT_ERROR.INVALID_AUDIENCE}: JWT audience is required but your app address has not been configured`
539
+ )
540
+ }
541
+ const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
542
+ const matchedAudience = audArray.find((item) => options.audience === item || options.callbackUrl === item)
543
+
544
+ if (typeof matchedAudience === 'undefined') {
545
+ throw new Error(`${JWT_ERROR.INVALID_AUDIENCE}: JWT audience does not match your DID or callback url`)
546
+ }
547
+ }
548
+
549
+ return { verified: true, payload, didResolutionResult, issuer, signer, jwt, policies: options.policies }
550
+ }
551
+ throw new Error(
552
+ `${JWT_ERROR.INVALID_SIGNATURE}: JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.`
553
+ )
554
+ }
555
+
556
+ /**
557
+ * Resolves relevant public keys or other authenticating material used to verify signature from the DID document of
558
+ * provided DID
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * resolveAuthenticator(resolver, 'ES256K', 'did:uport:2nQtiQG6Cgm1GYTBaaKAgr76uY7iSexUkqX').then(obj => {
563
+ * const payload = obj.payload
564
+ * const profile = obj.profile
565
+ * const jwt = obj.jwt
566
+ * // ...
567
+ * })
568
+ * ```
569
+ *
570
+ * @param resolver - {Resolvable} a DID resolver function that can obtain the `DIDDocument` for the `issuer`
571
+ * @param alg - {String} a JWT algorithm
572
+ * @param issuer - {String} a Decentralized Identifier (DID) to lookup
573
+ * @param proofPurpose - {ProofPurposeTypes} *Optional* Use the verificationMethod linked in that section of the
574
+ * issuer DID document
575
+ * @return {Promise<DIDAuthenticator>} a promise which resolves with an object containing an array of authenticators
576
+ * or rejects with an error if none exist
577
+ */
578
+ export async function resolveAuthenticator(
579
+ resolver: Resolvable,
580
+ alg: string,
581
+ issuer: string,
582
+ proofPurpose?: ProofPurposeTypes
583
+ ): Promise<DIDAuthenticator> {
584
+ const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg as KNOWN_JWA]
585
+ if (!types || types.length === 0) {
586
+ throw new Error(`${JWT_ERROR.NOT_SUPPORTED}: No supported signature types for algorithm ${alg}`)
587
+ }
588
+ let didResult: DIDResolutionResult
589
+
590
+ const result = (await resolver.resolve(issuer, { accept: DID_JSON })) as unknown
591
+ // support legacy resolvers that do not produce DIDResolutionResult
592
+ if (Object.getOwnPropertyNames(result).indexOf('didDocument') === -1) {
593
+ didResult = {
594
+ didDocument: result as DIDDocument,
595
+ didDocumentMetadata: {},
596
+ didResolutionMetadata: { contentType: DID_JSON },
597
+ }
598
+ } else {
599
+ didResult = result as DIDResolutionResult
600
+ }
601
+
602
+ if (didResult.didResolutionMetadata?.error || didResult.didDocument == null) {
603
+ const { error, message } = didResult.didResolutionMetadata
604
+ throw new Error(
605
+ `${JWT_ERROR.RESOLVER_ERROR}: Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`
606
+ )
607
+ }
608
+
609
+ const getPublicKeyById = (verificationMethods: VerificationMethod[], pubid?: string): VerificationMethod | null => {
610
+ const filtered = verificationMethods.filter(({ id }) => pubid === id)
611
+ return filtered.length > 0 ? filtered[0] : null
612
+ }
613
+
614
+ let publicKeysToCheck: VerificationMethod[] = [
615
+ ...(didResult?.didDocument?.verificationMethod || []),
616
+ ...(didResult?.didDocument?.publicKey || []),
617
+ ]
618
+ if (typeof proofPurpose === 'string') {
619
+ // support legacy DID Documents that do not list assertionMethod
620
+ if (
621
+ proofPurpose.startsWith('assertion') &&
622
+ !Object.getOwnPropertyNames(didResult?.didDocument).includes('assertionMethod')
623
+ ) {
624
+ didResult.didDocument = { ...(<DIDDocument>didResult.didDocument) }
625
+ didResult.didDocument.assertionMethod = [...publicKeysToCheck.map((pk) => pk.id)]
626
+ }
627
+
628
+ publicKeysToCheck = (didResult.didDocument[proofPurpose] || [])
629
+ .map((verificationMethod) => {
630
+ if (typeof verificationMethod === 'string') {
631
+ return getPublicKeyById(publicKeysToCheck, verificationMethod)
632
+ } else if (typeof (<LegacyVerificationMethod>verificationMethod).publicKey === 'string') {
633
+ // this is a legacy format
634
+ return getPublicKeyById(publicKeysToCheck, (<LegacyVerificationMethod>verificationMethod).publicKey)
635
+ } else {
636
+ return <VerificationMethod>verificationMethod
637
+ }
638
+ })
639
+ .filter((key) => key != null) as VerificationMethod[]
640
+ }
641
+
642
+ const authenticators: VerificationMethod[] = publicKeysToCheck.filter(({ type }) =>
643
+ types.find((supported) => supported === type)
644
+ )
645
+
646
+ if (typeof proofPurpose === 'string' && (!authenticators || authenticators.length === 0)) {
647
+ throw new Error(
648
+ `${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`
649
+ )
650
+ }
651
+ if (!authenticators || authenticators.length === 0) {
652
+ throw new Error(`${JWT_ERROR.NO_SUITABLE_KEYS}: DID document for ${issuer} does not have public keys for ${alg}`)
653
+ }
654
+ return { authenticators, issuer, didResolutionResult: didResult }
655
+ }