@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.
- package/LICENSE +201 -0
- package/README.md +6 -0
- package/dist/index.cjs +772 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +741 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
- package/src/__tests__/issue-verify-flow-vcdm2-jose.test.ts +130 -0
- package/src/agent/CredentialProviderVcdm2Jose.ts +557 -0
- package/src/did-jwt/JWT.ts +655 -0
- package/src/did-jwt/SignerAlgorithm.ts +67 -0
- package/src/did-jwt/VerifierAlgorithm.ts +181 -0
- package/src/did-jwt/util.ts +423 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IAgentContext,
|
|
3
|
+
IDIDManager,
|
|
4
|
+
IIdentifier,
|
|
5
|
+
IKey,
|
|
6
|
+
IKeyManager,
|
|
7
|
+
IResolver,
|
|
8
|
+
IVerifyResult,
|
|
9
|
+
VerifiableCredential,
|
|
10
|
+
VerifierAgentContext,
|
|
11
|
+
} from '@veramo/core'
|
|
12
|
+
import {
|
|
13
|
+
type ICanIssueCredentialTypeArgs,
|
|
14
|
+
type ICanVerifyDocumentTypeArgs,
|
|
15
|
+
type ICreateVerifiableCredentialLDArgs,
|
|
16
|
+
type ICreateVerifiablePresentationLDArgs,
|
|
17
|
+
type IVcdmCredentialProvider,
|
|
18
|
+
type IVcdmIssuerAgentContext,
|
|
19
|
+
IVcdmVerifierAgentContext,
|
|
20
|
+
IVerifyCredentialLDArgs,
|
|
21
|
+
IVerifyPresentationLDArgs,
|
|
22
|
+
pickSigningKey,
|
|
23
|
+
preProcessCredentialPayload,
|
|
24
|
+
preProcessPresentation,
|
|
25
|
+
} from '@sphereon/ssi-sdk.credential-vcdm'
|
|
26
|
+
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
import { normalizeCredential, normalizePresentation, verifyPresentation as verifyPresentationJWT } from 'did-jwt-vc'
|
|
29
|
+
|
|
30
|
+
import { type Resolvable } from 'did-resolver'
|
|
31
|
+
|
|
32
|
+
import { decodeJWT, JWT_ERROR } from 'did-jwt'
|
|
33
|
+
|
|
34
|
+
import Debug from 'debug'
|
|
35
|
+
import { asArray, intersect, VerifiableCredentialSP, VerifiablePresentationSP } from '@sphereon/ssi-sdk.core'
|
|
36
|
+
import { contextHasPlugin } from '@sphereon/ssi-sdk.agent-config'
|
|
37
|
+
import { IJwtService, JwsHeader, JwsPayload } from '@sphereon/ssi-sdk-ext.jwt-service'
|
|
38
|
+
import { ExternalIdentifierDidOpts, IIdentifierResolution, isDidIdentifier } from '@sphereon/ssi-sdk-ext.identifier-resolution'
|
|
39
|
+
import { CredentialMapper, isVcdm2Credential, OriginalVerifiableCredential } from '@sphereon/ssi-types'
|
|
40
|
+
|
|
41
|
+
import { SELF_ISSUED_V0_1, SELF_ISSUED_V2, SELF_ISSUED_V2_VC_INTEROP } from '../did-jwt/JWT'
|
|
42
|
+
// import {validateCredentialPayload} from "did-jwt-vc/src";
|
|
43
|
+
|
|
44
|
+
const debug = Debug('sphereon:ssi-sdk:credential-jwt')
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A handler that implements the {@link IVcdmCredentialProvider} methods.
|
|
48
|
+
*
|
|
49
|
+
* @beta This API may change without a BREAKING CHANGE notice.
|
|
50
|
+
*/
|
|
51
|
+
export class CredentialProviderVcdm2Jose implements IVcdmCredentialProvider {
|
|
52
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.matchKeyForType} */
|
|
53
|
+
matchKeyForType(key: IKey): boolean {
|
|
54
|
+
return this.matchKeyForJWT(key)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.getTypeProofFormat} */
|
|
58
|
+
getTypeProofFormat(): string {
|
|
59
|
+
return 'vcdm2_jose'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.canIssueCredentialType} */
|
|
63
|
+
canIssueCredentialType(args: ICanIssueCredentialTypeArgs): boolean {
|
|
64
|
+
// TODO: Create type
|
|
65
|
+
return args.proofFormat === 'vcdm2_jose' || args.proofFormat === 'vcdm_jose' || args.proofFormat === 'jose'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.canVerifyDocumentType */
|
|
69
|
+
canVerifyDocumentType(args: ICanVerifyDocumentTypeArgs): boolean {
|
|
70
|
+
const { document } = args
|
|
71
|
+
const jwt = typeof document === 'string' ? document : (<VerifiableCredential>document)?.proof?.jwt
|
|
72
|
+
if (!jwt) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
const { payload } = decodeJWT(jwt)
|
|
76
|
+
return isVcdm2Credential(payload)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.createVerifiableCredential} */
|
|
80
|
+
async createVerifiableCredential(args: ICreateVerifiableCredentialLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiableCredentialSP> {
|
|
81
|
+
const { keyRef } = args
|
|
82
|
+
const agent = assertContext(context).agent
|
|
83
|
+
const { credential, issuer } = preProcessCredentialPayload(args)
|
|
84
|
+
if (!isVcdm2Credential(credential)) {
|
|
85
|
+
return Promise.reject(new Error('invalid_argument: credential must be a VCDM2 credential. Context: ' + credential['@context']))
|
|
86
|
+
}
|
|
87
|
+
let identifier: IIdentifier
|
|
88
|
+
try {
|
|
89
|
+
identifier = await agent.didManagerGet({ did: issuer })
|
|
90
|
+
} catch (e) {
|
|
91
|
+
throw new Error(`invalid_argument: ${credential.issuer} must be a DID managed by this agent. ${e}`)
|
|
92
|
+
}
|
|
93
|
+
const managedIdentifier = await agent.identifierManagedGetByDid({ identifier: identifier.did, kmsKeyRef: keyRef })
|
|
94
|
+
const key = await pickSigningKey({ identifier, kmsKeyRef: keyRef }, context)
|
|
95
|
+
|
|
96
|
+
debug('Signing VC with', identifier.did)
|
|
97
|
+
let alg = 'ES256'
|
|
98
|
+
if (key.type === 'Ed25519') {
|
|
99
|
+
alg = 'EdDSA'
|
|
100
|
+
} else if (key.type === 'Secp256k1') {
|
|
101
|
+
alg = 'ES256K'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const header: JwsHeader = {
|
|
105
|
+
kid: key.meta.verificationMethod.id ?? key.kid,
|
|
106
|
+
alg,
|
|
107
|
+
typ: 'vc+jwt',
|
|
108
|
+
cty: 'vc',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const jwt = await context.agent.jwtCreateJwsCompactSignature({
|
|
112
|
+
mode: 'did',
|
|
113
|
+
issuer: managedIdentifier,
|
|
114
|
+
payload: credential,
|
|
115
|
+
protectedHeader: header,
|
|
116
|
+
clientIdScheme: 'did',
|
|
117
|
+
})
|
|
118
|
+
//FIXME: flagging this as a potential privacy leak.
|
|
119
|
+
debug(jwt)
|
|
120
|
+
return normalizeCredential(jwt.jwt)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** {@inheritdoc ICredentialVerifier.verifyCredential} */
|
|
124
|
+
async verifyCredential(args: IVerifyCredentialLDArgs, context: VerifierAgentContext): Promise<IVerifyResult> {
|
|
125
|
+
let { credential /*policies, ...otherOptions*/ } = args
|
|
126
|
+
const uniform = CredentialMapper.toUniformCredential(credential as OriginalVerifiableCredential)
|
|
127
|
+
// let verifiedCredential: VerifiableCredential
|
|
128
|
+
if (!isVcdm2Credential(uniform)) {
|
|
129
|
+
return Promise.reject(new Error('invalid_argument: credential must be a VCDM2 credential. Context: ' + credential['@context']))
|
|
130
|
+
}
|
|
131
|
+
let verificationResult: IVerifyResult = { verified: false }
|
|
132
|
+
let jwt: string | undefined = typeof credential === 'string' ? credential : asArray(uniform.proof)?.[0]?.jwt
|
|
133
|
+
if (!jwt) {
|
|
134
|
+
return Promise.reject(new Error('invalid_argument: credential must be a VCDM2 credential in JOSE format (string)'))
|
|
135
|
+
}
|
|
136
|
+
verificationResult = await verifierSignature({ jwt }, context)
|
|
137
|
+
return verificationResult
|
|
138
|
+
/* let errorCode, message
|
|
139
|
+
const resolver = {
|
|
140
|
+
resolve: (didUrl: string) =>
|
|
141
|
+
context.agent.resolveDid({
|
|
142
|
+
didUrl,
|
|
143
|
+
options: otherOptions?.resolutionOptions,
|
|
144
|
+
}),
|
|
145
|
+
} as Resolvable
|
|
146
|
+
try {
|
|
147
|
+
// needs broader credential as well to check equivalence with jwt
|
|
148
|
+
verificationResult = await verifyCredentialJWT(jwt, resolver, {
|
|
149
|
+
...otherOptions,
|
|
150
|
+
policies: {
|
|
151
|
+
...policies,
|
|
152
|
+
nbf: policies?.nbf ?? policies?.issuanceDate,
|
|
153
|
+
iat: policies?.iat ?? policies?.issuanceDate,
|
|
154
|
+
exp: policies?.exp ?? policies?.expirationDate,
|
|
155
|
+
aud: policies?.aud ?? policies?.audience,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
verifiedCredential = verificationResult.verifiableCredential
|
|
159
|
+
|
|
160
|
+
const nbf = policies?.issuanceDate === false ? false : undefined
|
|
161
|
+
const exp = policies?.expirationDate === false ? false : undefined
|
|
162
|
+
const options = { ...otherOptions, policies: { ...policies, nbf, exp, iat: nbf, format: policies?.format ?? true } }
|
|
163
|
+
|
|
164
|
+
const verified: Partial<VerifiedCredential> = await verifyDIDJWT(asArray(uniform.proof)[0].jwt, { resolver, ...options }, context)
|
|
165
|
+
verified.verifiableCredential = normalizeCredential(verified.jwt as string, true)
|
|
166
|
+
if (options?.policies?.format !== false) {
|
|
167
|
+
validateCredentialPayload(verified.verifiableCredential)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// if credential was presented with other fields, make sure those fields match what's in the JWT
|
|
171
|
+
if (typeof credential !== 'string' && asArray(credential.proof)[0].type === 'JwtProof2020') {
|
|
172
|
+
const credentialCopy = JSON.parse(JSON.stringify(credential))
|
|
173
|
+
delete credentialCopy.proof.jwt
|
|
174
|
+
|
|
175
|
+
const verifiedCopy = JSON.parse(JSON.stringify(verifiedCredential))
|
|
176
|
+
delete verifiedCopy.proof.jwt
|
|
177
|
+
|
|
178
|
+
if (canonicalize(credentialCopy) !== canonicalize(verifiedCopy)) {
|
|
179
|
+
verificationResult.verified = false
|
|
180
|
+
verificationResult.error = new Error('invalid_credential: Credential JSON does not match JWT payload')
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (e: any) {
|
|
184
|
+
errorCode = e.errorCode
|
|
185
|
+
message = e.message
|
|
186
|
+
}
|
|
187
|
+
if (verificationResult.verified) {
|
|
188
|
+
return verificationResult
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
verified: false,
|
|
192
|
+
error: {
|
|
193
|
+
message,
|
|
194
|
+
errorCode: errorCode ? errorCode : message?.split(':')[0],
|
|
195
|
+
},
|
|
196
|
+
}*/
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.createVerifiablePresentation} */
|
|
200
|
+
async createVerifiablePresentation(args: ICreateVerifiablePresentationLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiablePresentationSP> {
|
|
201
|
+
const { presentation, holder } = preProcessPresentation(args)
|
|
202
|
+
let { domain, challenge, keyRef /* removeOriginalFields, keyRef, now, ...otherOptions*/ } = args
|
|
203
|
+
|
|
204
|
+
const agent = assertContext(context).agent
|
|
205
|
+
|
|
206
|
+
const managedIdentifier = await agent.identifierManagedGetByDid({ identifier: holder, kmsKeyRef: keyRef })
|
|
207
|
+
const identifier = managedIdentifier.identifier
|
|
208
|
+
const key = await pickSigningKey({ identifier: managedIdentifier.identifier, kmsKeyRef: managedIdentifier.kmsKeyRef }, context)
|
|
209
|
+
|
|
210
|
+
debug('Signing VC with', identifier.did)
|
|
211
|
+
let alg = 'ES256'
|
|
212
|
+
if (key.type === 'Ed25519') {
|
|
213
|
+
alg = 'EdDSA'
|
|
214
|
+
} else if (key.type === 'Secp256k1') {
|
|
215
|
+
alg = 'ES256K'
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const header: JwsHeader = {
|
|
219
|
+
kid: key.meta.verificationMethod.id ?? key.kid,
|
|
220
|
+
alg,
|
|
221
|
+
typ: 'vp+jwt',
|
|
222
|
+
cty: 'vp',
|
|
223
|
+
}
|
|
224
|
+
const payload: JwsPayload = {
|
|
225
|
+
...presentation,
|
|
226
|
+
...(domain && { aud: domain }),
|
|
227
|
+
...(challenge && { nonce: challenge }),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const jwt = await agent.jwtCreateJwsCompactSignature({
|
|
231
|
+
mode: 'did',
|
|
232
|
+
issuer: managedIdentifier,
|
|
233
|
+
payload,
|
|
234
|
+
protectedHeader: header,
|
|
235
|
+
clientIdScheme: 'did',
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
debug(jwt)
|
|
239
|
+
return normalizePresentation(jwt.jwt)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.verifyPresentation} */
|
|
243
|
+
async verifyPresentation(args: IVerifyPresentationLDArgs, context: VerifierAgentContext): Promise<IVerifyResult> {
|
|
244
|
+
let { presentation, domain, challenge, fetchRemoteContexts, policies, ...otherOptions } = args
|
|
245
|
+
let jwt: string
|
|
246
|
+
if (typeof presentation === 'string') {
|
|
247
|
+
jwt = presentation
|
|
248
|
+
} else {
|
|
249
|
+
jwt = asArray(presentation.proof)[0].jwt
|
|
250
|
+
}
|
|
251
|
+
const resolver = {
|
|
252
|
+
resolve: (didUrl: string) =>
|
|
253
|
+
context.agent.resolveDid({
|
|
254
|
+
didUrl,
|
|
255
|
+
options: otherOptions?.resolutionOptions,
|
|
256
|
+
}),
|
|
257
|
+
} as Resolvable
|
|
258
|
+
|
|
259
|
+
let audience = domain
|
|
260
|
+
if (!audience) {
|
|
261
|
+
const { payload } = await decodeJWT(jwt)
|
|
262
|
+
if (payload.aud) {
|
|
263
|
+
// automatically add a managed DID as audience if one is found
|
|
264
|
+
const intendedAudience = asArray(payload.aud)
|
|
265
|
+
const managedDids = await context.agent.didManagerFind()
|
|
266
|
+
const filtered = managedDids.filter((identifier) => intendedAudience.includes(identifier.did))
|
|
267
|
+
if (filtered.length > 0) {
|
|
268
|
+
audience = filtered[0].did
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let message, errorCode
|
|
274
|
+
try {
|
|
275
|
+
const result = await verifyPresentationJWT(jwt, resolver, {
|
|
276
|
+
challenge,
|
|
277
|
+
domain,
|
|
278
|
+
audience,
|
|
279
|
+
policies: {
|
|
280
|
+
...policies,
|
|
281
|
+
nbf: policies?.nbf ?? policies?.issuanceDate,
|
|
282
|
+
iat: policies?.iat ?? policies?.issuanceDate,
|
|
283
|
+
exp: policies?.exp ?? policies?.expirationDate,
|
|
284
|
+
aud: policies?.aud ?? policies?.audience,
|
|
285
|
+
},
|
|
286
|
+
...otherOptions,
|
|
287
|
+
})
|
|
288
|
+
if (result) {
|
|
289
|
+
return {
|
|
290
|
+
verified: true,
|
|
291
|
+
verifiablePresentation: result,
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (e: any) {
|
|
295
|
+
message = e.message
|
|
296
|
+
errorCode = e.errorCode
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
verified: false,
|
|
300
|
+
error: {
|
|
301
|
+
message,
|
|
302
|
+
errorCode: errorCode ? errorCode : message?.split(':')[0],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Checks if a key is suitable for signing JWT payloads.
|
|
309
|
+
* @param key - the key to check
|
|
310
|
+
* @param context - the Veramo agent context, unused here
|
|
311
|
+
*
|
|
312
|
+
* @beta
|
|
313
|
+
*/
|
|
314
|
+
matchKeyForJWT(key: IKey): boolean {
|
|
315
|
+
switch (key.type) {
|
|
316
|
+
case 'Ed25519':
|
|
317
|
+
case 'Secp256r1':
|
|
318
|
+
return true
|
|
319
|
+
case 'Secp256k1':
|
|
320
|
+
return intersect(key.meta?.algorithms ?? [], ['ES256K', 'ES256K-R']).length > 0
|
|
321
|
+
default:
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
wrapSigner(context: IAgentContext<Pick<IKeyManager, 'keyManagerSign'>>, key: IKey, algorithm?: string) {
|
|
327
|
+
return async (data: string | Uint8Array): Promise<string> => {
|
|
328
|
+
const result = await context.agent.keyManagerSign({ keyRef: key.kid, data: <string>data, algorithm })
|
|
329
|
+
return result
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function verifierSignature(
|
|
335
|
+
{ jwt }: { jwt: string /*resolver: Resolvable*/ },
|
|
336
|
+
verifierContext: VerifierAgentContext,
|
|
337
|
+
): Promise<IVerifyResult> {
|
|
338
|
+
let credIssuer: string | undefined = undefined
|
|
339
|
+
const context = assertContext(verifierContext)
|
|
340
|
+
const agent = context.agent
|
|
341
|
+
const { payload, header /*signature, data*/ } = decodeJWT(jwt)
|
|
342
|
+
|
|
343
|
+
if (!payload.iss && !payload.client_id) {
|
|
344
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT iss or client_id are required`)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (payload.iss === SELF_ISSUED_V2 || payload.iss === SELF_ISSUED_V2_VC_INTEROP) {
|
|
348
|
+
if (!payload.sub) {
|
|
349
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT sub is required`)
|
|
350
|
+
}
|
|
351
|
+
if (typeof payload.sub_jwk === 'undefined') {
|
|
352
|
+
credIssuer = payload.sub
|
|
353
|
+
} else {
|
|
354
|
+
credIssuer = (header.kid || '').split('#')[0]
|
|
355
|
+
}
|
|
356
|
+
} else if (payload.iss === SELF_ISSUED_V0_1) {
|
|
357
|
+
if (!payload.did) {
|
|
358
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT did is required`)
|
|
359
|
+
}
|
|
360
|
+
credIssuer = payload.did
|
|
361
|
+
} else if (!payload.iss && payload.scope === 'openid' && payload.redirect_uri) {
|
|
362
|
+
// SIOP Request payload
|
|
363
|
+
// https://identity.foundation/jwt-vc-presentation-profile/#self-issued-op-request-object
|
|
364
|
+
if (!payload.client_id) {
|
|
365
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT client_id is required`)
|
|
366
|
+
}
|
|
367
|
+
credIssuer = payload.client_id
|
|
368
|
+
} else if (payload.iss?.indexOf('did:') === 0) {
|
|
369
|
+
credIssuer = payload.iss
|
|
370
|
+
} else if (header.kid?.indexOf('did:') === 0) {
|
|
371
|
+
// OID4VCI expects iss to be the client and kid, to be the DID VM
|
|
372
|
+
credIssuer = (header.kid || '').split('#')[0]
|
|
373
|
+
} else if (payload.iss) {
|
|
374
|
+
credIssuer = payload.iss
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!credIssuer) {
|
|
378
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: No DID has been found in the JWT`)
|
|
379
|
+
}
|
|
380
|
+
const resolution = await agent.identifierExternalResolve({ identifier: credIssuer })
|
|
381
|
+
|
|
382
|
+
const didOpts = { method: 'did', identifier: credIssuer } satisfies ExternalIdentifierDidOpts
|
|
383
|
+
const jwtResult = await agent.jwtVerifyJwsSignature({
|
|
384
|
+
jws: jwt,
|
|
385
|
+
// @ts-ignore
|
|
386
|
+
jwk: resolution.jwks[0].jwk,
|
|
387
|
+
opts: { ...(isDidIdentifier(credIssuer) && { did: didOpts }) },
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if (jwtResult.error) {
|
|
391
|
+
return {
|
|
392
|
+
verified: false,
|
|
393
|
+
error: { message: jwtResult.message, errorCode: jwtResult.name },
|
|
394
|
+
payload,
|
|
395
|
+
didResolutionResult: resolution,
|
|
396
|
+
jwt,
|
|
397
|
+
} as IVerifyResult
|
|
398
|
+
}
|
|
399
|
+
return { verified: true, payload, didResolutionResult: resolution, jwt } as IVerifyResult
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/*
|
|
403
|
+
export async function verifyDIDJWT(
|
|
404
|
+
jwt: string,
|
|
405
|
+
options: JWTVerifyOptions = {
|
|
406
|
+
resolver: undefined,
|
|
407
|
+
auth: undefined,
|
|
408
|
+
audience: undefined,
|
|
409
|
+
callbackUrl: undefined,
|
|
410
|
+
skewTime: undefined,
|
|
411
|
+
proofPurpose: undefined,
|
|
412
|
+
policies: {},
|
|
413
|
+
},
|
|
414
|
+
verifierContext: VerifierAgentContext,
|
|
415
|
+
): Promise<JWTVerified> {
|
|
416
|
+
const context = assertContext(verifierContext)
|
|
417
|
+
const agent = context.agent
|
|
418
|
+
if (!options.resolver) throw new Error('missing_resolver: No DID resolver has been configured')
|
|
419
|
+
const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt)
|
|
420
|
+
const proofPurpose: ProofPurposeTypes | undefined = Object.prototype.hasOwnProperty.call(options, 'auth')
|
|
421
|
+
? options.auth
|
|
422
|
+
? 'authentication'
|
|
423
|
+
: undefined
|
|
424
|
+
: options.proofPurpose
|
|
425
|
+
|
|
426
|
+
let credIssuer: string | undefined = undefined
|
|
427
|
+
|
|
428
|
+
if (!payload.iss && !payload.client_id) {
|
|
429
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT iss or client_id are required`)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (payload.iss === SELF_ISSUED_V2 || payload.iss === SELF_ISSUED_V2_VC_INTEROP) {
|
|
433
|
+
if (!payload.sub) {
|
|
434
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT sub is required`)
|
|
435
|
+
}
|
|
436
|
+
if (typeof payload.sub_jwk === 'undefined') {
|
|
437
|
+
credIssuer = payload.sub
|
|
438
|
+
} else {
|
|
439
|
+
credIssuer = (header.kid || '').split('#')[0]
|
|
440
|
+
}
|
|
441
|
+
} else if (payload.iss === SELF_ISSUED_V0_1) {
|
|
442
|
+
if (!payload.did) {
|
|
443
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT did is required`)
|
|
444
|
+
}
|
|
445
|
+
credIssuer = payload.did
|
|
446
|
+
} else if (!payload.iss && payload.scope === 'openid' && payload.redirect_uri) {
|
|
447
|
+
// SIOP Request payload
|
|
448
|
+
// https://identity.foundation/jwt-vc-presentation-profile/#self-issued-op-request-object
|
|
449
|
+
if (!payload.client_id) {
|
|
450
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT client_id is required`)
|
|
451
|
+
}
|
|
452
|
+
credIssuer = payload.client_id
|
|
453
|
+
} else if (payload.iss?.indexOf('did:') === 0) {
|
|
454
|
+
credIssuer = payload.iss
|
|
455
|
+
} else if (header.kid?.indexOf('did:') === 0) {
|
|
456
|
+
// OID4VCI expects iss to be the client and kid, to be the DID VM
|
|
457
|
+
credIssuer = (header.kid || '').split('#')[0]
|
|
458
|
+
} else if (payload.iss) {
|
|
459
|
+
credIssuer = payload.iss
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!credIssuer) {
|
|
463
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: No DID has been found in the JWT`)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const resolution = await agent.identifierExternalResolve({ identifier: credIssuer })
|
|
467
|
+
|
|
468
|
+
const didOpts = { method: 'did', identifier: credIssuer } satisfies ExternalIdentifierDidOpts
|
|
469
|
+
const jwtResult = await agent.jwtVerifyJwsSignature({
|
|
470
|
+
jws: jwt,
|
|
471
|
+
// @ts-ignore
|
|
472
|
+
jwk: resolution.jwks[0],
|
|
473
|
+
opts: { ...(isDidIdentifier(credIssuer) && { did: didOpts }) },
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
if (jwtResult.error) {
|
|
477
|
+
return Promise.reject(Error(`Error validating credential: ${jwtResult.error}`))
|
|
478
|
+
}
|
|
479
|
+
const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
|
|
480
|
+
options.resolver,
|
|
481
|
+
header.alg,
|
|
482
|
+
credIssuer,
|
|
483
|
+
proofPurpose,
|
|
484
|
+
)
|
|
485
|
+
const signer: VerificationMethod = verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
|
|
486
|
+
const now: number = typeof options.policies?.now === 'number' ? options.policies.now : Math.floor(Date.now() / 1000)
|
|
487
|
+
const skewTime = typeof options.skewTime !== 'undefined' && options.skewTime >= 0 ? options.skewTime : NBF_SKEW
|
|
488
|
+
if (signer) {
|
|
489
|
+
const nowSkewed = now + skewTime
|
|
490
|
+
if (options.policies?.nbf !== false && payload.nbf) {
|
|
491
|
+
if (payload.nbf > nowSkewed) {
|
|
492
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid before nbf: ${payload.nbf}`)
|
|
493
|
+
}
|
|
494
|
+
} else if (options.policies?.iat !== false && payload.iat && payload.iat > nowSkewed) {
|
|
495
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT not valid yet (issued in the future) iat: ${payload.iat}`)
|
|
496
|
+
}
|
|
497
|
+
if (options.policies?.exp !== false && payload.exp && payload.exp <= now - skewTime) {
|
|
498
|
+
throw new Error(`${JWT_ERROR.INVALID_JWT}: JWT has expired: exp: ${payload.exp} < now: ${now}`)
|
|
499
|
+
}
|
|
500
|
+
if (options.policies?.aud !== false && payload.aud) {
|
|
501
|
+
if (!options.audience && !options.callbackUrl) {
|
|
502
|
+
throw new Error(`${JWT_ERROR.INVALID_AUDIENCE}: JWT audience is required but your app address has not been configured`)
|
|
503
|
+
}
|
|
504
|
+
const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
|
|
505
|
+
const matchedAudience = audArray.find((item: any) => options.audience === item || options.callbackUrl === item)
|
|
506
|
+
|
|
507
|
+
if (typeof matchedAudience === 'undefined') {
|
|
508
|
+
throw new Error(`${JWT_ERROR.INVALID_AUDIENCE}: JWT audience does not match your DID or callback url`)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return { verified: true, payload, didResolutionResult, issuer, signer, jwt, policies: options.policies }
|
|
512
|
+
}
|
|
513
|
+
throw new Error(
|
|
514
|
+
`${JWT_ERROR.INVALID_SIGNATURE}: JWT not valid. issuer DID document does not contain a verificationMethod that matches the signature.`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubKeys: VerificationMethod | VerificationMethod[]): VerificationMethod {
|
|
519
|
+
if (!Array.isArray(pubKeys)) pubKeys = [pubKeys]
|
|
520
|
+
const signer: VerificationMethod = VerifierAlgorithm(header.alg)(data, signature, pubKeys)
|
|
521
|
+
return signer
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
export function validateCredentialPayload(payload: CredentialPayload): void {
|
|
526
|
+
validateContext(asArray(payload['@context']))
|
|
527
|
+
validateVcType(payload.type)
|
|
528
|
+
validateCredentialSubject(payload.credentialSubject)
|
|
529
|
+
if (payload.validFrom) validateTimestamp(payload.validFrom)
|
|
530
|
+
if (payload.validUntil) validateTimestamp(payload.validUntil)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function validateContext(value: string | string[]): void {
|
|
534
|
+
const input = asArray(value)
|
|
535
|
+
if (input.length < 1 || input.indexOf(VCDM_CREDENTIAL_CONTEXT_V2) === -1) {
|
|
536
|
+
throw new TypeError(`${VC_ERROR.SCHEMA_ERROR}: @context is missing default context "${VCDM_CREDENTIAL_CONTEXT_V2}"`)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
*/
|
|
540
|
+
function assertContext(
|
|
541
|
+
context: IVcdmIssuerAgentContext | IVcdmVerifierAgentContext,
|
|
542
|
+
): IAgentContext<
|
|
543
|
+
IResolver & IDIDManager & Pick<IKeyManager, 'keyManagerGet' | 'keyManagerSign' | 'keyManagerVerify'> & IJwtService & IIdentifierResolution
|
|
544
|
+
> {
|
|
545
|
+
if (!contextHasPlugin<IJwtService>(context, 'jwtPrepareJws')) {
|
|
546
|
+
throw Error(
|
|
547
|
+
'JwtService plugin not found, which is required for JWT signing in the VCDM2 Jose credential provider. Please add the JwtService plugin to your agent configuration.',
|
|
548
|
+
)
|
|
549
|
+
} else if (!contextHasPlugin<IIdentifierResolution>(context, 'identifierManagedGet')) {
|
|
550
|
+
throw Error(
|
|
551
|
+
'Identifier resolution plugin not found, which is required for JWT signing in the VCDM2 Jose credential provider. Please add the JwtService plugin to your agent configuration.',
|
|
552
|
+
)
|
|
553
|
+
}
|
|
554
|
+
return context as IAgentContext<
|
|
555
|
+
IResolver & IDIDManager & Pick<IKeyManager, 'keyManagerGet' | 'keyManagerSign' | 'keyManagerVerify'> & IJwtService & IIdentifierResolution
|
|
556
|
+
>
|
|
557
|
+
}
|