@blamejs/blamejs-shop 0.4.124 → 0.4.126
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +33 -30
- package/lib/vendor/MANIFEST.json +13 -11
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +2 -2
- package/lib/vendor/blamejs/lib/crypto-field.js +1 -1
- package/lib/vendor/blamejs/lib/external-db.js +1 -1
- package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +12 -1
- package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +1 -1
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.34.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +7 -5
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-pgp.test.js +50 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.4.x
|
|
10
10
|
|
|
11
|
+
- v0.4.126 (2026-06-27) — **Storefront error responses no longer echo internal error detail, and order/account pages render images from the configured asset prefix.** Two storefront correctness fixes. First, around fifteen storefront routes (cart line edit/remove, product compare, saved-for-later, account actions and more) returned the raw error message to the client on an unexpected server error — a string that can carry an internal database column, a vault/crypto detail, or other implementation internals. Those routes now distinguish a client-shape validation error (a 400 that still shows its operator-authored message) from an unexpected server error (a 500 that logs the real error server-side, correlated by the request id, and returns only a generic message plus that id) — the same redaction the WebAuthn ceremony routes already used. Second, the order-detail and account-dashboard pages were rendering product thumbnails from the default "/assets/" path instead of the operator's configured asset prefix, so on a deployment that serves assets from a CDN or object-store prefix those two pages showed broken images; both pages now pass the configured prefix like every other page. **Fixed:** *Storefront routes no longer echo the internal error message on a 500* — About fifteen storefront routes — cart line update/remove, product-compare add/clear, saved-for-later move/remove, and several account actions — handled a caught error with `status(TypeError ? 400 : 500)` and then returned the raw error message for both cases, so an unexpected (non-validation) server error sent its internal message string to the client. These routes now return that message only for a client-shape validation error (a 400); any other error is a 500 that records the real error server-side against the request id and returns a generic "Something went wrong" message plus that id for support correlation. This matches the redaction the WebAuthn register/login routes already applied. · *Order-detail and account pages render images from the configured asset prefix* — The order-detail (`/orders/:id`) and account-dashboard (`/account`) renderers were not passed the configured asset prefix, so they fell back to the default `/assets/` path for product thumbnails while every other page used the operator's prefix. On a deployment that serves static assets from a CDN or object-store prefix, those two pages showed broken images. Both now pass the configured prefix, so their images load from the same place as the rest of the storefront.
|
|
12
|
+
|
|
13
|
+
- v0.4.125 (2026-06-27) — **Vendored framework refreshed to 0.15.34 — RSA OpenPGP signatures with a high zero byte now verify reliably.** Updates the vendored blamejs framework to 0.15.34. The upstream release fixes the framework's OpenPGP verification: an RSA signature whose value began with one or more zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct, because the OpenPGP wire format strips those leading zero bytes and the verifier handed the shortened signature straight to RSA verification (which requires the signature to be exactly the key's modulus length). The signature is now left-padded back to the modulus width before verification, so every valid RSA signature verifies reliably, including signatures produced by other OpenPGP implementations. The release also carries internal test-suite tooling with no runtime effect. **Changed:** *Vendored framework refreshed to 0.15.34 (reliable RSA OpenPGP signature verification)* — Refreshes the vendored blamejs framework to 0.15.34. The upstream fix left-pads an RSA OpenPGP signature back to the key's modulus byte length before verifying it, so a valid signature whose value carried one or more leading zero bytes (~1 in 256, for any key) is no longer intermittently rejected — matching the padding the Ed25519 verification path already applied. Any code path that verifies OpenPGP RSA signatures through the framework now accepts every correct signature. The rest of the release is internal test-suite tooling with no runtime, public-API, or wire-format change.
|
|
14
|
+
|
|
11
15
|
- v0.4.124 (2026-06-27) — **Adding the same item to a cart from two requests at once always sums the quantity, and the manager role can configure delivery cutoffs and postal zones.** Two correctness fixes. First, adding the same product to a cart from two requests at once — a double-click, or two open tabs — could drop one of the quantity increases: the cart line's quantity was read and then written back as an absolute value, so the second write overwrote the first instead of adding to it. Adding to the cart now sums the quantity in a single atomic database statement, so simultaneous adds of the same item always total correctly; the same atomic write covers applying a bundle's units onto an existing line and merging a guest cart into a signed-in cart at login. Second, an operator with the manager role was wrongly denied the delivery-estimate cutoff-time and postal-zone screens — those two actions were not mapped to the catalog-write permission and fell through to the owner-only fallback, so only the owner could reach them; managers can now manage them alongside the other delivery-estimate settings. **Fixed:** *Simultaneous add-to-cart requests for the same item sum the quantity instead of losing one* — Adding a line to a cart read the existing quantity and then wrote back the absolute sum, so two requests adding the same item to the same cart at the same time (a rapid double-click, or two tabs) could each read the same starting quantity and overwrite each other — one increase was lost. The add is now a single atomic upsert that increments the stored quantity in the database (`qty = qty + added`), so concurrent adds of the same item always total correctly and a concurrent first-add no longer fails on the cart's unique-line constraint. A plain re-add still keeps the line's existing price. The same atomic write applies to placing a bundle's units onto an existing line (the per-unit price re-blends to the combined value, rounded up so it never charges below it) and to merging a guest cart into a signed-in cart at login. · *The manager role can configure delivery cutoff times and postal zones* — The delivery-estimate cutoff-time and postal-zone actions were not mapped to the catalog-write permission, so they fell through to the owner-only fallback and an operator with the manager role was denied access to those two screens while being allowed the adjacent transit-time and holiday screens. Both actions now require catalog-write, matching the rest of the delivery-estimate settings, so managers can configure them.
|
|
12
16
|
|
|
13
17
|
- v0.4.123 (2026-06-26) — **Vendored framework refreshed to 0.15.33.** Updates the vendored blamejs framework to 0.15.33. The upstream release is internal test-suite tooling only: it re-verifies three more of the framework's guard-suite suppression classes from scratch and renames their markers to descriptive tokens, recording the old names as retired so a re-verified class cannot inherit a stale approval. No runtime code, public API, or wire format changed upstream, so there is no runtime or behavior change for this storefront and no operator action is required. **Changed:** *Vendored framework refreshed to 0.15.33* — Refreshes the vendored blamejs framework to 0.15.33, an upstream release scoped to test-suite tooling — three more guard-suite suppression classes re-verified and their markers renamed to descriptive tokens, with the old names retired. The shipped framework runtime, public API, and wire format are unchanged, so no operator action is required.
|
package/lib/asset-manifest.json
CHANGED
package/lib/storefront.js
CHANGED
|
@@ -80,6 +80,22 @@ function _authServerError(req, res, e, where) {
|
|
|
80
80
|
return res.end ? res.end(msg) : res.send(msg);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Uniform terminal error for a text/HTML route: a malformed-input TypeError is
|
|
84
|
+
// a 400 whose message is operator-authored validation copy (safe to show), but
|
|
85
|
+
// any OTHER error is a 500 whose message could carry an internal string (a DB
|
|
86
|
+
// column, a vault/crypto internal) — so it routes through _authServerError,
|
|
87
|
+
// which logs the real error server-side and returns only a generic message +
|
|
88
|
+
// the correlating request id. Replaces the `res.status(e instanceof TypeError ?
|
|
89
|
+
// 400 : 500); res.end(e.message)` shape, which leaked the raw error on the 500.
|
|
90
|
+
function _routeError(req, res, e) {
|
|
91
|
+
if (e instanceof TypeError) {
|
|
92
|
+
res.status(400);
|
|
93
|
+
var m = (e && e.message) || "Invalid request";
|
|
94
|
+
return res.end ? res.end(m) : res.send(m);
|
|
95
|
+
}
|
|
96
|
+
return _authServerError(req, res, e, (req && req.method && req.url) ? (req.method + " " + req.url) : "route");
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
// Payment-webhook signatures (Stripe's HMAC, PayPal's, …) are computed over
|
|
84
100
|
// the EXACT raw request bytes, but the global JSON body-parser reparses and
|
|
85
101
|
// discards them. This middleware buffers the raw body for the given POST
|
|
@@ -14248,8 +14264,7 @@ function mount(router, deps) {
|
|
|
14248
14264
|
return _compareRedirect(res, dest, "full");
|
|
14249
14265
|
}
|
|
14250
14266
|
// A malformed product id (or session id) is a client error.
|
|
14251
|
-
|
|
14252
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
14267
|
+
return _routeError(req, res, e);
|
|
14253
14268
|
}
|
|
14254
14269
|
|
|
14255
14270
|
if (!dest) {
|
|
@@ -14269,8 +14284,7 @@ function mount(router, deps) {
|
|
|
14269
14284
|
try { await deps.productCompare.clearCompareList({ session_id: sid }); }
|
|
14270
14285
|
catch (e) {
|
|
14271
14286
|
if (!(e instanceof TypeError)) {
|
|
14272
|
-
res
|
|
14273
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
14287
|
+
return _authServerError(req, res, e, "POST /compare/clear");
|
|
14274
14288
|
}
|
|
14275
14289
|
// A malformed session cookie can't address a basket — treat
|
|
14276
14290
|
// the clear as a no-op rather than a 400 (the cookie grants
|
|
@@ -15592,6 +15606,7 @@ function mount(router, deps) {
|
|
|
15592
15606
|
}
|
|
15593
15607
|
}
|
|
15594
15608
|
_send(res, 200, renderOrder({
|
|
15609
|
+
asset_prefix: deps.asset_prefix || "/assets/",
|
|
15595
15610
|
order: o,
|
|
15596
15611
|
product_lookup: productLookup,
|
|
15597
15612
|
recommendations: recommendations,
|
|
@@ -16596,6 +16611,7 @@ function mount(router, deps) {
|
|
|
16596
16611
|
|
|
16597
16612
|
var cartCount = await _cartCountForReq(req);
|
|
16598
16613
|
_send(res, 200, renderAccount({
|
|
16614
|
+
asset_prefix: deps.asset_prefix || "/assets/",
|
|
16599
16615
|
customer: customer,
|
|
16600
16616
|
orders: orders,
|
|
16601
16617
|
order_product_lookup: orderProductLookup,
|
|
@@ -17556,8 +17572,7 @@ function mount(router, deps) {
|
|
|
17556
17572
|
if (already) { await deps.wishlist.remove({ customer_id: auth.customer_id, product_id: productId }); removed = true; }
|
|
17557
17573
|
else await deps.wishlist.add({ customer_id: auth.customer_id, product_id: productId });
|
|
17558
17574
|
} catch (e) {
|
|
17559
|
-
|
|
17560
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
17575
|
+
return _routeError(req, res, e);
|
|
17561
17576
|
}
|
|
17562
17577
|
var okKind = removed ? "removed" : "added";
|
|
17563
17578
|
var rt = (req.body || {}).return_to;
|
|
@@ -17715,8 +17730,7 @@ function mount(router, deps) {
|
|
|
17715
17730
|
await deps.wishlistAlerts.unsubscribeFromAlertKind({ customer_id: auth.customer_id, trigger: trigger });
|
|
17716
17731
|
}
|
|
17717
17732
|
} catch (e) {
|
|
17718
|
-
|
|
17719
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
17733
|
+
return _routeError(req, res, e);
|
|
17720
17734
|
}
|
|
17721
17735
|
res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?prefs=alerts");
|
|
17722
17736
|
return res.end ? res.end() : res.send("");
|
|
@@ -17759,8 +17773,7 @@ function mount(router, deps) {
|
|
|
17759
17773
|
}
|
|
17760
17774
|
}
|
|
17761
17775
|
} catch (e) {
|
|
17762
|
-
|
|
17763
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
17776
|
+
return _routeError(req, res, e);
|
|
17764
17777
|
}
|
|
17765
17778
|
res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?prefs=digest");
|
|
17766
17779
|
return res.end ? res.end() : res.send("");
|
|
@@ -17856,8 +17869,7 @@ function mount(router, deps) {
|
|
|
17856
17869
|
privacy: "unlisted",
|
|
17857
17870
|
});
|
|
17858
17871
|
} catch (e) {
|
|
17859
|
-
|
|
17860
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
17872
|
+
return _routeError(req, res, e);
|
|
17861
17873
|
}
|
|
17862
17874
|
// The full public URL the giver opens. Built from this request's
|
|
17863
17875
|
// ORIGIN (scheme + host) so the one-time link is a correct absolute
|
|
@@ -17905,8 +17917,7 @@ function mount(router, deps) {
|
|
|
17905
17917
|
try {
|
|
17906
17918
|
await deps.wishlistSharing.revokeShareLink({ link_id: shareId, reason: "owner revoked from account" });
|
|
17907
17919
|
} catch (e) {
|
|
17908
|
-
|
|
17909
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
17920
|
+
return _routeError(req, res, e);
|
|
17910
17921
|
}
|
|
17911
17922
|
res.status(303); res.setHeader && res.setHeader("location", "/account/wishlist?share=revoked");
|
|
17912
17923
|
return res.end ? res.end() : res.send("");
|
|
@@ -18521,8 +18532,7 @@ function mount(router, deps) {
|
|
|
18521
18532
|
line_id: req.params && req.params.line_id,
|
|
18522
18533
|
});
|
|
18523
18534
|
} catch (e) {
|
|
18524
|
-
|
|
18525
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
18535
|
+
return _routeError(req, res, e);
|
|
18526
18536
|
}
|
|
18527
18537
|
res.status(303); res.setHeader && res.setHeader("location", "/cart");
|
|
18528
18538
|
return res.end ? res.end() : res.send("");
|
|
@@ -18606,8 +18616,7 @@ function mount(router, deps) {
|
|
|
18606
18616
|
try {
|
|
18607
18617
|
await deps.saveForLater.remove({ customer_id: auth.customer_id, save_id: req.params && req.params.save_id });
|
|
18608
18618
|
} catch (e) {
|
|
18609
|
-
|
|
18610
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
18619
|
+
return _routeError(req, res, e);
|
|
18611
18620
|
}
|
|
18612
18621
|
res.status(303); res.setHeader && res.setHeader("location", "/account/saved?ok=removed");
|
|
18613
18622
|
return res.end ? res.end() : res.send("");
|
|
@@ -18758,8 +18767,7 @@ function mount(router, deps) {
|
|
|
18758
18767
|
var addr = await _ownedAddress(req, res, auth); if (!addr) return;
|
|
18759
18768
|
try { await fn(addr.id); }
|
|
18760
18769
|
catch (e) {
|
|
18761
|
-
|
|
18762
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
18770
|
+
return _routeError(req, res, e);
|
|
18763
18771
|
}
|
|
18764
18772
|
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=" + okKind);
|
|
18765
18773
|
return res.end ? res.end() : res.send("");
|
|
@@ -18788,8 +18796,7 @@ function mount(router, deps) {
|
|
|
18788
18796
|
var addr = await _ownedAddress(req, res, auth); if (!addr) return;
|
|
18789
18797
|
try { await deps.addresses.archive(addr.id); }
|
|
18790
18798
|
catch (e) {
|
|
18791
|
-
|
|
18792
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
18799
|
+
return _routeError(req, res, e);
|
|
18793
18800
|
}
|
|
18794
18801
|
res.status(303);
|
|
18795
18802
|
res.setHeader && res.setHeader("location", "/account/addresses?ok=removed&undo=" + encodeURIComponent(addr.id));
|
|
@@ -18813,8 +18820,7 @@ function mount(router, deps) {
|
|
|
18813
18820
|
}
|
|
18814
18821
|
try { await deps.addresses.unarchive(addr.id); }
|
|
18815
18822
|
catch (e) {
|
|
18816
|
-
|
|
18817
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
18823
|
+
return _routeError(req, res, e);
|
|
18818
18824
|
}
|
|
18819
18825
|
res.status(303); res.setHeader && res.setHeader("location", "/account/addresses?ok=restored");
|
|
18820
18826
|
return res.end ? res.end() : res.send("");
|
|
@@ -21282,8 +21288,7 @@ function mount(router, deps) {
|
|
|
21282
21288
|
});
|
|
21283
21289
|
}
|
|
21284
21290
|
} catch (e) {
|
|
21285
|
-
|
|
21286
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
21291
|
+
return _routeError(req, res, e);
|
|
21287
21292
|
}
|
|
21288
21293
|
res.status(303);
|
|
21289
21294
|
res.setHeader && res.setHeader("location", "/cart");
|
|
@@ -21319,8 +21324,7 @@ function mount(router, deps) {
|
|
|
21319
21324
|
return res.end ? res.end("Line not found") : res.send("Line not found");
|
|
21320
21325
|
}
|
|
21321
21326
|
} catch (e) {
|
|
21322
|
-
|
|
21323
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
21327
|
+
return _routeError(req, res, e);
|
|
21324
21328
|
}
|
|
21325
21329
|
res.status(303);
|
|
21326
21330
|
res.setHeader && res.setHeader("location", "/cart");
|
|
@@ -21348,8 +21352,7 @@ function mount(router, deps) {
|
|
|
21348
21352
|
try {
|
|
21349
21353
|
await deps.cart.removeLine(lineId, cart.id);
|
|
21350
21354
|
} catch (e) {
|
|
21351
|
-
|
|
21352
|
-
return res.end ? res.end((e && e.message) || "Error") : res.send((e && e.message) || "Error");
|
|
21355
|
+
return _routeError(req, res, e);
|
|
21353
21356
|
}
|
|
21354
21357
|
res.status(303);
|
|
21355
21358
|
res.setHeader && res.setHeader("location", "/cart");
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.15.
|
|
7
|
-
"tag": "v0.15.
|
|
6
|
+
"version": "0.15.34",
|
|
7
|
+
"tag": "v0.15.34",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -856,6 +856,7 @@
|
|
|
856
856
|
"release-notes/v0.15.31.json": "lib/vendor/blamejs/release-notes/v0.15.31.json",
|
|
857
857
|
"release-notes/v0.15.32.json": "lib/vendor/blamejs/release-notes/v0.15.32.json",
|
|
858
858
|
"release-notes/v0.15.33.json": "lib/vendor/blamejs/release-notes/v0.15.33.json",
|
|
859
|
+
"release-notes/v0.15.34.json": "lib/vendor/blamejs/release-notes/v0.15.34.json",
|
|
859
860
|
"release-notes/v0.15.4.json": "lib/vendor/blamejs/release-notes/v0.15.4.json",
|
|
860
861
|
"release-notes/v0.15.5.json": "lib/vendor/blamejs/release-notes/v0.15.5.json",
|
|
861
862
|
"release-notes/v0.15.6.json": "lib/vendor/blamejs/release-notes/v0.15.6.json",
|
|
@@ -1564,7 +1565,7 @@
|
|
|
1564
1565
|
".npmrc": "sha256:66f104e7d07c496d2d0409988225e8c0e4ceb8d247dbcac3be75b2128d20ce66",
|
|
1565
1566
|
".pinact.yaml": "sha256:0213ffda55961dc49b64c0a5dfa3c0567419633b1499d57eaf7c8d842d7da6c7",
|
|
1566
1567
|
"ARCHITECTURE.md": "sha256:9b1c8d2b1b7a41838eb348b0a008e4b4369718fd72bfe2974b37155f7536d35b",
|
|
1567
|
-
"CHANGELOG.md": "sha256:
|
|
1568
|
+
"CHANGELOG.md": "sha256:98f034fdf893233e36e89e1f8eaa1548f1de448d67f5afef4e07b401bccb4aa2",
|
|
1568
1569
|
"CODE_OF_CONDUCT.md": "sha256:148a281960fff7c2fe6554dab66da572c72245ddeb00b0d14811558397bff386",
|
|
1569
1570
|
"CONTRIBUTING.md": "sha256:bb4dbdbc8598da31dbce653a8ed322e08ff46560173f2eb67a4d684653948332",
|
|
1570
1571
|
"GOVERNANCE.md": "sha256:906df6afb1f552b27b9acb50f7f96c47b917a2f1021cd4e987dbf4ee0e0a821b",
|
|
@@ -1574,7 +1575,7 @@
|
|
|
1574
1575
|
"NOTICE": "sha256:f487fa47a11aca0f89e2615cdd3c713e9842abf7a30d8d328eeeae1c864aa774",
|
|
1575
1576
|
"README.md": "sha256:3ddcc197b003da0b02db8bdd1aef1e943c94f7eab613c633d6a45bb11d0a80e9",
|
|
1576
1577
|
"SECURITY.md": "sha256:77d8c2bcc04b425a08ef30e51204cebddbd48954b028a259c7a8412dbfeab40a",
|
|
1577
|
-
"api-snapshot.json": "sha256:
|
|
1578
|
+
"api-snapshot.json": "sha256:610a4caba49d95d48cd4a129b4071e1756903eaea0de8eb904e6d76e1a887501",
|
|
1578
1579
|
"assets/BlameJS_Logo.png": "sha256:3c65699753c771b48ef9ac7f45bb40815ec19a23afcdd0cd30ef4601bbbe293e",
|
|
1579
1580
|
"assets/BlameJS_Logo.svg": "sha256:dda44f3fb1343d5de9db6b1fcdb75fc649c57e7a99a8e8239fcf852e3841e1a8",
|
|
1580
1581
|
"bench/README.md": "sha256:74202f2507fd840bfc1ac6c681975d9273cf36cca6e0f72655f138337304033c",
|
|
@@ -1909,7 +1910,7 @@
|
|
|
1909
1910
|
"lib/cra-report.js": "sha256:77e6c28bcb00273d652bd9578ca45fccb56d83ef47a18d4cac80929e17d59dff",
|
|
1910
1911
|
"lib/crdt.js": "sha256:be5f4675e072eda28e9bbccf49213ad4350b5f53ae78b1ec28cc1d36bf1ea129",
|
|
1911
1912
|
"lib/credential-hash.js": "sha256:9f4bf10f3ee86f03fba90e32778d99e0f9eb5966b826cbd1dc83e6c55000400e",
|
|
1912
|
-
"lib/crypto-field.js": "sha256:
|
|
1913
|
+
"lib/crypto-field.js": "sha256:30312fdf8e4245cae93d8b1914d84dded46480372953c0b4785ee6e14ea48077",
|
|
1913
1914
|
"lib/crypto-hpke-pq.js": "sha256:f086e23f4f80de9d0713826890bf8bafc0a8ddfa53fe7e87f5a0fed8ffa35caf",
|
|
1914
1915
|
"lib/crypto-hpke.js": "sha256:e9fb595fc16206237edeb738bfe4b037eeee91de9558e09ffe41e0f1e37558a7",
|
|
1915
1916
|
"lib/crypto-oprf.js": "sha256:5c98b56ba2322d3c3f40ac5c849370d927b76a535f19f76448469f349ac2b408",
|
|
@@ -1944,7 +1945,7 @@
|
|
|
1944
1945
|
"lib/error-page.js": "sha256:4ed2e8a118b35d3dc76a072b233713ae1fdaedb54334df68ad081e19f765cb88",
|
|
1945
1946
|
"lib/events.js": "sha256:b5f0034fda33285f571b9b247db50bff08f632762d9bf69b83784b435eeea75c",
|
|
1946
1947
|
"lib/external-db-migrate.js": "sha256:990014016e71a818672b3f2c32eaa2ff6815553feb52efafb88ec765df9a570a",
|
|
1947
|
-
"lib/external-db.js": "sha256:
|
|
1948
|
+
"lib/external-db.js": "sha256:d39513c1ef842397374218d8416c62ff0ec36e73ab0e9c507298dcfb9d0bda63",
|
|
1948
1949
|
"lib/fapi2.js": "sha256:6ae4679931675a629fa3f9e9efba71172fe69251dec2dd5d8c995cfb9dcad8f7",
|
|
1949
1950
|
"lib/fda-21cfr11.js": "sha256:9256bcea7d526a7280d5d3b1beed5be1daf2402ded2d5a18d602466d01a7884c",
|
|
1950
1951
|
"lib/fdx.js": "sha256:67a29b140b6d07a767da31d6b55d1db0c2219cdaa780531fdec0fec63a5c551c",
|
|
@@ -2070,7 +2071,7 @@
|
|
|
2070
2071
|
"lib/mail-auth.js": "sha256:da409e8f2b3bae1308f1d008b6df27f468075a83ceaeae8ee7cda0ab9c4a1027",
|
|
2071
2072
|
"lib/mail-bimi.js": "sha256:b22afc5e8ad04503fee7e41f3b34c05c9f0759579436bfbe5f7706180c466155",
|
|
2072
2073
|
"lib/mail-bounce.js": "sha256:461e885dd2ea0664e9a4af39ae5c36f3a5d9c7dc914dad4c40641b705952fd04",
|
|
2073
|
-
"lib/mail-crypto-pgp.js": "sha256:
|
|
2074
|
+
"lib/mail-crypto-pgp.js": "sha256:2e94f51d2cb13b92c612e84f73cf96e83eb17843d57b9dc9aef688ce55a1f9b4",
|
|
2074
2075
|
"lib/mail-crypto-smime.js": "sha256:066d0456f385c470000dd05dcf978592c369298675dd8a25263592c8f15c7934",
|
|
2075
2076
|
"lib/mail-crypto.js": "sha256:19d5f2a9fe6376d2ed6b0153a944dd1d05d8b893172bad173e2df9733c0dafd3",
|
|
2076
2077
|
"lib/mail-dav.js": "sha256:3bfe6d8c0fa1c0a5d56d78a07778de932828eeebd6e091d2f988ebeaabcf451a",
|
|
@@ -2136,7 +2137,7 @@
|
|
|
2136
2137
|
"lib/middleware/headers.js": "sha256:d53abbe53b1981df5b934aad06dc4588729ce2024f014d2b4b0aa5ff0701cebd",
|
|
2137
2138
|
"lib/middleware/health.js": "sha256:d9e95a38cd18a1236b4787901f6145b258e9a63d98f98eb34e6200489673a4e3",
|
|
2138
2139
|
"lib/middleware/host-allowlist.js": "sha256:c5572e12b460c69dcc98664eb95a4c810f08c95e66ee8f24f860e08f70b8e3fa",
|
|
2139
|
-
"lib/middleware/idempotency-key.js": "sha256:
|
|
2140
|
+
"lib/middleware/idempotency-key.js": "sha256:3310288d1d42e30b950cfef829daab48600dd8c4a796fe7adbd05d52aeb7bf2b",
|
|
2140
2141
|
"lib/middleware/index.js": "sha256:01643697e716ab912fab7f214d5d51eb1d70ca4363ad67fba5b912be2ccc0c16",
|
|
2141
2142
|
"lib/middleware/nel.js": "sha256:712165cd41a504493f797903b2cc0a1f360f7e2f03695163bd3d93219079f8e8",
|
|
2142
2143
|
"lib/middleware/network-allowlist.js": "sha256:61055651e394d7cadb9750c77d78b8b07e194059399f77c87dc6149c17020464",
|
|
@@ -2345,7 +2346,7 @@
|
|
|
2345
2346
|
"oss-fuzz/projects/blamejs/README.md": "sha256:ae13b7bb79ed8d69b1b3276e5562807a0349fb6e6b7d11cf1f683aad1eafdb4b",
|
|
2346
2347
|
"oss-fuzz/projects/blamejs/build.sh": "sha256:0ced1cf21782c97be7f8d74faf5e27a308b60b2f858836fb5ca3b8c4e939a8f7",
|
|
2347
2348
|
"oss-fuzz/projects/blamejs/project.yaml": "sha256:59f2cb83aa622325a175b77416fe155be15b70a9c798bd1a78bba05763b1b03d",
|
|
2348
|
-
"package.json": "sha256:
|
|
2349
|
+
"package.json": "sha256:ca031cc44ce6ade614e436bd1f29d01cd2d495ad41c55a819c02c85a5f3a2f96",
|
|
2349
2350
|
"release-notes/v0.0.x.json": "sha256:7a49819f30068ee119000cad7010194882bb8bfaa12acbdab4dfc066efb7982f",
|
|
2350
2351
|
"release-notes/v0.1.x.json": "sha256:6742a8c17f947c5cb76f69dead7eea86b942d80621d914b774ba5488e09937e5",
|
|
2351
2352
|
"release-notes/v0.10.x.json": "sha256:fe498045daf88337bd3d987e5964aa42c99a50e1685b6f09e51f698b8687726f",
|
|
@@ -2381,6 +2382,7 @@
|
|
|
2381
2382
|
"release-notes/v0.15.31.json": "sha256:f1d259dcee41503588b9c54060e56e436eb5fa7db502872a895c84ebb96627a9",
|
|
2382
2383
|
"release-notes/v0.15.32.json": "sha256:af99a8a782dcc30aff3a135c27179b064d63181fc4c302c31da632c1615b68ce",
|
|
2383
2384
|
"release-notes/v0.15.33.json": "sha256:295ea9939b74498c7f0be6afdc8bc511f97cb2383a436c3040c58b0600598b19",
|
|
2385
|
+
"release-notes/v0.15.34.json": "sha256:2a1b36d897d8e9f2cece051c098030f08d9c6ee6bef136800bd19453ad65f188",
|
|
2384
2386
|
"release-notes/v0.15.4.json": "sha256:6ac7fa0ef1728c27e71b2050d1b07a810f9b4b1440ccddbf28ad56e2f54d8585",
|
|
2385
2387
|
"release-notes/v0.15.5.json": "sha256:cca1d0edd5d6fc41b512d19d98be224b990dcab41478622c11962f0fcb1bb09a",
|
|
2386
2388
|
"release-notes/v0.15.6.json": "sha256:0e3b9e5e43b70b61dd258c3003d1b8729cd3c26c62a34dedcca81bbec5d31077",
|
|
@@ -2627,7 +2629,7 @@
|
|
|
2627
2629
|
"test/layer-0-primitives/cluster-storage.test.js": "sha256:238b3b3db0eba3e6312a863710533178f566347b90e161e564481aa826707647",
|
|
2628
2630
|
"test/layer-0-primitives/cluster-vault-rotation.test.js": "sha256:3514e9e71d6c39e805248f58ad2f41528d091e196c0f3766a032675677161b2d",
|
|
2629
2631
|
"test/layer-0-primitives/cms-codec.test.js": "sha256:7e46078ed82be5b69d22c48f22dba37ea5015371c2a8cf5f94fb1a792fb7bb78",
|
|
2630
|
-
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:
|
|
2632
|
+
"test/layer-0-primitives/codebase-patterns.test.js": "sha256:69d9da97b002a40c9edb39a6848d7369c2b43e2e09b935bdf9e9e8228765f40a",
|
|
2631
2633
|
"test/layer-0-primitives/codepoint-class.test.js": "sha256:19d1b69efa7e0e9f7ef2392ca264167767a5ec5890ee339b2cfd8a7818035d27",
|
|
2632
2634
|
"test/layer-0-primitives/compliance-ai-act.test.js": "sha256:5ee4ad05d12233cb3c5457ef10a727833710bbc1ce1318838f9f9ef5d2cb8d4b",
|
|
2633
2635
|
"test/layer-0-primitives/compliance-cascade.test.js": "sha256:ee02cf14541a837a9d7977c6ea6bf7f9210bed293925d93c976e31f270aebec4",
|
|
@@ -2822,7 +2824,7 @@
|
|
|
2822
2824
|
"test/layer-0-primitives/mail-bounce.test.js": "sha256:d4ef09adc71e1c9e336a9fc5b059e6b264d9abcd0c032023bfa7b15a9173a4b7",
|
|
2823
2825
|
"test/layer-0-primitives/mail-canspam.test.js": "sha256:c21dec043c9984d191af0b1f3425e85953b559ec6dfdf281a5f746f7121eb281",
|
|
2824
2826
|
"test/layer-0-primitives/mail-crypto-pgp-experimental.test.js": "sha256:5b00a55d58cc48eb2864c6f7c6386ce44423e478b1033e5b11bb5130b4bb60c5",
|
|
2825
|
-
"test/layer-0-primitives/mail-crypto-pgp.test.js": "sha256:
|
|
2827
|
+
"test/layer-0-primitives/mail-crypto-pgp.test.js": "sha256:a5340ca432e631104496e6aa9ebe4a99a1c26d9de6819a1f6df9534821b8ed43",
|
|
2826
2828
|
"test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js": "sha256:e7bdef1d3d3aafacfd328a7fb7e0dacb45bf66103e18ad19d97d6e308e4aee31",
|
|
2827
2829
|
"test/layer-0-primitives/mail-crypto-smime.test.js": "sha256:2b840ad3706840a8d206c9fa09337e0c13ecb83a7ea4d97fceb49d4fb4454dfa",
|
|
2828
2830
|
"test/layer-0-primitives/mail-dav.test.js": "sha256:5b4dcb1b9d8942f0cc2235a99487b076cf4f984933c609d737bb40471ae7333c",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.15.x
|
|
10
10
|
|
|
11
|
+
- v0.15.34 (2026-06-26) — **`b.mail.crypto.pgp.verify` now accepts every valid RSA OpenPGP signature — one whose value happened to have a high zero byte (about 1 in 256) was being rejected.** An OpenPGP RSA signature is an integer modulo the key's modulus, and ~1 in 256 signatures have a value that begins with a zero byte. The MPI encoding strips those leading zero bytes (RFC 9580 §3.2), but b.mail.crypto.pgp.verify passed the stripped value straight to the RSA verification, which requires a signature exactly the modulus byte length — so a perfectly valid signature was intermittently reported invalid. The signature is now left-padded back to the modulus width before verification, exactly as the Ed25519 path already pads its components. Verification of every valid RSA signature is now reliable, including signatures produced by other OpenPGP implementations. Also includes internal test-tooling: two more guard-suite suppression classes were re-verified and their tokens retired. **Fixed:** *RSA OpenPGP signatures with a high zero byte now verify reliably* — b.mail.crypto.pgp.verify read the RSA signature MPI (whose leading zero bytes the OpenPGP wire format strips) and handed it to the RSA verification without restoring the stripped bytes. Node's RSA verify requires the signature to be exactly the modulus byte length, so a signature whose value had one or more high zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct. The signature is now left-padded to the modulus width before verification (the same correction the Ed25519 verification path already applied to its R/S components). This affects signatures the framework produces and signatures from other OpenPGP implementations alike. A deterministic regression test searches for a short-MPI signature and asserts it verifies. **Detectors:** *Two more suppression-marker classes re-verified and their tokens retired* — The raw-hash-compare and seal-without-aad guard classes were re-read and confirmed (a data-residency region tag compared with === — not a secret; and two intentional non-AEAD-bound seals — a non-regulated plain-mode column whose AAD is enforced by the posture seal-envelope floor where it matters, and a throwaway vault-readiness probe), then renamed to descriptive tokens with their old names added to the retired-token set. Test-suite tooling only.
|
|
12
|
+
|
|
11
13
|
- v0.15.33 (2026-06-26) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** Three more suppression classes in the codebase-patterns guard suite were re-verified from scratch and renamed to descriptive tokens, with their old names recorded as retired and their in-source marker comments updated to match. No runtime code, public API, or wire format changed. **Detectors:** *Three more suppression-marker classes re-verified and their tokens retired* — Continuing the re-verification pass: each marked site for the math-random-noncrypto, raw-new-url, and dynamic-require guard classes was re-read and confirmed (non-security jitter/sampling; URL parsing for shape/origin inspection or behind the safe wrapper; operator-supplied module loads), then the class was renamed to a descriptive token and its old name added to the retired-token set. This is test-suite tooling; no shipped framework behavior changed.
|
|
12
14
|
|
|
13
15
|
- v0.15.32 (2026-06-26) — **Internal test-suite hardening only — the published library's runtime behavior and public API are unchanged (source-comment marker text aside).** Three more suppression classes in the codebase-patterns guard suite were re-verified from scratch and renamed to descriptive tokens, with their old names recorded as retired so the prior approval cannot be silently resurrected and their in-source marker comments updated to match. No runtime code, public API, or wire format changed. **Detectors:** *Three more suppression-marker classes re-verified and their tokens retired* — Continuing the re-verification pass: each marked site for the process-exit, hand-rolled buffer-collect, and raw-outbound-http guard classes was re-read and confirmed (operator-opt-in exits; bounded protocol-framing / TLV assembly; framework-routed outbound calls), then the class was renamed to a descriptive token and its old name added to the retired-token set. This is test-suite tooling; no shipped framework behavior changed.
|
|
@@ -1090,7 +1090,7 @@ function sealRow(table, row, opts) {
|
|
|
1090
1090
|
out[field] = vaultAad.seal(_encodeTyped(out[field]),
|
|
1091
1091
|
_aadParts(s, table, field, out));
|
|
1092
1092
|
} else {
|
|
1093
|
-
// allow:seal-without-aad — plain-mode legacy table; operator
|
|
1093
|
+
// allow:seal-without-aad-by-design — plain-mode legacy table; operator
|
|
1094
1094
|
// opts into AAD via registerTable({aad:true})
|
|
1095
1095
|
out[field] = vault.seal(_encodeTyped(out[field]));
|
|
1096
1096
|
}
|
|
@@ -1709,7 +1709,7 @@ function _crossBorderRegulated(posture) {
|
|
|
1709
1709
|
|
|
1710
1710
|
function _residencyCompatible(primaryTag, replicaTag) {
|
|
1711
1711
|
if (!primaryTag || !replicaTag) return true;
|
|
1712
|
-
if (primaryTag === replicaTag) return true; // allow:raw-hash-compare — residency tag string, not a secret hash
|
|
1712
|
+
if (primaryTag === replicaTag) return true; // allow:raw-hash-compare-nonsecret-tag — residency tag string, not a secret hash
|
|
1713
1713
|
if (primaryTag === "unrestricted" || replicaTag === "unrestricted") return true;
|
|
1714
1714
|
return false;
|
|
1715
1715
|
}
|
|
@@ -865,11 +865,22 @@ function verify(opts) {
|
|
|
865
865
|
var ok;
|
|
866
866
|
if (parsed.pubAlg === PUB_ALG_RSA) {
|
|
867
867
|
var rsaMpi = _readMpi(parsed.sigMpisBytes, 0);
|
|
868
|
+
// The signature is an integer in [0, n); when its value has one or more
|
|
869
|
+
// high zero bytes (~1/256 of signatures) the OpenPGP MPI encoding strips
|
|
870
|
+
// them (RFC 9580 §3.2), but node's RSA verify requires a signature exactly
|
|
871
|
+
// the modulus byte length. Left-pad the stripped MPI back to the modulus
|
|
872
|
+
// width — the same correction the Ed25519 branch applies to its R/S
|
|
873
|
+
// components below — or a valid signature is rejected.
|
|
874
|
+
var rsaSigBytes = rsaMpi.value;
|
|
875
|
+
var modLen = rsaPub.n.length;
|
|
876
|
+
if (rsaSigBytes.length < modLen) {
|
|
877
|
+
rsaSigBytes = Buffer.concat([Buffer.alloc(modLen - rsaSigBytes.length), rsaSigBytes]);
|
|
878
|
+
}
|
|
868
879
|
try {
|
|
869
880
|
ok = nodeCrypto.verify(hashName, hashInput, {
|
|
870
881
|
key: publicKey,
|
|
871
882
|
padding: nodeCrypto.constants.RSA_PKCS1_PADDING,
|
|
872
|
-
},
|
|
883
|
+
}, rsaSigBytes);
|
|
873
884
|
} catch (e) {
|
|
874
885
|
return _fail("mail-crypto/pgp/verify-error",
|
|
875
886
|
"RSA verify threw: " + ((e && e.message) || String(e)));
|
|
@@ -290,7 +290,7 @@ function dbStore(opts) {
|
|
|
290
290
|
var sealEnabled = false;
|
|
291
291
|
if (sealReq) {
|
|
292
292
|
try {
|
|
293
|
-
// allow:seal-without-aad — vault-readiness probe; throwaway
|
|
293
|
+
// allow:seal-without-aad-by-design — vault-readiness probe; throwaway
|
|
294
294
|
// sentinel value, not row-bound data
|
|
295
295
|
vault.seal("__idempotency_seal_probe__");
|
|
296
296
|
sealEnabled = true;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.34",
|
|
4
|
+
"date": "2026-06-26",
|
|
5
|
+
"headline": "`b.mail.crypto.pgp.verify` now accepts every valid RSA OpenPGP signature — one whose value happened to have a high zero byte (about 1 in 256) was being rejected",
|
|
6
|
+
"summary": "An OpenPGP RSA signature is an integer modulo the key's modulus, and ~1 in 256 signatures have a value that begins with a zero byte. The MPI encoding strips those leading zero bytes (RFC 9580 §3.2), but b.mail.crypto.pgp.verify passed the stripped value straight to the RSA verification, which requires a signature exactly the modulus byte length — so a perfectly valid signature was intermittently reported invalid. The signature is now left-padded back to the modulus width before verification, exactly as the Ed25519 path already pads its components. Verification of every valid RSA signature is now reliable, including signatures produced by other OpenPGP implementations. Also includes internal test-tooling: two more guard-suite suppression classes were re-verified and their tokens retired.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "RSA OpenPGP signatures with a high zero byte now verify reliably",
|
|
13
|
+
"body": "b.mail.crypto.pgp.verify read the RSA signature MPI (whose leading zero bytes the OpenPGP wire format strips) and handed it to the RSA verification without restoring the stripped bytes. Node's RSA verify requires the signature to be exactly the modulus byte length, so a signature whose value had one or more high zero bytes — about 1 in 256 of all signatures, for any key — was rejected as invalid even though it was correct. The signature is now left-padded to the modulus width before verification (the same correction the Ed25519 verification path already applied to its R/S components). This affects signatures the framework produces and signatures from other OpenPGP implementations alike. A deterministic regression test searches for a short-MPI signature and asserts it verifies."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Detectors",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "Two more suppression-marker classes re-verified and their tokens retired",
|
|
22
|
+
"body": "The raw-hash-compare and seal-without-aad guard classes were re-read and confirmed (a data-residency region tag compared with === — not a secret; and two intentional non-AEAD-bound seals — a non-regulated plain-mode column whose AAD is enforced by the posture seal-envelope floor where it matters, and a throwaway vault-readiness probe), then renamed to descriptive tokens with their old names added to the retired-token set. Test-suite tooling only."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -344,7 +344,7 @@ var VALID_ALLOW_CLASSES = {
|
|
|
344
344
|
"primitive-unreachable": 1,
|
|
345
345
|
"process-exit-operator-optin": 1,
|
|
346
346
|
"raw-byte-literal": 1,
|
|
347
|
-
"raw-hash-compare": 1,
|
|
347
|
+
"raw-hash-compare-nonsecret-tag": 1,
|
|
348
348
|
"raw-new-url-parse-only": 1,
|
|
349
349
|
"raw-outbound-http-framework-internal": 1,
|
|
350
350
|
"raw-process-env-bootstrap": 1,
|
|
@@ -353,7 +353,7 @@ var VALID_ALLOW_CLASSES = {
|
|
|
353
353
|
"raw-timing-safe-equal-boot-prechecked": 1,
|
|
354
354
|
"raw-xfp-telemetry-only": 1,
|
|
355
355
|
"regex-no-length-cap": 1,
|
|
356
|
-
"seal-without-aad": 1,
|
|
356
|
+
"seal-without-aad-by-design": 1,
|
|
357
357
|
"silent-catch-stream-teardown": 1,
|
|
358
358
|
"slsa-framework-action-not-sha-pinned": 1,
|
|
359
359
|
"timer-no-unref-unrefed-below": 1,
|
|
@@ -387,6 +387,8 @@ var RETIRED_ALLOW_TOKENS = {
|
|
|
387
387
|
"math-random-noncrypto": "renamed to 'math-random-noncrypto-jitter-sampling' (2026-06-26 re-verify pass) — every site is jitter / sampling / backoff where predictability is not a threat; re-verify before reusing",
|
|
388
388
|
"raw-new-url": "renamed to 'raw-new-url-parse-only' (2026-06-26 re-verify pass) — every site parses for shape/origin inspection, is the safeUrl wrapper itself, or is re-validated downstream — none is an unguarded outbound-fetch target; re-verify before reusing",
|
|
389
389
|
"dynamic-require": "renamed to 'dynamic-require-operator-module' (2026-06-26 re-verify pass) — every site loads an operator-supplied path (migration / seed / extensibility entry, centralized in module-loader.js) or is diagnostic message text; re-verify before reusing",
|
|
390
|
+
"raw-hash-compare": "renamed to 'raw-hash-compare-nonsecret-tag' (2026-06-26 re-verify pass) — the one site compares a data-residency region tag with ===, not a secret hash; re-verify before reusing",
|
|
391
|
+
"seal-without-aad": "renamed to 'seal-without-aad-by-design' (2026-06-26 re-verify pass) — both sites intentionally seal without AAD (a non-regulated plain-mode table whose AAD is enforced by the posture sealEnvelopeFloor where required, and a throwaway vault-readiness probe sentinel); re-verify before reusing",
|
|
390
392
|
};
|
|
391
393
|
|
|
392
394
|
function testNoRetiredAllowTokenReRegistered() {
|
|
@@ -1665,7 +1667,7 @@ function testNoRawHashCompare() {
|
|
|
1665
1667
|
// with === are timing-attack-prone. Should use `crypto.timingSafeEqual`.
|
|
1666
1668
|
var matches = _scan(
|
|
1667
1669
|
/\b\w*(Hash|Token|Sig|Signature|Mac|Digest|Tag)\s*===\s*\w*(Hash|Token|Sig|Signature|Mac|Digest|Tag)/);
|
|
1668
|
-
matches = _filterMarkers(matches, "raw-hash-compare");
|
|
1670
|
+
matches = _filterMarkers(matches, "raw-hash-compare-nonsecret-tag");
|
|
1669
1671
|
_report("hash/token/sig/mac/digest compared with timingSafeEqual " +
|
|
1670
1672
|
"(or has allow marker for known-non-secret comparison)",
|
|
1671
1673
|
matches);
|
|
@@ -11124,7 +11126,7 @@ function testSealWithoutAad() {
|
|
|
11124
11126
|
}
|
|
11125
11127
|
}
|
|
11126
11128
|
}
|
|
11127
|
-
bad = _filterMarkers(bad, "seal-without-aad");
|
|
11129
|
+
bad = _filterMarkers(bad, "seal-without-aad-by-design");
|
|
11128
11130
|
_report("dbStore-shaped sealed-row code routes through vault.aad.seal " +
|
|
11129
11131
|
"(or has allow marker) — CRYPTO-1",
|
|
11130
11132
|
bad);
|
|
@@ -11177,7 +11179,7 @@ function testNoHexShaCompareEquals() {
|
|
|
11177
11179
|
matches = matches.concat(_scan(
|
|
11178
11180
|
/\b\w+Hex\s*(===|!==)\s*(hmac\w*Hex|mac\w*Hex|signature\w*Hex|sigVerify\w*Hex)/i));
|
|
11179
11181
|
matches = _filterMarkers(matches, "hex-sha-compare-equals");
|
|
11180
|
-
matches = _filterMarkers(matches, "raw-hash-compare");
|
|
11182
|
+
matches = _filterMarkers(matches, "raw-hash-compare-nonsecret-tag");
|
|
11181
11183
|
_report("hex HMAC / MAC / signature compared with timingSafeEqual " +
|
|
11182
11184
|
"(CVE-2026-21713 — memcmp leaks per-byte timing)",
|
|
11183
11185
|
matches);
|
|
@@ -159,6 +159,55 @@ function testPgpRsaRoundTrip() {
|
|
|
159
159
|
check("rsa verify reports fingerprint", verify.signerFingerprint === rv.fingerprint);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Minimal parse of an ASCII-armored detached signature down to the RSA
|
|
163
|
+
// signature MPI's value byte-length, so the test can DETERMINISTICALLY find a
|
|
164
|
+
// signature whose high zero byte was stripped (rather than relying on a ~1/256
|
|
165
|
+
// random round-trip to flake). New-format packet (RFC 9580 §4.2.1): tag byte +
|
|
166
|
+
// length octet(s), then v4 sig body: ver/type/pubalg/hashalg(4) + hashedLen(2) +
|
|
167
|
+
// hashed + unhashedLen(2) + unhashed + hashLeft16(2) + MPI(2-byte bits + value).
|
|
168
|
+
function _rsaSigMpiByteLen(armored) {
|
|
169
|
+
var lines = armored.replace(/\r/g, "").split("\n");
|
|
170
|
+
var collecting = false, body = "";
|
|
171
|
+
for (var i = 0; i < lines.length; i++) {
|
|
172
|
+
var L = lines[i];
|
|
173
|
+
if (L.indexOf("-----BEGIN") === 0) { collecting = false; continue; }
|
|
174
|
+
if (L.indexOf("-----END") === 0) break;
|
|
175
|
+
if (L === "") { collecting = true; continue; } // blank line precedes the body
|
|
176
|
+
if (collecting) { if (L.charAt(0) === "=") break; body += L; } // '=' is the CRC line
|
|
177
|
+
}
|
|
178
|
+
var pkt = Buffer.from(body, "base64");
|
|
179
|
+
var p = 1; // skip the new-format tag byte
|
|
180
|
+
var l0 = pkt[p];
|
|
181
|
+
if (l0 < 192) p += 1; else if (l0 < 224) p += 2; else if (l0 === 255) p += 5; else p += 1;
|
|
182
|
+
p += 4; // ver, type, pubalg, hashalg
|
|
183
|
+
var hashedLen = pkt.readUInt16BE(p); p += 2 + hashedLen;
|
|
184
|
+
var unhashedLen = pkt.readUInt16BE(p); p += 2 + unhashedLen;
|
|
185
|
+
p += 2; // hashLeft16
|
|
186
|
+
return Math.ceil(pkt.readUInt16BE(p) / 8); // MPI bit-length → value byte-length
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function testPgpRsaVerifyLeadingZeroSignature() {
|
|
190
|
+
// An RSA signature is an integer in [0, n); ~1/256 of (key, message) pairs
|
|
191
|
+
// produce a value with a high zero byte, which the OpenPGP MPI encoding
|
|
192
|
+
// strips (RFC 9580 §3.2). verify() must left-pad the stripped MPI back to the
|
|
193
|
+
// modulus byte length before the RSA op, or it rejects a VALID signature.
|
|
194
|
+
// That bug surfaced only as a ~0.4% flake in the random-key round-trip above;
|
|
195
|
+
// this finds the case deterministically and proves the signature still verifies.
|
|
196
|
+
var kp = _rsaKeypair(2048);
|
|
197
|
+
var MOD_BYTES = 256; // 2048-bit modulus
|
|
198
|
+
var tested = false;
|
|
199
|
+
for (var i = 0; i < 4000 && !tested; i++) {
|
|
200
|
+
var message = "rsa-leading-zero-probe-" + i;
|
|
201
|
+
var rv = pgp.sign({ message: message, privateKeyPem: kp.privateKey });
|
|
202
|
+
if (_rsaSigMpiByteLen(rv.armored) < MOD_BYTES) {
|
|
203
|
+
var verify = pgp.verify({ message: message, armored: rv.armored, publicKeyPem: kp.publicKey });
|
|
204
|
+
check("rsa verify accepts a high-zero-byte (short-MPI) signature", verify.ok === true);
|
|
205
|
+
tested = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
check("found a short-MPI RSA signature to exercise (within 4000 messages)", tested);
|
|
209
|
+
}
|
|
210
|
+
|
|
162
211
|
// ---- Tamper detection ----
|
|
163
212
|
|
|
164
213
|
function testPgpTamperDetection() {
|
|
@@ -308,6 +357,7 @@ function run() {
|
|
|
308
357
|
testPgpSignInputValidation();
|
|
309
358
|
testPgpEd25519RoundTrip();
|
|
310
359
|
testPgpRsaRoundTrip();
|
|
360
|
+
testPgpRsaVerifyLeadingZeroSignature();
|
|
311
361
|
testPgpTamperDetection();
|
|
312
362
|
testPgpVerifyInputValidation();
|
|
313
363
|
testPgpDocBlockNamesEfail();
|
package/package.json
CHANGED