@blamejs/core 0.7.106 → 0.8.0
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/CHANGELOG.md +19 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +16 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit.js +6 -0
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/sd-jwt-vc-disclosure.js +95 -0
- package/lib/auth/sd-jwt-vc-holder.js +203 -0
- package/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/auth/sd-jwt-vc.js +526 -0
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/compliance-ai-act-logging.js +186 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +2 -0
- package/lib/crypto.js +32 -0
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/inbox.js +367 -0
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/pqc-software.js +195 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +829 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.sdJwtVc — Selective Disclosure JWT for Verifiable Credentials
|
|
4
|
+
* (draft-ietf-oauth-sd-jwt-vc).
|
|
5
|
+
*
|
|
6
|
+
* SD-JWT VC is the IETF format aligned with the EU Digital Identity
|
|
7
|
+
* Wallet (EUDI Wallet) roll-out and the EU AI Act Article 50
|
|
8
|
+
* disclosure requirements. It allows an issuer to mint a credential
|
|
9
|
+
* containing selectively-disclosable claims; the holder presents only
|
|
10
|
+
* the subset they choose to a verifier; the verifier validates the
|
|
11
|
+
* issuer signature + the cryptographic binding between disclosures
|
|
12
|
+
* and the issuer's `_sd` digest array.
|
|
13
|
+
*
|
|
14
|
+
* // Issuer side
|
|
15
|
+
* var sdJwt = b.auth.sdJwtVc.issue({
|
|
16
|
+
* issuer: "https://issuer.example.com",
|
|
17
|
+
* subject: "did:web:alice.example.com",
|
|
18
|
+
* vct: "https://credentials.example.com/identity_credential",
|
|
19
|
+
* claims: {
|
|
20
|
+
* given_name: "Alice",
|
|
21
|
+
* family_name: "Smith",
|
|
22
|
+
* birthdate: "1990-01-15",
|
|
23
|
+
* nationality: "US",
|
|
24
|
+
* },
|
|
25
|
+
* selectivelyDisclosed: ["given_name", "family_name", "birthdate"],
|
|
26
|
+
* issuerKey: issuerPrivKeyPem,
|
|
27
|
+
* algorithm: "ES256",
|
|
28
|
+
* ttlMs: C.TIME.days(30),
|
|
29
|
+
* holderKey: holderPubJwk, // optional cnf binding
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Holder presents subset
|
|
33
|
+
* var presentation = b.auth.sdJwtVc.present({
|
|
34
|
+
* sdJwt: sdJwt.token,
|
|
35
|
+
* disclosedClaimNames: ["given_name"], // selective release
|
|
36
|
+
* audience: "https://verifier.example.com",
|
|
37
|
+
* nonce: nonceFromVerifier,
|
|
38
|
+
* holderKey: holderPrivKeyPem, // for KB-JWT signing
|
|
39
|
+
* algorithm: "ES256",
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* // Verifier validates
|
|
43
|
+
* var result = await b.auth.sdJwtVc.verify(presentation, {
|
|
44
|
+
* issuerKeyResolver: async function (header) { return issuerPubKeyPem; },
|
|
45
|
+
* audience: "https://verifier.example.com",
|
|
46
|
+
* nonce: nonceForReplayDefense,
|
|
47
|
+
* });
|
|
48
|
+
* // → { valid: true, claims: { vct, given_name }, holderKey, kbValidated }
|
|
49
|
+
*
|
|
50
|
+
* Supported signature algorithms (issuer + KB-JWT):
|
|
51
|
+
* - ES256 (ECDSA + P-256 + SHA-256) — default per spec
|
|
52
|
+
* - ES384 (ECDSA + P-384 + SHA-384)
|
|
53
|
+
* - EdDSA (Ed25519)
|
|
54
|
+
* - ML-DSA-87 (PQC; framework's default) — draft IETF allocation
|
|
55
|
+
* - ML-DSA-65 (PQC; lighter)
|
|
56
|
+
*
|
|
57
|
+
* Hash algorithm for `_sd` digests: SHA-256 (spec default). Operators
|
|
58
|
+
* with PQC strict deployments specify "sha3-256" or "sha-512" via
|
|
59
|
+
* opts.hashAlg at issue time; verify() reads `_sd_alg` from the
|
|
60
|
+
* issuer payload to know how to recompute digests.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
var nodeCrypto = require("node:crypto");
|
|
64
|
+
var safeBuffer = require("../safe-buffer");
|
|
65
|
+
var safeJson = require("../safe-json");
|
|
66
|
+
var validateOpts = require("../validate-opts");
|
|
67
|
+
var disclosure = require("./sd-jwt-vc-disclosure");
|
|
68
|
+
var sdJwtVcIssuer = require("./sd-jwt-vc-issuer");
|
|
69
|
+
var sdJwtVcHolder = require("./sd-jwt-vc-holder");
|
|
70
|
+
var { AuthError } = require("../framework-error");
|
|
71
|
+
|
|
72
|
+
var SUPPORTED_ALGS = Object.freeze([
|
|
73
|
+
"ES256", "ES384", "EdDSA", "ML-DSA-87", "ML-DSA-65",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
var SUPPORTED_HASH_ALGS = Object.freeze({
|
|
77
|
+
"sha-256": "sha256",
|
|
78
|
+
"sha-512": "sha512",
|
|
79
|
+
"sha3-256": "sha3-256",
|
|
80
|
+
"sha3-512": "sha3-512",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
var DEFAULT_ALG = "ES256";
|
|
84
|
+
var DEFAULT_HASH_ALG = "sha-256";
|
|
85
|
+
|
|
86
|
+
function _b64uEncode(str) {
|
|
87
|
+
return Buffer.from(str, "utf8").toString("base64url");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _b64uEncodeBuf(buf) {
|
|
91
|
+
return buf.toString("base64url");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _b64uDecodeStr(s) {
|
|
95
|
+
return Buffer.from(s, "base64url").toString("utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _b64uDecodeBuf(s) {
|
|
99
|
+
return Buffer.from(s, "base64url");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _hashDisclosure(disclosureStr, hashAlg) {
|
|
103
|
+
var nodeAlg = SUPPORTED_HASH_ALGS[hashAlg];
|
|
104
|
+
if (!nodeAlg) {
|
|
105
|
+
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
106
|
+
"Unsupported hash algorithm: " + hashAlg);
|
|
107
|
+
}
|
|
108
|
+
var h = nodeCrypto.createHash(nodeAlg);
|
|
109
|
+
h.update(disclosureStr, "ascii");
|
|
110
|
+
return h.digest().toString("base64url");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _signJwt(header, payload, privateKey, algorithm) {
|
|
114
|
+
var headerStr = _b64uEncode(safeJson.stringify(header));
|
|
115
|
+
var payloadStr = _b64uEncode(safeJson.stringify(payload));
|
|
116
|
+
var signingInput = headerStr + "." + payloadStr;
|
|
117
|
+
var sigAlgo = _resolveSigAlgo(algorithm);
|
|
118
|
+
var sig = nodeCrypto.sign(sigAlgo, Buffer.from(signingInput, "ascii"), privateKey);
|
|
119
|
+
return signingInput + "." + sig.toString("base64url");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _verifyJwt(token, publicKey, algorithm) {
|
|
123
|
+
var parts = token.split(".");
|
|
124
|
+
if (parts.length !== 3) {
|
|
125
|
+
throw new AuthError("auth-sd-jwt-vc/malformed-jwt",
|
|
126
|
+
"JWT must have 3 dot-separated parts");
|
|
127
|
+
}
|
|
128
|
+
var signingInput = parts[0] + "." + parts[1];
|
|
129
|
+
var sig = _b64uDecodeBuf(parts[2]);
|
|
130
|
+
var sigAlgo = _resolveSigAlgo(algorithm);
|
|
131
|
+
var ok = nodeCrypto.verify(sigAlgo, Buffer.from(signingInput, "ascii"), publicKey, sig);
|
|
132
|
+
if (!ok) {
|
|
133
|
+
throw new AuthError("auth-sd-jwt-vc/bad-signature",
|
|
134
|
+
"JWT signature verification failed");
|
|
135
|
+
}
|
|
136
|
+
var headerStr = _b64uDecodeStr(parts[0]);
|
|
137
|
+
var payloadStr = _b64uDecodeStr(parts[1]);
|
|
138
|
+
return {
|
|
139
|
+
header: safeJson.parse(headerStr, { maxBytes: 64 * 1024 }), // allow:bare-json-parse — header from cryptographically-verified JWT; signature verifies the bytes // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
140
|
+
payload: safeJson.parse(payloadStr, { maxBytes: 1024 * 1024 }), // allow:bare-json-parse — payload from cryptographically-verified JWT; signature verifies the bytes // allow:raw-byte-literal — JWT payload cap (1 MB)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _resolveSigAlgo(algorithm) {
|
|
145
|
+
// Node 24+ accepts these algorithm hints; ES256 / ES384 use the
|
|
146
|
+
// EC private key's curve to dispatch. EdDSA + ML-DSA-* are
|
|
147
|
+
// signature-algorithm hints handled directly by Node.js crypto.
|
|
148
|
+
switch (algorithm) {
|
|
149
|
+
case "ES256": return "sha256";
|
|
150
|
+
case "ES384": return "sha384";
|
|
151
|
+
case "EdDSA": return null; // pass-through, Node infers
|
|
152
|
+
case "ML-DSA-87": return null;
|
|
153
|
+
case "ML-DSA-65": return null;
|
|
154
|
+
default:
|
|
155
|
+
throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
|
|
156
|
+
"Unsupported algorithm: " + algorithm);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- issue ----
|
|
161
|
+
|
|
162
|
+
function issue(opts) {
|
|
163
|
+
validateOpts.requireObject(opts, "auth.sdJwtVc.issue", AuthError);
|
|
164
|
+
validateOpts(opts, [
|
|
165
|
+
"issuer", "subject", "vct", "claims",
|
|
166
|
+
"selectivelyDisclosed", "issuerKey", "algorithm",
|
|
167
|
+
"hashAlg", "ttlMs", "issuedAt",
|
|
168
|
+
"holderKey", "extraHeader",
|
|
169
|
+
], "auth.sdJwtVc.issue");
|
|
170
|
+
|
|
171
|
+
validateOpts.requireNonEmptyString(opts.issuer,
|
|
172
|
+
"issue: issuer", AuthError, "auth-sd-jwt-vc/bad-opts");
|
|
173
|
+
validateOpts.requireNonEmptyString(opts.vct,
|
|
174
|
+
"issue: vct", AuthError, "auth-sd-jwt-vc/bad-opts");
|
|
175
|
+
if (!opts.claims || typeof opts.claims !== "object" || Array.isArray(opts.claims)) {
|
|
176
|
+
throw new AuthError("auth-sd-jwt-vc/bad-opts",
|
|
177
|
+
"issue: claims must be a plain object");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
var algorithm = opts.algorithm || DEFAULT_ALG;
|
|
181
|
+
if (SUPPORTED_ALGS.indexOf(algorithm) === -1) {
|
|
182
|
+
throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
|
|
183
|
+
"issue: algorithm must be one of " + SUPPORTED_ALGS.join(", "));
|
|
184
|
+
}
|
|
185
|
+
var hashAlg = opts.hashAlg || DEFAULT_HASH_ALG;
|
|
186
|
+
if (!SUPPORTED_HASH_ALGS[hashAlg]) {
|
|
187
|
+
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
188
|
+
"issue: hashAlg must be one of " + Object.keys(SUPPORTED_HASH_ALGS).join(", "));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!opts.issuerKey) {
|
|
192
|
+
throw new AuthError("auth-sd-jwt-vc/no-key", "issue: issuerKey required");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var sdNames = Array.isArray(opts.selectivelyDisclosed)
|
|
196
|
+
? opts.selectivelyDisclosed.slice() : [];
|
|
197
|
+
var unknownSdNames = sdNames.filter(function (n) {
|
|
198
|
+
return !(n in opts.claims);
|
|
199
|
+
});
|
|
200
|
+
if (unknownSdNames.length > 0) {
|
|
201
|
+
throw new AuthError("auth-sd-jwt-vc/unknown-claim",
|
|
202
|
+
"issue: selectivelyDisclosed includes claim(s) not present in claims: " +
|
|
203
|
+
unknownSdNames.join(", "));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Build disclosures + digest array
|
|
207
|
+
var disclosures = [];
|
|
208
|
+
var sdDigests = [];
|
|
209
|
+
var plainClaims = {};
|
|
210
|
+
var allClaimNames = Object.keys(opts.claims);
|
|
211
|
+
for (var i = 0; i < allClaimNames.length; i++) {
|
|
212
|
+
var name = allClaimNames[i];
|
|
213
|
+
var value = opts.claims[name];
|
|
214
|
+
if (sdNames.indexOf(name) !== -1) {
|
|
215
|
+
var d = disclosure.encode(name, value);
|
|
216
|
+
disclosures.push(d);
|
|
217
|
+
sdDigests.push(_hashDisclosure(d, hashAlg));
|
|
218
|
+
} else {
|
|
219
|
+
plainClaims[name] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Spec: shuffle digests so order doesn't leak claim order
|
|
223
|
+
sdDigests.sort();
|
|
224
|
+
|
|
225
|
+
var now = (typeof opts.issuedAt === "number" && isFinite(opts.issuedAt))
|
|
226
|
+
? Math.floor(opts.issuedAt / 1000) : Math.floor(Date.now() / 1000); // allow:raw-byte-literal — ms→s conversion factor
|
|
227
|
+
var ttlSec = opts.ttlMs ? Math.floor(opts.ttlMs / 1000) : 30 * 24 * 60 * 60; // allow:raw-byte-literal — ms→s conversion + 30-day default in seconds
|
|
228
|
+
|
|
229
|
+
var payload = Object.assign({}, plainClaims, {
|
|
230
|
+
iss: opts.issuer,
|
|
231
|
+
iat: now,
|
|
232
|
+
exp: now + ttlSec,
|
|
233
|
+
vct: opts.vct,
|
|
234
|
+
_sd: sdDigests,
|
|
235
|
+
_sd_alg: hashAlg,
|
|
236
|
+
});
|
|
237
|
+
if (opts.subject) payload.sub = opts.subject;
|
|
238
|
+
if (opts.holderKey) {
|
|
239
|
+
// Holder binding via cnf claim per draft §4.2.2
|
|
240
|
+
if (typeof opts.holderKey !== "object" || !opts.holderKey.kty) {
|
|
241
|
+
throw new AuthError("auth-sd-jwt-vc/bad-cnf",
|
|
242
|
+
"issue: holderKey must be a JWK with kty");
|
|
243
|
+
}
|
|
244
|
+
payload.cnf = { jwk: opts.holderKey };
|
|
245
|
+
}
|
|
246
|
+
var header = Object.assign({}, opts.extraHeader || {}, {
|
|
247
|
+
alg: algorithm,
|
|
248
|
+
typ: "vc+sd-jwt",
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
var jwt = _signJwt(header, payload, opts.issuerKey, algorithm);
|
|
252
|
+
var token = jwt + "~" + disclosures.join("~") + "~";
|
|
253
|
+
return {
|
|
254
|
+
token: token,
|
|
255
|
+
jwt: jwt,
|
|
256
|
+
disclosures: disclosures,
|
|
257
|
+
payload: payload,
|
|
258
|
+
header: header,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- present (holder side) ----
|
|
263
|
+
|
|
264
|
+
function present(opts) {
|
|
265
|
+
validateOpts.requireObject(opts, "auth.sdJwtVc.present", AuthError);
|
|
266
|
+
validateOpts(opts, [
|
|
267
|
+
"sdJwt", "disclosedClaimNames", "audience",
|
|
268
|
+
"nonce", "holderKey", "algorithm", "issuedAt",
|
|
269
|
+
], "auth.sdJwtVc.present");
|
|
270
|
+
|
|
271
|
+
validateOpts.requireNonEmptyString(opts.sdJwt,
|
|
272
|
+
"present: sdJwt", AuthError, "auth-sd-jwt-vc/no-token");
|
|
273
|
+
var parts = opts.sdJwt.split("~");
|
|
274
|
+
if (parts.length < 2) {
|
|
275
|
+
throw new AuthError("auth-sd-jwt-vc/malformed",
|
|
276
|
+
"present: sdJwt must contain at least one ~-separator");
|
|
277
|
+
}
|
|
278
|
+
var jwt = parts[0];
|
|
279
|
+
var allDisclosures = parts.slice(1).filter(function (p) { return p.length > 0; });
|
|
280
|
+
|
|
281
|
+
// Decode disclosures + filter by name
|
|
282
|
+
var disclosedNames = Array.isArray(opts.disclosedClaimNames)
|
|
283
|
+
? opts.disclosedClaimNames.slice() : [];
|
|
284
|
+
var releasedDisclosures = [];
|
|
285
|
+
for (var i = 0; i < allDisclosures.length; i++) {
|
|
286
|
+
try {
|
|
287
|
+
var decoded = disclosure.decode(allDisclosures[i]);
|
|
288
|
+
if (decoded && disclosedNames.indexOf(decoded.name) !== -1) {
|
|
289
|
+
releasedDisclosures.push(allDisclosures[i]);
|
|
290
|
+
}
|
|
291
|
+
} catch (_e) { /* malformed — skip */ }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Build presentation
|
|
295
|
+
var presentation = jwt + "~";
|
|
296
|
+
if (releasedDisclosures.length > 0) {
|
|
297
|
+
presentation += releasedDisclosures.join("~") + "~";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Optional Key Binding JWT
|
|
301
|
+
if (opts.audience && opts.nonce && opts.holderKey) {
|
|
302
|
+
var algorithm = opts.algorithm || DEFAULT_ALG;
|
|
303
|
+
if (SUPPORTED_ALGS.indexOf(algorithm) === -1) {
|
|
304
|
+
throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
|
|
305
|
+
"present: algorithm must be one of " + SUPPORTED_ALGS.join(", "));
|
|
306
|
+
}
|
|
307
|
+
var now = (typeof opts.issuedAt === "number" && isFinite(opts.issuedAt))
|
|
308
|
+
? Math.floor(opts.issuedAt / 1000) : Math.floor(Date.now() / 1000); // allow:raw-byte-literal — ms→s conversion factor
|
|
309
|
+
// The KB-JWT's hash binds it to the specific SD-JWT + presentation
|
|
310
|
+
var kbHashInput = presentation; // jwt~d1~d2~ (without KB)
|
|
311
|
+
var sdHash = nodeCrypto.createHash("sha256")
|
|
312
|
+
.update(kbHashInput, "ascii")
|
|
313
|
+
.digest()
|
|
314
|
+
.toString("base64url");
|
|
315
|
+
var kbPayload = {
|
|
316
|
+
nonce: opts.nonce,
|
|
317
|
+
aud: opts.audience,
|
|
318
|
+
iat: now,
|
|
319
|
+
sd_hash: sdHash,
|
|
320
|
+
};
|
|
321
|
+
var kbHeader = { alg: algorithm, typ: "kb+jwt" };
|
|
322
|
+
var kbJwt = _signJwt(kbHeader, kbPayload, opts.holderKey, algorithm);
|
|
323
|
+
presentation += kbJwt;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
presentation: presentation,
|
|
328
|
+
jwt: jwt,
|
|
329
|
+
disclosures: releasedDisclosures,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- verify ----
|
|
334
|
+
|
|
335
|
+
async function verify(presentation, opts) {
|
|
336
|
+
validateOpts.requireObject(opts, "auth.sdJwtVc.verify", AuthError);
|
|
337
|
+
validateOpts(opts, [
|
|
338
|
+
"issuerKeyResolver", "audience", "nonce",
|
|
339
|
+
"now", "expectedVct", "maxClockSkewSec",
|
|
340
|
+
"requireKeyBinding",
|
|
341
|
+
], "auth.sdJwtVc.verify");
|
|
342
|
+
|
|
343
|
+
if (typeof presentation !== "string" || presentation.length === 0) {
|
|
344
|
+
throw new AuthError("auth-sd-jwt-vc/no-token",
|
|
345
|
+
"verify: presentation must be a non-empty string");
|
|
346
|
+
}
|
|
347
|
+
if (typeof opts.issuerKeyResolver !== "function") {
|
|
348
|
+
throw new AuthError("auth-sd-jwt-vc/no-resolver",
|
|
349
|
+
"verify: issuerKeyResolver must be an async function");
|
|
350
|
+
}
|
|
351
|
+
var parts = presentation.split("~");
|
|
352
|
+
if (parts.length < 2) {
|
|
353
|
+
throw new AuthError("auth-sd-jwt-vc/malformed",
|
|
354
|
+
"verify: presentation must contain at least one ~-separator");
|
|
355
|
+
}
|
|
356
|
+
var jwt = parts[0];
|
|
357
|
+
// Last part is empty (trailing ~) or KB-JWT (3-dot-separated)
|
|
358
|
+
var maybeKbJwt = null;
|
|
359
|
+
var lastPart = parts[parts.length - 1];
|
|
360
|
+
if (lastPart && lastPart.split(".").length === 3) {
|
|
361
|
+
maybeKbJwt = lastPart;
|
|
362
|
+
}
|
|
363
|
+
var disclosureParts = parts.slice(1, parts.length - (maybeKbJwt ? 1 : 0))
|
|
364
|
+
.filter(function (p) { return p.length > 0; });
|
|
365
|
+
|
|
366
|
+
// 1. Verify issuer JWT signature
|
|
367
|
+
var jwtParts = jwt.split(".");
|
|
368
|
+
if (jwtParts.length !== 3) {
|
|
369
|
+
throw new AuthError("auth-sd-jwt-vc/malformed-jwt",
|
|
370
|
+
"verify: JWT must have 3 dot-separated parts");
|
|
371
|
+
}
|
|
372
|
+
var headerObj;
|
|
373
|
+
try { headerObj = safeJson.parse(_b64uDecodeStr(jwtParts[0]), { maxBytes: 64 * 1024 }); } // allow:bare-json-parse — pre-verify header parse to look up the key resolver; checked again post-signature // allow:raw-byte-literal — JWT header cap (64 KB)
|
|
374
|
+
catch (e) {
|
|
375
|
+
throw new AuthError("auth-sd-jwt-vc/bad-header",
|
|
376
|
+
"verify: malformed JWT header: " + e.message);
|
|
377
|
+
}
|
|
378
|
+
var alg = headerObj.alg;
|
|
379
|
+
if (SUPPORTED_ALGS.indexOf(alg) === -1) {
|
|
380
|
+
throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
|
|
381
|
+
"verify: header alg \"" + alg + "\" not in supported set");
|
|
382
|
+
}
|
|
383
|
+
var typ = headerObj.typ;
|
|
384
|
+
if (typ && typ !== "vc+sd-jwt" && typ !== "JWT") {
|
|
385
|
+
throw new AuthError("auth-sd-jwt-vc/bad-typ",
|
|
386
|
+
"verify: header typ must be \"vc+sd-jwt\" (got \"" + typ + "\")");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var issuerKey = await opts.issuerKeyResolver(headerObj);
|
|
390
|
+
if (!issuerKey) {
|
|
391
|
+
throw new AuthError("auth-sd-jwt-vc/key-not-found",
|
|
392
|
+
"verify: issuerKeyResolver returned no key");
|
|
393
|
+
}
|
|
394
|
+
var jwtParsed = _verifyJwt(jwt, issuerKey, alg);
|
|
395
|
+
|
|
396
|
+
// 2. Validate iss / iat / exp / vct
|
|
397
|
+
var nowSec = (typeof opts.now === "number" && isFinite(opts.now))
|
|
398
|
+
? Math.floor(opts.now / 1000) : Math.floor(Date.now() / 1000); // allow:raw-byte-literal — ms→s conversion factor
|
|
399
|
+
var skew = (typeof opts.maxClockSkewSec === "number") ? opts.maxClockSkewSec : 60; // allow:raw-time-literal — default 60s clock-skew tolerance
|
|
400
|
+
if (typeof jwtParsed.payload.iat === "number" && jwtParsed.payload.iat > nowSec + skew) {
|
|
401
|
+
throw new AuthError("auth-sd-jwt-vc/iat-future",
|
|
402
|
+
"verify: iat is in the future (clock skew?)");
|
|
403
|
+
}
|
|
404
|
+
if (typeof jwtParsed.payload.exp === "number" && jwtParsed.payload.exp < nowSec - skew) {
|
|
405
|
+
throw new AuthError("auth-sd-jwt-vc/expired",
|
|
406
|
+
"verify: token is expired");
|
|
407
|
+
}
|
|
408
|
+
if (opts.expectedVct && jwtParsed.payload.vct !== opts.expectedVct) {
|
|
409
|
+
throw new AuthError("auth-sd-jwt-vc/wrong-vct",
|
|
410
|
+
"verify: vct mismatch (got \"" + jwtParsed.payload.vct +
|
|
411
|
+
"\", expected \"" + opts.expectedVct + "\")");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 3. Reconstruct disclosed claims from disclosures
|
|
415
|
+
var hashAlg = jwtParsed.payload._sd_alg || DEFAULT_HASH_ALG;
|
|
416
|
+
if (!SUPPORTED_HASH_ALGS[hashAlg]) {
|
|
417
|
+
throw new AuthError("auth-sd-jwt-vc/bad-hash",
|
|
418
|
+
"verify: _sd_alg \"" + hashAlg + "\" not supported");
|
|
419
|
+
}
|
|
420
|
+
var sdDigests = Array.isArray(jwtParsed.payload._sd) ? jwtParsed.payload._sd : [];
|
|
421
|
+
var disclosedClaims = {};
|
|
422
|
+
for (var i = 0; i < disclosureParts.length; i++) {
|
|
423
|
+
var d = disclosure.decode(disclosureParts[i]);
|
|
424
|
+
if (!d) continue;
|
|
425
|
+
var digest = _hashDisclosure(disclosureParts[i], hashAlg);
|
|
426
|
+
if (sdDigests.indexOf(digest) === -1) {
|
|
427
|
+
throw new AuthError("auth-sd-jwt-vc/disclosure-mismatch",
|
|
428
|
+
"verify: disclosure for claim \"" + d.name + "\" does not match any _sd digest");
|
|
429
|
+
}
|
|
430
|
+
disclosedClaims[d.name] = d.value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 4. Optionally verify Key Binding JWT
|
|
434
|
+
var kbValidated = false;
|
|
435
|
+
var holderKey = null;
|
|
436
|
+
if (jwtParsed.payload.cnf && jwtParsed.payload.cnf.jwk) {
|
|
437
|
+
holderKey = jwtParsed.payload.cnf.jwk;
|
|
438
|
+
}
|
|
439
|
+
if (maybeKbJwt) {
|
|
440
|
+
if (!holderKey) {
|
|
441
|
+
throw new AuthError("auth-sd-jwt-vc/no-cnf",
|
|
442
|
+
"verify: KB-JWT present but issuer payload has no cnf.jwk");
|
|
443
|
+
}
|
|
444
|
+
// Verify KB-JWT signature
|
|
445
|
+
var kbHeaderObj;
|
|
446
|
+
try { kbHeaderObj = safeJson.parse(_b64uDecodeStr(maybeKbJwt.split(".")[0]), { maxBytes: 4096 }); } // allow:bare-json-parse — kb header from validated KB-JWT; signature verifies // allow:raw-byte-literal — kb-header cap (4 KB)
|
|
447
|
+
catch (e) {
|
|
448
|
+
throw new AuthError("auth-sd-jwt-vc/bad-kb-header",
|
|
449
|
+
"verify: malformed KB-JWT header: " + e.message);
|
|
450
|
+
}
|
|
451
|
+
if (kbHeaderObj.typ !== "kb+jwt") {
|
|
452
|
+
throw new AuthError("auth-sd-jwt-vc/bad-kb-typ",
|
|
453
|
+
"verify: KB-JWT typ must be \"kb+jwt\"");
|
|
454
|
+
}
|
|
455
|
+
var kbAlg = kbHeaderObj.alg;
|
|
456
|
+
if (SUPPORTED_ALGS.indexOf(kbAlg) === -1) {
|
|
457
|
+
throw new AuthError("auth-sd-jwt-vc/unsupported-alg",
|
|
458
|
+
"verify: KB-JWT alg unsupported");
|
|
459
|
+
}
|
|
460
|
+
var holderKeyObj = nodeCrypto.createPublicKey({ key: holderKey, format: "jwk" });
|
|
461
|
+
var kbParsed = _verifyJwt(maybeKbJwt, holderKeyObj, kbAlg);
|
|
462
|
+
if (opts.audience && kbParsed.payload.aud !== opts.audience) {
|
|
463
|
+
throw new AuthError("auth-sd-jwt-vc/wrong-audience",
|
|
464
|
+
"verify: KB-JWT aud mismatch");
|
|
465
|
+
}
|
|
466
|
+
if (opts.nonce && kbParsed.payload.nonce !== opts.nonce) {
|
|
467
|
+
throw new AuthError("auth-sd-jwt-vc/wrong-nonce",
|
|
468
|
+
"verify: KB-JWT nonce mismatch (replay defense)");
|
|
469
|
+
}
|
|
470
|
+
// Validate KB-JWT sd_hash matches the presentation
|
|
471
|
+
var kbHashInput = jwt + "~";
|
|
472
|
+
if (disclosureParts.length > 0) kbHashInput += disclosureParts.join("~") + "~";
|
|
473
|
+
var expectedSdHash = nodeCrypto.createHash("sha256")
|
|
474
|
+
.update(kbHashInput, "ascii")
|
|
475
|
+
.digest()
|
|
476
|
+
.toString("base64url");
|
|
477
|
+
if (kbParsed.payload.sd_hash !== expectedSdHash) {
|
|
478
|
+
throw new AuthError("auth-sd-jwt-vc/sd-hash-mismatch",
|
|
479
|
+
"verify: KB-JWT sd_hash does not match the presentation hash (presentation tampered with?)");
|
|
480
|
+
}
|
|
481
|
+
if (typeof kbParsed.payload.iat === "number" && kbParsed.payload.iat > nowSec + skew) {
|
|
482
|
+
throw new AuthError("auth-sd-jwt-vc/kb-iat-future",
|
|
483
|
+
"verify: KB-JWT iat is in the future");
|
|
484
|
+
}
|
|
485
|
+
kbValidated = true;
|
|
486
|
+
} else if (opts.requireKeyBinding) {
|
|
487
|
+
throw new AuthError("auth-sd-jwt-vc/missing-kb",
|
|
488
|
+
"verify: KB-JWT required (requireKeyBinding=true) but not present");
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 5. Build the resolved-claim set (issuer claims + disclosed)
|
|
492
|
+
var resolved = Object.assign({}, jwtParsed.payload);
|
|
493
|
+
delete resolved._sd;
|
|
494
|
+
delete resolved._sd_alg;
|
|
495
|
+
Object.keys(disclosedClaims).forEach(function (k) {
|
|
496
|
+
resolved[k] = disclosedClaims[k];
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
valid: true,
|
|
501
|
+
claims: resolved,
|
|
502
|
+
disclosedClaims: disclosedClaims,
|
|
503
|
+
issuerHeader: jwtParsed.header,
|
|
504
|
+
issuerPayload: jwtParsed.payload,
|
|
505
|
+
holderKey: holderKey,
|
|
506
|
+
kbValidated: kbValidated,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
module.exports = {
|
|
511
|
+
issue: issue,
|
|
512
|
+
present: present,
|
|
513
|
+
verify: verify,
|
|
514
|
+
disclosure: disclosure,
|
|
515
|
+
issuer: sdJwtVcIssuer,
|
|
516
|
+
holder: sdJwtVcHolder,
|
|
517
|
+
SUPPORTED_ALGS: SUPPORTED_ALGS,
|
|
518
|
+
SUPPORTED_HASH_ALGS: Object.freeze(Object.keys(SUPPORTED_HASH_ALGS)),
|
|
519
|
+
DEFAULT_ALG: DEFAULT_ALG,
|
|
520
|
+
DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
|
|
521
|
+
// Test hooks
|
|
522
|
+
_hashDisclosure: _hashDisclosure,
|
|
523
|
+
// unused-token tag so safeBuffer module isn't dropped from the
|
|
524
|
+
// bundle — we keep it imported for future signature-input bound checks.
|
|
525
|
+
_safeBufferRef: safeBuffer,
|
|
526
|
+
};
|