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