@sphereon/ssi-sdk.sd-jwt 0.34.1-next.6 → 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.
@@ -0,0 +1,316 @@
1
+ import { beforeAll, describe, expect, it } from 'vitest'
2
+ import { KBJwt } from '@sd-jwt/core'
3
+ import { decodeSdJwt } from '@sd-jwt/decode'
4
+ import { SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'
5
+ import { DisclosureFrame, kbPayload } from '@sd-jwt/types'
6
+ import { JwkDIDProvider } from '@sphereon/ssi-sdk-ext.did-provider-jwk'
7
+ import { getDidJwkResolver } from '@sphereon/ssi-sdk-ext.did-resolver-jwk'
8
+ import { IdentifierResolution, IIdentifierResolution } from '@sphereon/ssi-sdk-ext.identifier-resolution'
9
+ import { IJwtService, JwtService } from '@sphereon/ssi-sdk-ext.jwt-service'
10
+ import { MemoryKeyStore, MemoryPrivateKeyStore, SphereonKeyManager } from '@sphereon/ssi-sdk-ext.key-manager'
11
+ import { SphereonKeyManagementSystem } from '@sphereon/ssi-sdk-ext.kms-local'
12
+ import { ImDLMdoc, MDLMdoc } from '@sphereon/ssi-sdk.mdl-mdoc'
13
+ import { createAgent, IDIDManager, IKeyManager, IResolver, TAgent } from '@veramo/core'
14
+ import { DIDManager, MemoryDIDStore } from '@veramo/did-manager'
15
+ import { DIDResolverPlugin } from '@veramo/did-resolver'
16
+ import { DIDDocument, Resolver, VerificationMethod } from 'did-resolver'
17
+ import { defaultGenerateDigest } from '../defaultCallbacks'
18
+ import { ISDJwtPlugin, SDJwtPlugin } from '../index'
19
+ import { SdJwtVcdm2Payload } from '@sphereon/ssi-types'
20
+
21
+ type AgentType = IDIDManager & IKeyManager & IIdentifierResolution & IJwtService & IResolver & ISDJwtPlugin & ImDLMdoc
22
+
23
+ describe('Agent plugin', () => {
24
+ let agent: TAgent<AgentType>
25
+
26
+ let issuer: string
27
+
28
+ let holder: string
29
+
30
+ // Issuer Define the claims object with the user's information
31
+ const claims = {
32
+ sub: '',
33
+ credentialSubject: {
34
+ given_name: 'John',
35
+ family_name: 'Deo',
36
+ email: 'johndeo@example.com',
37
+ phone: '+1-202-555-0101',
38
+ address: {
39
+ street_address: '123 Main St',
40
+ locality: 'Anytown',
41
+ region: 'Anystate',
42
+ country: 'US',
43
+ },
44
+ birthdate: '1940-01-01',
45
+ },
46
+ }
47
+
48
+ // Issuer Define the disclosure frame to specify which claims can be disclosed
49
+ const disclosureFrame: DisclosureFrame<typeof claims> = {
50
+ credentialSubject: {
51
+ _sd: ['given_name', 'family_name', 'email', 'phone', 'address', 'birthdate'],
52
+ },
53
+ }
54
+ let testVcdm2SdJwtCredentialPayload: SdJwtVcdm2Payload
55
+
56
+ beforeAll(async () => {
57
+ agent = createAgent<AgentType>({
58
+ plugins: [
59
+ new SDJwtPlugin(),
60
+ new IdentifierResolution(),
61
+ new JwtService(),
62
+ new SphereonKeyManager({
63
+ store: new MemoryKeyStore(),
64
+ kms: {
65
+ local: new SphereonKeyManagementSystem(new MemoryPrivateKeyStore()),
66
+ },
67
+ }),
68
+ new DIDResolverPlugin({
69
+ resolver: new Resolver({
70
+ ...getDidJwkResolver(),
71
+ }),
72
+ }),
73
+ new DIDManager({
74
+ store: new MemoryDIDStore(),
75
+ defaultProvider: 'did:jwk',
76
+ providers: {
77
+ 'did:jwk': new JwkDIDProvider({
78
+ defaultKms: 'local',
79
+ }),
80
+ },
81
+ }),
82
+ ],
83
+ })
84
+ issuer = await agent
85
+ .didManagerCreate({
86
+ kms: 'local',
87
+ provider: 'did:jwk',
88
+ alias: 'issuer',
89
+ //we use this curve since nodejs does not support ES256k which is the default one.
90
+ options: { keyType: 'Secp256r1' },
91
+ })
92
+ .then((did) => {
93
+ // we add a key reference
94
+ return `${did.did}#0`
95
+ })
96
+ holder = await agent
97
+ .didManagerCreate({
98
+ kms: 'local',
99
+ provider: 'did:jwk',
100
+ alias: 'holder',
101
+ //we use this curve since nodejs does not support ES256k which is the default one.
102
+ options: { keyType: 'Secp256r1' },
103
+ })
104
+ .then((did) => `${did.did}#0`)
105
+ claims.sub = holder
106
+
107
+ testVcdm2SdJwtCredentialPayload = {
108
+ ...claims,
109
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
110
+ type: ['VerifiableCredential'],
111
+ issuer,
112
+ validFrom: '2021-01-01T00:00:00Z',
113
+ // iat: Math.floor(new Date().getTime() / 1000),
114
+ }
115
+ })
116
+
117
+ it('create a sd-jwt vcdm2', async () => {
118
+ const credentialPayload: SdJwtVcdm2Payload = {
119
+ ...claims,
120
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
121
+ type: ['VerifiableCredential'],
122
+ issuer,
123
+ validFrom: '2021-01-01T00:00:00Z',
124
+ iat: Math.floor(new Date().getTime() / 1000),
125
+ }
126
+ const credential = await agent.createSdJwtVc({
127
+ credentialPayload,
128
+ disclosureFrame,
129
+ })
130
+ expect(credential).toBeDefined()
131
+ })
132
+
133
+ it('create sd-jwt vcdm2 without an issuer', async () => {
134
+ const credentialPayload = {
135
+ ...claims,
136
+ '@context': ['https://www.w3.org/ns/credentials/v2'],
137
+ type: ['VerifiableCredential'],
138
+ validFrom: '2021-01-01T00:00:00Z',
139
+ // iat: Math.floor(new Date().getTime() / 1000),
140
+ }
141
+ await expect(
142
+ agent.createSdJwtVc({
143
+ credentialPayload: credentialPayload as unknown as SdJwtVcPayload,
144
+ disclosureFrame,
145
+ }),
146
+ ).rejects.toThrow('No issuer (iss or VCDM 2 issuer) found in SD-JWT or no VCDM2 SD-JWT or SD-JWT VC')
147
+ })
148
+
149
+ it('verify a sd-jwt vcdm2', async () => {
150
+ const credential = await agent.createSdJwtVc({
151
+ credentialPayload: testVcdm2SdJwtCredentialPayload,
152
+ disclosureFrame: disclosureFrame,
153
+ })
154
+ const result = await agent.verifySdJwtVc({
155
+ credential: credential.credential,
156
+ })
157
+ expect(result.payload).toBeDefined()
158
+ }, 5000)
159
+
160
+ it('create a presentation', async () => {
161
+ const credentialPayload = testVcdm2SdJwtCredentialPayload
162
+ const credential = await agent.createSdJwtVc({
163
+ credentialPayload,
164
+ disclosureFrame,
165
+ })
166
+ const presentation = await agent.createSdJwtPresentation({
167
+ presentation: credential.credential,
168
+ presentationFrame: { given_name: true },
169
+ kb: {
170
+ payload: {
171
+ aud: '1',
172
+ iat: 1,
173
+ nonce: '342',
174
+ },
175
+ },
176
+ })
177
+ expect(presentation).toBeDefined()
178
+ const decoded = await decodeSdJwt(presentation.presentation, defaultGenerateDigest)
179
+ expect(decoded.kbJwt).toBeDefined()
180
+ expect(((decoded.kbJwt as KBJwt).payload as kbPayload).aud).toBe('1')
181
+ })
182
+
183
+ it('create presentation with cnf', async () => {
184
+ const did = await agent.didManagerFind({ alias: 'holder' }).then((dids) => dids[0])
185
+ const resolvedDid = await agent.resolveDid({ didUrl: `${did.did}#0` })
186
+ const jwk: JsonWebKey = ((resolvedDid.didDocument as DIDDocument).verificationMethod as VerificationMethod[])[0].publicKeyJwk as JsonWebKey
187
+ const credentialPayload = {
188
+ ...testVcdm2SdJwtCredentialPayload,
189
+ cnf: {
190
+ jwk,
191
+ },
192
+ }
193
+ const credential = await agent.createSdJwtVc({
194
+ credentialPayload,
195
+ disclosureFrame,
196
+ })
197
+ const presentation = await agent.createSdJwtPresentation({
198
+ presentation: credential.credential,
199
+ presentationFrame: { given_name: true },
200
+ kb: {
201
+ payload: {
202
+ aud: '1',
203
+ iat: 1,
204
+ nonce: '342',
205
+ },
206
+ },
207
+ })
208
+ expect(presentation).toBeDefined()
209
+ const decoded = await decodeSdJwt(presentation.presentation, defaultGenerateDigest)
210
+ expect(decoded.kbJwt).toBeDefined()
211
+ expect(((decoded.kbJwt as KBJwt).payload as kbPayload).aud).toBe('1')
212
+ })
213
+
214
+ it('includes no holder reference', async () => {
215
+ const newClaims = JSON.parse(JSON.stringify(claims))
216
+ newClaims.sub = undefined
217
+ const credentialPayload: SdJwtVcPayload = {
218
+ ...newClaims,
219
+ iss: issuer,
220
+ iat: Math.floor(new Date().getTime() / 1000),
221
+ vct: '',
222
+ }
223
+ const credential = await agent.createSdJwtVc({
224
+ credentialPayload,
225
+ disclosureFrame,
226
+ })
227
+ const presentation = agent.createSdJwtPresentation({
228
+ presentation: credential.credential,
229
+ presentationFrame: { given_name: true },
230
+ kb: {
231
+ payload: {
232
+ aud: '1',
233
+ iat: 1,
234
+ nonce: '342',
235
+ },
236
+ },
237
+ })
238
+ await expect(presentation).rejects.toThrow('credential does not include a holder reference')
239
+ })
240
+
241
+ it('verify a presentation', async () => {
242
+ const holderDId = await agent.resolveDid({ didUrl: holder })
243
+ const jwk: JsonWebKey = ((holderDId.didDocument as DIDDocument).verificationMethod as VerificationMethod[])[0].publicKeyJwk as JsonWebKey
244
+ const credentialPayload: SdJwtVcPayload = {
245
+ ...claims,
246
+ iss: issuer,
247
+ iat: Math.floor(new Date().getTime() / 1000),
248
+ vct: '',
249
+ cnf: {
250
+ jwk,
251
+ },
252
+ }
253
+ const credential = await agent.createSdJwtVc({
254
+ credentialPayload,
255
+ disclosureFrame,
256
+ })
257
+ const presentation = await agent.createSdJwtPresentation({
258
+ presentation: credential.credential,
259
+ presentationFrame: { credentialSubject: {given_name: true} },
260
+ kb: {
261
+ payload: {
262
+ aud: '1',
263
+ iat: 1,
264
+ nonce: '342',
265
+ },
266
+ },
267
+ })
268
+ const result = await agent.verifySdJwtPresentation({
269
+ presentation: presentation.presentation,
270
+ requiredClaimKeys: ['credentialSubject.given_name'],
271
+ // we are not able to verify the kb yet since we have no reference to the public key of the holder.
272
+ keyBindingAud: '1',
273
+ keyBindingNonce: '342',
274
+ })
275
+ expect(result).toBeDefined()
276
+ expect((result.payload as typeof claims).credentialSubject.given_name).toBe('John')
277
+ })
278
+
279
+ it('verify a presentation with sub set', async () => {
280
+ const holderDId = await agent.resolveDid({ didUrl: holder })
281
+ const jwk: JsonWebKey = ((holderDId.didDocument as DIDDocument).verificationMethod as VerificationMethod[])[0].publicKeyJwk as JsonWebKey
282
+ const credentialPayload: SdJwtVcPayload = {
283
+ ...claims,
284
+ iss: issuer,
285
+ iat: Math.floor(new Date().getTime() / 1000),
286
+ vct: '',
287
+ cnf: {
288
+ jwk,
289
+ },
290
+ }
291
+ const credential = await agent.createSdJwtVc({
292
+ credentialPayload,
293
+ disclosureFrame,
294
+ })
295
+ const presentation = await agent.createSdJwtPresentation({
296
+ presentation: credential.credential,
297
+ presentationFrame: { credentialSubject: {given_name: true} },
298
+ kb: {
299
+ payload: {
300
+ aud: '1',
301
+ iat: 1,
302
+ nonce: '342',
303
+ },
304
+ },
305
+ })
306
+ const result = await agent.verifySdJwtPresentation({
307
+ presentation: presentation.presentation,
308
+ requiredClaimKeys: ['credentialSubject.given_name'],
309
+ // we are not able to verify the kb yet since we have no reference to the public key of the holder.
310
+ keyBindingAud: '1',
311
+ keyBindingNonce: '342',
312
+ })
313
+ expect(result).toBeDefined()
314
+ expect((result.payload as typeof claims).credentialSubject.given_name).toBe('John')
315
+ })
316
+ })
@@ -1,16 +1,16 @@
1
- import { Jwt, SDJwt } from '@sd-jwt/core'
2
- import { SDJwtVcInstance, SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'
3
- import { DisclosureFrame, Hasher, JwtPayload, KbVerifier, PresentationFrame, Signer, Verifier } from '@sd-jwt/types'
1
+ import { Jwt, SDJwt, type SdJwtPayload, type VerifierOptions } from '@sd-jwt/core'
2
+ import { SDJwtVcInstance, type SdJwtVcPayload } from '@sd-jwt/sd-jwt-vc'
3
+ import type { DisclosureFrame, HashAlgorithm, Hasher, JwtPayload, KbVerifier, PresentationFrame, Signer, Verifier } from '@sd-jwt/types'
4
4
  import { calculateJwkThumbprint, signatureAlgorithmFromKey } from '@sphereon/ssi-sdk-ext.key-utils'
5
- import { X509CertificateChainValidationOpts } from '@sphereon/ssi-sdk-ext.x509-utils'
6
- import { HasherSync, JsonWebKey, JWK, SdJwtTypeMetadata } from '@sphereon/ssi-types'
7
- import { IAgentPlugin } from '@veramo/core'
8
- import { decodeBase64url } from '@veramo/utils'
5
+ import type { X509CertificateChainValidationOpts } from '@sphereon/ssi-sdk-ext.x509-utils'
6
+ import type { HasherSync, JsonWebKey, JWK, SdJwtTypeMetadata } from '@sphereon/ssi-types'
7
+ import type { IAgentPlugin } from '@veramo/core'
8
+ // import { decodeBase64url } from '@veramo/utils'
9
9
  import Debug from 'debug'
10
10
  import { defaultGenerateDigest, defaultGenerateSalt, defaultVerifySignature } from './defaultCallbacks'
11
11
  import { funkeTestCA, sphereonCA } from './trustAnchors'
12
- import { assertValidTypeMetadata, fetchUrlWithErrorHandling, validateIntegrity } from './utils'
13
- import {
12
+ import { assertValidTypeMetadata, fetchUrlWithErrorHandling, getIssuerFromSdJwt, isSdjwtVcPayload, isVcdm2SdJwtPayload, validateIntegrity } from './utils'
13
+ import type {
14
14
  Claims,
15
15
  FetchSdJwtTypeMetadataFromVctUrlArgs,
16
16
  GetSignerForIdentifierArgs,
@@ -30,6 +30,10 @@ import {
30
30
  SignKeyArgs,
31
31
  SignKeyResult,
32
32
  } from './types'
33
+ import { SDJwtVcdm2Instance, SDJwtVcdmInstanceFactory } from './sdJwtVcdm2Instance'
34
+
35
+ // @ts-ignore
36
+ import * as u8a from 'uint8arrays'
33
37
 
34
38
  const debug = Debug('@sphereon/ssi-sdk.sd-jwt')
35
39
 
@@ -101,27 +105,47 @@ export class SDJwtPlugin implements IAgentPlugin {
101
105
  * @returns A signed SD-JWT credential.
102
106
  */
103
107
  async createSdJwtVc(args: ICreateSdJwtVcArgs, context: IRequiredContext): Promise<ICreateSdJwtVcResult> {
104
- const issuer = args.credentialPayload.iss
108
+ const payload = args.credentialPayload
109
+ const isVcdm2 = isVcdm2SdJwtPayload(payload)
110
+ const isSdJwtVc = isSdjwtVcPayload(payload)
111
+ const type = args.type ?? (isVcdm2 ? 'vc+sd-jwt' : 'dc+sd-jwt')
112
+
113
+ const issuer = getIssuerFromSdJwt(args.credentialPayload)
105
114
  if (!issuer) {
106
115
  throw new Error('credential.issuer must not be empty')
107
116
  }
108
117
  const { alg, signer, signingKey } = await this.getSignerForIdentifier({ identifier: issuer, resolution: args.resolution }, context)
109
- const sdjwt = new SDJwtVcInstance({
118
+ const signAlg = alg ?? signingKey?.alg ?? 'ES256'
119
+ const hashAlg: HashAlgorithm = /(\d{3})$/.test(signAlg) ? (`sha-${signAlg.slice(-3)}` as HashAlgorithm) : 'sha-256'
120
+ const sdjwt = SDJwtVcdmInstanceFactory.create(type, {
121
+ omitTyp: true,
110
122
  signer,
111
123
  hasher: this.registeredImplementations.hasher,
112
124
  saltGenerator: this.registeredImplementations.saltGenerator,
113
- signAlg: alg ?? 'ES256',
114
- hashAlg: 'sha-256',
125
+ signAlg,
126
+ hashAlg,
115
127
  })
116
128
 
117
- const credential = await sdjwt.issue(args.credentialPayload, args.disclosureFrame as DisclosureFrame<typeof args.credentialPayload>, {
118
- header: {
119
- ...(signingKey?.key.kid !== undefined && { kid: signingKey.key.kid }),
120
- ...(signingKey?.key.x5c !== undefined && { x5c: signingKey.key.x5c }),
121
- },
122
- })
129
+ const header = {
130
+ ...(signingKey?.key.kid !== undefined && { kid: signingKey.key.kid }),
131
+ ...(signingKey?.key.x5c !== undefined && { x5c: signingKey.key.x5c }),
132
+ ...(type && { typ: type }),
133
+ }
134
+ let credential: string
135
+ if (isVcdm2) {
136
+ credential = await (sdjwt as SDJwtVcdm2Instance).issue(
137
+ payload,
138
+ // @ts-ignore
139
+ args.disclosureFrame as DisclosureFrame<typeof payload>,
140
+ { header },
141
+ )
142
+ } else if (isSdJwtVc) {
143
+ credential = await (sdjwt as SDJwtVcInstance).issue(payload, args.disclosureFrame as DisclosureFrame<typeof payload>, { header })
144
+ } else {
145
+ return Promise.reject(new Error(`invalid_argument: credential '${type}' type is not supported`))
146
+ }
123
147
 
124
- return { credential }
148
+ return { type, credential }
125
149
  }
126
150
 
127
151
  /**
@@ -183,10 +207,13 @@ export class SDJwtPlugin implements IAgentPlugin {
183
207
  * @returns A signed SD-JWT presentation.
184
208
  */
185
209
  async createSdJwtPresentation(args: ICreateSdJwtPresentationArgs, context: IRequiredContext): Promise<ICreateSdJwtPresentationResult> {
210
+ const type = args.type ?? 'dc+sd-jwt'
211
+
186
212
  const cred = await SDJwt.fromEncode(args.presentation, this.registeredImplementations.hasher!)
213
+
187
214
  const claims = await cred.getClaims<Claims>(this.registeredImplementations.hasher!)
188
215
  let holder: string
189
- // we primarly look for a cnf field, if it's not there we look for a sub field. If this is also not given, we throw an error since we can not sign it.
216
+ // we primarily look for a cnf field, if it's not there, we look for a sub field. If this is also not given, we throw an error since we can not sign it.
190
217
  if (args.holder) {
191
218
  holder = args.holder
192
219
  } else if (claims.cnf?.jwk) {
@@ -201,15 +228,17 @@ export class SDJwtPlugin implements IAgentPlugin {
201
228
  }
202
229
  const { alg, signer } = await this.getSignerForIdentifier({ identifier: holder }, context)
203
230
 
204
- const sdjwt = new SDJwtVcInstance({
205
- hasher: this.registeredImplementations.hasher ?? defaultGenerateDigest,
231
+ const sdjwt = SDJwtVcdmInstanceFactory.create(type, {
232
+ omitTyp: true,
233
+ hasher: this.registeredImplementations.hasher,
206
234
  saltGenerator: this.registeredImplementations.saltGenerator,
207
235
  kbSigner: signer,
208
236
  kbSignAlg: alg ?? 'ES256',
209
237
  })
238
+
210
239
  const presentation = await sdjwt.present(args.presentation, args.presentationFrame as PresentationFrame<SdJwtVcPayload>, { kb: args.kb })
211
240
 
212
- return { presentation }
241
+ return { type, presentation }
213
242
  }
214
243
 
215
244
  /**
@@ -220,11 +249,17 @@ export class SDJwtPlugin implements IAgentPlugin {
220
249
  */
221
250
  async verifySdJwtVc(args: IVerifySdJwtVcArgs, context: IRequiredContext): Promise<IVerifySdJwtVcResult> {
222
251
  // callback
223
- const verifier: Verifier = async (data: string, signature: string) => this.verify(sdjwt, context, data, signature)
224
- const sdjwt = new SDJwtVcInstance({ verifier, hasher: this.registeredImplementations.hasher ?? defaultGenerateDigest })
252
+ const verifier: Verifier = async (data: string, signature: string) => this.verifyCallbackImpl(sdjwt, context, data, signature)
253
+
254
+ const cred = await SDJwt.fromEncode(args.credential, this.registeredImplementations.hasher!)
255
+ const type = isVcdm2SdJwtPayload(cred.jwt?.payload as SdJwtPayload) ? 'vc+sd-jwt' : 'dc+sd-jwt'
256
+
257
+
258
+ const sdjwt = SDJwtVcdmInstanceFactory.create(type, {verifier, hasher: this.registeredImplementations.hasher ?? defaultGenerateDigest })
225
259
  const { header = {}, payload, kb } = await sdjwt.verify(args.credential)
226
260
 
227
- return { header, payload: payload as SdJwtVcPayload, kb }
261
+
262
+ return { type, header, payload, kb }
228
263
  }
229
264
 
230
265
  /**
@@ -236,10 +271,14 @@ export class SDJwtPlugin implements IAgentPlugin {
236
271
  * @param payload - The payload of the SD-JWT
237
272
  * @returns
238
273
  */
239
- private verifyKb(sdjwt: SDJwtVcInstance, context: IRequiredContext, data: string, signature: string, payload: JwtPayload): Promise<boolean> {
274
+ private verifyKb(context: IRequiredContext, data: string, signature: string, payload: JwtPayload): Promise<boolean> {
240
275
  if (!payload.cnf) {
241
276
  throw Error('other method than cnf is not supported yet')
242
277
  }
278
+
279
+ // TODO add aud verification
280
+
281
+
243
282
  return this.verifySignatureCallback(context)(data, signature, this.getJwk(payload))
244
283
  }
245
284
 
@@ -251,15 +290,16 @@ export class SDJwtPlugin implements IAgentPlugin {
251
290
  * @param signature - The signature
252
291
  * @returns
253
292
  */
254
- async verify(
255
- sdjwt: SDJwtVcInstance,
293
+ async verifyCallbackImpl(
294
+ sdjwt: SDJwtVcInstance | SDJwtVcdm2Instance,
256
295
  context: IRequiredContext,
257
296
  data: string,
258
297
  signature: string,
259
298
  opts?: { x5cValidation?: X509CertificateChainValidationOpts },
260
299
  ): Promise<boolean> {
261
300
  const decodedVC = await sdjwt.decode(`${data}.${signature}`)
262
- const issuer: string = ((decodedVC.jwt as Jwt).payload as Record<string, unknown>).iss as string
301
+ const payload: SdJwtPayload = (decodedVC.jwt as Jwt).payload as SdJwtPayload
302
+ const issuer: string = getIssuerFromSdJwt(payload)
263
303
  const header = (decodedVC.jwt as Jwt).header as Record<string, any>
264
304
  const x5c: string[] | undefined = header?.x5c as string[]
265
305
  let jwk: JWK | JsonWebKey | undefined = header.jwk
@@ -329,16 +369,21 @@ export class SDJwtPlugin implements IAgentPlugin {
329
369
  */
330
370
  async verifySdJwtPresentation(args: IVerifySdJwtPresentationArgs, context: IRequiredContext): Promise<IVerifySdJwtPresentationResult> {
331
371
  let sdjwt: SDJwtVcInstance
332
- const verifier: Verifier = async (data: string, signature: string) => this.verify(sdjwt, context, data, signature)
372
+ const verifier: Verifier = async (data: string, signature: string) => this.verifyCallbackImpl(sdjwt, context, data, signature)
333
373
  const verifierKb: KbVerifier = async (data: string, signature: string, payload: JwtPayload) =>
334
- this.verifyKb(sdjwt, context, data, signature, payload)
374
+ this.verifyKb(context, data, signature, payload)
335
375
  sdjwt = new SDJwtVcInstance({
336
376
  verifier,
337
377
  hasher: this.registeredImplementations.hasher,
338
378
  kbVerifier: verifierKb,
339
379
  })
340
380
 
341
- return sdjwt.verify(args.presentation, args.requiredClaimKeys, args.kb)
381
+ const verifierOpts: VerifierOptions = {
382
+ requiredClaimKeys: args.requiredClaimKeys,
383
+ keyBindingNonce: args.keyBindingNonce,
384
+ }
385
+
386
+ return sdjwt.verify(args.presentation, verifierOpts)
342
387
  }
343
388
 
344
389
  /**
@@ -411,7 +456,7 @@ export class SDJwtPlugin implements IAgentPlugin {
411
456
  // extract JWK from kid FIXME isn't there a did function for this already? Otherwise create one
412
457
  // FIXME this is a quick-fix to make verification but we need a real solution
413
458
  const encoded = this.extractBase64FromDIDJwk(payload.cnf.kid)
414
- const decoded = decodeBase64url(encoded)
459
+ const decoded = u8a.toString(u8a.fromString(encoded, 'base64url'), 'utf-8')
415
460
  const jwt = JSON.parse(decoded)
416
461
  return jwt as JsonWebKey
417
462
  }
@@ -0,0 +1,155 @@
1
+ import { SDJwtInstance, type VerifierOptions } from '@sd-jwt/core'
2
+ import type { DisclosureFrame, Hasher, SDJWTCompact } from '@sd-jwt/types'
3
+ import { SDJWTException } from '@sd-jwt/utils'
4
+ import { type SdJwtType, type SDJWTVCDM2Config, type SdJwtVcdm2Payload } from '@sphereon/ssi-types'
5
+ import { type SDJWTVCConfig, SDJwtVcInstance, type VerificationResult } from '@sd-jwt/sd-jwt-vc'
6
+ import { isVcdm2SdJwt } from './types'
7
+
8
+ interface SdJwtVcdm2VerificationResult extends Omit<VerificationResult, 'payload'> {
9
+ payload: SdJwtVcdm2Payload
10
+ }
11
+
12
+ export class SDJwtVcdmInstanceFactory {
13
+ static create(type: SdJwtType, config: SDJWTVCConfig | SDJWTVCDM2Config): SDJwtVcdm2Instance | SDJwtVcInstance {
14
+ if (isVcdm2SdJwt(type)) {
15
+ return new SDJwtVcdm2Instance(config as SDJWTVCDM2Config)
16
+ }
17
+ return new SDJwtVcInstance(config as SDJWTVCConfig)
18
+ }
19
+ }
20
+
21
+ // @ts-ignore
22
+ export class SDJwtVcdm2Instance extends SDJwtInstance<SdJwtVcdm2Payload> {
23
+ /**
24
+ * The type of the SD-JWT VCDM2 set in the header.typ field.
25
+ */
26
+ protected static type = 'vc+sd-jwt'
27
+
28
+ protected userConfig: SDJWTVCDM2Config = {}
29
+
30
+ constructor(userConfig?: SDJWTVCDM2Config) {
31
+ super(userConfig)
32
+ if (userConfig) {
33
+ this.userConfig = userConfig
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Validates if the disclosureFrame contains any reserved fields. If so it will throw an error.
39
+ * @param disclosureFrame
40
+ */
41
+ protected validateReservedFields(disclosureFrame: DisclosureFrame<SdJwtVcdm2Payload>): void {
42
+ //validate disclosureFrame according to https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-08.html#section-3.2.2.2
43
+ // @ts-ignore
44
+ if (disclosureFrame?._sd && Array.isArray(disclosureFrame._sd) && disclosureFrame._sd.length > 0) {
45
+ const reservedNames = ['iss', 'nbf', 'exp', 'cnf', '@context', 'type', 'credentialStatus', 'credentialSchema', 'relatedResource']
46
+ // check if there is any reserved names in the disclosureFrame._sd array
47
+ const reservedNamesInDisclosureFrame = (disclosureFrame._sd as string[]).filter((key) => reservedNames.includes(key))
48
+ if (reservedNamesInDisclosureFrame.length > 0) {
49
+ throw new SDJWTException(`Cannot disclose protected field(s): ${reservedNamesInDisclosureFrame.join(', ')}`)
50
+ }
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Verifies the SD-JWT-VC. It will validate the signature, the keybindings when required, the status, and the VCT.
56
+ * @param encodedSDJwt
57
+ * @param options
58
+ */
59
+ async verify(encodedSDJwt: string, options?: VerifierOptions) {
60
+ // Call the parent class's verify method
61
+ const result: SdJwtVcdm2VerificationResult = await super.verify(encodedSDJwt, options).then((res) => {
62
+ return {
63
+ payload: res.payload as SdJwtVcdm2Payload,
64
+ header: res.header,
65
+ kb: res.kb,
66
+ }
67
+ })
68
+
69
+ // await this.verifyStatus(result, options)
70
+
71
+ return result
72
+ }
73
+
74
+ /**
75
+ * Validates the integrity of the response if the integrity is passed. If the integrity does not match, an error is thrown.
76
+ * @param integrity
77
+ * @param response
78
+ */
79
+ private async validateIntegrity(response: Response, url: string, integrity?: string) {
80
+ if (integrity) {
81
+ // validate the integrity of the response according to https://www.w3.org/TR/SRI/
82
+ const arrayBuffer = await response.arrayBuffer()
83
+ const alg = integrity.split('-')[0]
84
+ //TODO: error handling when a hasher is passed that is not supporting the required algorithm acording to the spec
85
+ const hashBuffer = await (this.userConfig.hasher as Hasher)(arrayBuffer, alg)
86
+ const integrityHash = integrity.split('-')[1]
87
+ const hash = Array.from(new Uint8Array(hashBuffer))
88
+ .map((byte) => byte.toString(16).padStart(2, '0'))
89
+ .join('')
90
+ if (hash !== integrityHash) {
91
+ throw new Error(`Integrity check for ${url} failed: is ${hash}, but expected ${integrityHash}`)
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Fetches the content from the url with a timeout of 10 seconds.
98
+ * @param url
99
+ * @param integrity
100
+ * @returns
101
+ */
102
+ protected async fetch<T>(url: string, integrity?: string): Promise<T> {
103
+ try {
104
+ const response = await fetch(url, {
105
+ signal: AbortSignal.timeout(this.userConfig.timeout ?? 10000),
106
+ })
107
+ if (!response.ok) {
108
+ const errorText = await response.text()
109
+ return Promise.reject(new Error(`Error fetching ${url}: ${response.status} ${response.statusText} - ${errorText}`))
110
+ }
111
+ await this.validateIntegrity(response.clone(), url, integrity)
112
+ return response.json() as Promise<T>
113
+ } catch (error) {
114
+ if ((error as Error).name === 'TimeoutError') {
115
+ throw new Error(`Request to ${url} timed out`)
116
+ }
117
+ throw error
118
+ }
119
+ }
120
+
121
+ public async issue<Payload extends SdJwtVcdm2Payload>(
122
+ payload: Payload,
123
+ disclosureFrame?: DisclosureFrame<Payload>,
124
+ options?: {
125
+ header?: object; // This is for customizing the header of the jwt
126
+ },
127
+ ): Promise<SDJWTCompact> {
128
+ if (payload.iss && !payload.issuer) {
129
+ payload.issuer = {id: payload.iss}
130
+ delete payload.iss
131
+ }
132
+ if (payload.nbf && !payload.validFrom) {
133
+ payload.validFrom = toVcdm2Date(payload.nbf)
134
+ delete payload.nbf
135
+ }
136
+ if (payload.exp && !payload.validUntil) {
137
+ payload.validUntil = toVcdm2Date(payload.exp)
138
+ delete payload.exp
139
+ }
140
+ if (payload.sub && !Array.isArray(payload.credentialSubject) && !payload.credentialSubject.id) {
141
+ payload.credentialSubject.id = payload.sub
142
+ delete payload.sub
143
+ }
144
+ return super.issue(payload, disclosureFrame, options)
145
+ }
146
+ }
147
+
148
+ function toVcdm2Date(value: number | string): string {
149
+ const num = typeof value === 'string' ? Number(value) : value
150
+ if (!Number.isFinite(num)) {
151
+ throw new SDJWTException(`Invalid numeric date: ${value}`)
152
+ }
153
+ // Convert JWT NumericDate (seconds since epoch) to W3C VCDM 2 date-time string (RFC 3339 / ISO 8601)
154
+ return new Date(num * 1000).toISOString()
155
+ }