@sphereon/ssi-sdk.credential-vcdm2-sdjwt-provider 0.34.1-next.85

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