@sphereon/ssi-sdk.credential-vcdm1-jwt-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/dist/index.cjs +297 -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 +266 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/__tests__/issue-verify-flow-jwt.test.ts +125 -0
- package/src/agent/CredentialProviderJWT.ts +287 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import type { IAgentContext, IIdentifier, IKey, IKeyManager, IVerifyResult, VerifiableCredential, VerifierAgentContext } from '@veramo/core'
|
|
2
|
+
import {
|
|
3
|
+
type ICanIssueCredentialTypeArgs,
|
|
4
|
+
type ICanVerifyDocumentTypeArgs,
|
|
5
|
+
type ICreateVerifiableCredentialLDArgs,
|
|
6
|
+
type ICreateVerifiablePresentationLDArgs,
|
|
7
|
+
type IVcdmCredentialProvider,
|
|
8
|
+
type IVcdmIssuerAgentContext,
|
|
9
|
+
IVerifyCredentialLDArgs,
|
|
10
|
+
IVerifyPresentationLDArgs,
|
|
11
|
+
pickSigningKey,
|
|
12
|
+
preProcessCredentialPayload,
|
|
13
|
+
preProcessPresentation,
|
|
14
|
+
} from '@sphereon/ssi-sdk.credential-vcdm'
|
|
15
|
+
|
|
16
|
+
import canonicalize from 'canonicalize'
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createVerifiableCredentialJwt,
|
|
20
|
+
createVerifiablePresentationJwt,
|
|
21
|
+
normalizeCredential,
|
|
22
|
+
normalizePresentation,
|
|
23
|
+
verifyCredential as verifyCredentialJWT,
|
|
24
|
+
verifyPresentation as verifyPresentationJWT,
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
} from 'did-jwt-vc'
|
|
27
|
+
|
|
28
|
+
import { type Resolvable } from 'did-resolver'
|
|
29
|
+
|
|
30
|
+
import { decodeJWT } from 'did-jwt'
|
|
31
|
+
|
|
32
|
+
import Debug from 'debug'
|
|
33
|
+
import { asArray, intersect, VerifiableCredentialSP, VerifiablePresentationSP } from '@sphereon/ssi-sdk.core'
|
|
34
|
+
import { isVcdm1Credential } from '@sphereon/ssi-types'
|
|
35
|
+
|
|
36
|
+
const debug = Debug('sphereon:ssi-sdk:credential-jwt')
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A handler that implements the {@link IVcdmCredentialProvider} methods.
|
|
40
|
+
*
|
|
41
|
+
* @beta This API may change without a BREAKING CHANGE notice.
|
|
42
|
+
*/
|
|
43
|
+
export class CredentialProviderJWT implements IVcdmCredentialProvider {
|
|
44
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.matchKeyForType} */
|
|
45
|
+
matchKeyForType(key: IKey): boolean {
|
|
46
|
+
return this.matchKeyForJWT(key)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.getTypeProofFormat} */
|
|
50
|
+
getTypeProofFormat(): string {
|
|
51
|
+
return 'jwt'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.canIssueCredentialType} */
|
|
55
|
+
canIssueCredentialType(args: ICanIssueCredentialTypeArgs): boolean {
|
|
56
|
+
return args.proofFormat === 'jwt'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.canVerifyDocumentType */
|
|
60
|
+
canVerifyDocumentType(args: ICanVerifyDocumentTypeArgs): boolean {
|
|
61
|
+
const { document } = args
|
|
62
|
+
const jwt = typeof document === 'string' ? document : (<VerifiableCredential>document)?.proof?.jwt
|
|
63
|
+
if (!jwt) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
const { payload } = decodeJWT(jwt)
|
|
67
|
+
if ('vc' in payload) {
|
|
68
|
+
return isVcdm1Credential(payload.vc)
|
|
69
|
+
} else if ('vp' in payload) {
|
|
70
|
+
return isVcdm1Credential(payload.vp)
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.createVerifiableCredential} */
|
|
76
|
+
async createVerifiableCredential(args: ICreateVerifiableCredentialLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiableCredentialSP> {
|
|
77
|
+
let { keyRef, removeOriginalFields, ...otherOptions } = args
|
|
78
|
+
|
|
79
|
+
const { credential, issuer } = preProcessCredentialPayload(args)
|
|
80
|
+
let identifier: IIdentifier
|
|
81
|
+
try {
|
|
82
|
+
identifier = await context.agent.didManagerGet({ did: issuer })
|
|
83
|
+
} catch (e) {
|
|
84
|
+
throw new Error(`invalid_argument: ${credential.issuer} must be a DID managed by this agent. ${e}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const key = await pickSigningKey({ identifier, kmsKeyRef: keyRef }, context)
|
|
88
|
+
|
|
89
|
+
debug('Signing VC with', identifier.did)
|
|
90
|
+
let alg = 'ES256'
|
|
91
|
+
if (key.type === 'Ed25519') {
|
|
92
|
+
alg = 'EdDSA'
|
|
93
|
+
} else if (key.type === 'Secp256k1') {
|
|
94
|
+
alg = 'ES256K'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const signer = this.wrapSigner(context, key, alg)
|
|
98
|
+
const jwt = await createVerifiableCredentialJwt(
|
|
99
|
+
credential as any,
|
|
100
|
+
{ did: identifier.did, signer, alg, ...(key.meta.verificationMethod.id && { kid: key.meta.verificationMethod.id }) },
|
|
101
|
+
{ removeOriginalFields, ...otherOptions },
|
|
102
|
+
)
|
|
103
|
+
//FIXME: flagging this as a potential privacy leak.
|
|
104
|
+
debug(jwt)
|
|
105
|
+
return normalizeCredential(jwt)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** {@inheritdoc ICredentialVerifier.verifyCredential} */
|
|
109
|
+
async verifyCredential(args: IVerifyCredentialLDArgs, context: VerifierAgentContext): Promise<IVerifyResult> {
|
|
110
|
+
let { credential, policies, ...otherOptions } = args
|
|
111
|
+
let verifiedCredential: VerifiableCredential
|
|
112
|
+
let verificationResult: IVerifyResult = { verified: false }
|
|
113
|
+
let jwt: string = typeof credential === 'string' ? credential : asArray(credential.proof)[0].jwt
|
|
114
|
+
let errorCode, message
|
|
115
|
+
const resolver = {
|
|
116
|
+
resolve: (didUrl: string) =>
|
|
117
|
+
context.agent.resolveDid({
|
|
118
|
+
didUrl,
|
|
119
|
+
options: otherOptions?.resolutionOptions,
|
|
120
|
+
}),
|
|
121
|
+
} as Resolvable
|
|
122
|
+
try {
|
|
123
|
+
// needs broader credential as well to check equivalence with jwt
|
|
124
|
+
verificationResult = await verifyCredentialJWT(jwt, resolver, {
|
|
125
|
+
...otherOptions,
|
|
126
|
+
policies: {
|
|
127
|
+
...policies,
|
|
128
|
+
nbf: policies?.nbf ?? policies?.issuanceDate,
|
|
129
|
+
iat: policies?.iat ?? policies?.issuanceDate,
|
|
130
|
+
exp: policies?.exp ?? policies?.expirationDate,
|
|
131
|
+
aud: policies?.aud ?? policies?.audience,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
verifiedCredential = verificationResult.verifiableCredential
|
|
135
|
+
|
|
136
|
+
// if credential was presented with other fields, make sure those fields match what's in the JWT
|
|
137
|
+
if (typeof credential !== 'string' && asArray(credential.proof)[0].type === 'JwtProof2020') {
|
|
138
|
+
const credentialCopy = JSON.parse(JSON.stringify(credential))
|
|
139
|
+
delete credentialCopy.proof.jwt
|
|
140
|
+
|
|
141
|
+
const verifiedCopy = JSON.parse(JSON.stringify(verifiedCredential))
|
|
142
|
+
delete verifiedCopy.proof.jwt
|
|
143
|
+
|
|
144
|
+
if (canonicalize(credentialCopy) !== canonicalize(verifiedCopy)) {
|
|
145
|
+
verificationResult.verified = false
|
|
146
|
+
verificationResult.error = new Error('invalid_credential: Credential JSON does not match JWT payload')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (e: any) {
|
|
150
|
+
errorCode = e.errorCode
|
|
151
|
+
message = e.message
|
|
152
|
+
}
|
|
153
|
+
if (verificationResult.verified) {
|
|
154
|
+
return verificationResult
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
verified: false,
|
|
158
|
+
error: {
|
|
159
|
+
message,
|
|
160
|
+
errorCode: errorCode ? errorCode : message?.split(':')[0],
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.createVerifiablePresentation} */
|
|
166
|
+
async createVerifiablePresentation(args: ICreateVerifiablePresentationLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiablePresentationSP> {
|
|
167
|
+
const { presentation, holder } = preProcessPresentation(args)
|
|
168
|
+
let { domain, challenge, removeOriginalFields, keyRef, now, ...otherOptions } = args
|
|
169
|
+
|
|
170
|
+
let identifier: IIdentifier
|
|
171
|
+
try {
|
|
172
|
+
identifier = await context.agent.didManagerGet({ did: holder })
|
|
173
|
+
} catch (e) {
|
|
174
|
+
throw new Error('invalid_argument: presentation.holder must be a DID managed by this agent')
|
|
175
|
+
}
|
|
176
|
+
const key = await pickSigningKey({ identifier, kmsKeyRef: keyRef }, context)
|
|
177
|
+
|
|
178
|
+
debug('Signing VP with', identifier.did)
|
|
179
|
+
let alg = 'ES256'
|
|
180
|
+
if (key.type === 'Ed25519') {
|
|
181
|
+
alg = 'EdDSA'
|
|
182
|
+
} else if (key.type === 'Secp256k1') {
|
|
183
|
+
alg = 'ES256K'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const signer = this.wrapSigner(context, key, alg)
|
|
187
|
+
const jwt = await createVerifiablePresentationJwt(
|
|
188
|
+
presentation as any,
|
|
189
|
+
{ did: identifier.did, signer, alg },
|
|
190
|
+
{ removeOriginalFields, challenge, domain, ...otherOptions },
|
|
191
|
+
)
|
|
192
|
+
//FIXME: flagging this as a potential privacy leak.
|
|
193
|
+
debug(jwt)
|
|
194
|
+
return normalizePresentation(jwt)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** {@inheritdoc @veramo/credential-w3c#AbstractCredentialProvider.verifyPresentation} */
|
|
198
|
+
async verifyPresentation(args: IVerifyPresentationLDArgs, context: VerifierAgentContext): Promise<IVerifyResult> {
|
|
199
|
+
let { presentation, domain, challenge, fetchRemoteContexts, policies, ...otherOptions } = args
|
|
200
|
+
let jwt: string
|
|
201
|
+
if (typeof presentation === 'string') {
|
|
202
|
+
jwt = presentation
|
|
203
|
+
} else {
|
|
204
|
+
jwt = asArray(presentation.proof)[0].jwt
|
|
205
|
+
}
|
|
206
|
+
const resolver = {
|
|
207
|
+
resolve: (didUrl: string) =>
|
|
208
|
+
context.agent.resolveDid({
|
|
209
|
+
didUrl,
|
|
210
|
+
options: otherOptions?.resolutionOptions,
|
|
211
|
+
}),
|
|
212
|
+
} as Resolvable
|
|
213
|
+
|
|
214
|
+
let audience = domain
|
|
215
|
+
if (!audience) {
|
|
216
|
+
const { payload } = await decodeJWT(jwt)
|
|
217
|
+
if (payload.aud) {
|
|
218
|
+
// automatically add a managed DID as audience if one is found
|
|
219
|
+
const intendedAudience = asArray(payload.aud)
|
|
220
|
+
const managedDids = await context.agent.didManagerFind()
|
|
221
|
+
const filtered = managedDids.filter((identifier) => intendedAudience.includes(identifier.did))
|
|
222
|
+
if (filtered.length > 0) {
|
|
223
|
+
audience = filtered[0].did
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let message, errorCode
|
|
229
|
+
try {
|
|
230
|
+
const result = await verifyPresentationJWT(jwt, resolver, {
|
|
231
|
+
challenge,
|
|
232
|
+
domain,
|
|
233
|
+
audience,
|
|
234
|
+
policies: {
|
|
235
|
+
...policies,
|
|
236
|
+
nbf: policies?.nbf ?? policies?.issuanceDate,
|
|
237
|
+
iat: policies?.iat ?? policies?.issuanceDate,
|
|
238
|
+
exp: policies?.exp ?? policies?.expirationDate,
|
|
239
|
+
aud: policies?.aud ?? policies?.audience,
|
|
240
|
+
},
|
|
241
|
+
...otherOptions,
|
|
242
|
+
})
|
|
243
|
+
if (result) {
|
|
244
|
+
return {
|
|
245
|
+
verified: true,
|
|
246
|
+
verifiablePresentation: result,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (e: any) {
|
|
250
|
+
message = e.message
|
|
251
|
+
errorCode = e.errorCode
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
verified: false,
|
|
255
|
+
error: {
|
|
256
|
+
message,
|
|
257
|
+
errorCode: errorCode ? errorCode : message?.split(':')[0],
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Checks if a key is suitable for signing JWT payloads.
|
|
264
|
+
* @param key - the key to check
|
|
265
|
+
* @param context - the Veramo agent context, unused here
|
|
266
|
+
*
|
|
267
|
+
* @beta
|
|
268
|
+
*/
|
|
269
|
+
matchKeyForJWT(key: IKey): boolean {
|
|
270
|
+
switch (key.type) {
|
|
271
|
+
case 'Ed25519':
|
|
272
|
+
case 'Secp256r1':
|
|
273
|
+
return true
|
|
274
|
+
case 'Secp256k1':
|
|
275
|
+
return intersect(key.meta?.algorithms ?? [], ['ES256K', 'ES256K-R']).length > 0
|
|
276
|
+
default:
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
wrapSigner(context: IAgentContext<Pick<IKeyManager, 'keyManagerSign'>>, key: IKey, algorithm?: string) {
|
|
282
|
+
return async (data: string | Uint8Array): Promise<string> => {
|
|
283
|
+
const result = await context.agent.keyManagerSign({ keyRef: key.kid, data: <string>data, algorithm })
|
|
284
|
+
return result
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CredentialProviderJWT } from './agent/CredentialProviderJWT'
|