@sphereon/ssi-types 0.30.1-unstable.4 → 0.30.1
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 -201
- package/README.md +49 -49
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/logging/index.d.ts +1 -0
- package/dist/logging/index.d.ts.map +1 -1
- package/dist/logging/index.js +2 -2
- package/dist/logging/index.js.map +1 -1
- package/dist/mapper/jsonld-language-values.d.ts.map +1 -1
- package/dist/types/mso_mdoc.js +5 -5
- package/dist/types/mso_mdoc.js.map +1 -1
- package/dist/types/sd-jwt-vc.js +5 -5
- package/dist/types/sd-jwt-vc.js.map +1 -1
- package/dist/types/w3c-vc.js +3 -3
- package/dist/types/w3c-vc.js.map +1 -1
- package/package.json +4 -4
- package/src/events/index.ts +206 -206
- package/src/index.ts +9 -9
- package/src/logging/index.ts +224 -224
- package/src/mapper/credential-mapper.ts +919 -919
- package/src/mapper/index.ts +2 -2
- package/src/mapper/jsonld-language-values.ts +91 -91
- package/src/types/cose.ts +69 -69
- package/src/types/did.ts +268 -268
- package/src/types/generic.ts +48 -48
- package/src/types/index.ts +9 -9
- package/src/types/jose.ts +115 -115
- package/src/types/mso_mdoc.ts +174 -174
- package/src/types/pex.ts +36 -36
- package/src/types/sd-jwt-vc.ts +369 -369
- package/src/types/vc.ts +87 -87
- package/src/types/w3c-vc.ts +306 -306
- package/src/utils/index.ts +1 -1
- package/src/utils/object.ts +28 -28
package/src/types/sd-jwt-vc.ts
CHANGED
|
@@ -1,369 +1,369 @@
|
|
|
1
|
-
import { OriginalType, WrappedVerifiableCredential, WrappedVerifiablePresentation } from './vc'
|
|
2
|
-
import { decodeSdJwt, decodeSdJwtSync, getClaims, getClaimsSync } from '@sd-jwt/decode'
|
|
3
|
-
import { CompactJWT, IVerifiableCredential } from './w3c-vc'
|
|
4
|
-
import { IProofPurpose, IProofType } from './did'
|
|
5
|
-
|
|
6
|
-
type JsonValue = string | number | boolean | { [x: string]: JsonValue | undefined } | Array<JsonValue>
|
|
7
|
-
|
|
8
|
-
type SdJwtJsonValue =
|
|
9
|
-
| string
|
|
10
|
-
| number
|
|
11
|
-
| boolean
|
|
12
|
-
| {
|
|
13
|
-
[x: string]: SdJwtJsonValue | undefined
|
|
14
|
-
_sd?: string[]
|
|
15
|
-
}
|
|
16
|
-
| Array<SdJwtJsonValue | { '...': string }>
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Decoded 'pretty' SD JWT Verifiable Credential. This representation has all the `_sd` properties
|
|
20
|
-
* removed, and includes the disclosures directly within the payload.
|
|
21
|
-
*/
|
|
22
|
-
export interface SdJwtDecodedVerifiableCredentialPayload {
|
|
23
|
-
vct: string
|
|
24
|
-
iss: string
|
|
25
|
-
iat: number
|
|
26
|
-
nbf?: number
|
|
27
|
-
exp?: number
|
|
28
|
-
cnf?: {
|
|
29
|
-
jwk?: any
|
|
30
|
-
kid?: string
|
|
31
|
-
}
|
|
32
|
-
status?: {
|
|
33
|
-
idx: number
|
|
34
|
-
uri: string
|
|
35
|
-
}
|
|
36
|
-
sub?: string
|
|
37
|
-
|
|
38
|
-
[key: string]: JsonValue | undefined
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Represents a selective disclosure JWT vc in compact form.
|
|
43
|
-
*/
|
|
44
|
-
export type CompactSdJwtVc = string
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The signed payload of an SD-JWT. Includes fields such as `_sd`, `...` and `_sd_alg`
|
|
48
|
-
*/
|
|
49
|
-
interface SdJwtSignedVerifiableCredentialPayload extends SdJwtDecodedVerifiableCredentialPayload {
|
|
50
|
-
// Only present if there are any selectively discloseable claims
|
|
51
|
-
_sd?: string[]
|
|
52
|
-
_sd_alg?: string
|
|
53
|
-
|
|
54
|
-
[x: string]: SdJwtJsonValue | undefined
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
type SdJwtFrameValue = boolean | Array<SdJwtFrameValue> | { [x: string]: SdJwtFrameValue }
|
|
58
|
-
export type SdJwtDisclosureFrame = Record<string, SdJwtFrameValue>
|
|
59
|
-
export type SdJwtPresentationFrame = Record<string, SdJwtFrameValue>
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Input for creating a SD JWT Verifiable Credential. This representation optionally includes the disclosure frame,
|
|
63
|
-
* (as `__disclosureFrame`) to indicate which fields in the signed SD-JWT should be selectively discloseable
|
|
64
|
-
*/
|
|
65
|
-
export interface SdJwtCredentialInput extends SdJwtDecodedVerifiableCredentialPayload {
|
|
66
|
-
/**
|
|
67
|
-
* Disclosure frame, indicating which fields in the signed SD-JWT should be selectively discloseable
|
|
68
|
-
* Will be removed from the actual SD-JWT payload before signing
|
|
69
|
-
*/
|
|
70
|
-
__disclosureFrame?: SdJwtDisclosureFrame
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type SdJwtDecodedDisclosure = [string, string, JsonValue] | [string, JsonValue]
|
|
74
|
-
export interface SdJwtDisclosure {
|
|
75
|
-
// The encoded disclosure
|
|
76
|
-
encoded: string
|
|
77
|
-
|
|
78
|
-
// The decoded disclosure, in format [salt, claim, value] or in case of array entry [salt, value]
|
|
79
|
-
decoded: SdJwtDecodedDisclosure
|
|
80
|
-
|
|
81
|
-
// Digest over disclosure, can be used to match against a value within the SD JWT payload
|
|
82
|
-
digest: string
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* The decoded SD JWT Verifiable Credential. This representation includes multiple representations of the
|
|
87
|
-
* same SD-JWT, and allows to fully process an SD-JWT, as well as create a presentation SD-JWT (minus the KB-JWT) by removing
|
|
88
|
-
* certain disclosures from the compact SD-JWT.
|
|
89
|
-
*
|
|
90
|
-
* This representation is useful as it doesn't require a hasher implementation to match the different digests in the signed SD-JWT
|
|
91
|
-
* payload, with the different disclosures.
|
|
92
|
-
*/
|
|
93
|
-
export interface SdJwtDecodedVerifiableCredential {
|
|
94
|
-
/**
|
|
95
|
-
* The compact sd jwt is the sd-jwt encoded as string. It is a normal JWT,
|
|
96
|
-
* with the disclosures and kb-jwt appended separated by ~ */
|
|
97
|
-
compactSdJwtVc: string
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The disclosures included within the SD-JWT in both encoded and decoded format.
|
|
101
|
-
* The digests are also included, and allows the disclosures to be linked against
|
|
102
|
-
* the digests in the signed payload.
|
|
103
|
-
*/
|
|
104
|
-
disclosures: Array<SdJwtDisclosure>
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* The signed payload is the payload of the sd-jwt that is actually signed, and that includes
|
|
108
|
-
* the `_sd` and `...` digests.
|
|
109
|
-
*/
|
|
110
|
-
signedPayload: SdJwtSignedVerifiableCredentialPayload
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The decoded payload is the payload when all `_sd` and `...` digests have been replaced
|
|
114
|
-
* by the actual values from the disclosures. This format could also be seen as the 'pretty`
|
|
115
|
-
* version of the SD JWT payload.
|
|
116
|
-
*
|
|
117
|
-
* This is useful for displaying the contents of the SD JWT VC to the user, or for example
|
|
118
|
-
* for querying the contents of the SD JWT VC using a PEX presentation definition path.
|
|
119
|
-
*/
|
|
120
|
-
decodedPayload: SdJwtDecodedVerifiableCredentialPayload
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Key binding JWT
|
|
124
|
-
*/
|
|
125
|
-
kbJwt?: {
|
|
126
|
-
header: SdJwtVcKbJwtHeader
|
|
127
|
-
payload: SdJwtVcKbJwtPayload
|
|
128
|
-
compact?: CompactJWT
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface SdJwtVcKbJwtHeader {
|
|
133
|
-
typ: 'kb+jwt'
|
|
134
|
-
alg: string
|
|
135
|
-
[x: string]: any
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface SdJwtVcKbJwtPayload {
|
|
139
|
-
iat: number
|
|
140
|
-
aud: string
|
|
141
|
-
nonce: string
|
|
142
|
-
sd_hash: string
|
|
143
|
-
[key: string]: unknown
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export interface WrappedSdJwtVerifiableCredential {
|
|
147
|
-
/**
|
|
148
|
-
* Original VC that we've received. Can be either the encoded or decoded variant.
|
|
149
|
-
*/
|
|
150
|
-
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
|
|
151
|
-
/**
|
|
152
|
-
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT as the `decoded` property
|
|
153
|
-
* is used in e.g. PEX to check for path filters from fields. The full decoded credential can be found in the `credential` field.
|
|
154
|
-
*/
|
|
155
|
-
decoded: SdJwtDecodedVerifiableCredentialPayload
|
|
156
|
-
/**
|
|
157
|
-
* Type of this credential.
|
|
158
|
-
*/
|
|
159
|
-
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
|
|
160
|
-
/**
|
|
161
|
-
* The claim format, typically used during exchange transport protocols
|
|
162
|
-
*/
|
|
163
|
-
format: 'vc+sd-jwt'
|
|
164
|
-
/**
|
|
165
|
-
* Internal stable representation of a Credential
|
|
166
|
-
*/
|
|
167
|
-
credential: SdJwtDecodedVerifiableCredential
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface WrappedSdJwtVerifiablePresentation {
|
|
171
|
-
/**
|
|
172
|
-
* Original VP that we've received. Can be either the encoded or decoded variant.
|
|
173
|
-
*/
|
|
174
|
-
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
|
|
175
|
-
/**
|
|
176
|
-
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT.
|
|
177
|
-
*/
|
|
178
|
-
decoded: SdJwtDecodedVerifiableCredentialPayload
|
|
179
|
-
/**
|
|
180
|
-
* Type of this Presentation.
|
|
181
|
-
*/
|
|
182
|
-
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
|
|
183
|
-
/**
|
|
184
|
-
* The claim format, typically used during exchange transport protocols
|
|
185
|
-
*/
|
|
186
|
-
format: 'vc+sd-jwt'
|
|
187
|
-
/**
|
|
188
|
-
* Internal stable representation of a Presentation
|
|
189
|
-
*/
|
|
190
|
-
presentation: SdJwtDecodedVerifiableCredential
|
|
191
|
-
/**
|
|
192
|
-
* Wrapped Verifiable Credentials belonging to the Presentation. Will always be an array
|
|
193
|
-
* with a single SdJwtVerifiableCredential entry.
|
|
194
|
-
*/
|
|
195
|
-
vcs: [WrappedSdJwtVerifiableCredential]
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export function isWrappedSdJwtVerifiableCredential(vc: WrappedVerifiableCredential): vc is WrappedSdJwtVerifiableCredential {
|
|
199
|
-
return vc.format === 'vc+sd-jwt'
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function isWrappedSdJwtVerifiablePresentation(vp: WrappedVerifiablePresentation): vp is WrappedSdJwtVerifiablePresentation {
|
|
203
|
-
return vp.format === 'vc+sd-jwt'
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export type Hasher = (data: string, alg: string) => Uint8Array
|
|
207
|
-
export type AsyncHasher = (data: string, alg: string) => Promise<Uint8Array>
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
|
|
211
|
-
* signed payload, decoded payload and the compact SD-JWT vc.
|
|
212
|
-
*
|
|
213
|
-
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
|
|
214
|
-
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
|
|
215
|
-
*/
|
|
216
|
-
export function decodeSdJwtVc(compactSdJwtVc: CompactSdJwtVc, hasher: Hasher): SdJwtDecodedVerifiableCredential {
|
|
217
|
-
const { jwt, disclosures, kbJwt } = decodeSdJwtSync(compactSdJwtVc, hasher)
|
|
218
|
-
|
|
219
|
-
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
|
|
220
|
-
const decodedPayload = getClaimsSync(signedPayload, disclosures, hasher)
|
|
221
|
-
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
compactSdJwtVc,
|
|
225
|
-
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
|
|
226
|
-
disclosures: disclosures.map((d) => {
|
|
227
|
-
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
|
|
228
|
-
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
|
|
229
|
-
return {
|
|
230
|
-
decoded: decoded as SdJwtDecodedDisclosure,
|
|
231
|
-
digest: d._digest,
|
|
232
|
-
encoded: d.encode(),
|
|
233
|
-
} satisfies SdJwtDisclosure
|
|
234
|
-
}),
|
|
235
|
-
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
|
|
236
|
-
...(compactKeyBindingJwt &&
|
|
237
|
-
kbJwt && {
|
|
238
|
-
kbJwt: {
|
|
239
|
-
header: kbJwt.header as SdJwtVcKbJwtHeader,
|
|
240
|
-
compact: compactKeyBindingJwt,
|
|
241
|
-
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
|
|
242
|
-
},
|
|
243
|
-
}),
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
|
|
249
|
-
* signed payload, decoded payload and the compact SD-JWT vc.
|
|
250
|
-
*
|
|
251
|
-
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
|
|
252
|
-
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
|
|
253
|
-
*/
|
|
254
|
-
export async function decodeSdJwtVcAsync(compactSdJwtVc: CompactSdJwtVc, hasher: AsyncHasher): Promise<SdJwtDecodedVerifiableCredential> {
|
|
255
|
-
const { jwt, disclosures, kbJwt } = await decodeSdJwt(compactSdJwtVc, hasher)
|
|
256
|
-
|
|
257
|
-
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
|
|
258
|
-
const decodedPayload = await getClaims(signedPayload, disclosures, hasher)
|
|
259
|
-
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
compactSdJwtVc,
|
|
263
|
-
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
|
|
264
|
-
disclosures: disclosures.map((d) => {
|
|
265
|
-
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
|
|
266
|
-
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
|
|
267
|
-
return {
|
|
268
|
-
decoded: decoded as SdJwtDecodedDisclosure,
|
|
269
|
-
digest: d._digest,
|
|
270
|
-
encoded: d.encode(),
|
|
271
|
-
} satisfies SdJwtDisclosure
|
|
272
|
-
}),
|
|
273
|
-
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
|
|
274
|
-
...(compactKeyBindingJwt &&
|
|
275
|
-
kbJwt && {
|
|
276
|
-
kbJwt: {
|
|
277
|
-
header: kbJwt.header as SdJwtVcKbJwtHeader,
|
|
278
|
-
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
|
|
279
|
-
compact: compactKeyBindingJwt,
|
|
280
|
-
},
|
|
281
|
-
}),
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// TODO naive implementation of mapping a sd-jwt onto a IVerifiableCredential. Needs some fixes and further implementation and needs to be moved out of ssi-types
|
|
286
|
-
export const sdJwtDecodedCredentialToUniformCredential = (
|
|
287
|
-
decoded: SdJwtDecodedVerifiableCredential,
|
|
288
|
-
opts?: { maxTimeSkewInMS?: number }
|
|
289
|
-
): IVerifiableCredential => {
|
|
290
|
-
const { decodedPayload } = decoded // fixme: other params and proof
|
|
291
|
-
const { exp, nbf, iss, iat, vct, cnf, status, sub, jti } = decodedPayload
|
|
292
|
-
|
|
293
|
-
type DisclosuresAccumulator = {
|
|
294
|
-
[key: string]: any
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const credentialSubject = decoded.disclosures.reduce(
|
|
298
|
-
(acc: DisclosuresAccumulator, item: { decoded: Array<any>; digest: string; encoded: string }) => {
|
|
299
|
-
const key = item.decoded[1]
|
|
300
|
-
acc[key] = item.decoded[2]
|
|
301
|
-
|
|
302
|
-
return acc
|
|
303
|
-
},
|
|
304
|
-
{}
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
const maxSkewInMS = opts?.maxTimeSkewInMS ?? 1500
|
|
308
|
-
|
|
309
|
-
const expirationDate = jwtDateToISOString({ jwtClaim: exp, claimName: 'exp' })
|
|
310
|
-
let issuanceDateStr = jwtDateToISOString({ jwtClaim: iat, claimName: 'iat' })
|
|
311
|
-
|
|
312
|
-
let nbfDateAsStr: string | undefined
|
|
313
|
-
if (nbf) {
|
|
314
|
-
nbfDateAsStr = jwtDateToISOString({ jwtClaim: nbf, claimName: 'nbf' })
|
|
315
|
-
if (issuanceDateStr && nbfDateAsStr && issuanceDateStr !== nbfDateAsStr) {
|
|
316
|
-
const diff = Math.abs(new Date(nbfDateAsStr).getTime() - new Date(iss).getTime())
|
|
317
|
-
if (!maxSkewInMS || diff > maxSkewInMS) {
|
|
318
|
-
throw Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${iss})`)
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
issuanceDateStr = nbfDateAsStr
|
|
322
|
-
}
|
|
323
|
-
const issuanceDate = issuanceDateStr
|
|
324
|
-
if (!issuanceDate) {
|
|
325
|
-
throw Error(`JWT issuance date is required but was not present`)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const credential: Omit<IVerifiableCredential, 'issuer' | 'issuanceDate'> = {
|
|
329
|
-
type: [vct], // SDJwt is not a W3C VC, so no VerifiableCredential
|
|
330
|
-
'@context': [], // SDJwt has no JSON-LD by default. Certainly not the VC DM1 default context for JSON-LD
|
|
331
|
-
credentialSubject: {
|
|
332
|
-
...credentialSubject,
|
|
333
|
-
id: credentialSubject.id ?? sub ?? jti,
|
|
334
|
-
},
|
|
335
|
-
issuanceDate,
|
|
336
|
-
expirationDate,
|
|
337
|
-
issuer: iss,
|
|
338
|
-
...(cnf && { cnf }),
|
|
339
|
-
...(status && { status }),
|
|
340
|
-
proof: {
|
|
341
|
-
type: IProofType.SdJwtProof2024,
|
|
342
|
-
created: nbfDateAsStr ?? issuanceDate,
|
|
343
|
-
proofPurpose: IProofPurpose.authentication,
|
|
344
|
-
verificationMethod: iss,
|
|
345
|
-
jwt: decoded.compactSdJwtVc,
|
|
346
|
-
},
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return credential as IVerifiableCredential
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const jwtDateToISOString = ({
|
|
353
|
-
jwtClaim,
|
|
354
|
-
claimName,
|
|
355
|
-
isRequired = false,
|
|
356
|
-
}: {
|
|
357
|
-
jwtClaim?: number
|
|
358
|
-
claimName: string
|
|
359
|
-
isRequired?: boolean
|
|
360
|
-
}): string | undefined => {
|
|
361
|
-
if (jwtClaim) {
|
|
362
|
-
const claim = parseInt(jwtClaim.toString())
|
|
363
|
-
// change JWT seconds to millisecond for the date
|
|
364
|
-
return new Date(claim * (claim < 9999999999 ? 1000 : 1)).toISOString().replace(/\.000Z/, 'Z')
|
|
365
|
-
} else if (isRequired) {
|
|
366
|
-
throw Error(`JWT claim ${claimName} is required but was not present`)
|
|
367
|
-
}
|
|
368
|
-
return undefined
|
|
369
|
-
}
|
|
1
|
+
import { OriginalType, WrappedVerifiableCredential, WrappedVerifiablePresentation } from './vc'
|
|
2
|
+
import { decodeSdJwt, decodeSdJwtSync, getClaims, getClaimsSync } from '@sd-jwt/decode'
|
|
3
|
+
import { CompactJWT, IVerifiableCredential } from './w3c-vc'
|
|
4
|
+
import { IProofPurpose, IProofType } from './did'
|
|
5
|
+
|
|
6
|
+
type JsonValue = string | number | boolean | { [x: string]: JsonValue | undefined } | Array<JsonValue>
|
|
7
|
+
|
|
8
|
+
type SdJwtJsonValue =
|
|
9
|
+
| string
|
|
10
|
+
| number
|
|
11
|
+
| boolean
|
|
12
|
+
| {
|
|
13
|
+
[x: string]: SdJwtJsonValue | undefined
|
|
14
|
+
_sd?: string[]
|
|
15
|
+
}
|
|
16
|
+
| Array<SdJwtJsonValue | { '...': string }>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Decoded 'pretty' SD JWT Verifiable Credential. This representation has all the `_sd` properties
|
|
20
|
+
* removed, and includes the disclosures directly within the payload.
|
|
21
|
+
*/
|
|
22
|
+
export interface SdJwtDecodedVerifiableCredentialPayload {
|
|
23
|
+
vct: string
|
|
24
|
+
iss: string
|
|
25
|
+
iat: number
|
|
26
|
+
nbf?: number
|
|
27
|
+
exp?: number
|
|
28
|
+
cnf?: {
|
|
29
|
+
jwk?: any
|
|
30
|
+
kid?: string
|
|
31
|
+
}
|
|
32
|
+
status?: {
|
|
33
|
+
idx: number
|
|
34
|
+
uri: string
|
|
35
|
+
}
|
|
36
|
+
sub?: string
|
|
37
|
+
|
|
38
|
+
[key: string]: JsonValue | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Represents a selective disclosure JWT vc in compact form.
|
|
43
|
+
*/
|
|
44
|
+
export type CompactSdJwtVc = string
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The signed payload of an SD-JWT. Includes fields such as `_sd`, `...` and `_sd_alg`
|
|
48
|
+
*/
|
|
49
|
+
interface SdJwtSignedVerifiableCredentialPayload extends SdJwtDecodedVerifiableCredentialPayload {
|
|
50
|
+
// Only present if there are any selectively discloseable claims
|
|
51
|
+
_sd?: string[]
|
|
52
|
+
_sd_alg?: string
|
|
53
|
+
|
|
54
|
+
[x: string]: SdJwtJsonValue | undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type SdJwtFrameValue = boolean | Array<SdJwtFrameValue> | { [x: string]: SdJwtFrameValue }
|
|
58
|
+
export type SdJwtDisclosureFrame = Record<string, SdJwtFrameValue>
|
|
59
|
+
export type SdJwtPresentationFrame = Record<string, SdJwtFrameValue>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Input for creating a SD JWT Verifiable Credential. This representation optionally includes the disclosure frame,
|
|
63
|
+
* (as `__disclosureFrame`) to indicate which fields in the signed SD-JWT should be selectively discloseable
|
|
64
|
+
*/
|
|
65
|
+
export interface SdJwtCredentialInput extends SdJwtDecodedVerifiableCredentialPayload {
|
|
66
|
+
/**
|
|
67
|
+
* Disclosure frame, indicating which fields in the signed SD-JWT should be selectively discloseable
|
|
68
|
+
* Will be removed from the actual SD-JWT payload before signing
|
|
69
|
+
*/
|
|
70
|
+
__disclosureFrame?: SdJwtDisclosureFrame
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SdJwtDecodedDisclosure = [string, string, JsonValue] | [string, JsonValue]
|
|
74
|
+
export interface SdJwtDisclosure {
|
|
75
|
+
// The encoded disclosure
|
|
76
|
+
encoded: string
|
|
77
|
+
|
|
78
|
+
// The decoded disclosure, in format [salt, claim, value] or in case of array entry [salt, value]
|
|
79
|
+
decoded: SdJwtDecodedDisclosure
|
|
80
|
+
|
|
81
|
+
// Digest over disclosure, can be used to match against a value within the SD JWT payload
|
|
82
|
+
digest: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The decoded SD JWT Verifiable Credential. This representation includes multiple representations of the
|
|
87
|
+
* same SD-JWT, and allows to fully process an SD-JWT, as well as create a presentation SD-JWT (minus the KB-JWT) by removing
|
|
88
|
+
* certain disclosures from the compact SD-JWT.
|
|
89
|
+
*
|
|
90
|
+
* This representation is useful as it doesn't require a hasher implementation to match the different digests in the signed SD-JWT
|
|
91
|
+
* payload, with the different disclosures.
|
|
92
|
+
*/
|
|
93
|
+
export interface SdJwtDecodedVerifiableCredential {
|
|
94
|
+
/**
|
|
95
|
+
* The compact sd jwt is the sd-jwt encoded as string. It is a normal JWT,
|
|
96
|
+
* with the disclosures and kb-jwt appended separated by ~ */
|
|
97
|
+
compactSdJwtVc: string
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The disclosures included within the SD-JWT in both encoded and decoded format.
|
|
101
|
+
* The digests are also included, and allows the disclosures to be linked against
|
|
102
|
+
* the digests in the signed payload.
|
|
103
|
+
*/
|
|
104
|
+
disclosures: Array<SdJwtDisclosure>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The signed payload is the payload of the sd-jwt that is actually signed, and that includes
|
|
108
|
+
* the `_sd` and `...` digests.
|
|
109
|
+
*/
|
|
110
|
+
signedPayload: SdJwtSignedVerifiableCredentialPayload
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The decoded payload is the payload when all `_sd` and `...` digests have been replaced
|
|
114
|
+
* by the actual values from the disclosures. This format could also be seen as the 'pretty`
|
|
115
|
+
* version of the SD JWT payload.
|
|
116
|
+
*
|
|
117
|
+
* This is useful for displaying the contents of the SD JWT VC to the user, or for example
|
|
118
|
+
* for querying the contents of the SD JWT VC using a PEX presentation definition path.
|
|
119
|
+
*/
|
|
120
|
+
decodedPayload: SdJwtDecodedVerifiableCredentialPayload
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Key binding JWT
|
|
124
|
+
*/
|
|
125
|
+
kbJwt?: {
|
|
126
|
+
header: SdJwtVcKbJwtHeader
|
|
127
|
+
payload: SdJwtVcKbJwtPayload
|
|
128
|
+
compact?: CompactJWT
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface SdJwtVcKbJwtHeader {
|
|
133
|
+
typ: 'kb+jwt'
|
|
134
|
+
alg: string
|
|
135
|
+
[x: string]: any
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SdJwtVcKbJwtPayload {
|
|
139
|
+
iat: number
|
|
140
|
+
aud: string
|
|
141
|
+
nonce: string
|
|
142
|
+
sd_hash: string
|
|
143
|
+
[key: string]: unknown
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface WrappedSdJwtVerifiableCredential {
|
|
147
|
+
/**
|
|
148
|
+
* Original VC that we've received. Can be either the encoded or decoded variant.
|
|
149
|
+
*/
|
|
150
|
+
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
|
|
151
|
+
/**
|
|
152
|
+
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT as the `decoded` property
|
|
153
|
+
* is used in e.g. PEX to check for path filters from fields. The full decoded credential can be found in the `credential` field.
|
|
154
|
+
*/
|
|
155
|
+
decoded: SdJwtDecodedVerifiableCredentialPayload
|
|
156
|
+
/**
|
|
157
|
+
* Type of this credential.
|
|
158
|
+
*/
|
|
159
|
+
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
|
|
160
|
+
/**
|
|
161
|
+
* The claim format, typically used during exchange transport protocols
|
|
162
|
+
*/
|
|
163
|
+
format: 'vc+sd-jwt'
|
|
164
|
+
/**
|
|
165
|
+
* Internal stable representation of a Credential
|
|
166
|
+
*/
|
|
167
|
+
credential: SdJwtDecodedVerifiableCredential
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface WrappedSdJwtVerifiablePresentation {
|
|
171
|
+
/**
|
|
172
|
+
* Original VP that we've received. Can be either the encoded or decoded variant.
|
|
173
|
+
*/
|
|
174
|
+
original: SdJwtDecodedVerifiableCredential | CompactSdJwtVc
|
|
175
|
+
/**
|
|
176
|
+
* Decoded version of the SD-JWT payload. This is the decoded payload, rather than the whole SD-JWT.
|
|
177
|
+
*/
|
|
178
|
+
decoded: SdJwtDecodedVerifiableCredentialPayload
|
|
179
|
+
/**
|
|
180
|
+
* Type of this Presentation.
|
|
181
|
+
*/
|
|
182
|
+
type: OriginalType.SD_JWT_VC_DECODED | OriginalType.SD_JWT_VC_ENCODED
|
|
183
|
+
/**
|
|
184
|
+
* The claim format, typically used during exchange transport protocols
|
|
185
|
+
*/
|
|
186
|
+
format: 'vc+sd-jwt'
|
|
187
|
+
/**
|
|
188
|
+
* Internal stable representation of a Presentation
|
|
189
|
+
*/
|
|
190
|
+
presentation: SdJwtDecodedVerifiableCredential
|
|
191
|
+
/**
|
|
192
|
+
* Wrapped Verifiable Credentials belonging to the Presentation. Will always be an array
|
|
193
|
+
* with a single SdJwtVerifiableCredential entry.
|
|
194
|
+
*/
|
|
195
|
+
vcs: [WrappedSdJwtVerifiableCredential]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function isWrappedSdJwtVerifiableCredential(vc: WrappedVerifiableCredential): vc is WrappedSdJwtVerifiableCredential {
|
|
199
|
+
return vc.format === 'vc+sd-jwt'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function isWrappedSdJwtVerifiablePresentation(vp: WrappedVerifiablePresentation): vp is WrappedSdJwtVerifiablePresentation {
|
|
203
|
+
return vp.format === 'vc+sd-jwt'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type Hasher = (data: string, alg: string) => Uint8Array
|
|
207
|
+
export type AsyncHasher = (data: string, alg: string) => Promise<Uint8Array>
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
|
|
211
|
+
* signed payload, decoded payload and the compact SD-JWT vc.
|
|
212
|
+
*
|
|
213
|
+
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
|
|
214
|
+
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
|
|
215
|
+
*/
|
|
216
|
+
export function decodeSdJwtVc(compactSdJwtVc: CompactSdJwtVc, hasher: Hasher): SdJwtDecodedVerifiableCredential {
|
|
217
|
+
const { jwt, disclosures, kbJwt } = decodeSdJwtSync(compactSdJwtVc, hasher)
|
|
218
|
+
|
|
219
|
+
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
|
|
220
|
+
const decodedPayload = getClaimsSync(signedPayload, disclosures, hasher)
|
|
221
|
+
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
compactSdJwtVc,
|
|
225
|
+
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
|
|
226
|
+
disclosures: disclosures.map((d) => {
|
|
227
|
+
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
|
|
228
|
+
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
|
|
229
|
+
return {
|
|
230
|
+
decoded: decoded as SdJwtDecodedDisclosure,
|
|
231
|
+
digest: d._digest,
|
|
232
|
+
encoded: d.encode(),
|
|
233
|
+
} satisfies SdJwtDisclosure
|
|
234
|
+
}),
|
|
235
|
+
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
|
|
236
|
+
...(compactKeyBindingJwt &&
|
|
237
|
+
kbJwt && {
|
|
238
|
+
kbJwt: {
|
|
239
|
+
header: kbJwt.header as SdJwtVcKbJwtHeader,
|
|
240
|
+
compact: compactKeyBindingJwt,
|
|
241
|
+
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Decode an SD-JWT vc from its compact format (string) to an object containing the disclosures,
|
|
249
|
+
* signed payload, decoded payload and the compact SD-JWT vc.
|
|
250
|
+
*
|
|
251
|
+
* Both the input and output interfaces of this method are defined in `@sphereon/ssi-types`, so
|
|
252
|
+
* this method hides the actual implementation of SD-JWT (which is currently based on @sd-jwt/core)
|
|
253
|
+
*/
|
|
254
|
+
export async function decodeSdJwtVcAsync(compactSdJwtVc: CompactSdJwtVc, hasher: AsyncHasher): Promise<SdJwtDecodedVerifiableCredential> {
|
|
255
|
+
const { jwt, disclosures, kbJwt } = await decodeSdJwt(compactSdJwtVc, hasher)
|
|
256
|
+
|
|
257
|
+
const signedPayload = jwt.payload as SdJwtSignedVerifiableCredentialPayload
|
|
258
|
+
const decodedPayload = await getClaims(signedPayload, disclosures, hasher)
|
|
259
|
+
const compactKeyBindingJwt = kbJwt ? compactSdJwtVc.split('~').pop() : undefined
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
compactSdJwtVc,
|
|
263
|
+
decodedPayload: decodedPayload as SdJwtDecodedVerifiableCredentialPayload,
|
|
264
|
+
disclosures: disclosures.map((d) => {
|
|
265
|
+
const decoded = d.key ? [d.salt, d.key, d.value] : [d.salt, d.value]
|
|
266
|
+
if (!d._digest) throw new Error('Implementation error: digest not present in disclosure')
|
|
267
|
+
return {
|
|
268
|
+
decoded: decoded as SdJwtDecodedDisclosure,
|
|
269
|
+
digest: d._digest,
|
|
270
|
+
encoded: d.encode(),
|
|
271
|
+
} satisfies SdJwtDisclosure
|
|
272
|
+
}),
|
|
273
|
+
signedPayload: signedPayload as SdJwtSignedVerifiableCredentialPayload,
|
|
274
|
+
...(compactKeyBindingJwt &&
|
|
275
|
+
kbJwt && {
|
|
276
|
+
kbJwt: {
|
|
277
|
+
header: kbJwt.header as SdJwtVcKbJwtHeader,
|
|
278
|
+
payload: kbJwt.payload as SdJwtVcKbJwtPayload,
|
|
279
|
+
compact: compactKeyBindingJwt,
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// TODO naive implementation of mapping a sd-jwt onto a IVerifiableCredential. Needs some fixes and further implementation and needs to be moved out of ssi-types
|
|
286
|
+
export const sdJwtDecodedCredentialToUniformCredential = (
|
|
287
|
+
decoded: SdJwtDecodedVerifiableCredential,
|
|
288
|
+
opts?: { maxTimeSkewInMS?: number }
|
|
289
|
+
): IVerifiableCredential => {
|
|
290
|
+
const { decodedPayload } = decoded // fixme: other params and proof
|
|
291
|
+
const { exp, nbf, iss, iat, vct, cnf, status, sub, jti } = decodedPayload
|
|
292
|
+
|
|
293
|
+
type DisclosuresAccumulator = {
|
|
294
|
+
[key: string]: any
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const credentialSubject = decoded.disclosures.reduce(
|
|
298
|
+
(acc: DisclosuresAccumulator, item: { decoded: Array<any>; digest: string; encoded: string }) => {
|
|
299
|
+
const key = item.decoded[1]
|
|
300
|
+
acc[key] = item.decoded[2]
|
|
301
|
+
|
|
302
|
+
return acc
|
|
303
|
+
},
|
|
304
|
+
{}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
const maxSkewInMS = opts?.maxTimeSkewInMS ?? 1500
|
|
308
|
+
|
|
309
|
+
const expirationDate = jwtDateToISOString({ jwtClaim: exp, claimName: 'exp' })
|
|
310
|
+
let issuanceDateStr = jwtDateToISOString({ jwtClaim: iat, claimName: 'iat' })
|
|
311
|
+
|
|
312
|
+
let nbfDateAsStr: string | undefined
|
|
313
|
+
if (nbf) {
|
|
314
|
+
nbfDateAsStr = jwtDateToISOString({ jwtClaim: nbf, claimName: 'nbf' })
|
|
315
|
+
if (issuanceDateStr && nbfDateAsStr && issuanceDateStr !== nbfDateAsStr) {
|
|
316
|
+
const diff = Math.abs(new Date(nbfDateAsStr).getTime() - new Date(iss).getTime())
|
|
317
|
+
if (!maxSkewInMS || diff > maxSkewInMS) {
|
|
318
|
+
throw Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${iss})`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
issuanceDateStr = nbfDateAsStr
|
|
322
|
+
}
|
|
323
|
+
const issuanceDate = issuanceDateStr
|
|
324
|
+
if (!issuanceDate) {
|
|
325
|
+
throw Error(`JWT issuance date is required but was not present`)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const credential: Omit<IVerifiableCredential, 'issuer' | 'issuanceDate'> = {
|
|
329
|
+
type: [vct], // SDJwt is not a W3C VC, so no VerifiableCredential
|
|
330
|
+
'@context': [], // SDJwt has no JSON-LD by default. Certainly not the VC DM1 default context for JSON-LD
|
|
331
|
+
credentialSubject: {
|
|
332
|
+
...credentialSubject,
|
|
333
|
+
id: credentialSubject.id ?? sub ?? jti,
|
|
334
|
+
},
|
|
335
|
+
issuanceDate,
|
|
336
|
+
expirationDate,
|
|
337
|
+
issuer: iss,
|
|
338
|
+
...(cnf && { cnf }),
|
|
339
|
+
...(status && { status }),
|
|
340
|
+
proof: {
|
|
341
|
+
type: IProofType.SdJwtProof2024,
|
|
342
|
+
created: nbfDateAsStr ?? issuanceDate,
|
|
343
|
+
proofPurpose: IProofPurpose.authentication,
|
|
344
|
+
verificationMethod: iss,
|
|
345
|
+
jwt: decoded.compactSdJwtVc,
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return credential as IVerifiableCredential
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const jwtDateToISOString = ({
|
|
353
|
+
jwtClaim,
|
|
354
|
+
claimName,
|
|
355
|
+
isRequired = false,
|
|
356
|
+
}: {
|
|
357
|
+
jwtClaim?: number
|
|
358
|
+
claimName: string
|
|
359
|
+
isRequired?: boolean
|
|
360
|
+
}): string | undefined => {
|
|
361
|
+
if (jwtClaim) {
|
|
362
|
+
const claim = parseInt(jwtClaim.toString())
|
|
363
|
+
// change JWT seconds to millisecond for the date
|
|
364
|
+
return new Date(claim * (claim < 9999999999 ? 1000 : 1)).toISOString().replace(/\.000Z/, 'Z')
|
|
365
|
+
} else if (isRequired) {
|
|
366
|
+
throw Error(`JWT claim ${claimName} is required but was not present`)
|
|
367
|
+
}
|
|
368
|
+
return undefined
|
|
369
|
+
}
|