@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.
- package/LICENSE +201 -0
- package/README.md +6 -0
- package/dist/index.cjs +896 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +865 -0
- package/dist/index.js.map +1 -0
- package/package.json +94 -0
- package/src/__tests__/issue-verify-flow-vcdm2-jose.test.ts +141 -0
- package/src/agent/CredentialProviderVcdm2SdJwt.ts +641 -0
- package/src/did-jwt/JWT.ts +634 -0
- package/src/did-jwt/SignerAlgorithm.ts +67 -0
- package/src/did-jwt/VerifierAlgorithm.ts +167 -0
- package/src/did-jwt/util.ts +407 -0
- package/src/index.ts +1 -0
|
@@ -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
|