@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.
- package/dist/index.cjs +251 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -8
- package/dist/index.d.ts +66 -8
- package/dist/index.js +249 -40
- package/dist/index.js.map +1 -1
- package/package.json +21 -20
- package/src/__tests__/{sd-jwt.test.ts → sd-jwt-vc.test.ts} +6 -4
- package/src/__tests__/sd-jwt-vcdm2.test.ts +316 -0
- package/src/action-handler.ts +80 -35
- package/src/sdJwtVcdm2Instance.ts +155 -0
- package/src/types.ts +40 -6
- package/src/utils.ts +32 -1
|
@@ -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
|
+
})
|
package/src/action-handler.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
114
|
-
hashAlg
|
|
125
|
+
signAlg,
|
|
126
|
+
hashAlg,
|
|
115
127
|
})
|
|
116
128
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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 =
|
|
205
|
-
|
|
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.
|
|
224
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|