@sd-jwt/core 0.20.1 → 0.21.0-alpha-20260916100413
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 +2 -2
- package/README.md +82 -80
- package/dist/index.cjs +1526 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +696 -0
- package/dist/index.d.mts +537 -527
- package/dist/index.mjs +1422 -1998
- package/dist/index.mjs.map +1 -0
- package/package.json +29 -49
- package/CHANGELOG.md +0 -39
- package/dist/index.d.ts +0 -686
- package/dist/index.js +0 -2100
- package/src/decode/decode.ts +0 -407
- package/src/decode/index.ts +0 -1
- package/src/decoy.ts +0 -15
- package/src/flattenJSON.ts +0 -99
- package/src/generalJSON.ts +0 -157
- package/src/index.ts +0 -1000
- package/src/jwt.ts +0 -286
- package/src/kbjwt.ts +0 -101
- package/src/present/index.ts +0 -1
- package/src/present/present.ts +0 -216
- package/src/sdjwt.ts +0 -384
- package/src/test/decode/decode.spec.ts +0 -197
- package/src/test/decoy.spec.ts +0 -30
- package/src/test/flattenJSON.spec.ts +0 -122
- package/src/test/generalJSON.spec.ts +0 -198
- package/src/test/index.spec.ts +0 -986
- package/src/test/jwt.spec.ts +0 -301
- package/src/test/kbjwt.spec.ts +0 -555
- package/src/test/pass.spec.ts +0 -7
- package/src/test/present/present.spec.ts +0 -305
- package/src/test/sdjwt.spec.ts +0 -384
- package/src/test/types/type.spec.ts +0 -88
- package/src/test/utils/base64url.spec.ts +0 -33
- package/src/test/utils/disclosure.spec.ts +0 -170
- package/src/test/utils/error.spec.ts +0 -15
- package/src/types/index.ts +0 -2
- package/src/types/type.ts +0 -270
- package/src/types/verification-error.ts +0 -55
- package/src/utils/base64url.ts +0 -6
- package/src/utils/disclosure.ts +0 -121
- package/src/utils/error.ts +0 -25
- package/src/utils/index.ts +0 -3
- package/src/utils/strict-json.ts +0 -17
- package/test/app-e2e.spec.ts +0 -292
- package/test/array_data_types.json +0 -31
- package/test/array_full_sd.json +0 -21
- package/test/array_in_sd.json +0 -13
- package/test/array_nested_in_plain.json +0 -29
- package/test/array_none_disclosed.json +0 -17
- package/test/array_of_nulls.json +0 -15
- package/test/array_of_objects.json +0 -65
- package/test/array_of_scalars.json +0 -19
- package/test/array_recursive_sd.json +0 -35
- package/test/array_recursive_sd_some_disclosed.json +0 -63
- package/test/complex.json +0 -43
- package/test/header_mod.json +0 -48
- package/test/json_serialization.json +0 -48
- package/test/key_binding.json +0 -48
- package/test/no_sd.json +0 -36
- package/test/object_data_types.json +0 -62
- package/test/recursions.json +0 -117
- package/test/rfc9901-audit-fixes.spec.ts +0 -252
- package/test/rfc9901-validation.spec.ts +0 -150
- package/test/tsconfig.json +0 -4
- package/tsconfig.json +0 -7
- package/vitest.config.mts +0 -4
package/src/jwt.ts
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { decodeJwt } from './decode';
|
|
2
|
-
import type { Base64urlString, Signer, Verifier } from './types';
|
|
3
|
-
import { base64urlEncode, SDJWTException } from './utils';
|
|
4
|
-
|
|
5
|
-
export type JwtData<
|
|
6
|
-
Header extends Record<string, unknown>,
|
|
7
|
-
Payload extends Record<string, unknown>,
|
|
8
|
-
> = {
|
|
9
|
-
header?: Header;
|
|
10
|
-
payload?: Payload;
|
|
11
|
-
signature?: Base64urlString;
|
|
12
|
-
encoded?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Options for the JWT verifier
|
|
17
|
-
*/
|
|
18
|
-
export type VerifierOptions = {
|
|
19
|
-
/**
|
|
20
|
-
* current time in seconds since epoch
|
|
21
|
-
*/
|
|
22
|
-
currentDate?: number;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* allowed skew for the current time in seconds. Positive value that will lower the iat and nbf checks, and increase the exp check.
|
|
26
|
-
*/
|
|
27
|
-
skewSeconds?: number;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* required claim keys for the payload.
|
|
31
|
-
* If the payload does not contain these keys, the verification will fail.
|
|
32
|
-
*/
|
|
33
|
-
requiredClaimKeys?: string[];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Expected audience for the processed SD-JWT payload.
|
|
37
|
-
*/
|
|
38
|
-
expectedAudience?: string | string[];
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Allowed JOSE algorithms for issuer-signed JWTs. `none` is always rejected.
|
|
42
|
-
*/
|
|
43
|
-
allowedIssuerAlgorithms?: string[];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* nonce used to verify the key binding jwt to prevent replay attacks.
|
|
47
|
-
*/
|
|
48
|
-
keyBindingNonce?: string;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Expected audience for the Key Binding JWT.
|
|
52
|
-
*/
|
|
53
|
-
expectedKeyBindingAudience?: string | string[];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Maximum acceptable age of the Key Binding JWT, in seconds.
|
|
57
|
-
*/
|
|
58
|
-
keyBindingMaxAgeSeconds?: number;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Internal option used by SD-JWT validation to defer claim checks until after
|
|
62
|
-
* disclosures are processed.
|
|
63
|
-
*/
|
|
64
|
-
skipJwtClaimValidation?: boolean;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* disable the verification of the status claim in the payload.
|
|
68
|
-
*
|
|
69
|
-
* @default false
|
|
70
|
-
*/
|
|
71
|
-
disableStatusVerification?: boolean;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* any other custom options
|
|
75
|
-
*/
|
|
76
|
-
[key: string]: unknown;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const isStringArray = (value: unknown): value is string[] =>
|
|
80
|
-
Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
81
|
-
|
|
82
|
-
const validateNumericDate = (
|
|
83
|
-
payload: Record<string, unknown>,
|
|
84
|
-
claim: 'iat' | 'nbf' | 'exp',
|
|
85
|
-
) => {
|
|
86
|
-
const value = payload[claim];
|
|
87
|
-
if (value === undefined) return undefined;
|
|
88
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
89
|
-
throw new SDJWTException(`Verify Error: JWT ${claim} must be a number`);
|
|
90
|
-
}
|
|
91
|
-
return value;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const getAudiences = (audience: unknown): string[] | undefined => {
|
|
95
|
-
if (typeof audience === 'string') return [audience];
|
|
96
|
-
if (isStringArray(audience)) return audience;
|
|
97
|
-
return undefined;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const validateAudience = (
|
|
101
|
-
payload: Record<string, unknown>,
|
|
102
|
-
expectedAudience: string | string[] | undefined,
|
|
103
|
-
) => {
|
|
104
|
-
if (expectedAudience === undefined) return;
|
|
105
|
-
|
|
106
|
-
const expectedAudiences = Array.isArray(expectedAudience)
|
|
107
|
-
? expectedAudience
|
|
108
|
-
: [expectedAudience];
|
|
109
|
-
const audiences = getAudiences(payload.aud);
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
!audiences ||
|
|
113
|
-
!expectedAudiences.some((expected) => audiences.includes(expected))
|
|
114
|
-
) {
|
|
115
|
-
throw new SDJWTException('Verify Error: Invalid audience');
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export const validateJwtPayload = (
|
|
120
|
-
payload: Record<string, unknown> | undefined,
|
|
121
|
-
options?: VerifierOptions,
|
|
122
|
-
) => {
|
|
123
|
-
if (!payload) {
|
|
124
|
-
throw new SDJWTException('Verify Error: JWT payload is missing');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const skew = options?.skewSeconds ? options.skewSeconds : 0;
|
|
128
|
-
const currentDate = options?.currentDate ?? Math.floor(Date.now() / 1000);
|
|
129
|
-
const iat = validateNumericDate(payload, 'iat');
|
|
130
|
-
const nbf = validateNumericDate(payload, 'nbf');
|
|
131
|
-
const exp = validateNumericDate(payload, 'exp');
|
|
132
|
-
|
|
133
|
-
if (iat !== undefined && iat - skew > currentDate) {
|
|
134
|
-
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (nbf !== undefined && nbf - skew > currentDate) {
|
|
138
|
-
throw new SDJWTException('Verify Error: JWT is not yet valid');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (exp !== undefined && exp + skew <= currentDate) {
|
|
142
|
-
throw new SDJWTException('Verify Error: JWT is expired');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
validateAudience(payload, options?.expectedAudience);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// This class is used to create and verify JWT
|
|
149
|
-
// Contains header, payload, and signature
|
|
150
|
-
export class Jwt<
|
|
151
|
-
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
152
|
-
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
153
|
-
> {
|
|
154
|
-
public header?: Header;
|
|
155
|
-
public payload?: Payload;
|
|
156
|
-
public signature?: Base64urlString;
|
|
157
|
-
private encoded?: string;
|
|
158
|
-
|
|
159
|
-
constructor(data?: JwtData<Header, Payload>) {
|
|
160
|
-
this.header = data?.header;
|
|
161
|
-
this.payload = data?.payload;
|
|
162
|
-
this.signature = data?.signature;
|
|
163
|
-
this.encoded = data?.encoded;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
public static decodeJWT<
|
|
167
|
-
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
168
|
-
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
169
|
-
>(
|
|
170
|
-
jwt: string,
|
|
171
|
-
): { header: Header; payload: Payload; signature: Base64urlString } {
|
|
172
|
-
return decodeJwt(jwt);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
public static fromEncode<
|
|
176
|
-
Header extends Record<string, unknown> = Record<string, unknown>,
|
|
177
|
-
Payload extends Record<string, unknown> = Record<string, unknown>,
|
|
178
|
-
>(encodedJwt: string): Jwt<Header, Payload> {
|
|
179
|
-
const { header, payload, signature } = Jwt.decodeJWT<Header, Payload>(
|
|
180
|
-
encodedJwt,
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const jwt = new Jwt<Header, Payload>({
|
|
184
|
-
header,
|
|
185
|
-
payload,
|
|
186
|
-
signature,
|
|
187
|
-
encoded: encodedJwt,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
return jwt;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
public setHeader(header: Header): Jwt<Header, Payload> {
|
|
194
|
-
this.header = header;
|
|
195
|
-
this.encoded = undefined;
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public setPayload(payload: Payload): Jwt<Header, Payload> {
|
|
200
|
-
this.payload = payload;
|
|
201
|
-
this.encoded = undefined;
|
|
202
|
-
return this;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
protected getUnsignedToken() {
|
|
206
|
-
if (!this.header || !this.payload) {
|
|
207
|
-
throw new SDJWTException('Serialize Error: Invalid JWT');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (this.encoded) {
|
|
211
|
-
const parts = this.encoded.split('.');
|
|
212
|
-
if (parts.length !== 3) {
|
|
213
|
-
throw new SDJWTException(`Invalid JWT format: ${this.encoded}`);
|
|
214
|
-
}
|
|
215
|
-
const unsignedToken = parts.slice(0, 2).join('.');
|
|
216
|
-
return unsignedToken;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const header = base64urlEncode(JSON.stringify(this.header));
|
|
220
|
-
const payload = base64urlEncode(JSON.stringify(this.payload));
|
|
221
|
-
return `${header}.${payload}`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
public async sign(signer: Signer) {
|
|
225
|
-
if (!this.header || this.header.alg === 'none') {
|
|
226
|
-
throw new SDJWTException('Sign Error: alg "none" is not allowed');
|
|
227
|
-
}
|
|
228
|
-
const data = this.getUnsignedToken();
|
|
229
|
-
this.signature = await signer(data);
|
|
230
|
-
|
|
231
|
-
return this.encodeJwt();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
public encodeJwt(): string {
|
|
235
|
-
if (this.encoded) {
|
|
236
|
-
return this.encoded;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!this.header || !this.payload || !this.signature) {
|
|
240
|
-
throw new SDJWTException('Serialize Error: Invalid JWT');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const header = base64urlEncode(JSON.stringify(this.header));
|
|
244
|
-
const payload = base64urlEncode(JSON.stringify(this.payload));
|
|
245
|
-
const signature = this.signature;
|
|
246
|
-
const compact = `${header}.${payload}.${signature}`;
|
|
247
|
-
this.encoded = compact;
|
|
248
|
-
|
|
249
|
-
return compact;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Verify the JWT using the provided verifier function.
|
|
254
|
-
* It checks the signature and validates the iat, nbf, and exp claims if they are present.
|
|
255
|
-
* @param verifier
|
|
256
|
-
* @param options - Options for verification, such as current date and skew seconds
|
|
257
|
-
* @returns
|
|
258
|
-
*/
|
|
259
|
-
public async verify<T>(verifier: Verifier<T>, options?: T & VerifierOptions) {
|
|
260
|
-
const alg = this.header?.alg;
|
|
261
|
-
if (typeof alg !== 'string' || alg === 'none') {
|
|
262
|
-
throw new SDJWTException('Verify Error: alg "none" is not allowed');
|
|
263
|
-
}
|
|
264
|
-
if (
|
|
265
|
-
options?.allowedIssuerAlgorithms &&
|
|
266
|
-
!options.allowedIssuerAlgorithms.includes(alg)
|
|
267
|
-
) {
|
|
268
|
-
throw new SDJWTException(`Verify Error: Disallowed alg ${alg}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!options?.skipJwtClaimValidation) {
|
|
272
|
-
validateJwtPayload(this.payload, options);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (!this.signature) {
|
|
276
|
-
throw new SDJWTException('Verify Error: no signature in JWT');
|
|
277
|
-
}
|
|
278
|
-
const data = this.getUnsignedToken();
|
|
279
|
-
|
|
280
|
-
const verified = await verifier(data, this.signature, options);
|
|
281
|
-
if (!verified) {
|
|
282
|
-
throw new SDJWTException('Verify Error: Invalid JWT Signature');
|
|
283
|
-
}
|
|
284
|
-
return { payload: this.payload, header: this.header };
|
|
285
|
-
}
|
|
286
|
-
}
|
package/src/kbjwt.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { Jwt, type VerifierOptions } from './jwt';
|
|
2
|
-
import {
|
|
3
|
-
KB_JWT_TYP,
|
|
4
|
-
type KbVerifier,
|
|
5
|
-
type kbHeader,
|
|
6
|
-
type kbPayload,
|
|
7
|
-
} from './types';
|
|
8
|
-
import { SDJWTException } from './utils';
|
|
9
|
-
|
|
10
|
-
export class KBJwt<
|
|
11
|
-
Header extends kbHeader = kbHeader,
|
|
12
|
-
Payload extends kbPayload = kbPayload,
|
|
13
|
-
> extends Jwt<Header, Payload> {
|
|
14
|
-
// Checking the validity of the key binding jwt
|
|
15
|
-
// the type unknown is not good, but we don't know at this point how to get the public key of the signer, this is defined in the kbVerifier
|
|
16
|
-
public async verifyKB(values: {
|
|
17
|
-
verifier: KbVerifier;
|
|
18
|
-
payload: Record<string, unknown>;
|
|
19
|
-
nonce: string;
|
|
20
|
-
/**
|
|
21
|
-
* Options forwarded to the common JWT verification, e.g. currentDate and
|
|
22
|
-
* skewSeconds used to validate the iat, nbf and exp claims.
|
|
23
|
-
*/
|
|
24
|
-
options?: VerifierOptions;
|
|
25
|
-
}) {
|
|
26
|
-
if (!this.header || !this.payload || !this.signature) {
|
|
27
|
-
throw new SDJWTException('Verify Error: Invalid JWT');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (
|
|
31
|
-
typeof this.header.alg !== 'string' ||
|
|
32
|
-
this.header.alg === 'none' ||
|
|
33
|
-
typeof this.header.typ !== 'string' ||
|
|
34
|
-
this.header.typ !== KB_JWT_TYP ||
|
|
35
|
-
typeof this.payload.iat !== 'number' ||
|
|
36
|
-
typeof this.payload.aud !== 'string' ||
|
|
37
|
-
typeof this.payload.nonce !== 'string' ||
|
|
38
|
-
typeof this.payload.sd_hash !== 'string' ||
|
|
39
|
-
this.payload.sd_hash.length === 0
|
|
40
|
-
) {
|
|
41
|
-
throw new SDJWTException('Invalid Key Binding Jwt');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (this.payload.nonce !== values.nonce) {
|
|
45
|
-
throw new SDJWTException('Verify Error: Invalid Nonce');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (values.options?.expectedKeyBindingAudience !== undefined) {
|
|
49
|
-
const expectedAudiences = Array.isArray(
|
|
50
|
-
values.options.expectedKeyBindingAudience,
|
|
51
|
-
)
|
|
52
|
-
? values.options.expectedKeyBindingAudience
|
|
53
|
-
: [values.options.expectedKeyBindingAudience];
|
|
54
|
-
if (!expectedAudiences.includes(this.payload.aud)) {
|
|
55
|
-
throw new SDJWTException('Verify Error: Invalid Key Binding audience');
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (values.options?.keyBindingMaxAgeSeconds !== undefined) {
|
|
60
|
-
const currentDate =
|
|
61
|
-
values.options.currentDate ?? Math.floor(Date.now() / 1000);
|
|
62
|
-
const skew = values.options.skewSeconds ?? 0;
|
|
63
|
-
if (
|
|
64
|
-
this.payload.iat + values.options.keyBindingMaxAgeSeconds + skew <
|
|
65
|
-
currentDate
|
|
66
|
-
) {
|
|
67
|
-
throw new SDJWTException('Verify Error: Key Binding JWT is too old');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Delegate signature verification and common JWT claim validation
|
|
72
|
-
// (iat, nbf, exp) to the shared Jwt.verify implementation. The kbVerifier
|
|
73
|
-
// needs the kb+jwt payload (e.g. the holder's cnf key), so we wrap it to
|
|
74
|
-
// forward values.payload instead of the base verifier's options argument.
|
|
75
|
-
await this.verify(
|
|
76
|
-
(data, sig) => values.verifier(data, sig, values.payload),
|
|
77
|
-
values.options,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return { payload: this.payload, header: this.header };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// This function is for creating KBJwt object for verify properly
|
|
84
|
-
public static fromKBEncode<
|
|
85
|
-
Header extends kbHeader = kbHeader,
|
|
86
|
-
Payload extends kbPayload = kbPayload,
|
|
87
|
-
>(encodedJwt: string): KBJwt<Header, Payload> {
|
|
88
|
-
const { header, payload, signature } = Jwt.decodeJWT<Header, Payload>(
|
|
89
|
-
encodedJwt,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const jwt = new KBJwt<Header, Payload>({
|
|
93
|
-
header,
|
|
94
|
-
payload,
|
|
95
|
-
signature,
|
|
96
|
-
encoded: encodedJwt,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return jwt;
|
|
100
|
-
}
|
|
101
|
-
}
|
package/src/present/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './present';
|
package/src/present/present.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createHashMapping,
|
|
3
|
-
createHashMappingSync,
|
|
4
|
-
decodeSdJwt,
|
|
5
|
-
decodeSdJwtSync,
|
|
6
|
-
getSDAlgAndPayload,
|
|
7
|
-
splitSdJwt,
|
|
8
|
-
unpack,
|
|
9
|
-
unpackObj,
|
|
10
|
-
unpackSync,
|
|
11
|
-
} from '../decode';
|
|
12
|
-
import type { Extensible, HasherSync } from '../types';
|
|
13
|
-
import {
|
|
14
|
-
encodePath,
|
|
15
|
-
type Hasher,
|
|
16
|
-
type PresentationFrame,
|
|
17
|
-
SD_SEPARATOR,
|
|
18
|
-
} from '../types';
|
|
19
|
-
import { Disclosure, SDJWTException } from '../utils';
|
|
20
|
-
|
|
21
|
-
// Presentable keys
|
|
22
|
-
// The presentable keys are the path of JSON object that are presentable in the SD JWT
|
|
23
|
-
// e.g. if the SD JWT has the following payload and set sd like this:
|
|
24
|
-
// {
|
|
25
|
-
// "foo": "bar", // sd
|
|
26
|
-
// "arr": [ // sd
|
|
27
|
-
// "1", // sd
|
|
28
|
-
// "2",
|
|
29
|
-
// {
|
|
30
|
-
// "a": "1" // sd
|
|
31
|
-
// }
|
|
32
|
-
// ],
|
|
33
|
-
// "test": {
|
|
34
|
-
// "zzz": "xxx" // sd
|
|
35
|
-
// }
|
|
36
|
-
// }
|
|
37
|
-
// The presentable keys are: ["arr", "arr.0", "arr.2.a", "foo", "test.zzz"]
|
|
38
|
-
export const presentableKeys = async (
|
|
39
|
-
rawPayload: Record<string, unknown>,
|
|
40
|
-
disclosures: Array<Disclosure>,
|
|
41
|
-
hasher: Hasher,
|
|
42
|
-
): Promise<string[]> => {
|
|
43
|
-
const { disclosureKeymap } = await unpack(rawPayload, disclosures, hasher);
|
|
44
|
-
return Object.keys(disclosureKeymap).sort();
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const presentableKeysSync = (
|
|
48
|
-
rawPayload: Record<string, unknown>,
|
|
49
|
-
disclosures: Array<Disclosure>,
|
|
50
|
-
hasher: HasherSync,
|
|
51
|
-
): string[] => {
|
|
52
|
-
const { disclosureKeymap } = unpackSync(rawPayload, disclosures, hasher);
|
|
53
|
-
return Object.keys(disclosureKeymap).sort();
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const present = async <T extends Record<string, unknown>>(
|
|
57
|
-
sdJwt: string,
|
|
58
|
-
presentFrame: PresentationFrame<T>,
|
|
59
|
-
hasher: Hasher,
|
|
60
|
-
): Promise<string> => {
|
|
61
|
-
const { jwt, kbJwt } = splitSdJwt(sdJwt);
|
|
62
|
-
const {
|
|
63
|
-
jwt: { payload },
|
|
64
|
-
disclosures,
|
|
65
|
-
} = await decodeSdJwt(sdJwt, hasher);
|
|
66
|
-
|
|
67
|
-
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
|
|
68
|
-
const hash = { alg, hasher };
|
|
69
|
-
const keys = transformPresentationFrame(presentFrame);
|
|
70
|
-
|
|
71
|
-
// hashmap: <digest> => <disclosure>
|
|
72
|
-
// to match the digest with the disclosure
|
|
73
|
-
const hashmap = await createHashMapping(disclosures, hash);
|
|
74
|
-
const { disclosureKeymap } = await unpack(payload, disclosures, hasher);
|
|
75
|
-
const presentedDisclosures = keys
|
|
76
|
-
.map((k) => hashmap[disclosureKeymap[k]])
|
|
77
|
-
.filter((d) => d !== undefined);
|
|
78
|
-
|
|
79
|
-
return [
|
|
80
|
-
jwt,
|
|
81
|
-
...presentedDisclosures.map((d) => d.encode()),
|
|
82
|
-
kbJwt ?? '',
|
|
83
|
-
].join(SD_SEPARATOR);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const presentSync = <T extends Record<string, unknown>>(
|
|
87
|
-
sdJwt: string,
|
|
88
|
-
presentFrame: PresentationFrame<T>,
|
|
89
|
-
hasher: HasherSync,
|
|
90
|
-
): string => {
|
|
91
|
-
const { jwt, kbJwt } = splitSdJwt(sdJwt);
|
|
92
|
-
const {
|
|
93
|
-
jwt: { payload },
|
|
94
|
-
disclosures,
|
|
95
|
-
} = decodeSdJwtSync(sdJwt, hasher);
|
|
96
|
-
|
|
97
|
-
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
|
|
98
|
-
const hash = { alg, hasher };
|
|
99
|
-
const keys = transformPresentationFrame(presentFrame);
|
|
100
|
-
|
|
101
|
-
// hashmap: <digest> => <disclosure>
|
|
102
|
-
// to match the digest with the disclosure
|
|
103
|
-
const hashmap = createHashMappingSync(disclosures, hash);
|
|
104
|
-
const { disclosureKeymap } = unpackSync(payload, disclosures, hasher);
|
|
105
|
-
|
|
106
|
-
const presentedDisclosures = keys
|
|
107
|
-
.map((k) => hashmap[disclosureKeymap[k]])
|
|
108
|
-
.filter((d) => d !== undefined);
|
|
109
|
-
|
|
110
|
-
return [
|
|
111
|
-
jwt,
|
|
112
|
-
...presentedDisclosures.map((d) => d.encode()),
|
|
113
|
-
kbJwt ?? '',
|
|
114
|
-
].join(SD_SEPARATOR);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Transform the object keys into an array of strings. We are not sorting the array in any way.
|
|
119
|
-
* @param obj The object to transform
|
|
120
|
-
* @param prefix The prefix to add to the keys
|
|
121
|
-
* @returns
|
|
122
|
-
*/
|
|
123
|
-
export const transformPresentationFrame = (
|
|
124
|
-
obj: PresentationFrame<Extensible>,
|
|
125
|
-
prefix: string[] = [],
|
|
126
|
-
): string[] => {
|
|
127
|
-
return Object.entries(obj).reduce<string[]>((acc, [key, value]) => {
|
|
128
|
-
const newPrefix = [...prefix, key];
|
|
129
|
-
const encodedPrefix = encodePath(newPrefix);
|
|
130
|
-
if (typeof value === 'boolean') {
|
|
131
|
-
// only add it, when it's true
|
|
132
|
-
if (value) {
|
|
133
|
-
acc.push(encodedPrefix);
|
|
134
|
-
}
|
|
135
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
136
|
-
acc.push(
|
|
137
|
-
encodedPrefix,
|
|
138
|
-
...transformPresentationFrame(
|
|
139
|
-
value as PresentationFrame<Extensible>,
|
|
140
|
-
newPrefix,
|
|
141
|
-
),
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return acc;
|
|
145
|
-
}, []);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
export type SerializedDisclosure = {
|
|
149
|
-
digest: string;
|
|
150
|
-
encoded: string;
|
|
151
|
-
salt: string;
|
|
152
|
-
key: string | undefined;
|
|
153
|
-
value: unknown;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
export const createHashMappingForSerializedDisclosure = (
|
|
157
|
-
disclosures: SerializedDisclosure[],
|
|
158
|
-
) => {
|
|
159
|
-
const map: Record<string, Disclosure> = {};
|
|
160
|
-
for (let i = 0; i < disclosures.length; i++) {
|
|
161
|
-
const disclosure = disclosures[i];
|
|
162
|
-
const { digest, encoded, key, salt, value } = disclosure;
|
|
163
|
-
// we made Disclosure to fit the interface of unpack
|
|
164
|
-
map[digest] = Disclosure.fromArray(
|
|
165
|
-
key ? [salt, key, value] : [salt, value],
|
|
166
|
-
{ digest, encoded },
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
return map;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* This function selects the serialized disclosures from the payload
|
|
174
|
-
* and array of serialized disclosure based on the presentation frame.
|
|
175
|
-
* If you want to know what is serialized disclosures, check type SerializedDisclosure.
|
|
176
|
-
* @param payload: Record<string, unknown>
|
|
177
|
-
* @param disclosures: SerializedDisclosure[]
|
|
178
|
-
* @param presentationFrame: PresentationFrame<T>
|
|
179
|
-
*/
|
|
180
|
-
export const selectDisclosures = <T extends Record<string, unknown>>(
|
|
181
|
-
payload: Record<string, unknown>,
|
|
182
|
-
disclosures: SerializedDisclosure[],
|
|
183
|
-
presentationFrame: PresentationFrame<T>,
|
|
184
|
-
) => {
|
|
185
|
-
if (disclosures.length === 0) {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const hashmap = createHashMappingForSerializedDisclosure(disclosures);
|
|
190
|
-
const { disclosureKeymap } = unpackObj(payload, hashmap);
|
|
191
|
-
const keys = transformPresentationFrame(presentationFrame);
|
|
192
|
-
|
|
193
|
-
const presentedDisclosures = keys
|
|
194
|
-
.map((k) => hashmap[disclosureKeymap[k]])
|
|
195
|
-
.filter((d) => d !== undefined);
|
|
196
|
-
|
|
197
|
-
const selectedDisclosures: SerializedDisclosure[] = presentedDisclosures.map(
|
|
198
|
-
(d) => {
|
|
199
|
-
const { salt, key, value, _digest } = d;
|
|
200
|
-
if (!_digest) {
|
|
201
|
-
throw new SDJWTException(
|
|
202
|
-
'Implementation error: _digest is not defined',
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
digest: _digest,
|
|
207
|
-
encoded: d.encode(),
|
|
208
|
-
salt,
|
|
209
|
-
key,
|
|
210
|
-
value,
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
return selectedDisclosures;
|
|
216
|
-
};
|