@blamejs/blamejs-shop 0.4.91 → 0.4.93
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/security-middleware.js +21 -3
- package/lib/vendor/MANIFEST.json +49 -41
- package/lib/vendor/blamejs/CHANGELOG.md +6 -0
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +208 -2
- package/lib/vendor/blamejs/examples/wiki/test/e2e.js +7 -4
- package/lib/vendor/blamejs/examples/wiki/test/integration.js +15 -12
- package/lib/vendor/blamejs/index.js +2 -0
- package/lib/vendor/blamejs/lib/audit-sign.js +34 -1
- package/lib/vendor/blamejs/lib/backup/manifest.js +191 -44
- package/lib/vendor/blamejs/lib/codepoint-class.js +284 -77
- package/lib/vendor/blamejs/lib/framework-error.js +14 -0
- package/lib/vendor/blamejs/lib/fsm.js +80 -24
- package/lib/vendor/blamejs/lib/log.js +32 -0
- package/lib/vendor/blamejs/lib/middleware/rate-limit.js +18 -2
- package/lib/vendor/blamejs/lib/middleware/request-id.js +24 -4
- package/lib/vendor/blamejs/lib/request-helpers.js +50 -0
- package/lib/vendor/blamejs/lib/safe-path.js +24 -10
- package/lib/vendor/blamejs/lib/sql.js +133 -0
- package/lib/vendor/blamejs/lib/totp.js +98 -33
- package/lib/vendor/blamejs/lib/ws-client.js +39 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.21.json +51 -0
- package/lib/vendor/blamejs/release-notes/v0.15.22.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.15.23.json +22 -0
- package/lib/vendor/blamejs/test/00-primitives.js +80 -0
- package/lib/vendor/blamejs/test/_smoke-worker.js +81 -0
- package/lib/vendor/blamejs/test/integration/federation-auth.test.js +7 -4
- package/lib/vendor/blamejs/test/integration/mail-crypto-smime.test.js +7 -4
- package/lib/vendor/blamejs/test/layer-0-primitives/backup-manifest-signature.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +65 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codepoint-class.test.js +58 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/defineguard-default-gate-posture-caps.test.js +5 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/dpop-middleware-replaystore-required.test.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/fsm.test.js +99 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/money.test.js +30 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/otlp-attr-redaction.test.js +9 -6
- package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-xff-spoofing.test.js +36 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-helpers.test.js +33 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/request-id-async-context.test.js +117 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/safe-path.test.js +64 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/sql.test.js +96 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ws-client.test.js +55 -0
- package/lib/vendor/blamejs/test/smoke.js +93 -10
- package/package.json +1 -1
|
@@ -64,7 +64,33 @@ var structuredFields = require("./structured-fields");
|
|
|
64
64
|
var C = require("./constants");
|
|
65
65
|
var { defineClass } = require("./framework-error");
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// Codes that are TRANSIENT — a consumer's reconnect loop (and the client's own
|
|
68
|
+
// auto-reconnect) SHOULD retry. Everything else is terminal: a bad URL / config
|
|
69
|
+
// error, a 4xx handshake rejection, an accept-mismatch, or a protocol-violation
|
|
70
|
+
// / oversized / malicious frame, where redialing the same hopeless target
|
|
71
|
+
// forever is the worse failure. bad-status is split by status below (5xx
|
|
72
|
+
// transient, 4xx terminal). Default-terminal so a new/forgotten code fails
|
|
73
|
+
// closed (no infinite redial) rather than open.
|
|
74
|
+
var WS_TRANSIENT_CODES = {
|
|
75
|
+
"ws-client/handshake-timeout": true, // server slow to complete the upgrade
|
|
76
|
+
"ws-client/pong-timeout": true, // keepalive lapse on an otherwise-live link
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function _wsErrorIsPermanent(code, statusCode) {
|
|
80
|
+
if (code === "ws-client/bad-status") {
|
|
81
|
+
// 5xx handshake rejection is transient (server overloaded / restarting);
|
|
82
|
+
// 4xx (and any non-5xx) is terminal (auth / bad request / not-found).
|
|
83
|
+
return !(typeof statusCode === "number" && statusCode >= 500 && statusCode < 600);
|
|
84
|
+
}
|
|
85
|
+
// hasOwnProperty membership so a code of "__proto__" / "constructor" can't
|
|
86
|
+
// index the prototype and be misread as transient.
|
|
87
|
+
return !Object.prototype.hasOwnProperty.call(WS_TRANSIENT_CODES, code);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// permanent is DERIVED per-error from the code (+ status) at every construction
|
|
91
|
+
// — so err.permanent is a usable terminal/transient signal for a consumer's
|
|
92
|
+
// reconnect loop, not a blanket sentinel. Constructor: (code, message, status).
|
|
93
|
+
var WsClientError = defineClass("WsClientError", { permanentClassifier: _wsErrorIsPermanent });
|
|
68
94
|
|
|
69
95
|
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455 §1.3
|
|
70
96
|
|
|
@@ -88,33 +114,15 @@ var CLOSE_NORMAL = 1000; // RFC 6455 close code
|
|
|
88
114
|
var CLOSE_GOING_AWAY = 1001; // RFC 6455 close code
|
|
89
115
|
var CLOSE_ABNORMAL = 1006; // RFC 6455 close code (synthetic — never on wire)
|
|
90
116
|
|
|
91
|
-
// Permanent vs transient
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"ws-client/bad-upgrade": true,
|
|
99
|
-
"ws-client/bad-status-line": true,
|
|
100
|
-
"ws-client/bad-subprotocol": true,
|
|
101
|
-
"ws-client/bad-header": true,
|
|
102
|
-
"ws-client/handshake-too-large": true,
|
|
103
|
-
"ws-client/control-too-big": true,
|
|
104
|
-
"ws-client/control-fragmented": true,
|
|
105
|
-
"ws-client/protocol-error": true,
|
|
106
|
-
"ws-client/invalid-utf8": true,
|
|
107
|
-
"ws-client/rsv1-on-continuation": true,
|
|
108
|
-
"ws-client/message-too-big": true,
|
|
109
|
-
"ws-client/deflate-error": true,
|
|
110
|
-
"ws-client/payload-too-big": true,
|
|
111
|
-
};
|
|
112
|
-
|
|
117
|
+
// Permanent vs transient classifier for the reconnect logic. WsClientError now
|
|
118
|
+
// carries a per-code permanent flag (see _wsErrorIsPermanent), so this reads it
|
|
119
|
+
// directly. A raw socket error (ECONNRESET / ECONNREFUSED mid-dial, no
|
|
120
|
+
// .permanent) is treated as transient — the connection dropped, retrying (up to
|
|
121
|
+
// reconnect.maxAttempts) is correct. Previously WsClientError was alwaysPermanent
|
|
122
|
+
// so this returned true for EVERY framework error, silently disabling the
|
|
123
|
+
// advertised auto-reconnect for transient handshake/keepalive failures.
|
|
113
124
|
function _isPermanentError(err) {
|
|
114
|
-
|
|
115
|
-
if (err.permanent === true) return true; // defineClass-marked
|
|
116
|
-
if (err.code && PERMANENT_CODES[err.code]) return true;
|
|
117
|
-
return false;
|
|
125
|
+
return !!(err && err.permanent === true);
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
// Synchronous bounded inflate — runs zlib.inflateRawSync with
|
|
@@ -513,8 +521,11 @@ class WsClient extends EventEmitter {
|
|
|
513
521
|
// the message string.
|
|
514
522
|
var bodyText = "";
|
|
515
523
|
try { bodyText = rest.toString("utf8"); } catch (_e) { /* drop-silent */ }
|
|
524
|
+
// Pass status as the 3rd arg so the classifier derives permanent from it
|
|
525
|
+
// (4xx terminal, 5xx transient) and statusCode is set; keep .status as the
|
|
526
|
+
// pre-existing alias callers may already read.
|
|
516
527
|
var statusErr = new WsClientError("ws-client/bad-status",
|
|
517
|
-
"handshake response status was " + status + " (expected 101 Switching Protocols)");
|
|
528
|
+
"handshake response status was " + status + " (expected 101 Switching Protocols)", status);
|
|
518
529
|
statusErr.status = status;
|
|
519
530
|
statusErr.body = bodyText;
|
|
520
531
|
this._handleSocketError(statusErr);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.21",
|
|
4
|
+
"date": "2026-06-24",
|
|
5
|
+
"headline": "Adds a portable compare-and-swap UPDATE, an IPv6 /64 rate-limit key, a verify-only legacy-TOTP path, request-id AsyncLocalStorage scoping, schema-agnostic backup signing, side-effect-free FSM transition resolution, and the public codepoint-threat catalog — and fixes an FSM state transition that committed without an audit record when its entry hook threw",
|
|
6
|
+
"summary": "A batch of additive primitives that close gaps operators hit composing the framework: b.sql.guardedUpdate builds the cross-instance-safe compare-and-swap UPDATE (the conditional-INSERT sibling) with b.sql.casWon reading the won/lost result; b.requestHelpers.ipKey (and a rateLimit ipKeyMode) key an IPv6 client by its routing-significant /64 so one end-site can't rotate the low 64 bits to evade a per-IP limit; b.auth.totp gains an opt-in verify-only SHA-1 path for one-final-login legacy-secret migration while generation stays SHA-512-only; b.middleware.requestId can bind the id into an AsyncLocalStorage scope that survives the awaited route chain; b.backupManifest.signBytes/verifyBytes authenticate a consumer's own canonical bytes with the framework signing key; b.fsm gains a side-effect-free instance.target() and a way to defer its transition audit when composed with an external claim; and the codepoint-threat catalog the guard family composes is exposed as b.codepointClass. A correctness fix: an FSM transition whose onEnter hook threw left the state committed but emitted no audit record — the record now always fires (stamped a failure outcome with the hook error) so a state change is never unaudited.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "b.sql.guardedUpdate + b.sql.casWon — portable compare-and-swap UPDATE",
|
|
13
|
+
"body": "b.sql.guardedUpdate(table) builds an UPDATE whose required guardWhere(col, expected) fence makes it land only when the row is still in the expected value — the cross-instance-safe way to advance a status or version on a single-statement-per-request backend (a D1-over-HTTP bridge or any autocommit-only adapter without interactive transactions), and the conditional-UPDATE sibling of b.sql.insertSelectWhere. It refuses to render without a fence (an unfenced guardedUpdate is just a plain update). b.sql.casWon(result) reads the won/lost verdict from the affected-row count, normalizing the rowCount / changes / affectedRows field-name divergence across adapters and throwing on an indeterminate result so a phantom win can't ship. Standard SQL across SQLite / Postgres / MySQL; guardWhereOp(col, op, expected) supports a non-equality fence (an optimistic-version or balance guard), and a null fence renders IS NULL."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "b.requestHelpers.ipKey + rateLimit ipKeyMode — IPv6 /64 keying",
|
|
17
|
+
"body": "b.requestHelpers.ipKey(ip, { ipv6Bits }) derives a stable rate-limit / blocklist key: an IPv4 address verbatim (one IPv4 is one host) but an IPv6 address collapsed to its routing-significant /64 prefix. A single IPv6 end-site is allocated a whole /64 (RFC 6177 / RFC 4291) and freely rotates the low 64 bits, so keying on the full 128-bit address lets one site mint unlimited fresh keys and walk a per-IP throttle or evade an exact-address block; keying on the /64 closes that while still distinguishing real end-sites. b.middleware.rateLimit gains ipKeyMode: \"prefix64\" to apply this to its default key (the audit record still logs the full client IP)."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "b.auth.totp verify-only SHA-1 path for legacy-secret migration",
|
|
21
|
+
"body": "b.auth.totp.verify(secret, code, { algorithm: \"sha1\", verifyOnly: true }) authenticates a single legacy code during a re-enrollment flow, so a consumer migrating pre-existing RFC-6238-default (SHA-1) secrets can reuse the maintained verifier — separator stripping, 64-bit counter, drift / replay semantics — instead of hand-rolling a parallel HOTP. SHA-1 is still refused on every generation path (compute / generate / uri), so new-enrollment posture stays SHA-512-only, and the flag is honored only by verify(); each such verification emits an auth.totp.legacy_sha1_verify audit signal."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "b.middleware.requestId AsyncLocalStorage scoping",
|
|
25
|
+
"body": "b.middleware.requestId({ asyncContext: true }) binds the request id into the framework's AsyncLocalStorage scope so b.log.getRequestId() (and every b.log.create-built logger) returns it inside awaited route-handler code, not just on req.requestId. Because the b.router dispatch model runs the route handler after the middleware returns, the binding uses AsyncLocalStorage.enterWith (which persists forward across the awaited chain) rather than a callback wrap that would close before the handler runs; each request runs in its own async context so the binding stays request-scoped. The underlying b.log.enterRequestId(id) is exposed for callers wiring their own middleware."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"title": "b.backupManifest.signBytes / verifyBytes — schema-agnostic signing",
|
|
29
|
+
"body": "b.backupManifest.signBytes(canonicalBytes) and verifyBytes(canonicalBytes, signatureBlock, { expectedFingerprint }) sign and verify a consumer's own canonical bytes with the same audit-sign keypair and fingerprint pinning as the v1-manifest sign() / verifySignature(), without adopting the framework's manifest schema — so a bespoke backup-header format can authenticate itself against the framework signing key. Fingerprint pinning is bound to the key the signature actually verifies under: the pin is checked against the fingerprint recomputed from the signature block's own public key (the new b.auditSign.fingerprintOf(publicKeyPem) helper), not the block's self-asserted fingerprint field, so a block can't claim a trusted fingerprint while being signed by a different key. The same binding now also applies to the v1-manifest verifySignature(). b.auditSign.verify additionally no longer requires init() when given an explicit public key, so a downstream verifier that holds only a trusted public key can check a detached signature."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"title": "b.fsm side-effect-free transition resolution + deferrable audit",
|
|
33
|
+
"body": "instance.target(event) resolves a transition's destination state side-effect-free — the same edge and guard check as can() but returning the to-state (or null when the edge is illegal or guard-refused) — so a consumer composing an external compare-and-swap (b.sql.guardedUpdate on an autocommit-only substrate) can build the SET status = <to> claim without calling transition(), which would mutate state and emit an audit before the cross-instance claim is known to land. transition(event, { audit: false }) suppresses the built-in emit so that composition can emit its own enriched record once the claim resolves."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"title": "b.codepointClass — the codepoint-threat catalog on the public surface",
|
|
37
|
+
"body": "The Unicode bidi-override / C0-control / zero-width / null-byte / Unicode-Tags tables and the UTS #39 confusable-script detector that the b.guard* family composes internally are now exposed as b.codepointClass, so a consumer can build a custom unconstrained-free-text screen — detectCharThreats / assertNoCharThreats / applyCharStripPolicies / scriptFor / detectMixedScripts plus the compiled regexes — without re-rolling the regexes (where the zero-width class is mistyped and the astral Unicode-Tags block forgotten) or coupling to an internal module path. For a ready-made free-text guard, b.guardText remains the first-class entry point."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"heading": "Fixed",
|
|
43
|
+
"items": [
|
|
44
|
+
{
|
|
45
|
+
"title": "An FSM transition committed its state change without an audit record when the entry hook threw",
|
|
46
|
+
"body": "b.fsm commits the new state and pushes the history entry before running the destination state's onEnter hook. When that hook threw, control skipped the audit emission entirely — leaving a committed state transition with no audit record, a compliance gap for any regime that requires state changes to be auditable. The transition audit now always fires even when onEnter throws: it records the committed transition stamped with a failure outcome and the hook error, and still re-raises the error to the caller (the documented contract that an onEnter throw surfaces so the operator can roll back is unchanged)."
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.22",
|
|
4
|
+
"date": "2026-06-24",
|
|
5
|
+
"headline": "Internal test-harness reliability only — the published library is byte-for-byte identical to 0.15.21",
|
|
6
|
+
"summary": "This release changes only the repository's own test harness and test files; nothing under the shipped tarball (index.js, bin/, lib/) is touched, so the API, behavior, and published files are identical to 0.15.21 and operators have nothing to do. The smoke worker now attributes a fire-and-forget failure — an unawaited promise or an unreleased handle (a retry timer, a shutdown wait, a watcher/reload restart) that throws AFTER a test's assertions already passed — to the exact test that caused it and retries it once, instead of the prior unattributable, intermittent failure that only surfaced on a resource-starved CI runner. Five test files that invoked their run() at module scope (re-running and, in some cases, exiting the worker before it could report a result) are corrected to run only under require.main === module, and a test-discipline check keeps the pattern from returning. An opt-in handle-leak audit (SMOKE_AUDIT_HANDLES) surfaces tests that hold a timer / socket / server / worker past completion, for a follow-up cleanup pass.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Changed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "Smoke test harness attributes late-async-error failures and fixes module-level test self-execution",
|
|
13
|
+
"body": "The forked smoke worker installs unhandledRejection / uncaughtException handlers and a settle tick so a failure that fires after a test's assertions pass — a leaked handle's callback or an unawaited promise from retry / shutdown / reload logic — is reported against the test that caused it and retried once (a transient passes, a persistent one fails again and names the bug), replacing the prior intermittent, unattributable 'fork failed' that only appeared on a starved CI runner. Five test files (two integration, three layer-0, plus two wiki-suite harnesses) that called run() at module scope are corrected to execute only under require.main === module so they no longer double-run or exit the worker prematurely when required, and a codebase-patterns test-discipline detector prevents the unguarded-module-level-run shape from recurring. A SMOKE_AUDIT_HANDLES opt-in reports per-file handle leaks for a tracked cleanup. None of this is in the published package."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.15.23",
|
|
4
|
+
"date": "2026-06-24",
|
|
5
|
+
"headline": "`b.wsClient` error objects now carry a usable terminal-vs-transient signal (and the client's auto-reconnect, previously dead, works again), and `b.safePath` resolves cross-platform containment with the target platform's path semantics — fixing both a false refusal of in-base paths and a backslash-traversal escape when validating for Windows on a POSIX host",
|
|
6
|
+
"summary": "Two correctness fixes. b.wsClient marked every WsClientError permanent, so a consumer driving its own reconnect loop could not tell a terminal handshake failure (a bad URL, a 4xx rejection, an accept-mismatch, a protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong/handshake timeout, a dropped socket) and had to re-derive the taxonomy from error codes. err.permanent now reflects the actual transience of each error, the single bad-status code is split by the carried HTTP status (4xx terminal, 5xx transient) with the status exposed as err.statusCode, and — because the client's own auto-reconnect keyed off the same always-true flag — auto-reconnect was silently disabled for every transient failure and now fires correctly. Separately, b.safePath.resolve / resolveOrNull / validate performed its lexical containment with the runtime path module while the per-segment naming walk used opts.platform. The two disagreeing broke cross-platform validation both ways: every legitimate in-base path was refused with safe-path/escapes-base when opts.platform differed from the host, and — more seriously — a POSIX host validating opts.platform: \"windows\" accepted a backslash traversal (ok\\..\\..\\outside), because the runtime resolver treats \\ as an ordinary filename character and never collapsed the .. segments, so the path escaped the base once a Windows consumer read the backslashes. The lexical resolve and containment boundary now use the target platform's path module (node:path.win32 / node:path.posix), matching the segment walk, so in-base paths resolve and cross-platform traversals are refused under any opts.platform override; the realpath check, which touches the live filesystem, keeps its runtime resolve.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Fixed",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "b.wsClient: WsClientError carries a real terminal/transient signal, and auto-reconnect works again",
|
|
13
|
+
"body": "WsClientError was declared always-permanent, so err.permanent was true for every error — a consumer's reconnect loop could not distinguish a terminal handshake failure (bad URL, bad subprotocol, malformed handshake header, accept-mismatch, bad upgrade, bad status line, a 4xx rejection, an oversized/protocol-violation frame) from a transient one (a 5xx handshake rejection, a pong- or handshake-timeout, a dropped socket) and had to maintain its own error-code list tracking the framework's taxonomy. err.permanent now reflects each error's actual transience, derived from its code (a new/unknown code defaults to terminal, so it fails closed rather than redialing a hopeless target forever); the single ws-client/bad-status code is split by the response status (4xx terminal, 5xx transient) and the status is exposed as err.statusCode (the existing err.status alias is preserved). The same always-permanent flag also drove the client's built-in auto-reconnect, which therefore never retried any framework-surfaced transient failure (a 5xx handshake or a keepalive timeout); reconnect now fires for transient failures and still skips terminal ones."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "b.safePath: cross-platform containment resolves with the target platform's path semantics",
|
|
17
|
+
"body": "b.safePath.resolve / resolveOrNull / validate performed its lexical containment (resolve rel under base, then bound the result with a separator slice) using the runtime path module, while the per-segment naming walk used opts.platform. When the two disagreed, validation broke both ways. Benign direction: a Linux service validating server-origin names against the stricter Windows ruleset (the recommended cross-platform pattern) had every legitimate in-base path refused with safe-path/escapes-base, because the runtime-separated resolved path could never match a Windows-separator boundary. Security direction: a POSIX host validating opts.platform: \"windows\" accepted a backslash traversal such as ok\\..\\..\\outside — the segment walk splits on \\ for Windows, but the runtime resolver on POSIX treats \\ as an ordinary filename character and never collapsed the .. segments, so the path passed containment and resolved to <base>/ok\\..\\..\\outside, which escapes the base once a Windows consumer interprets the backslashes. The lexical resolve and the containment boundary now use the target platform's path module (node:path.win32 when validating for Windows, node:path.posix otherwise), matching the segment walk, so in-base paths resolve correctly and a genuine traversal is refused under any opts.platform override. The realpath check, which resolves symlinks on the live filesystem, keeps a separate runtime resolve because a foreign-platform path cannot be symlink-resolved on the host. opts.platform continues to gate the per-segment naming rules (reserved names, trailing dot/space, NTFS ADS colon)."
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -944,6 +944,85 @@ function testAuthTotpBadAlgorithmRejected() {
|
|
|
944
944
|
threwSha1 && threwSha1.code === "auth-totp/bad-alg");
|
|
945
945
|
}
|
|
946
946
|
|
|
947
|
+
// #354 — verify-only SHA-1 path for legacy-secret migration. SHA-1 stays
|
|
948
|
+
// refused on every generation path (compute/generate/uri); verify() accepts
|
|
949
|
+
// it ONLY when the caller passes { verifyOnly: true }, so a consumer can
|
|
950
|
+
// authenticate a final legacy code during re-enrollment without hand-rolling
|
|
951
|
+
// a parallel HOTP that drifts from the maintained verifier.
|
|
952
|
+
function testAuthTotpVerifyOnlySha1() {
|
|
953
|
+
var t = b.auth.totp;
|
|
954
|
+
// RFC 6238 Appendix B SHA-1 key K = ASCII("12345678901234567890") (20 B)
|
|
955
|
+
// and its 8-digit reference codes.
|
|
956
|
+
var KEY_SHA1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ";
|
|
957
|
+
var nowMs = 59 * 1000; // RFC T=59
|
|
958
|
+
var stepAt59 = Math.floor(59 / 30); // = 1
|
|
959
|
+
|
|
960
|
+
// GREEN: verify-only SHA-1 authenticates the RFC vector.
|
|
961
|
+
var matched = t.verify(KEY_SHA1, "94287082", {
|
|
962
|
+
algorithm: "sha1", verifyOnly: true, digits: 8, driftSteps: 0, now: nowMs,
|
|
963
|
+
});
|
|
964
|
+
check("verify-only SHA-1 accepts RFC 6238 vector T=59", matched === stepAt59);
|
|
965
|
+
|
|
966
|
+
// Separator stripping / maintained verifier semantics carry over.
|
|
967
|
+
check("verify-only SHA-1 strips paste separators",
|
|
968
|
+
t.verify(KEY_SHA1, "9428-7082", {
|
|
969
|
+
algorithm: "sha1", verifyOnly: true, digits: 8, driftSteps: 0, now: nowMs,
|
|
970
|
+
}) === stepAt59);
|
|
971
|
+
check("verify-only SHA-1 rejects a wrong code",
|
|
972
|
+
t.verify(KEY_SHA1, "00000000", {
|
|
973
|
+
algorithm: "sha1", verifyOnly: true, digits: 8, now: nowMs,
|
|
974
|
+
}) === false);
|
|
975
|
+
|
|
976
|
+
// verify() with sha1 but WITHOUT verifyOnly must THROW bad-alg — never a
|
|
977
|
+
// silent accept, so a config typo can't quietly downgrade the verifier.
|
|
978
|
+
var threw = null;
|
|
979
|
+
try {
|
|
980
|
+
t.verify(KEY_SHA1, "94287082", { algorithm: "sha1", digits: 8, now: nowMs });
|
|
981
|
+
} catch (e) { threw = e; }
|
|
982
|
+
check("verify(sha1) without verifyOnly throws bad-alg",
|
|
983
|
+
threw && threw.code === "auth-totp/bad-alg");
|
|
984
|
+
|
|
985
|
+
// Every generation path refuses SHA-1 even with verifyOnly:true — the flag
|
|
986
|
+
// is meaningless outside verify(); new-enrollment posture stays SHA-512.
|
|
987
|
+
threw = null;
|
|
988
|
+
try { t.compute(KEY_SHA1, stepAt59, { algorithm: "sha1", verifyOnly: true, digits: 8 }); }
|
|
989
|
+
catch (e) { threw = e; }
|
|
990
|
+
check("compute refuses sha1 even with verifyOnly:true",
|
|
991
|
+
threw && threw.code === "auth-totp/bad-alg");
|
|
992
|
+
|
|
993
|
+
threw = null;
|
|
994
|
+
try { t.generate(KEY_SHA1, { algorithm: "sha1", verifyOnly: true }); }
|
|
995
|
+
catch (e) { threw = e; }
|
|
996
|
+
check("generate refuses sha1 even with verifyOnly:true",
|
|
997
|
+
threw && threw.code === "auth-totp/bad-alg");
|
|
998
|
+
|
|
999
|
+
threw = null;
|
|
1000
|
+
try { t.uri(KEY_SHA1, "alice@example.com", { issuer: "X", algorithm: "sha1", verifyOnly: true }); }
|
|
1001
|
+
catch (e) { threw = e; }
|
|
1002
|
+
check("uri refuses sha1 even with verifyOnly:true",
|
|
1003
|
+
threw && threw.code === "auth-totp/bad-alg");
|
|
1004
|
+
|
|
1005
|
+
// Replay defense (lastUsedStep) still binds on the legacy path.
|
|
1006
|
+
check("verify-only SHA-1 honors the replay guard",
|
|
1007
|
+
t.verify(KEY_SHA1, "94287082", {
|
|
1008
|
+
algorithm: "sha1", verifyOnly: true, digits: 8, driftSteps: 0,
|
|
1009
|
+
now: nowMs, lastUsedStep: stepAt59,
|
|
1010
|
+
}) === false);
|
|
1011
|
+
|
|
1012
|
+
// A still-unsupported generation algorithm (md5) is not silently allowed by
|
|
1013
|
+
// verifyOnly — only the curated VERIFY_ONLY_ALGORITHMS set (sha1) is.
|
|
1014
|
+
threw = null;
|
|
1015
|
+
try { t.verify(KEY_SHA1, "94287082", { algorithm: "md5", verifyOnly: true, digits: 8, now: nowMs }); }
|
|
1016
|
+
catch (e) { threw = e; }
|
|
1017
|
+
check("verify-only does not widen to md5 (only the curated set)",
|
|
1018
|
+
threw && threw.code === "auth-totp/bad-alg");
|
|
1019
|
+
|
|
1020
|
+
check("auth.totp.VERIFY_ONLY_ALGORITHMS = [sha1]",
|
|
1021
|
+
Array.isArray(t.VERIFY_ONLY_ALGORITHMS) &&
|
|
1022
|
+
t.VERIFY_ONLY_ALGORITHMS.length === 1 &&
|
|
1023
|
+
t.VERIFY_ONLY_ALGORITHMS[0] === "sha1");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
947
1026
|
function testAuthTotpSurface() {
|
|
948
1027
|
var t = b.auth.totp;
|
|
949
1028
|
check("auth.totp namespace present", typeof b.auth.totp === "object");
|
|
@@ -18260,6 +18339,7 @@ async function run() {
|
|
|
18260
18339
|
testAuthTotpUriShape();
|
|
18261
18340
|
testAuthTotpBackupCodes();
|
|
18262
18341
|
testAuthTotpBadAlgorithmRejected();
|
|
18342
|
+
testAuthTotpVerifyOnlySha1();
|
|
18263
18343
|
// auth.passkey — WebAuthn
|
|
18264
18344
|
await testAuthPasskeySurface();
|
|
18265
18345
|
await testAuthPasskeyStartRegistrationOptions();
|
|
@@ -20,6 +20,58 @@ if (!modulePath) {
|
|
|
20
20
|
process.exit(2);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// Capture a FIRE-AND-FORGET async failure — an unawaited promise that rejects,
|
|
24
|
+
// or a handle callback (socket / timer / server) that throws — that fires
|
|
25
|
+
// AFTER the test's assertions have already passed. Without this, Node's default
|
|
26
|
+
// unhandled-rejection handling bumps the exit code to 1 and RACES the worker's
|
|
27
|
+
// own process.exit(0): on a fast runner exit(0) usually wins (the leak is
|
|
28
|
+
// invisible), but on a slow / starved runner (macos-latest, 3 cores) the
|
|
29
|
+
// rejection wins first and the file reads as an unattributable "fork failed".
|
|
30
|
+
// Recording it lets the worker report it DETERMINISTICALLY, named to this test,
|
|
31
|
+
// on every runner — turning an intermittent CI flake into a reproducible bug.
|
|
32
|
+
var _lateError = null;
|
|
33
|
+
function _recordLate(kind, err) { if (!_lateError) _lateError = { kind: kind, err: err }; }
|
|
34
|
+
process.on("unhandledRejection", function (reason) { _recordLate("unhandledRejection", reason); });
|
|
35
|
+
process.on("uncaughtException", function (err) { _recordLate("uncaughtException", err); });
|
|
36
|
+
|
|
37
|
+
// Leaked-handle audit. A test that schedules async work it never cancels — a
|
|
38
|
+
// retry timer, a shutdown wait, a watcher/reload restart, an unclosed server /
|
|
39
|
+
// socket / db connection / worker thread — leaves a handle that keeps the
|
|
40
|
+
// event loop alive. process.exit(0) hides it locally (it force-exits), but the
|
|
41
|
+
// lingering handle is what delays a slow CI runner and is the same root that,
|
|
42
|
+
// when its callback later throws, becomes a late "fork failed". Snapshot the
|
|
43
|
+
// handles the module holds BEFORE run() (load-time handles are legitimate),
|
|
44
|
+
// then diff AFTER run() so only handles created-and-not-released during the
|
|
45
|
+
// test are reported. Report-only (a leak doesn't fail the file) so the sweep
|
|
46
|
+
// surfaces the population without a flag-day; Immediate/TickObject are the
|
|
47
|
+
// worker's own exit scheduling and are ignored.
|
|
48
|
+
function _handleCounts() {
|
|
49
|
+
if (typeof process.getActiveResourcesInfo !== "function") return null;
|
|
50
|
+
var c = {};
|
|
51
|
+
process.getActiveResourcesInfo().forEach(function (t) { c[t] = (c[t] || 0) + 1; });
|
|
52
|
+
return c;
|
|
53
|
+
}
|
|
54
|
+
// Infrastructure resource types that are NOT leaks: the worker's own exit
|
|
55
|
+
// scheduling (Immediate / TickObject) and the stdio + IPC pipes (PipeWrap /
|
|
56
|
+
// TTYWrap), which only register as "active" once written to — a test writing
|
|
57
|
+
// its own "OK" line activates stdout AFTER the baseline snapshot, so they would
|
|
58
|
+
// otherwise show as a phantom +1. The leaks we care about keep the event loop
|
|
59
|
+
// alive across the grace tick: Timeout, TCP*/Server, FSEvent/StatWatcher,
|
|
60
|
+
// MessagePort (un-terminated Worker), TLSWrap, FSReqCallback, etc.
|
|
61
|
+
var _HANDLE_IGNORE = { Immediate: 1, TickObject: 1, PipeWrap: 1, TTYWrap: 1 };
|
|
62
|
+
function _leakedHandles(baseline) {
|
|
63
|
+
var now = _handleCounts();
|
|
64
|
+
if (!now || !baseline) return [];
|
|
65
|
+
var out = [];
|
|
66
|
+
Object.keys(now).forEach(function (t) {
|
|
67
|
+
if (_HANDLE_IGNORE[t]) return;
|
|
68
|
+
var delta = now[t] - (baseline[t] || 0);
|
|
69
|
+
if (delta > 0) out.push(t + (delta > 1 ? " x" + delta : ""));
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
var _baselineHandles = null;
|
|
74
|
+
|
|
23
75
|
(async function () {
|
|
24
76
|
var helpers, mod;
|
|
25
77
|
try {
|
|
@@ -34,6 +86,9 @@ if (!modulePath) {
|
|
|
34
86
|
process.exit(1);
|
|
35
87
|
}
|
|
36
88
|
try {
|
|
89
|
+
// Snapshot load-time handles so the post-run diff only reports handles the
|
|
90
|
+
// TEST created and never released (not ones the module legitimately holds).
|
|
91
|
+
_baselineHandles = _handleCounts();
|
|
37
92
|
if (typeof mod.run === "function") await mod.run();
|
|
38
93
|
if (Array.isArray(mod.groups) && mod.groups.length > 0) {
|
|
39
94
|
for (var i = 0; i < mod.groups.length; i++) {
|
|
@@ -52,9 +107,35 @@ if (!modulePath) {
|
|
|
52
107
|
}
|
|
53
108
|
}
|
|
54
109
|
}
|
|
110
|
+
// Let fire-and-forget microtasks / immediates settle so a late rejection or
|
|
111
|
+
// teardown throw surfaces HERE, attributed to this test, instead of racing
|
|
112
|
+
// process.exit(0) and reading as an unattributable "fork failed" on a slow
|
|
113
|
+
// runner. Registering the handlers above also suppresses Node's default
|
|
114
|
+
// exit-on-unhandled-rejection, so the worker — not a race — decides.
|
|
115
|
+
await new Promise(function (r) { setImmediate(r); });
|
|
116
|
+
await new Promise(function (r) { setImmediate(r); });
|
|
117
|
+
if (_lateError) {
|
|
118
|
+
var le = _lateError.err;
|
|
119
|
+
process.stdout.write("\n" + JSON.stringify({
|
|
120
|
+
ok: false,
|
|
121
|
+
checks: helpers.getChecks(),
|
|
122
|
+
// lateError flags this as a process-level transient (a fire-and-forget
|
|
123
|
+
// promise / unreleased handle that threw after the assertions passed),
|
|
124
|
+
// so the parent retries it ONCE — a transient passes on retry, a
|
|
125
|
+
// persistent one fails again and surfaces a real, reproducible bug.
|
|
126
|
+
// Either way the file is now NAMED, not an unattributable "fork failed".
|
|
127
|
+
lateError: true,
|
|
128
|
+
error: "assertions passed but a late " + _lateError.kind + " fired after run() — a " +
|
|
129
|
+
"fire-and-forget promise or unreleased handle (retry timer, shutdown wait, " +
|
|
130
|
+
"watcher/reload restart): " + ((le && le.message) || String(le)),
|
|
131
|
+
stack: le && le.stack,
|
|
132
|
+
}));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
55
135
|
process.stdout.write("\n" + JSON.stringify({
|
|
56
136
|
ok: true,
|
|
57
137
|
checks: helpers.getChecks(),
|
|
138
|
+
leaks: _leakedHandles(_baselineHandles),
|
|
58
139
|
}));
|
|
59
140
|
process.exit(0);
|
|
60
141
|
} catch (err) {
|
|
@@ -605,7 +605,10 @@ async function run() {
|
|
|
605
605
|
check("OIDC Native SSO: deferred (Keycloak's device_secret support is preview-only)", true);
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
run
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
);
|
|
608
|
+
module.exports = { run: run };
|
|
609
|
+
if (require.main === module) {
|
|
610
|
+
run().then(
|
|
611
|
+
function () { console.log("[federation-auth] OK"); },
|
|
612
|
+
function (e) { console.error("[federation-auth] FAIL:", e.stack || e); process.exit(1); }
|
|
613
|
+
);
|
|
614
|
+
}
|
|
@@ -314,7 +314,10 @@ async function run() {
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
run
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
);
|
|
317
|
+
module.exports = { run: run };
|
|
318
|
+
if (require.main === module) {
|
|
319
|
+
run().then(
|
|
320
|
+
function () { console.log("[mail-crypto-smime] OK"); },
|
|
321
|
+
function (e) { console.error("[mail-crypto-smime] FAIL:", e.stack || e); process.exit(1); }
|
|
322
|
+
);
|
|
323
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
var helpers = require("../helpers");
|
|
8
|
+
var nodeCrypto = require("node:crypto");
|
|
8
9
|
var b = helpers.b;
|
|
9
10
|
var check = helpers.check;
|
|
10
11
|
|
|
@@ -12,6 +13,8 @@ async function run() {
|
|
|
12
13
|
check("backupManifest.sign is fn", typeof b.backupManifest.sign === "function");
|
|
13
14
|
check("backupManifest.verifySignature is fn", typeof b.backupManifest.verifySignature === "function");
|
|
14
15
|
check("backupManifest.signingPayload is fn", typeof b.backupManifest.signingPayload === "function");
|
|
16
|
+
check("backupManifest.signBytes is fn", typeof b.backupManifest.signBytes === "function");
|
|
17
|
+
check("backupManifest.verifyBytes is fn", typeof b.backupManifest.verifyBytes === "function");
|
|
15
18
|
check("backupBundle.verifyManifestSignature is fn",
|
|
16
19
|
typeof b.backup.verifyManifestSignature === "function");
|
|
17
20
|
|
|
@@ -89,6 +92,43 @@ async function run() {
|
|
|
89
92
|
var viaWrapper = b.backup.verifyManifestSignature({ manifest: reparsed });
|
|
90
93
|
check("backup.verifyManifestSignature accepts parsed manifest", viaWrapper.ok === true);
|
|
91
94
|
|
|
95
|
+
// #351 — schema-agnostic signBytes/verifyBytes: a consumer with a bespoke
|
|
96
|
+
// (non-manifest) format signs its OWN canonical bytes with the same keypair +
|
|
97
|
+
// fingerprint pinning, without adopting the v1 manifest schema.
|
|
98
|
+
var customBytes = Buffer.from("custom-header\x00v=2\x00payload=" + "ab".repeat(40), "utf8");
|
|
99
|
+
var sigBlock = b.backupManifest.signBytes(customBytes);
|
|
100
|
+
check("signBytes returns a detached signature block",
|
|
101
|
+
sigBlock && typeof sigBlock.algorithm === "string" &&
|
|
102
|
+
sigBlock.publicKey.indexOf("-----BEGIN") === 0 &&
|
|
103
|
+
typeof sigBlock.fingerprint === "string" &&
|
|
104
|
+
typeof sigBlock.value === "string" && sigBlock.value.length > 0);
|
|
105
|
+
|
|
106
|
+
var okBytes = b.backupManifest.verifyBytes(customBytes, sigBlock);
|
|
107
|
+
check("verifyBytes on the exact bytes → ok", okBytes.ok === true && okBytes.fingerprint === sigBlock.fingerprint);
|
|
108
|
+
|
|
109
|
+
// A string with the SAME bytes verifies too (UTF-8 normalization parity).
|
|
110
|
+
var okStr = b.backupManifest.verifyBytes(customBytes.toString("utf8"), sigBlock);
|
|
111
|
+
check("verifyBytes accepts an equivalent string payload", okStr.ok === true);
|
|
112
|
+
|
|
113
|
+
// Tamper one byte → verification fails.
|
|
114
|
+
var tamperedBytes = Buffer.from(customBytes); tamperedBytes[0] = tamperedBytes[0] ^ 0x01;
|
|
115
|
+
var badBytes = b.backupManifest.verifyBytes(tamperedBytes, sigBlock);
|
|
116
|
+
check("verifyBytes on tampered bytes → ok=false", badBytes.ok === false);
|
|
117
|
+
|
|
118
|
+
// Fingerprint pinning: a wrong expected fingerprint refuses even valid bytes.
|
|
119
|
+
var pinned = b.backupManifest.verifyBytes(customBytes, sigBlock, { expectedFingerprint: sigBlock.fingerprint });
|
|
120
|
+
check("verifyBytes with the correct pinned fingerprint → ok", pinned.ok === true);
|
|
121
|
+
var pinnedBad = b.backupManifest.verifyBytes(customBytes, sigBlock, { expectedFingerprint: "deadbeef" });
|
|
122
|
+
check("verifyBytes with a wrong pinned fingerprint → ok=false", pinnedBad.ok === false);
|
|
123
|
+
|
|
124
|
+
// A malformed signature block is refused, not thrown.
|
|
125
|
+
var noBlock = b.backupManifest.verifyBytes(customBytes, { algorithm: "x" });
|
|
126
|
+
check("verifyBytes with an incomplete block → ok=false", noBlock.ok === false);
|
|
127
|
+
// signBytes rejects a non-string/Buffer payload at the boundary.
|
|
128
|
+
var threwIn = null;
|
|
129
|
+
try { b.backupManifest.signBytes({ not: "bytes" }); } catch (e) { threwIn = e; }
|
|
130
|
+
check("signBytes rejects a non-bytes payload", threwIn && threwIn.code === "backup-manifest/bad-input");
|
|
131
|
+
|
|
92
132
|
// Manifest without signature returns ok:false (no signature to verify)
|
|
93
133
|
var unsigned = b.backupManifest.create({
|
|
94
134
|
vaultKeySalt: "0011",
|
|
@@ -100,6 +140,57 @@ async function run() {
|
|
|
100
140
|
});
|
|
101
141
|
var noSig = b.backupManifest.verifySignature(unsigned);
|
|
102
142
|
check("verifySignature on unsigned manifest → ok=false", noSig.ok === false);
|
|
143
|
+
|
|
144
|
+
// Fingerprint-pinning substitution attack: an attacker signs arbitrary bytes
|
|
145
|
+
// with their OWN key, then sets the block's self-asserted `fingerprint` field
|
|
146
|
+
// to the trusted value. The pin MUST be checked against the fingerprint
|
|
147
|
+
// recomputed from the block's publicKey (the key the signature verifies
|
|
148
|
+
// under), not the attacker-controlled `fingerprint` string, or the forged
|
|
149
|
+
// block passes pinning.
|
|
150
|
+
var trustedFp = b.auditSign.getPublicKeyFingerprint();
|
|
151
|
+
var atkBytes = Buffer.from("attacker-controlled-payload", "utf8");
|
|
152
|
+
var atk = nodeCrypto.generateKeyPairSync("ed25519");
|
|
153
|
+
var atkPubPem = atk.publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
154
|
+
var atkSig = nodeCrypto.sign(null, atkBytes, atk.privateKey);
|
|
155
|
+
var forged = {
|
|
156
|
+
algorithm: "ed25519", publicKey: atkPubPem,
|
|
157
|
+
fingerprint: trustedFp, // the lie
|
|
158
|
+
value: atkSig.toString("base64"), signedAt: new Date(0).toISOString(),
|
|
159
|
+
};
|
|
160
|
+
// The forged signature genuinely verifies under the attacker's own key.
|
|
161
|
+
check("forged signature verifies under the attacker key (precondition)",
|
|
162
|
+
nodeCrypto.verify(null, atkBytes, atkPubPem, atkSig) === true);
|
|
163
|
+
// ...but pinning to the trusted fingerprint MUST reject it.
|
|
164
|
+
var forgedRes = b.backupManifest.verifyBytes(atkBytes, forged, { expectedFingerprint: trustedFp });
|
|
165
|
+
check("verifyBytes rejects a forged-fingerprint block under pinning", forgedRes.ok === false);
|
|
166
|
+
check("verifyBytes forged-rejection cites the fingerprint mismatch",
|
|
167
|
+
/does not match expectedFingerprint/.test(forgedRes.reason || ""));
|
|
168
|
+
// verifySignature (manifest path) shares the fix — same substitution refused.
|
|
169
|
+
var forgedManifest = JSON.parse(JSON.stringify(fixture));
|
|
170
|
+
forgedManifest.signature = forged;
|
|
171
|
+
var forgedMRes = b.backupManifest.verifySignature(forgedManifest, { expectedFingerprint: trustedFp });
|
|
172
|
+
check("verifySignature rejects a forged-fingerprint manifest under pinning", forgedMRes.ok === false);
|
|
173
|
+
|
|
174
|
+
// b.auditSign.fingerprintOf recomputes the fingerprint from a PEM, no init.
|
|
175
|
+
check("auditSign.fingerprintOf is a function", typeof b.auditSign.fingerprintOf === "function");
|
|
176
|
+
check("auditSign.fingerprintOf(active pubkey) == active fingerprint",
|
|
177
|
+
b.auditSign.fingerprintOf(b.auditSign.getPublicKey()) === trustedFp);
|
|
178
|
+
|
|
179
|
+
// Verifier-only path: a process that never ran auditSign.init() must still be
|
|
180
|
+
// able to verify a detached block (it holds only a trusted public key). Reset
|
|
181
|
+
// LAST so earlier checks keep their initialized signer.
|
|
182
|
+
var honestBytes = Buffer.from("downstream-verifier-payload", "utf8");
|
|
183
|
+
var honestBlock = b.backupManifest.signBytes(honestBytes);
|
|
184
|
+
b.auditSign._resetForTest();
|
|
185
|
+
var reThrew = null;
|
|
186
|
+
try { b.auditSign.getPublicKey(); } catch (e) { reThrew = e; }
|
|
187
|
+
check("verifier-only precondition: audit-sign is uninitialized", reThrew !== null);
|
|
188
|
+
check("verifyBytes works in a verifier-only process (no init)",
|
|
189
|
+
b.backupManifest.verifyBytes(honestBytes, honestBlock).ok === true);
|
|
190
|
+
check("verifyBytes pinned works in a verifier-only process",
|
|
191
|
+
b.backupManifest.verifyBytes(honestBytes, honestBlock, { expectedFingerprint: trustedFp }).ok === true);
|
|
192
|
+
check("verifyBytes still rejects tampered bytes in a verifier-only process",
|
|
193
|
+
b.backupManifest.verifyBytes(Buffer.from("tampered"), honestBlock).ok === false);
|
|
103
194
|
}
|
|
104
195
|
|
|
105
196
|
module.exports = { run: run };
|