@blamejs/blamejs-shop 0.0.129 → 0.1.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/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/lib/admin.js +275 -9
- package/lib/affiliates.js +4 -3
- package/lib/analytics.js +3 -2
- package/lib/api-keys.js +1 -1
- package/lib/assembly-instructions.js +2 -1
- package/lib/auto-replenish.js +4 -3
- package/lib/backorder.js +2 -1
- package/lib/business-hours.js +8 -1
- package/lib/carrier-accounts.js +1 -1
- package/lib/carrier-rates.js +1 -1
- package/lib/cart-abandonment.js +3 -2
- package/lib/cart-bulk-ops.js +2 -1
- package/lib/cart-recovery.js +5 -4
- package/lib/cart.js +6 -2
- package/lib/catalog-drafts.js +1 -1
- package/lib/click-and-collect.js +3 -2
- package/lib/clickstream.js +4 -3
- package/lib/config.js +2 -1
- package/lib/cookie-consent.js +2 -1
- package/lib/credit-limits.js +2 -1
- package/lib/currency-display.js +2 -1
- package/lib/customer-activity.js +3 -2
- package/lib/customer-impersonation.js +3 -3
- package/lib/customer-merge.js +4 -3
- package/lib/customer-portal.js +4 -4
- package/lib/customer-risk-profile.js +2 -1
- package/lib/customer-segments.js +2 -1
- package/lib/customer-surveys.js +6 -3
- package/lib/delivery-estimate.js +2 -2
- package/lib/demand-forecast.js +2 -1
- package/lib/discount-analytics.js +2 -2
- package/lib/dunning.js +4 -1
- package/lib/email-warmup.js +6 -1
- package/lib/email.js +1 -8
- package/lib/error-log.js +3 -2
- package/lib/event-log.js +3 -2
- package/lib/fraud-screen.js +3 -1
- package/lib/fulfillment-sla.js +3 -1
- package/lib/index.js +11 -3
- package/lib/inventory-allocations.js +3 -0
- package/lib/inventory-snapshots.js +2 -1
- package/lib/invoice-renderer.js +2 -1
- package/lib/line-gift-wrap.js +6 -1
- package/lib/live-chat.js +2 -1
- package/lib/loyalty-redemption.js +2 -1
- package/lib/newsletter.js +6 -1
- package/lib/operator-activity-feed.js +4 -3
- package/lib/operator-sessions.js +7 -7
- package/lib/order-exchanges.js +1 -0
- package/lib/order-timeline.js +2 -1
- package/lib/payment-retries.js +2 -1
- package/lib/payment.js +5 -4
- package/lib/pixel-events.js +6 -5
- package/lib/preorder.js +2 -1
- package/lib/print-queue.js +2 -1
- package/lib/product-compare.js +2 -1
- package/lib/product-qa.js +2 -1
- package/lib/push-notifications.js +6 -5
- package/lib/recently-viewed.js +7 -2
- package/lib/recommendations.js +7 -2
- package/lib/referral-leaderboard.js +2 -1
- package/lib/refund-automation.js +1 -1
- package/lib/refund-policy.js +1 -1
- package/lib/reorder-reminders.js +2 -1
- package/lib/reorder-thresholds.js +2 -1
- package/lib/robots-config.js +1 -0
- package/lib/sales-reports.js +17 -14
- package/lib/sales-tax-filings.js +2 -1
- package/lib/save-for-later.js +2 -1
- package/lib/search-suggestions.js +1 -1
- package/lib/shipping-insurance.js +2 -1
- package/lib/shipping-labels.js +3 -2
- package/lib/shipping-zones.js +1 -0
- package/lib/shrinkage-report.js +9 -8
- package/lib/sms-dispatcher.js +6 -5
- package/lib/stock-alerts.js +1 -1
- package/lib/stock-receipts.js +2 -1
- package/lib/store-credit.js +2 -1
- package/lib/storefront-forms.js +1 -1
- package/lib/storefront.js +93 -112
- package/lib/subscription-analytics.js +7 -2
- package/lib/subscription-controls.js +9 -8
- package/lib/subscription-gifts.js +2 -1
- package/lib/subscriptions.js +2 -0
- package/lib/support-tickets.js +4 -4
- package/lib/tax-cert-renewals.js +2 -1
- package/lib/tax-remittance.js +2 -1
- package/lib/theme-assets.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +16 -0
- package/lib/vendor/blamejs/README.md +6 -4
- package/lib/vendor/blamejs/SECURITY.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +255 -2
- package/lib/vendor/blamejs/index.js +1 -0
- package/lib/vendor/blamejs/lib/cose.js +284 -10
- package/lib/vendor/blamejs/lib/crypto.js +119 -0
- package/lib/vendor/blamejs/lib/did.js +416 -0
- package/lib/vendor/blamejs/lib/mdoc.js +122 -0
- package/lib/vendor/blamejs/lib/network-dnssec.js +328 -0
- package/lib/vendor/blamejs/lib/network.js +1 -0
- package/lib/vendor/blamejs/lib/vc.js +231 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.41.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.42.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.43.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.44.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.45.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.46.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.47.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.48.json +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +47 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +101 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/crypto-self-test.test.js +74 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/did.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/dnssec.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +52 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +63 -0
- package/lib/vendor-invoices.js +1 -1
- package/lib/webhook-receiver.js +8 -2
- package/lib/webhook-subscriptions.js +1 -1
- package/lib/webhooks.js +6 -5
- package/lib/winback-campaigns.js +2 -1
- package/lib/wishlist-alerts.js +2 -1
- package/lib/wishlist-digest.js +2 -1
- package/package.json +1 -1
|
@@ -51,6 +51,11 @@ var VCDM_V2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
|
|
|
51
51
|
var JOSE_TYP = "vc+jwt";
|
|
52
52
|
var COSE_TYP = "application/vc+cose";
|
|
53
53
|
var COSE_CONTENT_TYPE = "application/vc";
|
|
54
|
+
var VP_JOSE_TYP = "vp+jwt";
|
|
55
|
+
var VP_COSE_TYP = "application/vp+cose";
|
|
56
|
+
var VP_COSE_CONTENT_TYPE = "application/vp";
|
|
57
|
+
var MAX_PRESENTATION_CREDENTIALS = 64; // allow:raw-byte-literal — bounded count of enveloped VCs per presentation
|
|
58
|
+
var ENVELOPED_VC_TYPE = "EnvelopedVerifiableCredential";
|
|
54
59
|
var HDR_COSE_TYP = 16; // allow:raw-byte-literal — COSE "typ" header label (RFC 9596)
|
|
55
60
|
|
|
56
61
|
// JOSE signature algorithms (final RFC 7518 / 8037), mapped to node
|
|
@@ -165,36 +170,43 @@ async function issue(credential, opts) {
|
|
|
165
170
|
_validateVcdm(credential, null);
|
|
166
171
|
if (!opts.privateKey) throw new VcError("vc/no-key", "vc.issue: opts.privateKey is required");
|
|
167
172
|
|
|
173
|
+
return _sign(credential, opts, JOSE_TYP, COSE_TYP, COSE_CONTENT_TYPE, "vc.issue");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Secure a JSON document (credential or presentation) as a compact JWS
|
|
177
|
+
// (jose) or COSE_Sign1 (cose) with the given media-type headers. The
|
|
178
|
+
// document is the exact signed payload — no claims wrapper.
|
|
179
|
+
function _sign(doc, opts, joseTyp, coseTyp, coseContentType, fnName) {
|
|
168
180
|
if (opts.securing === "cose") {
|
|
169
181
|
var protectedHeaders = {};
|
|
170
|
-
protectedHeaders[HDR_COSE_TYP] =
|
|
171
|
-
return cose.sign(Buffer.from(JSON.stringify(
|
|
182
|
+
protectedHeaders[HDR_COSE_TYP] = coseTyp;
|
|
183
|
+
return cose.sign(Buffer.from(JSON.stringify(doc), "utf8"), {
|
|
172
184
|
alg: opts.alg,
|
|
173
185
|
privateKey: opts.privateKey,
|
|
174
186
|
kid: opts.kid,
|
|
175
|
-
contentType:
|
|
187
|
+
contentType: coseContentType,
|
|
176
188
|
protectedHeaders: protectedHeaders,
|
|
177
189
|
});
|
|
178
190
|
}
|
|
179
191
|
if (opts.securing === "jose") {
|
|
180
192
|
var params = JOSE_ALGS[opts.alg];
|
|
181
193
|
if (!params) {
|
|
182
|
-
throw new VcError("vc/bad-alg", "
|
|
194
|
+
throw new VcError("vc/bad-alg", fnName + ": JOSE securing requires alg ES256/384/512 or EdDSA (got " + opts.alg + ")");
|
|
183
195
|
}
|
|
184
196
|
var key = _toKey(opts.privateKey, "private");
|
|
185
|
-
var header = { alg: opts.alg, typ:
|
|
197
|
+
var header = { alg: opts.alg, typ: joseTyp };
|
|
186
198
|
if (typeof opts.kid === "string") header.kid = opts.kid;
|
|
187
199
|
if (typeof opts.cty === "string") header.cty = opts.cty;
|
|
188
|
-
var signingInput = _b64urlJson(header) + "." + _b64urlJson(
|
|
200
|
+
var signingInput = _b64urlJson(header) + "." + _b64urlJson(doc);
|
|
189
201
|
var sig = params.nodeHash === null
|
|
190
202
|
? nodeCrypto.sign(null, Buffer.from(signingInput, "ascii"), key)
|
|
191
203
|
: nodeCrypto.sign(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: key, dsaEncoding: params.dsaEncoding });
|
|
192
204
|
return signingInput + "." + sig.toString("base64url");
|
|
193
205
|
}
|
|
194
|
-
throw new VcError("vc/bad-securing", "
|
|
206
|
+
throw new VcError("vc/bad-securing", fnName + ": securing must be 'jose' or 'cose'");
|
|
195
207
|
}
|
|
196
208
|
|
|
197
|
-
function _verifyJose(token, opts) {
|
|
209
|
+
function _verifyJose(token, opts, expectedTyp) {
|
|
198
210
|
var parts = token.split(".");
|
|
199
211
|
if (parts.length !== 3) {
|
|
200
212
|
throw new VcError("vc/malformed", "vc.verify: not a compact JWS (expected three dot-separated segments)");
|
|
@@ -202,8 +214,8 @@ function _verifyJose(token, opts) {
|
|
|
202
214
|
var header;
|
|
203
215
|
try { header = safeJson.parse(Buffer.from(parts[0], "base64url").toString("utf8")); }
|
|
204
216
|
catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS header is not valid base64url-JSON"); }
|
|
205
|
-
if (!header || header.typ !==
|
|
206
|
-
throw new VcError("vc/bad-typ", "vc.verify: JWS typ must be '" +
|
|
217
|
+
if (!header || header.typ !== expectedTyp) {
|
|
218
|
+
throw new VcError("vc/bad-typ", "vc.verify: JWS typ must be '" + expectedTyp + "'");
|
|
207
219
|
}
|
|
208
220
|
// crit-bypass defense (RFC 7515 §4.1.11): a `crit` header marks
|
|
209
221
|
// extensions the verifier MUST understand and process. This verifier
|
|
@@ -228,16 +240,16 @@ function _verifyJose(token, opts) {
|
|
|
228
240
|
? nodeCrypto.verify(null, Buffer.from(signingInput, "ascii"), pub, sig)
|
|
229
241
|
: nodeCrypto.verify(params.nodeHash, Buffer.from(signingInput, "ascii"), { key: pub, dsaEncoding: params.dsaEncoding }, sig);
|
|
230
242
|
if (!ok) throw new VcError("vc/bad-signature", "vc.verify: JWS signature did not verify");
|
|
231
|
-
var
|
|
232
|
-
try {
|
|
243
|
+
var payload;
|
|
244
|
+
try { payload = safeJson.parse(Buffer.from(parts[1], "base64url").toString("utf8")); }
|
|
233
245
|
catch (_e) { throw new VcError("vc/malformed", "vc.verify: JWS payload is not valid base64url-JSON"); }
|
|
234
|
-
return {
|
|
246
|
+
return { payload: payload, alg: header.alg };
|
|
235
247
|
}
|
|
236
248
|
|
|
237
|
-
async function _verifyCose(bytes, opts) {
|
|
249
|
+
async function _verifyCose(bytes, opts, expectedTyp) {
|
|
238
250
|
var algorithms = opts.algorithms.filter(function (a) { return a in cose.ALGORITHMS; });
|
|
239
251
|
if (!algorithms.length) {
|
|
240
|
-
throw new VcError("vc/no-cose-alg", "vc.verify: opts.algorithms has no COSE algorithm for a
|
|
252
|
+
throw new VcError("vc/no-cose-alg", "vc.verify: opts.algorithms has no COSE algorithm for a COSE-secured credential");
|
|
241
253
|
}
|
|
242
254
|
var out = await cose.verify(bytes, {
|
|
243
255
|
algorithms: algorithms,
|
|
@@ -245,13 +257,25 @@ async function _verifyCose(bytes, opts) {
|
|
|
245
257
|
keyResolver: opts.keyResolver,
|
|
246
258
|
});
|
|
247
259
|
var typ = out.protectedHeaders.get(HDR_COSE_TYP);
|
|
248
|
-
if (typ !== undefined && typ !==
|
|
249
|
-
throw new VcError("vc/bad-typ", "vc.verify: COSE typ header is '" + typ + "', expected '" +
|
|
260
|
+
if (typ !== undefined && typ !== expectedTyp) {
|
|
261
|
+
throw new VcError("vc/bad-typ", "vc.verify: COSE typ header is '" + typ + "', expected '" + expectedTyp + "'");
|
|
250
262
|
}
|
|
251
|
-
var
|
|
252
|
-
try {
|
|
263
|
+
var payload;
|
|
264
|
+
try { payload = safeJson.parse(out.payload.toString("utf8")); }
|
|
253
265
|
catch (_e) { throw new VcError("vc/malformed", "vc.verify: COSE payload is not valid JSON"); }
|
|
254
|
-
return {
|
|
266
|
+
return { payload: payload, alg: out.alg };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Verify a secured JSON document (the JOSE/COSE envelope) → { payload,
|
|
270
|
+
// alg, securing }. Shared by credential + presentation verification.
|
|
271
|
+
async function _verifySecured(secured, opts, joseTyp, coseTyp) {
|
|
272
|
+
if (typeof secured === "string") {
|
|
273
|
+
return Object.assign({ securing: "jose" }, _verifyJose(secured, opts, joseTyp));
|
|
274
|
+
}
|
|
275
|
+
if (Buffer.isBuffer(secured) || secured instanceof Uint8Array) {
|
|
276
|
+
return Object.assign({ securing: "cose" }, await _verifyCose(Buffer.from(secured), opts, coseTyp));
|
|
277
|
+
}
|
|
278
|
+
throw new VcError("vc/bad-input", "vc.verify: secured must be a compact-JWS string or COSE_Sign1 bytes");
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
/**
|
|
@@ -300,28 +324,202 @@ async function verify(secured, opts) {
|
|
|
300
324
|
at = opts.at;
|
|
301
325
|
}
|
|
302
326
|
|
|
303
|
-
var
|
|
304
|
-
if (typeof secured === "string") {
|
|
305
|
-
securing = "jose";
|
|
306
|
-
result = _verifyJose(secured, opts);
|
|
307
|
-
} else if (Buffer.isBuffer(secured) || secured instanceof Uint8Array) {
|
|
308
|
-
securing = "cose";
|
|
309
|
-
result = await _verifyCose(Buffer.from(secured), opts);
|
|
310
|
-
} else {
|
|
311
|
-
throw new VcError("vc/bad-input", "vc.verify: secured must be a compact-JWS string or COSE_Sign1 bytes");
|
|
312
|
-
}
|
|
327
|
+
var result = await _verifySecured(secured, opts, JOSE_TYP, COSE_TYP);
|
|
313
328
|
|
|
314
|
-
_validateVcdm(result.
|
|
315
|
-
var issuer = _issuerId(result.
|
|
329
|
+
_validateVcdm(result.payload, { temporal: true, at: at });
|
|
330
|
+
var issuer = _issuerId(result.payload);
|
|
316
331
|
if (opts.expectedIssuer !== undefined && issuer !== opts.expectedIssuer) {
|
|
317
332
|
throw new VcError("vc/issuer-mismatch", "vc.verify: credential issuer does not match expectedIssuer");
|
|
318
333
|
}
|
|
319
|
-
return { credential: result.
|
|
334
|
+
return { credential: result.payload, securing: result.securing, alg: result.alg, issuer: issuer };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// VCDM 2.0 presentation structural rules.
|
|
338
|
+
function _validateVp(vp) {
|
|
339
|
+
if (!vp || typeof vp !== "object" || Array.isArray(vp)) {
|
|
340
|
+
throw new VcError("vc/bad-presentation", "vc: presentation must be a JSON object");
|
|
341
|
+
}
|
|
342
|
+
var ctx = vp["@context"];
|
|
343
|
+
if (!Array.isArray(ctx) || ctx[0] !== VCDM_V2_CONTEXT) {
|
|
344
|
+
throw new VcError("vc/bad-context", "vc: presentation @context must start with '" + VCDM_V2_CONTEXT + "'");
|
|
345
|
+
}
|
|
346
|
+
var types = Array.isArray(vp.type) ? vp.type : [vp.type];
|
|
347
|
+
if (types.indexOf("VerifiablePresentation") === -1) {
|
|
348
|
+
throw new VcError("vc/bad-type", "vc: type must include 'VerifiablePresentation'");
|
|
349
|
+
}
|
|
350
|
+
// verifiableCredential, when present, MUST be an array — a non-array
|
|
351
|
+
// value must fail closed rather than coerce to empty (which would let
|
|
352
|
+
// a holder bypass credential verification with a malformed container).
|
|
353
|
+
if (vp.verifiableCredential !== undefined && !Array.isArray(vp.verifiableCredential)) {
|
|
354
|
+
throw new VcError("vc/bad-presentation", "vc: verifiableCredential must be an array");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// An enveloped VC (VC-JOSE-COSE §enveloping): a data: URI whose media
|
|
359
|
+
// type selects the securing and whose body is the secured credential.
|
|
360
|
+
function _envelopeVc(securedVc) {
|
|
361
|
+
if (typeof securedVc === "string") {
|
|
362
|
+
return { "@context": [VCDM_V2_CONTEXT], type: ENVELOPED_VC_TYPE, id: "data:application/vc+jwt," + securedVc };
|
|
363
|
+
}
|
|
364
|
+
if (Buffer.isBuffer(securedVc) || securedVc instanceof Uint8Array) {
|
|
365
|
+
return { "@context": [VCDM_V2_CONTEXT], type: ENVELOPED_VC_TYPE,
|
|
366
|
+
id: "data:application/vc+cose;base64," + Buffer.from(securedVc).toString("base64") };
|
|
367
|
+
}
|
|
368
|
+
throw new VcError("vc/bad-credential", "vc.present: each credential must be a compact-JWS string or COSE_Sign1 bytes");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function _parseEnvelopedVc(entry) {
|
|
372
|
+
if (!entry || typeof entry !== "object" || entry.type !== ENVELOPED_VC_TYPE || typeof entry.id !== "string") {
|
|
373
|
+
throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: verifiableCredential entries must be EnvelopedVerifiableCredential data: URIs");
|
|
374
|
+
}
|
|
375
|
+
var comma = entry.id.indexOf(",");
|
|
376
|
+
if (entry.id.indexOf("data:") !== 0 || comma === -1) {
|
|
377
|
+
throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: enveloped credential id is not a data: URI");
|
|
378
|
+
}
|
|
379
|
+
var meta = entry.id.slice("data:".length, comma);
|
|
380
|
+
var body = entry.id.slice(comma + 1);
|
|
381
|
+
if (meta.indexOf("application/vc+cose") === 0) return Buffer.from(body, "base64");
|
|
382
|
+
if (meta.indexOf("application/vc+jwt") === 0) return body;
|
|
383
|
+
throw new VcError("vc/bad-enveloped", "vc.verifyPresentation: unsupported enveloped media type '" + meta + "'");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @primitive b.vc.present
|
|
388
|
+
* @signature b.vc.present(opts)
|
|
389
|
+
* @since 0.12.42
|
|
390
|
+
* @status experimental
|
|
391
|
+
* @compliance gdpr, soc2
|
|
392
|
+
* @related b.vc.verifyPresentation, b.vc.issue
|
|
393
|
+
*
|
|
394
|
+
* Build and sign a W3C Verifiable Presentation: a holder-signed envelope
|
|
395
|
+
* wrapping one or more secured credentials (each enveloped per
|
|
396
|
+
* VC-JOSE-COSE). <code>securing</code> and the algorithms match
|
|
397
|
+
* <code>b.vc.issue</code> (compact JWS <code>vp+jwt</code>, or COSE_Sign1
|
|
398
|
+
* <code>application/vp+cose</code>). Supply <code>nonce</code> /
|
|
399
|
+
* <code>audience</code> for holder-binding / replay protection — they
|
|
400
|
+
* are embedded in the signed presentation and checked at verification.
|
|
401
|
+
*
|
|
402
|
+
* @opts
|
|
403
|
+
* {
|
|
404
|
+
* credentials: array, // secured VCs (compact-JWS strings or COSE_Sign1 bytes)
|
|
405
|
+
* holder: string, // the presenter (a DID or other id)
|
|
406
|
+
* securing: string, // "jose" | "cose"
|
|
407
|
+
* alg: string, // JOSE: ES256/384/512 | EdDSA. COSE: + ML-DSA-87
|
|
408
|
+
* privateKey: object, // the holder's key
|
|
409
|
+
* kid: string, // optional key id
|
|
410
|
+
* nonce: string, // optional verifier challenge (embedded + checked)
|
|
411
|
+
* audience: string, // optional intended verifier (embedded + checked)
|
|
412
|
+
* }
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* var vp = await b.vc.present({ credentials: [jws], holder: holderDid, securing: "jose", alg: "ES256", privateKey: holderKey, nonce: challenge });
|
|
416
|
+
*/
|
|
417
|
+
async function present(opts) {
|
|
418
|
+
validateOpts.requireObject(opts, "vc.present", VcError);
|
|
419
|
+
validateOpts(opts, ["credentials", "holder", "securing", "alg", "privateKey", "kid", "nonce", "audience"], "vc.present");
|
|
420
|
+
if (!Array.isArray(opts.credentials) || opts.credentials.length === 0) {
|
|
421
|
+
throw new VcError("vc/no-credentials", "vc.present: opts.credentials must be a non-empty array");
|
|
422
|
+
}
|
|
423
|
+
if (opts.credentials.length > MAX_PRESENTATION_CREDENTIALS) {
|
|
424
|
+
throw new VcError("vc/too-many-credentials", "vc.present: at most " + MAX_PRESENTATION_CREDENTIALS + " credentials per presentation");
|
|
425
|
+
}
|
|
426
|
+
if (typeof opts.holder !== "string" || !opts.holder) {
|
|
427
|
+
throw new VcError("vc/no-holder", "vc.present: opts.holder is required (the presenter id / DID)");
|
|
428
|
+
}
|
|
429
|
+
if (!opts.privateKey) throw new VcError("vc/no-key", "vc.present: opts.privateKey is required");
|
|
430
|
+
|
|
431
|
+
var vp = {
|
|
432
|
+
"@context": [VCDM_V2_CONTEXT],
|
|
433
|
+
type: ["VerifiablePresentation"],
|
|
434
|
+
holder: opts.holder,
|
|
435
|
+
verifiableCredential: opts.credentials.map(_envelopeVc),
|
|
436
|
+
};
|
|
437
|
+
if (typeof opts.nonce === "string") vp.nonce = opts.nonce;
|
|
438
|
+
if (typeof opts.audience === "string") vp.audience = opts.audience;
|
|
439
|
+
|
|
440
|
+
return _sign(vp, opts, VP_JOSE_TYP, VP_COSE_TYP, VP_COSE_CONTENT_TYPE, "vc.present");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* @primitive b.vc.verifyPresentation
|
|
445
|
+
* @signature b.vc.verifyPresentation(secured, opts)
|
|
446
|
+
* @since 0.12.42
|
|
447
|
+
* @status experimental
|
|
448
|
+
* @compliance gdpr, soc2
|
|
449
|
+
* @related b.vc.present, b.vc.verify
|
|
450
|
+
*
|
|
451
|
+
* Verify a Verifiable Presentation: the holder signature (auto-detected
|
|
452
|
+
* jose / cose, mandatory algorithm allowlist, JOSE <code>none</code>
|
|
453
|
+
* refused), the VCDM structure, and the embedded <code>nonce</code> /
|
|
454
|
+
* <code>audience</code> / <code>expectedHolder</code> when given. With
|
|
455
|
+
* <code>verifyCredentials: true</code> each enveloped credential is
|
|
456
|
+
* verified through <code>b.vc.verify</code> (using
|
|
457
|
+
* <code>opts.credentialOpts</code>) and returned.
|
|
458
|
+
*
|
|
459
|
+
* @opts
|
|
460
|
+
* {
|
|
461
|
+
* algorithms: string[], // required — holder-signature alg allowlist
|
|
462
|
+
* publicKey: object, // the holder verification key
|
|
463
|
+
* keyResolver: function, // (header) → holder key
|
|
464
|
+
* expectedHolder: string, // require presentation holder to match
|
|
465
|
+
* nonce: string, // require embedded nonce to match
|
|
466
|
+
* audience: string, // require embedded audience to match
|
|
467
|
+
* verifyCredentials: boolean, // verify each enveloped VC via b.vc.verify
|
|
468
|
+
* credentialOpts: object, // opts passed to b.vc.verify for each VC
|
|
469
|
+
* }
|
|
470
|
+
*
|
|
471
|
+
* @example
|
|
472
|
+
* var out = await b.vc.verifyPresentation(vp, {
|
|
473
|
+
* algorithms: ["ES256"], publicKey: holderKey, nonce: challenge,
|
|
474
|
+
* verifyCredentials: true, credentialOpts: { algorithms: ["ES256"], publicKey: issuerKey },
|
|
475
|
+
* });
|
|
476
|
+
* // → { presentation, holder, credentials: [verified VCs], securing, alg }
|
|
477
|
+
*/
|
|
478
|
+
async function verifyPresentation(secured, opts) {
|
|
479
|
+
validateOpts.requireObject(opts, "vc.verifyPresentation", VcError);
|
|
480
|
+
validateOpts(opts, ["algorithms", "publicKey", "keyResolver", "expectedHolder", "nonce", "audience", "verifyCredentials", "credentialOpts"], "vc.verifyPresentation");
|
|
481
|
+
if (!Array.isArray(opts.algorithms) || opts.algorithms.length === 0) {
|
|
482
|
+
throw new VcError("vc/algorithms-required", "vc.verifyPresentation: opts.algorithms is required");
|
|
483
|
+
}
|
|
484
|
+
if (!opts.publicKey && typeof opts.keyResolver !== "function") {
|
|
485
|
+
throw new VcError("vc/no-key", "vc.verifyPresentation: pass publicKey or keyResolver");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
var result = await _verifySecured(secured, opts, VP_JOSE_TYP, VP_COSE_TYP);
|
|
489
|
+
var vp = result.payload;
|
|
490
|
+
_validateVp(vp);
|
|
491
|
+
|
|
492
|
+
if (opts.expectedHolder !== undefined && vp.holder !== opts.expectedHolder) {
|
|
493
|
+
throw new VcError("vc/holder-mismatch", "vc.verifyPresentation: presentation holder does not match expectedHolder");
|
|
494
|
+
}
|
|
495
|
+
if (opts.nonce !== undefined && vp.nonce !== opts.nonce) {
|
|
496
|
+
throw new VcError("vc/nonce-mismatch", "vc.verifyPresentation: presentation nonce does not match");
|
|
497
|
+
}
|
|
498
|
+
if (opts.audience !== undefined && vp.audience !== opts.audience) {
|
|
499
|
+
throw new VcError("vc/audience-mismatch", "vc.verifyPresentation: presentation audience does not match");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var entries = vp.verifiableCredential || []; // _validateVp guarantees array-or-absent
|
|
503
|
+
if (entries.length > MAX_PRESENTATION_CREDENTIALS) {
|
|
504
|
+
throw new VcError("vc/too-many-credentials", "vc.verifyPresentation: presentation carries more than " + MAX_PRESENTATION_CREDENTIALS + " credentials");
|
|
505
|
+
}
|
|
506
|
+
var credentials = [];
|
|
507
|
+
if (opts.verifyCredentials) {
|
|
508
|
+
var credOpts = opts.credentialOpts;
|
|
509
|
+
validateOpts.requireObject(credOpts, "vc.verifyPresentation.credentialOpts", VcError);
|
|
510
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
511
|
+
credentials.push(await verify(_parseEnvelopedVc(entries[i]), credOpts));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return { presentation: vp, holder: vp.holder, credentials: credentials, securing: result.securing, alg: result.alg };
|
|
320
516
|
}
|
|
321
517
|
|
|
322
518
|
module.exports = {
|
|
323
519
|
issue: issue,
|
|
324
520
|
verify: verify,
|
|
521
|
+
present: present,
|
|
522
|
+
verifyPresentation: verifyPresentation,
|
|
325
523
|
JOSE_ALGS: JOSE_ALGS,
|
|
326
524
|
VCDM_V2_CONTEXT: VCDM_V2_CONTEXT,
|
|
327
525
|
VcError: VcError,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.41",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`b.did` — W3C DID resolution (did:key + did:web) feeding the credential verifiers",
|
|
6
|
+
"summary": "Resolve W3C Decentralized Identifiers (DID Core 1.0) to verification keys — the link that lets a credential's issuer be named by a DID rather than a raw key. Resolve the issuer DID of a b.vc / b.mdoc / b.scitt credential to a node:crypto KeyObject and hand it to the verifier. did:key encodes the public key in the identifier (multicodec + base58btc), so resolution is deterministic and offline — Ed25519, P-256, P-384, and secp256k1 round-trip; did:web places the DID document at an HTTPS URL derived from the identifier, with the network fetch left to the operator (the framework parses the operator-fetched document and extracts its verification methods, as publicKeyMultibase or publicKeyJwk). b.did.keyToDid encodes a KeyObject as a did:key (an issuer naming itself), b.did.parse splits the identifier (and returns the did:web URL to fetch), and b.did.resolve returns the document and verification keys. DID Core 1.0 is a W3C Recommendation; the method specs (did:key W3C CCG report, did:web DID method registry — EUDI-mandated) are deployed-stable. Composes node:crypto; no new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.did.resolve(did, opts?)` / `b.did.keyToDid(publicKey)` / `b.did.parse(did)`",
|
|
13
|
+
"body": "`resolve` returns `{ didDocument, verificationMethods: [{ id, controller, type, publicKey }] }` with each `publicKey` a `node:crypto` KeyObject ready for `b.vc.verify` / `b.mdoc.verifyIssuerSigned` / `b.scitt.verifyStatement`. did:key resolves deterministically and offline (base58btc + multicodec → Ed25519 raw key or EC compressed point, rebuilt via SPKI); did:web requires the operator to pass the fetched DID document as `opts.document` (the URL to GET is on `b.did.parse(did).url`) and the document `id` must match the requested DID. A publicKeyJwk in a DID document is imported only after its `kty`/`crv` is allowlisted (Ed25519 / P-256 / P-384 / secp256k1) — an unexpected key type from an untrusted document is refused, not blindly imported. `keyToDid` encodes an Ed25519 / P-256 / P-384 / secp256k1 KeyObject as a did:key; `parse` derives the did:web HTTPS URL (`host[:port][:path]` → `https://host/path/did.json`, or `/.well-known/did.json`). Unknown methods, malformed base58, unsupported multicodec codes, and unsupported key types are each refused."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.42",
|
|
4
|
+
"date": "2026-05-24",
|
|
5
|
+
"headline": "`b.vc.present` / `b.vc.verifyPresentation` — W3C Verifiable Presentations",
|
|
6
|
+
"summary": "Completes b.vc with the holder side: a Verifiable Presentation is a holder-signed envelope wrapping one or more credentials, proving the presenter controls the key the credentials were issued to. b.vc.present builds and signs a VerifiablePresentation (each credential enveloped per VC-JOSE-COSE) as a compact JWS (vp+jwt) or COSE_Sign1 (application/vp+cose), matching b.vc.issue's algorithms; an optional nonce / audience is embedded in the signed presentation for holder-binding and replay protection. b.vc.verifyPresentation verifies the holder signature (auto-detected jose/cose, mandatory algorithm allowlist, JOSE none refused), the VCDM structure, and the embedded nonce / audience / expectedHolder when given, and — with verifyCredentials: true — verifies each enveloped credential through b.vc.verify and returns them. The holder is typically a DID, resolved to a key via b.did. Composes b.cose; no new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.vc.present(opts)` / `b.vc.verifyPresentation(secured, opts)`",
|
|
13
|
+
"body": "`present` wraps `opts.credentials` (secured VCs — compact-JWS strings or COSE_Sign1 bytes, each enveloped as an `EnvelopedVerifiableCredential` data: URI) in a `VerifiablePresentation` signed by the holder, with optional `nonce` / `audience` embedded for binding. `verifyPresentation` verifies the holder signature against the mandatory `opts.algorithms` allowlist (JOSE `none` always refused), re-checks the VCDM structure, enforces `expectedHolder` / `nonce` / `audience` when supplied, and with `verifyCredentials: true` verifies each enveloped credential through `b.vc.verify` (using `opts.credentialOpts`), returning `{ presentation, holder, credentials, securing, alg }`. The enveloped-credential count is bounded. A `vp+jwt` presentation is refused by `b.vc.verify` and a `vc+jwt` credential is refused by `verifyPresentation` — the media-type binding keeps the two surfaces distinct."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.43",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.crypto.selfTest` — FIPS 140-3-style power-on self-test for the crypto stack",
|
|
6
|
+
"summary": "A power-on self-test over the framework's cryptographic primitives — the integrity check a FIPS 140-3-validated module runs at start-up. The hash / XOF checks are known-answer tests against NIST FIPS 202 published vectors (SHA3-256 / SHA3-512 / SHAKE256), so they confirm the framework's hashing matches the standard rather than merely itself; the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; and the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f (a fresh keypair must encaps/decaps and sign/verify consistently and reject a tampered signature — FIPS 140-3 §10.3 pairwise consistency, since the runtime exposes no seed-injection API for a fixed-seed KAT). selfTest returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output. Operators in regulated deployments can run it at start-up as a self-integrity gate.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.crypto.selfTest(opts?)`",
|
|
13
|
+
"body": "Runs eight checks — SHA3-512 / SHA3-256 / SHAKE256 known-answer tests (NIST FIPS 202), HMAC-SHA3-512 determinism, XChaCha20-Poly1305 round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests — and returns `{ ok, results: [{ name, ok, detail? }], failures, ranAt }`. Throws `crypto/self-test-failed` (with the report attached) on any failure unless `opts.throwOnFailure` is `false`. Exercises the framework's real primitive paths so a self-test failure means the shipped crypto is broken."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.44",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.did` adds the did:jwk method",
|
|
6
|
+
"summary": "Completes b.did's method set with did:jwk alongside did:key and did:web. did:jwk encodes a public key as a base64url-encoded JWK directly in the identifier, so resolution is deterministic and offline — the same self-contained shape as did:key but in JWK form, which is what OpenID4VCI and the EU Digital Identity Wallet ecosystem commonly use. b.did.resolve(\"did:jwk:…\") returns the verification key as a node:crypto KeyObject (kty/crv allowlisted — Ed25519 / P-256 / P-384 / secp256k1 — so an unexpected key type is refused, not blindly imported), and b.did.keyToDid(publicKey, { method: \"jwk\" }) produces a did:jwk from a key (the private member is stripped). No new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "did:jwk in `b.did.resolve` / `b.did.keyToDid`",
|
|
13
|
+
"body": "`resolve` decodes the base64url JWK (bounded via `b.safeJson`), allowlists its `kty`/`crv`, and returns `{ didDocument, verificationMethods: [{ publicKey, … }] }` with the key as a KeyObject ready for `b.vc` / `b.mdoc` / `b.scitt`; `keyToDid(publicKey, { method: \"jwk\" })` encodes a public key as `did:jwk:<base64url-JWK>` (default remains `did:key`). Malformed base64url-JSON is refused with `did/bad-jwk` and an unsupported key type with `did/unsupported-key`."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.45",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.cose` adds detached-payload sign/verify + `b.cose.importKey` (COSE_Key)",
|
|
6
|
+
"summary": "Two RFC 9052 / 9053 completions to the COSE substrate, both useable today and the prerequisites for mdoc device authentication and C2PA claim verification. Detached payloads (RFC 9052 §4.1): b.cose.sign with detached:true emits a COSE_Sign1 whose payload slot is nil — the signature still covers the payload, and the caller transmits it out of band; b.cose.verify takes the payload back as opts.externalPayload and binds it into the Sig_structure. A detached token verified without externalPayload is refused, and supplying externalPayload for an attached token is refused as ambiguous. COSE_Key import (RFC 9052 §7): b.cose.importKey turns a COSE_Key CBOR map into a node:crypto public KeyObject for b.cose.verify, accepting EC2 (P-256 / P-384 / P-521) and OKP (Ed25519) with the curve allowlisted so an unexpected key type is refused. No new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Detached COSE_Sign1 payloads + `b.cose.importKey(coseKey)`",
|
|
13
|
+
"body": "`b.cose.sign(payload, { detached: true })` emits a nil-payload COSE_Sign1 (the signature covers the payload regardless); `b.cose.verify(coseSign1, { externalPayload })` reconstructs the Sig_structure from the supplied payload, refusing a detached token with no `externalPayload` (`cose/detached-no-payload`) and refusing `externalPayload` on an attached token (`cose/payload-ambiguous`). `b.cose.importKey(coseKey)` maps a COSE_Key map (`kty` 2/EC2 with `crv` P-256/384/521, or `kty` 1/OKP with Ed25519) to a public KeyObject, allowlisting `kty`/`crv` and refusing anything else with `cose/unsupported-key` — the verification key embedded in an mdoc MSO or COSE_Key header is consumed this way."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.46",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.mdoc.verifyDeviceAuth` — ISO 18013-5 mdoc device authentication",
|
|
6
|
+
"summary": "Completes mdoc verification with the holder-binding half (ISO 18013-5 §9.1.3, signature variant). verifyIssuerSigned proves the data is issuer-signed; verifyDeviceAuth proves the presenter controls the device key the issuer bound into the MSO, so a captured issuer-signed document cannot be replayed by anyone else. The device's COSE_Sign1 (deviceSigned.deviceAuth.deviceSignature) is verified over the detached DeviceAuthentication structure [\"DeviceAuthentication\", SessionTranscript, DocType, DeviceNameSpacesBytes] using the device key from verifyIssuerSigned().deviceKey (now surfaced) and the operator-supplied SessionTranscript that binds the proof to this exact exchange (the presentation protocol — e.g. OpenID4VP — defines the transcript). Composes the v0.12.45 b.cose detached-payload verify + importKey. The MAC variant (deviceMac / COSE_Mac0, used in proximity flows with a reader ephemeral key) is deferred and refused with mdoc/device-mac-unsupported. No new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.mdoc.verifyDeviceAuth(opts)` + `deviceKey` on the verifyIssuerSigned result",
|
|
13
|
+
"body": "`verifyDeviceAuth({ deviceKey, deviceSigned, docType, sessionTranscript, algorithms })` imports the device key (a COSE_Key via `b.cose.importKey`, or a KeyObject), reconstructs the detached `DeviceAuthentication` payload, and verifies the `deviceSignature` COSE_Sign1 against the mandatory algorithm allowlist — a mismatched `sessionTranscript` or `docType` fails the signature. `verifyIssuerSigned` now returns `deviceKey` (the MSO `deviceKeyInfo.deviceKey`) so the two checks chain. The MAC variant (`deviceMac`) is refused with `mdoc/device-mac-unsupported` pending COSE_Mac0 + reader-key support."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.47",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.cose.mac0` / `b.cose.macVerify0` — COSE_Mac0 (RFC 9052 §6.2)",
|
|
6
|
+
"summary": "Completes the COSE message-type set (COSE_Sign1 / COSE_Encrypt0 / COSE_Mac0) with single shared-key MACs. b.cose.mac0 produces a tagged COSE_Mac0 over a payload using HMAC-SHA-256/384/512 (the COSE-standard MAC algorithms; HMAC is symmetric, so its post-quantum strength is preserved). b.cose.macVerify0 recomputes the tag over the MAC_structure and compares it in constant time, with a mandatory algorithm allowlist. Use when both parties hold a shared key — e.g. an ECDH-derived key — and a non-repudiable signature is not wanted; detached payloads are supported (the proximity mdoc device-MAC variant and MACed CWTs are the consumers). Composes b.cbor + the framework's constant-time compare; no new runtime dependency.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.cose.mac0(payload, opts)` / `b.cose.macVerify0(coseMac0, opts)`",
|
|
13
|
+
"body": "`mac0` emits a tagged COSE_Mac0 (tag 17) with `alg` (`HMAC-256/256` | `HMAC-384/384` | `HMAC-512/512`) in the protected header and the HMAC tag computed over the MAC_structure `[\"MAC0\", protected, external_aad, payload]`; `detached: true` emits a nil payload. `macVerify0` reads the algorithm from the protected header (must be in the required `opts.algorithms` allowlist), recomputes the tag, and compares it constant-time — a wrong key, tampered tag, or `external_aad` mismatch is refused with `cose/bad-tag`; a detached payload is supplied via `opts.externalPayload`. `external_aad` binds context into the tag."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.12.48",
|
|
4
|
+
"date": "2026-05-25",
|
|
5
|
+
"headline": "`b.network.dns.dnssec` — local DNSSEC signature verification (RFC 4035)",
|
|
6
|
+
"summary": "Verify a DNS answer's RRSIG signature yourself instead of trusting the upstream resolver's AD bit. b.network.dns.dnssec.verifyRrset reconstructs the RFC 4034 §3.1.8.1 signed data — the RRSIG RDATA without the signature, followed by the RRset in canonical form (owner names lowercased, RRs ordered by canonical RDATA, the RRSIG's Original TTL) — and checks the signature against the DNSKEY, enforcing the inception / expiration window. Supports RSA/SHA-256 (alg 8), ECDSA P-256/SHA-256 (13), ECDSA P-384/SHA-384 (14), and Ed25519 (15) — the modern deployed set. verifyDs checks a delegation-signer digest against a DNSKEY (SHA-256 / SHA-384) and keyTag computes the RFC 4034 Appendix B key tag. The verification core is what a chain-walker composes; it defends against a compromised or on-path resolver that lies about authentication.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.network.dns.dnssec.verifyRrset(opts)`",
|
|
13
|
+
"body": "Verifies an RRSIG over a canonicalised RRset against a DNSKEY. `opts` carries the owner `name`, the RR `type`, the wire-format `rdatas`, the parsed `rrsig` (algorithm / labels / originalTtl / inception / expiration / keyTag / signerName / signature), and the `dnskey` (algorithm + raw public key). The signed data is rebuilt per RFC 4034 §3.1.8.1: the RRSIG prefix (type covered | algorithm | labels | original TTL | expiration | inception | key tag | canonical signer name) followed by each RR in canonical form (lowercased owner | type | class | original TTL | rdlen | rdata), sorted by `Buffer.compare` on the RDATA. The validity window is enforced against `opts.at` (defaults to now; an invalid Date is refused, not treated as now). An RRSIG whose algorithm disagrees with the DNSKEY is refused before any key is built. RR types that embed domain names in their RDATA (NS, CNAME, SOA, MX, SRV, …) need RDATA-internal name-lowercasing this version does not perform, so they are refused with `dnssec/uncanonicalizable-type` rather than mis-validated; the security-critical DNSKEY / DS and the name-free address / text types (A, AAAA, TXT, CAA, TLSA, …) are fully supported."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`b.network.dns.dnssec.verifyDs(opts)` / `b.network.dns.dnssec.keyTag(dnskeyRdata)`",
|
|
17
|
+
"body": "`verifyDs` confirms a delegation-signer record matches a DNSKEY: it checks the key tag, then compares the DS digest (SHA-256 type 2 / SHA-384 type 4) against the digest computed over the canonical owner name and the DNSKEY RDATA, constant-time. `keyTag` computes the RFC 4034 Appendix B 16-bit key tag from a DNSKEY's full RDATA — the identifier an RRSIG or DS uses to select the signing key. Together with `verifyRrset` these are the per-RRset building blocks a recursive chain-walk (root → TLD → zone) composes; the chain-walk itself, NSEC / NSEC3 denial-of-existence, and the bundled IANA root trust anchor are not part of this core."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -2241,6 +2241,7 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2241
2241
|
mode: "family-subset",
|
|
2242
2242
|
files: [
|
|
2243
2243
|
"lib/cose.js:verify",
|
|
2244
|
+
"lib/cose.js:macVerify0",
|
|
2244
2245
|
"lib/auth/sd-jwt-vc-issuer.js:create",
|
|
2245
2246
|
"lib/break-glass.js:_validatePolicySet",
|
|
2246
2247
|
"lib/calendar.js:validate",
|
|
@@ -2256,10 +2257,20 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2256
2257
|
mode: "family-subset",
|
|
2257
2258
|
files: [
|
|
2258
2259
|
"lib/mdoc.js:verifyIssuerSigned",
|
|
2260
|
+
"lib/network-dnssec.js:verifyRrset",
|
|
2259
2261
|
"lib/tsa.js:verifyToken",
|
|
2260
2262
|
"lib/vc.js:verify",
|
|
2261
2263
|
],
|
|
2262
|
-
reason: "v0.12.40 — signature-verify entry preamble shared by
|
|
2264
|
+
reason: "v0.12.40 — signature-verify entry preamble shared by four credential / token / DNS verifiers: `validateOpts(allowedKeys) + mandatory algorithms-allowlist check + opts.at valid-Date guard + publicKey/keyResolver presence check`, then divergent domain logic. tsa.verifyToken verifies an RFC 3161 timestamp token (CMS SignedData + message-imprint + EKU); vc.verify verifies a W3C VC-JOSE-COSE credential (JWS/COSE + VCDM structural + validity window); mdoc.verifyIssuerSigned verifies an ISO 18013-5 mdoc (COSE_Sign1 IssuerAuth + MSO valueDigests matching); network-dnssec.verifyRrset verifies a DNSSEC RRSIG (RFC 4034 canonical RRset + RRSIG-prefix reconstruction). Each consumes a different wire format, returns a different shape, and throws a primitive-specific typed error — the shingle is the validate-then-guard preamble, not behaviour. Same family as the v0.12.33 cose.verify cluster.",
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
mode: "family-subset",
|
|
2268
|
+
files: [
|
|
2269
|
+
"lib/cose.js:_coseKeyBytes",
|
|
2270
|
+
"lib/mdoc.js:_bytes",
|
|
2271
|
+
"lib/network-dnssec.js:_bytes",
|
|
2272
|
+
],
|
|
2273
|
+
reason: "v0.12.48 — Buffer-coercion guard (`if (Buffer.isBuffer(x)) return x; if (x instanceof Uint8Array) return Buffer.from(x); throw <Error>`) repeats across three byte-string-consuming primitives. Each throws a MODULE-LOCAL typed error code (cose/bad-cose-key, mdoc/bad-input, dnssec/bad-bytes) naming the local argument; the duplicated three-line shape is the symptom, the cause is that JS can't throw a caller-namespaced ErrorClass without the local closure. Same documented exception as the v0.12.7 require-non-empty-string cluster — the typed-error CODE is the divergence the dup detector can't see.",
|
|
2263
2274
|
},
|
|
2264
2275
|
{
|
|
2265
2276
|
mode: "family-subset",
|
|
@@ -2270,6 +2281,15 @@ async function testNoDuplicateCodeBlocks() {
|
|
|
2270
2281
|
],
|
|
2271
2282
|
reason: "v0.12.40 — validateOpts-then-guard prelude shared between a create-style validator (dual-control.create builds a two-person-rule grant after validating its opts) and the timestamp / mdoc verifiers. The common shingle is the `validateOpts(allowedKeys) + chained guard + typed-error` idiom; the bodies diverge entirely (dual-control persists a control record; tsa/mdoc verify cryptographic structures). Same validate-then-guard family as the v0.12.29 / v0.12.33 clusters.",
|
|
2272
2283
|
},
|
|
2284
|
+
{
|
|
2285
|
+
mode: "family-subset",
|
|
2286
|
+
files: [
|
|
2287
|
+
"lib/cert.js:create",
|
|
2288
|
+
"lib/mail-send-deliver.js:deliver",
|
|
2289
|
+
"lib/vc.js:present",
|
|
2290
|
+
],
|
|
2291
|
+
reason: "v0.12.42 — validateOpts-then-guard prelude shared by three builder-style functions: cert.create mints a certificate, mail-send-deliver.deliver sends a message, vc.present builds + signs a Verifiable Presentation. The common shingle is the `validateOpts(allowedKeys) + required-field / non-empty-array guards + typed-error throw` idiom; the bodies diverge entirely (X.509 minting / SMTP delivery / VC-JOSE-COSE presentation envelope). Same validate-then-guard family as the v0.12.29 / v0.12.33 / v0.12.40 clusters.",
|
|
2292
|
+
},
|
|
2273
2293
|
{
|
|
2274
2294
|
mode: "family-subset",
|
|
2275
2295
|
files: [
|
|
@@ -6215,8 +6235,33 @@ var KNOWN_ANTIPATTERNS = [
|
|
|
6215
6235
|
// jwtExternal._assertAlgKtyMatch BEFORE createPublicKey on
|
|
6216
6236
|
// the browser-supplied DBSC binding JWK.
|
|
6217
6237
|
"lib/dbsc.js",
|
|
6238
|
+
// did.js — _jwkToKey allowlists the JWK's kty/crv (OKP/Ed25519 or
|
|
6239
|
+
// EC/P-256/P-384/secp256k1) and refuses any other type BEFORE
|
|
6240
|
+
// createPublicKey, which is the DID-context equivalent of the
|
|
6241
|
+
// alg/kty cross-check: a DID document carries verification keys,
|
|
6242
|
+
// not a verification alg (the consuming verifier — b.vc / b.mdoc —
|
|
6243
|
+
// supplies the alg allowlist), so there is no `alg` to pass to
|
|
6244
|
+
// _assertAlgKtyMatch; the kty/crv allowlist is the confusion guard.
|
|
6245
|
+
"lib/did.js",
|
|
6246
|
+
// cose.js — importKey maps a COSE_Key to a KeyObject after
|
|
6247
|
+
// allowlisting kty (OKP/EC2) + crv (Ed25519 / P-256 / P-384 /
|
|
6248
|
+
// P-521 — the curves b.cose.verify has an algorithm for); the JWK
|
|
6249
|
+
// is constructed from the COSE_Key, not
|
|
6250
|
+
// attacker-chosen alg-vs-kty, and b.cose.verify supplies the alg
|
|
6251
|
+
// allowlist separately. Same kty/crv-allowlist confusion guard as
|
|
6252
|
+
// did.js — there is no verification `alg` carried in a COSE_Key.
|
|
6253
|
+
"lib/cose.js",
|
|
6254
|
+
// network-dnssec.js — _dnskeyToKey constructs the JWK ITSELF from
|
|
6255
|
+
// the DNSSEC algorithm number (8/13/14/15, validated against the
|
|
6256
|
+
// ALGORITHMS table) — kty/crv are derived from that number, never
|
|
6257
|
+
// read from an attacker-supplied JWK. verifyRrset also refuses an
|
|
6258
|
+
// RRSIG whose algorithm disagrees with the DNSKEY's before the key
|
|
6259
|
+
// is built, so the alg→kty/crv binding is fixed at the source. The
|
|
6260
|
+
// confused-deputy (alg-vs-kty) shape cannot arise — there is no
|
|
6261
|
+
// externally-chosen kty to confuse.
|
|
6262
|
+
"lib/network-dnssec.js",
|
|
6218
6263
|
],
|
|
6219
|
-
reason: "CVE-2026-22817 — every JWT verifier that resolves a JWK BY ATTACKER-CONTROLLED HEADER (kid / x5t) must cross-check the declared alg against the JWK's kty (and crv for EC) BEFORE handing the key to node:crypto.verify. Imports that skip the check are exactly the confused-deputy shape (RS256→HS256 family). The shared helper `jwtExternal._assertAlgKtyMatch(alg, jwk)` is the single point of enforcement; new code routes through it. Allowlist entries are sign-side / pinned-cert paths where the JWK is not attacker-supplied.",
|
|
6264
|
+
reason: "CVE-2026-22817 — every JWT verifier that resolves a JWK BY ATTACKER-CONTROLLED HEADER (kid / x5t) must cross-check the declared alg against the JWK's kty (and crv for EC) BEFORE handing the key to node:crypto.verify. Imports that skip the check are exactly the confused-deputy shape (RS256→HS256 family). The shared helper `jwtExternal._assertAlgKtyMatch(alg, jwk)` is the single point of enforcement; new code routes through it. Allowlist entries are sign-side / pinned-cert paths where the JWK is not attacker-supplied, or (did.js) where a kty/crv allowlist stands in for alg/kty because the format carries no verification alg.",
|
|
6220
6265
|
},
|
|
6221
6266
|
{
|
|
6222
6267
|
// CVE-2026-23552 — cross-realm JWT acceptance via non-CT iss
|