@blamejs/core 0.9.12 → 0.9.15
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 +3 -0
- package/lib/a2a.js +11 -11
- package/lib/acme.js +5 -5
- package/lib/ai-input.js +2 -2
- package/lib/api-key.js +4 -4
- package/lib/api-snapshot.js +10 -7
- package/lib/app-shutdown.js +2 -2
- package/lib/app.js +5 -5
- package/lib/archive.js +8 -8
- package/lib/argon2-builtin.js +2 -2
- package/lib/atomic-file.js +53 -53
- package/lib/audit-sign.js +8 -8
- package/lib/audit-tools.js +22 -22
- package/lib/audit.js +29 -17
- package/lib/auth/dpop.js +3 -3
- package/lib/auth/sd-jwt-vc.js +2 -2
- package/lib/backup/bundle.js +17 -17
- package/lib/backup/index.js +36 -36
- package/lib/budr.js +3 -3
- package/lib/bundler.js +20 -20
- package/lib/circuit-breaker.js +24 -9
- package/lib/cli.js +25 -26
- package/lib/cluster.js +2 -2
- package/lib/compliance-sanctions.js +2 -2
- package/lib/config-drift.js +15 -15
- package/lib/content-credentials.js +4 -4
- package/lib/credential-hash.js +3 -3
- package/lib/crypto.js +145 -0
- package/lib/daemon.js +19 -19
- package/lib/db-file-lifecycle.js +24 -24
- package/lib/db-schema.js +2 -2
- package/lib/db.js +35 -35
- package/lib/dev.js +10 -10
- package/lib/dr-runbook.js +5 -5
- package/lib/dsr.js +22 -15
- package/lib/dual-control.js +2 -2
- package/lib/external-db-migrate.js +2 -2
- package/lib/external-db.js +2 -2
- package/lib/fdx.js +2 -2
- package/lib/file-upload.js +30 -30
- package/lib/flag-providers.js +4 -4
- package/lib/gate-contract.js +5 -5
- package/lib/graphql-federation.js +4 -7
- package/lib/honeytoken.js +6 -6
- package/lib/http-client-cookie-jar.js +6 -6
- package/lib/http-client.js +18 -18
- package/lib/i18n.js +5 -5
- package/lib/inbox.js +21 -15
- package/lib/keychain.js +9 -9
- package/lib/legal-hold.js +2 -2
- package/lib/local-db-thin.js +9 -9
- package/lib/log-stream-local.js +17 -17
- package/lib/log-stream-syslog.js +2 -2
- package/lib/log-stream.js +3 -3
- package/lib/mail-bounce.js +2 -2
- package/lib/mail-mdn.js +2 -2
- package/lib/mail-srs.js +2 -2
- package/lib/mail.js +4 -4
- package/lib/mcp.js +2 -2
- package/lib/metrics.js +249 -2
- package/lib/middleware/api-encrypt.js +16 -16
- package/lib/middleware/body-parser.js +16 -16
- package/lib/middleware/compression.js +3 -3
- package/lib/middleware/csp-nonce.js +4 -4
- package/lib/middleware/health.js +7 -7
- package/lib/middleware/idempotency-key.js +250 -0
- package/lib/migrations.js +3 -3
- package/lib/mtls-ca.js +26 -26
- package/lib/mtls-engine-default.js +5 -5
- package/lib/network-dns.js +2 -2
- package/lib/network-nts.js +2 -2
- package/lib/network-proxy.js +3 -3
- package/lib/network-smtp-policy.js +2 -2
- package/lib/network-tls.js +17 -17
- package/lib/network.js +13 -13
- package/lib/notify.js +3 -3
- package/lib/object-store/gcs-bucket-ops.js +2 -2
- package/lib/object-store/gcs.js +5 -5
- package/lib/object-store/index.js +6 -6
- package/lib/object-store/local.js +19 -19
- package/lib/object-store/sigv4.js +3 -3
- package/lib/observability-tracer.js +4 -4
- package/lib/otel-export.js +3 -3
- package/lib/pagination.js +5 -5
- package/lib/parsers/safe-xml.js +3 -3
- package/lib/pqc-agent.js +116 -26
- package/lib/pqc-gate.js +5 -5
- package/lib/pubsub-redis.js +2 -2
- package/lib/queue-local.js +3 -3
- package/lib/queue.js +2 -2
- package/lib/redis-client.js +4 -4
- package/lib/restore-bundle.js +18 -18
- package/lib/restore-rollback.js +34 -34
- package/lib/restore.js +16 -16
- package/lib/retry.js +50 -0
- package/lib/router.js +13 -13
- package/lib/sandbox.js +8 -8
- package/lib/sec-cyber.js +3 -3
- package/lib/security-assert.js +2 -2
- package/lib/seeders.js +4 -4
- package/lib/self-update-standalone-verifier.js +280 -0
- package/lib/self-update.js +32 -26
- package/lib/session-device-binding.js +2 -2
- package/lib/static.js +22 -22
- package/lib/template.js +19 -19
- package/lib/testing.js +7 -7
- package/lib/tls-exporter.js +5 -5
- package/lib/tracing.js +3 -3
- package/lib/vault/index.js +11 -11
- package/lib/vault/passphrase-ops.js +37 -37
- package/lib/vault/passphrase-source.js +2 -2
- package/lib/vault/rotate.js +70 -66
- package/lib/vault/seal-pem-file.js +26 -26
- package/lib/watcher.js +23 -23
- package/lib/webhook.js +10 -10
- package/lib/worker-pool.js +6 -6
- package/lib/ws-client.js +4 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/sec-cyber.js
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
var audit = require("./audit");
|
|
70
70
|
var C = require("./constants");
|
|
71
71
|
var validateOpts = require("./validate-opts");
|
|
72
|
-
var
|
|
72
|
+
var numericBounds = require("./numeric-bounds");
|
|
73
73
|
var { defineClass } = require("./framework-error");
|
|
74
74
|
var SecCyberError = defineClass("SecCyberError", { alwaysPermanent: true });
|
|
75
75
|
|
|
@@ -104,9 +104,9 @@ function eightKArtifact(opts) {
|
|
|
104
104
|
"secCyber.eightKArtifact: registrant.name", SecCyberError, "BAD_REGISTRANT_NAME");
|
|
105
105
|
validateOpts.requireNonEmptyString(opts.registrant.cik,
|
|
106
106
|
"secCyber.eightKArtifact: registrant.cik", SecCyberError, "BAD_CIK");
|
|
107
|
-
|
|
107
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.detectedAt,
|
|
108
108
|
"secCyber.eightKArtifact: detectedAt", SecCyberError, "BAD_DETECTED_AT");
|
|
109
|
-
|
|
109
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.materialityDeterminedAt,
|
|
110
110
|
"secCyber.eightKArtifact: materialityDeterminedAt", SecCyberError, "BAD_MAT_AT");
|
|
111
111
|
|
|
112
112
|
if (FINDINGS.indexOf(opts.materialityFinding) === -1) {
|
package/lib/security-assert.js
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
* non-function extra entry, etc.) so the operator catches typos at
|
|
68
68
|
* boot, not at the moment they were trying to gate the boot.
|
|
69
69
|
*/
|
|
70
|
-
var
|
|
70
|
+
var nodeFs = require("fs");
|
|
71
71
|
var nodeTls = require("node:tls");
|
|
72
72
|
var lazyRequire = require("./lazy-require");
|
|
73
73
|
var safeEnv = require("./parsers/safe-env");
|
|
@@ -286,7 +286,7 @@ async function assertProduction(opts) {
|
|
|
286
286
|
if (typeof opts.dataDir === "string" && opts.dataDir.length > 0 && process.platform !== "win32") {
|
|
287
287
|
var maxMode = typeof opts.maxDataDirMode === "number" ? opts.maxDataDirMode : 0o750;
|
|
288
288
|
try {
|
|
289
|
-
var stat =
|
|
289
|
+
var stat = nodeFs.statSync(opts.dataDir);
|
|
290
290
|
var mode = stat.mode & 0o777;
|
|
291
291
|
if (mode > maxMode) {
|
|
292
292
|
failures.push({ ok: false, code: "security/datadir-permissions",
|
package/lib/seeders.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* module.exports = {
|
|
17
17
|
* description: "Create default admin user for local dev",
|
|
18
|
-
* // Optional — when omitted, the env is inferred from the
|
|
18
|
+
* // Optional — when omitted, the env is inferred from the nodePath.
|
|
19
19
|
* // When present, this seed only applies under one of these envs.
|
|
20
20
|
* envs: ["dev", "test"],
|
|
21
21
|
* // Default false — applied once and recorded in registry.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* applied state)
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
var
|
|
57
|
+
var nodePath = require("path");
|
|
58
58
|
var atomicFile = require("./atomic-file");
|
|
59
59
|
var C = require("./constants");
|
|
60
60
|
var dbSchema = require("./db-schema");
|
|
@@ -161,7 +161,7 @@ function _resolveDb(opts) {
|
|
|
161
161
|
// ---- Directory walking + seed loading ----
|
|
162
162
|
|
|
163
163
|
function _envDir(rootDir, env) {
|
|
164
|
-
return
|
|
164
|
+
return nodePath.join(rootDir, env);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function _listSeedFiles(rootDir, env) {
|
|
@@ -171,7 +171,7 @@ function _listSeedFiles(rootDir, env) {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
function _loadSeed(rootDir, env, file) {
|
|
174
|
-
var fullPath =
|
|
174
|
+
var fullPath = nodePath.join(_envDir(rootDir, env), file);
|
|
175
175
|
// Drop require cache for this path so a test rewriting a fixture
|
|
176
176
|
// between calls picks it up. Production restarts the process anyway.
|
|
177
177
|
try { delete require.cache[require.resolve(fullPath)]; } catch (_e) { /* not yet cached */ }
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.selfUpdate.standaloneVerifier
|
|
4
|
+
* @nav Production
|
|
5
|
+
* @title Self-Update Standalone Verifier
|
|
6
|
+
* @order 640
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Zero-dep companion to `b.selfUpdate.verify` for install-pipeline
|
|
10
|
+
* contexts that run BEFORE the framework itself is installed —
|
|
11
|
+
* Dockerfile build stages, `install.sh`, `update.sh`, SEA-bundle
|
|
12
|
+
* verification at deploy time. The full `b.selfUpdate.verify`
|
|
13
|
+
* chain reaches into `b.crypto`, `b.httpClient`, `b.audit`, vendor
|
|
14
|
+
* imports, etc.; none of those exist yet when an operator's
|
|
15
|
+
* install script runs `node verify-release.js` against the
|
|
16
|
+
* downloaded artifact.
|
|
17
|
+
*
|
|
18
|
+
* This module is intentionally hermetic — `node:crypto` + `node:fs`
|
|
19
|
+
* only, no framework imports, no third-party modules. Operators
|
|
20
|
+
* physically copy the file into their install pipeline alongside a
|
|
21
|
+
* public-key module they own. Both go into version control on the
|
|
22
|
+
* operator's side; neither updates without their explicit action.
|
|
23
|
+
*
|
|
24
|
+
* Surface (single function):
|
|
25
|
+
*
|
|
26
|
+
* verify(assetPath, signaturePath, pubkeyPem, opts?) → {
|
|
27
|
+
* ok: boolean,
|
|
28
|
+
* sha3_512: string, // hex digest of asset bytes (SBOM correlation)
|
|
29
|
+
* sha256: string, // hex digest of asset bytes (defense-in-depth)
|
|
30
|
+
* alg: string, // detected algorithm: "ecdsa-p384" | "ed25519" | "ml-dsa-87"
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* The function refuses to load the asset into memory in one go;
|
|
34
|
+
* it streams the bytes through both hashers + the signature
|
|
35
|
+
* verifier so multi-GB SEA bundles don't OOM the install runner.
|
|
36
|
+
*
|
|
37
|
+
* Throws on:
|
|
38
|
+
* - missing asset / signature / pubkey file
|
|
39
|
+
* - unrecognized pubkey PEM shape
|
|
40
|
+
* - signature length mismatch with the algorithm
|
|
41
|
+
* - cryptographic verify failure
|
|
42
|
+
*
|
|
43
|
+
* Per the operator's request that surfaced this primitive
|
|
44
|
+
* (hermitstash-sync 2026-05-13): the install pipeline needs P-384
|
|
45
|
+
* ECDSA + SHA3-512 as the baseline cross-check. ML-DSA-87 is also
|
|
46
|
+
* supported when the operator's pubkey carries the corresponding
|
|
47
|
+
* OID (Node 22+ via the FIPS 204 OIDs in node:crypto).
|
|
48
|
+
*
|
|
49
|
+
* ## How operators consume this
|
|
50
|
+
*
|
|
51
|
+
* ```sh
|
|
52
|
+
* # one-time copy at framework-install time:
|
|
53
|
+
* cp "$(node -p "require('@blamejs/core').selfUpdate.standaloneVerifier.path")" \
|
|
54
|
+
* install/standalone-verifier.js
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* ```js
|
|
58
|
+
* // install/verify-release.js (operator-owned, in their repo):
|
|
59
|
+
* var verifier = require("./standalone-verifier");
|
|
60
|
+
* var pubkey = require("./release-pubkey"); // operator-owned PEM
|
|
61
|
+
*
|
|
62
|
+
* var result = verifier.verify(
|
|
63
|
+
* "/tmp/blamejs-sea-bundle",
|
|
64
|
+
* "/tmp/blamejs-sea-bundle.sig",
|
|
65
|
+
* pubkey,
|
|
66
|
+
* );
|
|
67
|
+
* if (!result.ok) {
|
|
68
|
+
* process.stderr.write("release verification FAILED\n");
|
|
69
|
+
* process.exit(1);
|
|
70
|
+
* }
|
|
71
|
+
* process.stdout.write("verified " + result.alg + " sha3-512=" + result.sha3_512 + "\n");
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* The module is also reachable as `b.selfUpdate.standaloneVerifier.verify`
|
|
75
|
+
* from inside a fully-installed framework process — useful for tests
|
|
76
|
+
* that exercise the same code path the operator's install pipeline
|
|
77
|
+
* does, without forking a subprocess.
|
|
78
|
+
*
|
|
79
|
+
* @card
|
|
80
|
+
* Zero-dep verifier for use BEFORE the framework is installed.
|
|
81
|
+
* Install-pipeline scripts copy this file alongside an operator-owned
|
|
82
|
+
* pubkey to verify signed release artifacts during Dockerfile build
|
|
83
|
+
* or systemd `install.sh`. node:crypto + node:fs only.
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
var nodeCrypto = require("crypto");
|
|
87
|
+
var nodeFs = require("fs");
|
|
88
|
+
|
|
89
|
+
// _streamHashAndVerify — read the asset in 64 KiB chunks, feed each
|
|
90
|
+
// chunk into sha256, sha3-512, AND the signature verifier in parallel.
|
|
91
|
+
// Single pass over the file; no in-memory copy. node:crypto's
|
|
92
|
+
// `createVerify` consumes streaming input via `.update()` for ECDSA +
|
|
93
|
+
// EdDSA; ML-DSA's `crypto.verify` requires the full payload, so we
|
|
94
|
+
// also accumulate to a buffer ONLY when the alg requires it.
|
|
95
|
+
function _detectAlg(pubkeyPem) {
|
|
96
|
+
// Inspect the PEM header / SPKI for a recognizable curve / OID. The
|
|
97
|
+
// pubkey PEM carries the algorithm identifier in the SPKI ASN.1; we
|
|
98
|
+
// load it via createPublicKey() and read `asymmetricKeyType` +
|
|
99
|
+
// `asymmetricKeyDetails.namedCurve`.
|
|
100
|
+
var key;
|
|
101
|
+
try {
|
|
102
|
+
key = nodeCrypto.createPublicKey(pubkeyPem);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
throw new Error("standalone-verifier: pubkey PEM did not parse: " +
|
|
105
|
+
(e && e.message ? e.message : String(e)));
|
|
106
|
+
}
|
|
107
|
+
var t = key.asymmetricKeyType;
|
|
108
|
+
if (t === "ec") {
|
|
109
|
+
var curve = key.asymmetricKeyDetails && key.asymmetricKeyDetails.namedCurve;
|
|
110
|
+
if (curve === "P-384" || curve === "secp384r1") return { alg: "ecdsa-p384", key: key };
|
|
111
|
+
throw new Error("standalone-verifier: unsupported EC curve '" + curve + "' (need P-384)");
|
|
112
|
+
}
|
|
113
|
+
if (t === "ed25519") return { alg: "ed25519", key: key };
|
|
114
|
+
if (t === "ml-dsa-87" || t === "ml-dsa") return { alg: "ml-dsa-87", key: key };
|
|
115
|
+
throw new Error("standalone-verifier: unrecognized pubkey type '" + t + "' " +
|
|
116
|
+
"(need ecdsa-p384, ed25519, or ml-dsa-87)");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @primitive b.selfUpdate.standaloneVerifier.verify
|
|
121
|
+
* @signature b.selfUpdate.standaloneVerifier.verify(assetPath, signaturePath, pubkeyPem)
|
|
122
|
+
* @since 0.9.13
|
|
123
|
+
* @status stable
|
|
124
|
+
* @related b.selfUpdate.verify
|
|
125
|
+
*
|
|
126
|
+
* Verify a signed release asset using only `node:crypto` + `node:fs`
|
|
127
|
+
* (no framework imports). For install-pipeline contexts where the
|
|
128
|
+
* framework itself is not yet installed.
|
|
129
|
+
*
|
|
130
|
+
* Streams the asset in 64 KiB chunks through SHA-256 + SHA-3-512 + the
|
|
131
|
+
* signature verifier in parallel — single allocation peak (one buffer
|
|
132
|
+
* sized to fstat(asset).size for Ed25519 / ML-DSA-87, ECDSA P-384 needs
|
|
133
|
+
* no buffer because createVerify is incremental).
|
|
134
|
+
*
|
|
135
|
+
* Returns `{ ok, sha3_512, sha256, alg }` on success; throws on
|
|
136
|
+
* unrecognized pubkey shape, missing files, or signature mismatch.
|
|
137
|
+
* `alg` is one of `"ecdsa-p384"`, `"ed25519"`, `"ml-dsa-87"` (auto-
|
|
138
|
+
* detected from the pubkey PEM).
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* var verifier = require("./standalone-verifier");
|
|
142
|
+
* var pubkey = require("./release-pubkey");
|
|
143
|
+
* var result = verifier.verify(
|
|
144
|
+
* "/tmp/blamejs-sea-bundle",
|
|
145
|
+
* "/tmp/blamejs-sea-bundle.sig",
|
|
146
|
+
* pubkey,
|
|
147
|
+
* );
|
|
148
|
+
* if (!result.ok) process.exit(1);
|
|
149
|
+
* process.stdout.write("verified " + result.alg + " sha3-512=" + result.sha3_512 + "\n");
|
|
150
|
+
*/
|
|
151
|
+
function verify(assetPath, signaturePath, pubkeyPem) {
|
|
152
|
+
if (typeof assetPath !== "string" || assetPath.length === 0) {
|
|
153
|
+
throw new Error("standalone-verifier.verify: assetPath must be a non-empty string");
|
|
154
|
+
}
|
|
155
|
+
if (typeof signaturePath !== "string" || signaturePath.length === 0) {
|
|
156
|
+
throw new Error("standalone-verifier.verify: signaturePath must be a non-empty string");
|
|
157
|
+
}
|
|
158
|
+
if (typeof pubkeyPem !== "string" || pubkeyPem.indexOf("-----BEGIN ") !== 0) {
|
|
159
|
+
throw new Error("standalone-verifier.verify: pubkeyPem must be a PEM-encoded public key string");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Open both files BEFORE parsing the pubkey so we own stable fds
|
|
163
|
+
// against TOCTOU races (CodeQL js/file-system-race) — checking
|
|
164
|
+
// existsSync before readFileSync leaves a swap window. Asset opens
|
|
165
|
+
// first so a missing-asset path surfaces before a missing-sig path.
|
|
166
|
+
var assetFd;
|
|
167
|
+
try {
|
|
168
|
+
assetFd = nodeFs.openSync(assetPath, "r");
|
|
169
|
+
} catch (e) {
|
|
170
|
+
throw new Error("standalone-verifier.verify: asset not found at " + assetPath +
|
|
171
|
+
" — " + (e && e.message ? e.message : String(e)));
|
|
172
|
+
}
|
|
173
|
+
var sigFd;
|
|
174
|
+
try {
|
|
175
|
+
sigFd = nodeFs.openSync(signaturePath, "r");
|
|
176
|
+
} catch (e) {
|
|
177
|
+
nodeFs.closeSync(assetFd);
|
|
178
|
+
throw new Error("standalone-verifier.verify: signature not found at " + signaturePath +
|
|
179
|
+
" — " + (e && e.message ? e.message : String(e)));
|
|
180
|
+
}
|
|
181
|
+
var signature;
|
|
182
|
+
try {
|
|
183
|
+
var sigStat = nodeFs.fstatSync(sigFd);
|
|
184
|
+
signature = Buffer.allocUnsafe(sigStat.size);
|
|
185
|
+
if (sigStat.size > 0) nodeFs.readSync(sigFd, signature, 0, sigStat.size, 0);
|
|
186
|
+
} finally {
|
|
187
|
+
nodeFs.closeSync(sigFd);
|
|
188
|
+
}
|
|
189
|
+
if (signature.length === 0) {
|
|
190
|
+
nodeFs.closeSync(assetFd);
|
|
191
|
+
throw new Error("standalone-verifier.verify: signature file is empty");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
var detected;
|
|
195
|
+
try {
|
|
196
|
+
detected = _detectAlg(pubkeyPem);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
nodeFs.closeSync(assetFd);
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
var alg = detected.alg;
|
|
202
|
+
var key = detected.key;
|
|
203
|
+
|
|
204
|
+
// Stream the asset through both hashers. For ECDSA we stream through
|
|
205
|
+
// createVerify (incremental). For Ed25519 / ML-DSA we pre-allocate
|
|
206
|
+
// ONE buffer of stat.size and stream-fill it at increasing offsets —
|
|
207
|
+
// single allocation peak, not the 2× peak that Buffer.concat([...chunks])
|
|
208
|
+
// produces. 64 KiB chunks match the framework's hash-while-streaming
|
|
209
|
+
// convention elsewhere.
|
|
210
|
+
var sha256 = nodeCrypto.createHash("sha256");
|
|
211
|
+
var sha3 = nodeCrypto.createHash("sha3-512");
|
|
212
|
+
var verifier = (alg === "ecdsa-p384") ? nodeCrypto.createVerify("sha3-512") : null;
|
|
213
|
+
var fullBuf = null;
|
|
214
|
+
var fullOff = 0;
|
|
215
|
+
if (verifier === null) {
|
|
216
|
+
var assetStat = nodeFs.fstatSync(assetFd);
|
|
217
|
+
fullBuf = Buffer.allocUnsafe(assetStat.size);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
var chunk = Buffer.allocUnsafe(64 * 1024); // allow:raw-byte-literal — module is zero-dep by contract; cannot import C.BYTES
|
|
222
|
+
while (true) {
|
|
223
|
+
var n = nodeFs.readSync(assetFd, chunk, 0, chunk.length, null);
|
|
224
|
+
if (n === 0) break;
|
|
225
|
+
var slice = chunk.subarray(0, n);
|
|
226
|
+
sha256.update(slice);
|
|
227
|
+
sha3.update(slice);
|
|
228
|
+
if (verifier) verifier.update(slice);
|
|
229
|
+
if (fullBuf) {
|
|
230
|
+
slice.copy(fullBuf, fullOff);
|
|
231
|
+
fullOff += n;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} finally {
|
|
235
|
+
nodeFs.closeSync(assetFd);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
var sha256Hex = sha256.digest("hex");
|
|
239
|
+
var sha3Hex = sha3.digest("hex");
|
|
240
|
+
|
|
241
|
+
var ok = false;
|
|
242
|
+
if (alg === "ecdsa-p384") {
|
|
243
|
+
// P-384 IEEE-P1363 sigs are exactly 96 bytes (48-byte r || 48-byte s).
|
|
244
|
+
// P-384 DER sigs are variable (~100-104 bytes — ASN.1 SEQUENCE
|
|
245
|
+
// wrapping two INTEGERs). Detect by length so we only call
|
|
246
|
+
// verifier.verify ONCE — calling it a second time after a failed
|
|
247
|
+
// verify returns stale state and silently passes tampered assets.
|
|
248
|
+
// 96 = P-384 IEEE-P1363 signature length; protocol constant, not a byte-size.
|
|
249
|
+
var dsaEncoding = signature.length === 96 ? "ieee-p1363" : "der"; // allow:raw-byte-literal — IEEE-P1363 P-384 signature length
|
|
250
|
+
ok = verifier.verify({ key: key, dsaEncoding: dsaEncoding }, signature);
|
|
251
|
+
} else if (alg === "ed25519") {
|
|
252
|
+
// fullBuf may be shorter than allocated (sparse files / size-races);
|
|
253
|
+
// slice to fullOff so verify sees only the bytes we actually read.
|
|
254
|
+
ok = nodeCrypto.verify(null, fullBuf.subarray(0, fullOff), key, signature);
|
|
255
|
+
} else if (alg === "ml-dsa-87") {
|
|
256
|
+
ok = nodeCrypto.verify(null, fullBuf.subarray(0, fullOff), key, signature);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!ok) {
|
|
260
|
+
throw new Error("standalone-verifier.verify: " + alg + " signature INVALID for " +
|
|
261
|
+
assetPath + " (sha3-512=" + sha3Hex.slice(0, 16) + "...). " + // allow:raw-byte-literal — 16-char hex prefix for forensic display, not bytes
|
|
262
|
+
"Either the asset was tampered with after signing, the signature " +
|
|
263
|
+
"doesn't match this asset, or the pubkey doesn't match the signing key.");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
sha3_512: sha3Hex,
|
|
269
|
+
sha256: sha256Hex,
|
|
270
|
+
alg: alg,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
verify: verify,
|
|
276
|
+
// Absolute path to this module file. Operators copy it via:
|
|
277
|
+
// cp "$(node -p "require('@blamejs/core').selfUpdate.standaloneVerifier.path")" \
|
|
278
|
+
// install/standalone-verifier.js
|
|
279
|
+
path: __filename,
|
|
280
|
+
};
|
package/lib/self-update.js
CHANGED
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
* Framework / vendored-deps integrity check plus version pinning — refuses to install a new build when the asset's detached signature does not verify against the operator-supplied public key, or when the vendored SHA the new build would ship does not match the manifest the opera...
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
-
var
|
|
51
|
-
var
|
|
50
|
+
var nodeFs = require("fs");
|
|
51
|
+
var nodePath = require("path");
|
|
52
52
|
var nodeCrypto = require("crypto");
|
|
53
|
-
var
|
|
53
|
+
var numericBounds = require("./numeric-bounds");
|
|
54
54
|
var atomicFile = require("./atomic-file");
|
|
55
55
|
var validateOpts = require("./validate-opts");
|
|
56
|
-
var
|
|
56
|
+
var bCrypto = require("./crypto");
|
|
57
57
|
var httpClient = require("./http-client");
|
|
58
58
|
var safeJson = require("./safe-json");
|
|
59
59
|
var { URL: NodeUrl } = require("url");
|
|
60
60
|
var lazyRequire = require("./lazy-require");
|
|
61
61
|
var C = require("./constants");
|
|
62
|
+
var standaloneVerifier = require("./self-update-standalone-verifier");
|
|
62
63
|
var { boot } = require("./log");
|
|
63
64
|
var { defineClass } = require("./framework-error");
|
|
64
65
|
|
|
@@ -152,9 +153,9 @@ function _validatePollOpts(opts) {
|
|
|
152
153
|
throw new SelfUpdateError("selfupdate/bad-sig-pattern",
|
|
153
154
|
"selfUpdate.poll: opts.signaturePattern must be a RegExp or string when present");
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
|
|
156
157
|
"selfUpdate.poll: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
157
|
-
|
|
158
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.timeoutMs,
|
|
158
159
|
"selfUpdate.poll: opts.timeoutMs", SelfUpdateError, "selfupdate/bad-timeout");
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -177,7 +178,7 @@ function _matchAsset(name, pattern, fallback) {
|
|
|
177
178
|
* Fetch a releases feed and report whether a newer tag is available.
|
|
178
179
|
* Tags are compared semver-style with a leading `v` stripped. When
|
|
179
180
|
* `opts.etag` is supplied an `If-None-Match` header makes a 304 a fast
|
|
180
|
-
* "no update"
|
|
181
|
+
* "no update" nodePath. The match against asset and signature URLs uses
|
|
181
182
|
* `opts.assetPattern` and `opts.signaturePattern` (RegExp or substring)
|
|
182
183
|
* with conservative fallbacks. Throws SelfUpdateError on a non-2xx
|
|
183
184
|
* upstream, malformed JSON, or unexpected shape.
|
|
@@ -366,7 +367,7 @@ function _validateVerifyOpts(opts) {
|
|
|
366
367
|
throw new SelfUpdateError("selfupdate/bad-hash-algo",
|
|
367
368
|
"selfUpdate.verify: opts.hashAlgo must be one of " + ALLOWED_HASH_ALGS.join(", "));
|
|
368
369
|
}
|
|
369
|
-
|
|
370
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
|
|
370
371
|
"selfUpdate.verify: opts.maxBytes", SelfUpdateError, "selfupdate/bad-max-bytes");
|
|
371
372
|
}
|
|
372
373
|
|
|
@@ -425,7 +426,7 @@ async function verify(opts) {
|
|
|
425
426
|
}
|
|
426
427
|
|
|
427
428
|
var ok = false;
|
|
428
|
-
try { ok =
|
|
429
|
+
try { ok = bCrypto.verify(assetBytes, sigBytes, opts.pubkeyPem); }
|
|
429
430
|
catch (e) {
|
|
430
431
|
_safeAuditEmit("selfupdate.verify.failed", "denied", {
|
|
431
432
|
assetPath: opts.assetPath, signaturePath: opts.signaturePath,
|
|
@@ -514,19 +515,19 @@ async function swap(opts) {
|
|
|
514
515
|
var to = opts.to;
|
|
515
516
|
var backupTo = opts.backupTo;
|
|
516
517
|
|
|
517
|
-
if (!
|
|
518
|
+
if (!nodeFs.existsSync(from)) {
|
|
518
519
|
throw new SelfUpdateError("selfupdate/missing-from",
|
|
519
520
|
"selfUpdate.swap: from path does not exist: " + from);
|
|
520
521
|
}
|
|
521
522
|
|
|
522
|
-
var toDir =
|
|
523
|
-
var backupDir =
|
|
523
|
+
var toDir = nodePath.dirname(to);
|
|
524
|
+
var backupDir = nodePath.dirname(backupTo);
|
|
524
525
|
atomicFile.ensureDir(toDir);
|
|
525
526
|
atomicFile.ensureDir(backupDir);
|
|
526
527
|
|
|
527
528
|
// Step 2 — backup if `to` exists. Use atomicFile.copy so the backup
|
|
528
529
|
// hits disk via temp+fsync+rename.
|
|
529
|
-
var hadOriginal =
|
|
530
|
+
var hadOriginal = nodeFs.existsSync(to);
|
|
530
531
|
if (hadOriginal) {
|
|
531
532
|
try {
|
|
532
533
|
await atomicFile.copy(to, backupTo, { fileMode: 0o600 });
|
|
@@ -540,14 +541,14 @@ async function swap(opts) {
|
|
|
540
541
|
// Step 3 — install. Rename is atomic on same FS; on cross-device we
|
|
541
542
|
// fall back to copy + unlink.
|
|
542
543
|
try {
|
|
543
|
-
|
|
544
|
+
nodeFs.renameSync(from, to);
|
|
544
545
|
} catch (e) {
|
|
545
546
|
if (e && e.code === "EXDEV") {
|
|
546
547
|
// Cross-device — copy + unlink. Use atomicFile.copy for the safety
|
|
547
548
|
// net (temp+fsync+rename on dest FS); then remove the source.
|
|
548
549
|
try {
|
|
549
550
|
await atomicFile.copy(from, to, { fileMode: 0o600 });
|
|
550
|
-
try {
|
|
551
|
+
try { nodeFs.unlinkSync(from); } catch (_u) { /* tmp source leak — operator-cleanable */ }
|
|
551
552
|
} catch (ce) {
|
|
552
553
|
// Roll back from backup if we have one.
|
|
553
554
|
if (hadOriginal) {
|
|
@@ -612,12 +613,12 @@ async function rollback(opts) {
|
|
|
612
613
|
var to = opts.to;
|
|
613
614
|
var backupTo = opts.backupTo;
|
|
614
615
|
|
|
615
|
-
if (!
|
|
616
|
+
if (!nodeFs.existsSync(backupTo)) {
|
|
616
617
|
throw new SelfUpdateError("selfupdate/missing-backup",
|
|
617
618
|
"selfUpdate.rollback: backupTo path does not exist: " + backupTo);
|
|
618
619
|
}
|
|
619
620
|
|
|
620
|
-
atomicFile.ensureDir(
|
|
621
|
+
atomicFile.ensureDir(nodePath.dirname(to));
|
|
621
622
|
try {
|
|
622
623
|
await atomicFile.copy(backupTo, to, { fileMode: 0o600 });
|
|
623
624
|
} catch (e) {
|
|
@@ -625,7 +626,7 @@ async function rollback(opts) {
|
|
|
625
626
|
"selfUpdate.rollback: copy " + backupTo + " -> " + to + " failed: " +
|
|
626
627
|
((e && e.message) || String(e)));
|
|
627
628
|
}
|
|
628
|
-
atomicFile.fsyncDir(
|
|
629
|
+
atomicFile.fsyncDir(nodePath.dirname(to));
|
|
629
630
|
|
|
630
631
|
_safeAuditEmit("selfupdate.rollback.completed", "success", {
|
|
631
632
|
to: to, backupTo: backupTo,
|
|
@@ -635,13 +636,18 @@ async function rollback(opts) {
|
|
|
635
636
|
}
|
|
636
637
|
|
|
637
638
|
module.exports = {
|
|
638
|
-
poll:
|
|
639
|
-
verify:
|
|
640
|
-
swap:
|
|
641
|
-
rollback:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
639
|
+
poll: poll,
|
|
640
|
+
verify: verify,
|
|
641
|
+
swap: swap,
|
|
642
|
+
rollback: rollback,
|
|
643
|
+
// Standalone verifier — zero-dep companion for install-pipeline
|
|
644
|
+
// contexts that run BEFORE the framework is installed (Dockerfile
|
|
645
|
+
// build stages, install.sh, update.sh). See the module's intro for
|
|
646
|
+
// the copy-this-file workflow.
|
|
647
|
+
standaloneVerifier: standaloneVerifier,
|
|
648
|
+
SelfUpdateError: SelfUpdateError,
|
|
649
|
+
ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
|
|
650
|
+
DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
|
|
645
651
|
// Internal — exposed for the layer-0 test suite only.
|
|
646
|
-
_compareTags:
|
|
652
|
+
_compareTags: _compareTags,
|
|
647
653
|
};
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
var C = require("./constants");
|
|
72
|
-
var
|
|
72
|
+
var bCrypto = require("./crypto");
|
|
73
73
|
var nodeCrypto = require("crypto");
|
|
74
74
|
var lazyRequire = require("./lazy-require");
|
|
75
75
|
var requestHelpers = require("./request-helpers");
|
|
@@ -386,7 +386,7 @@ function create(opts) {
|
|
|
386
386
|
return { ok: false, reason: "missing-bind" };
|
|
387
387
|
}
|
|
388
388
|
if (!Buffer.isBuffer(stored) || stored.length !== fpResult.fingerprint.length ||
|
|
389
|
-
!
|
|
389
|
+
!bCrypto.timingSafeEqual(stored, fpResult.fingerprint)) {
|
|
390
390
|
_emitObs("session.device.drift", {});
|
|
391
391
|
_emitAudit("session.device.drift", _hashTokenForAudit(token), "denied",
|
|
392
392
|
{ components: fpResult.components, stage: "verify" }, req);
|