@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.
@@ -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'