@massu/core 1.10.4 → 1.10.5
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/dist/cli.js
CHANGED
|
@@ -14202,8 +14202,102 @@ var init_init = __esm({
|
|
|
14202
14202
|
}
|
|
14203
14203
|
});
|
|
14204
14204
|
|
|
14205
|
+
// src/security/license-pubkey.generated.ts
|
|
14206
|
+
var LICENSE_PUBKEY_ED25519, LICENSE_PUBKEY_FINGERPRINT_HEX, KNOWN_LICENSE_PUBKEY_FINGERPRINTS;
|
|
14207
|
+
var init_license_pubkey_generated = __esm({
|
|
14208
|
+
"src/security/license-pubkey.generated.ts"() {
|
|
14209
|
+
"use strict";
|
|
14210
|
+
LICENSE_PUBKEY_ED25519 = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
|
|
14211
|
+
LICENSE_PUBKEY_FINGERPRINT_HEX = "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475";
|
|
14212
|
+
KNOWN_LICENSE_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
|
|
14213
|
+
"18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
|
|
14214
|
+
]);
|
|
14215
|
+
}
|
|
14216
|
+
});
|
|
14217
|
+
|
|
14218
|
+
// src/security/license-response-verifier.ts
|
|
14219
|
+
import { createPublicKey } from "crypto";
|
|
14220
|
+
function verifyLicenseResponse(payload) {
|
|
14221
|
+
if (!KNOWN_LICENSE_PUBKEY_FINGERPRINTS.has(LICENSE_PUBKEY_FINGERPRINT_HEX)) {
|
|
14222
|
+
return {
|
|
14223
|
+
kind: "error",
|
|
14224
|
+
reason: `Bundled license pubkey fingerprint ${LICENSE_PUBKEY_FINGERPRINT_HEX} is not in the trusted allowlist. Possible build-time tamper.`
|
|
14225
|
+
};
|
|
14226
|
+
}
|
|
14227
|
+
const sig = payload._signature;
|
|
14228
|
+
const alg = payload._signature_alg;
|
|
14229
|
+
const payloadKeys = payload._signature_payload_keys;
|
|
14230
|
+
const sigPubkey = payload._signature_pubkey_fingerprint;
|
|
14231
|
+
if (typeof sig !== "string" || sig.length === 0) {
|
|
14232
|
+
return { kind: "missing_signature" };
|
|
14233
|
+
}
|
|
14234
|
+
if (alg !== "ed25519") {
|
|
14235
|
+
return { kind: "error", reason: `Unsupported signature algorithm: ${alg}` };
|
|
14236
|
+
}
|
|
14237
|
+
if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
|
|
14238
|
+
return { kind: "error", reason: "Missing _signature_payload_keys" };
|
|
14239
|
+
}
|
|
14240
|
+
if (typeof sigPubkey === "string" && sigPubkey !== LICENSE_PUBKEY_FINGERPRINT_HEX) {
|
|
14241
|
+
return { kind: "unknown_pubkey", got: sigPubkey };
|
|
14242
|
+
}
|
|
14243
|
+
const canonicalObj = {};
|
|
14244
|
+
for (const k3 of payloadKeys) {
|
|
14245
|
+
if (typeof k3 !== "string") continue;
|
|
14246
|
+
canonicalObj[k3] = payload[k3];
|
|
14247
|
+
}
|
|
14248
|
+
const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
|
|
14249
|
+
try {
|
|
14250
|
+
const spkiPrefix = Buffer.from([
|
|
14251
|
+
48,
|
|
14252
|
+
42,
|
|
14253
|
+
48,
|
|
14254
|
+
5,
|
|
14255
|
+
6,
|
|
14256
|
+
3,
|
|
14257
|
+
43,
|
|
14258
|
+
101,
|
|
14259
|
+
112,
|
|
14260
|
+
3,
|
|
14261
|
+
33,
|
|
14262
|
+
0
|
|
14263
|
+
]);
|
|
14264
|
+
const der = Buffer.concat([spkiPrefix, Buffer.from(LICENSE_PUBKEY_ED25519)]);
|
|
14265
|
+
const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
14266
|
+
const { verify } = __require("crypto");
|
|
14267
|
+
const ok = verify(
|
|
14268
|
+
null,
|
|
14269
|
+
Buffer.from(canonical, "utf-8"),
|
|
14270
|
+
pubkey,
|
|
14271
|
+
Buffer.from(sig, "base64")
|
|
14272
|
+
);
|
|
14273
|
+
return ok ? { kind: "valid" } : { kind: "bad_signature" };
|
|
14274
|
+
} catch (err) {
|
|
14275
|
+
return {
|
|
14276
|
+
kind: "error",
|
|
14277
|
+
reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`
|
|
14278
|
+
};
|
|
14279
|
+
}
|
|
14280
|
+
}
|
|
14281
|
+
function isLicenseSignatureRequired() {
|
|
14282
|
+
return process.env.MASSU_REQUIRE_SIGNED_LICENSE === "true";
|
|
14283
|
+
}
|
|
14284
|
+
var init_license_response_verifier = __esm({
|
|
14285
|
+
"src/security/license-response-verifier.ts"() {
|
|
14286
|
+
"use strict";
|
|
14287
|
+
init_license_pubkey_generated();
|
|
14288
|
+
}
|
|
14289
|
+
});
|
|
14290
|
+
|
|
14205
14291
|
// src/license.ts
|
|
14206
14292
|
import { createHash as createHash5 } from "crypto";
|
|
14293
|
+
function warnLicenseSigOnce(reason) {
|
|
14294
|
+
if (_warnedLicenseSig) return;
|
|
14295
|
+
_warnedLicenseSig = true;
|
|
14296
|
+
process.stderr.write(
|
|
14297
|
+
`[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). Acceptance permitted under transition mode. Operator: provision Supabase Edge Function LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.
|
|
14298
|
+
`
|
|
14299
|
+
);
|
|
14300
|
+
}
|
|
14207
14301
|
function tierLevel(tier) {
|
|
14208
14302
|
return TIER_LEVELS[tier] ?? 0;
|
|
14209
14303
|
}
|
|
@@ -14263,6 +14357,13 @@ async function validateLicense(apiKey) {
|
|
|
14263
14357
|
});
|
|
14264
14358
|
if (response.ok) {
|
|
14265
14359
|
const data = await response.json();
|
|
14360
|
+
const sigResult = verifyLicenseResponse(data);
|
|
14361
|
+
if (sigResult.kind !== "valid") {
|
|
14362
|
+
if (isLicenseSignatureRequired()) {
|
|
14363
|
+
throw new Error(`License response signature invalid: ${sigResult.kind}`);
|
|
14364
|
+
}
|
|
14365
|
+
warnLicenseSigOnce(sigResult.kind);
|
|
14366
|
+
}
|
|
14266
14367
|
if (data.valid) {
|
|
14267
14368
|
const tier = data.plan ? PLAN_TO_TIER_MAP[data.plan] ?? "free" : data.tier ?? "free";
|
|
14268
14369
|
const validUntil = data.validUntil ?? "";
|
|
@@ -14385,12 +14486,14 @@ async function handleLicenseToolCall(name, _args, _memDb) {
|
|
|
14385
14486
|
}
|
|
14386
14487
|
return { content: [{ type: "text", text: `Unknown license tool: ${name}` }] };
|
|
14387
14488
|
}
|
|
14388
|
-
var TIER_LEVELS, TOOL_TIER_MAP, PLAN_TO_TIER_MAP, TIER_LABELS, cachedTier, cachedTierTimestamp, IN_MEMORY_CACHE_TTL_MS;
|
|
14489
|
+
var _warnedLicenseSig, TIER_LEVELS, TOOL_TIER_MAP, PLAN_TO_TIER_MAP, TIER_LABELS, cachedTier, cachedTierTimestamp, IN_MEMORY_CACHE_TTL_MS;
|
|
14389
14490
|
var init_license = __esm({
|
|
14390
14491
|
"src/license.ts"() {
|
|
14391
14492
|
"use strict";
|
|
14392
14493
|
init_config();
|
|
14393
14494
|
init_memory_db();
|
|
14495
|
+
init_license_response_verifier();
|
|
14496
|
+
_warnedLicenseSig = false;
|
|
14394
14497
|
TIER_LEVELS = {
|
|
14395
14498
|
free: 0,
|
|
14396
14499
|
pro: 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 59 workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "src/server.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
|
|
25
25
|
"build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
26
26
|
"build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:@massu/adapter-rails --external:@massu/adapter-phoenix --external:@massu/adapter-aspnet --external:@massu/adapter-spring --external:@massu/adapter-go-chi --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
|
|
27
|
-
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && npm run build",
|
|
27
|
+
"prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && npm run build",
|
|
28
28
|
"bench:watch": "tsx test/perf/watch-benchmark.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
package/src/license.ts
CHANGED
|
@@ -17,6 +17,21 @@ import { createHash } from 'crypto';
|
|
|
17
17
|
import type { ToolDefinition, ToolResult } from './tools.ts';
|
|
18
18
|
import { getConfig } from './config.ts';
|
|
19
19
|
import { getMemoryDb } from './memory-db.ts';
|
|
20
|
+
import { verifyLicenseResponse, isLicenseSignatureRequired } from './security/license-response-verifier.ts';
|
|
21
|
+
|
|
22
|
+
// P-H019 one-shot warning gate. We emit the stderr warning at most once
|
|
23
|
+
// per process lifetime so the customer's terminal isn't spammed every
|
|
24
|
+
// session-start until the operator provisions the Edge Function signing key.
|
|
25
|
+
let _warnedLicenseSig = false;
|
|
26
|
+
function warnLicenseSigOnce(reason: string): void {
|
|
27
|
+
if (_warnedLicenseSig) return;
|
|
28
|
+
_warnedLicenseSig = true;
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`[massu] WARNING: license-validate response is unsigned or signature invalid (${reason}). ` +
|
|
31
|
+
`Acceptance permitted under transition mode. Operator: provision Supabase Edge Function ` +
|
|
32
|
+
`LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64 then set MASSU_REQUIRE_SIGNED_LICENSE=true to enforce strict mode.\n`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
20
35
|
|
|
21
36
|
// ============================================================
|
|
22
37
|
// Types
|
|
@@ -294,8 +309,35 @@ export async function validateLicense(apiKey: string): Promise<LicenseInfo> {
|
|
|
294
309
|
validUntil?: string;
|
|
295
310
|
features?: string[];
|
|
296
311
|
reason?: string;
|
|
312
|
+
_signature?: string;
|
|
313
|
+
_signature_alg?: string;
|
|
314
|
+
_signature_payload_keys?: readonly string[];
|
|
315
|
+
_signature_pubkey_fingerprint?: string;
|
|
297
316
|
};
|
|
298
317
|
|
|
318
|
+
// P-H019 (plan-stage-c-high-batch / 1.10.5): verify Ed25519
|
|
319
|
+
// signature on the validate-key response. Closes the bug class
|
|
320
|
+
// where MITM / malicious-cloud-endpoint / local SQLite edit
|
|
321
|
+
// could grant arbitrary tier.
|
|
322
|
+
//
|
|
323
|
+
// Transition mode (default): unsigned/invalid-sig responses are
|
|
324
|
+
// accepted with a one-shot stderr warning. Lets existing customers
|
|
325
|
+
// keep working while operators provision the Edge Function signing
|
|
326
|
+
// key.
|
|
327
|
+
//
|
|
328
|
+
// Strict mode (MASSU_REQUIRE_SIGNED_LICENSE=true, post-cutover):
|
|
329
|
+
// unsigned/invalid-sig responses are rejected; caller falls
|
|
330
|
+
// through to the grace-period cache or free tier.
|
|
331
|
+
const sigResult = verifyLicenseResponse(data);
|
|
332
|
+
if (sigResult.kind !== 'valid') {
|
|
333
|
+
if (isLicenseSignatureRequired()) {
|
|
334
|
+
// Strict mode — reject. Caller falls through to grace period.
|
|
335
|
+
throw new Error(`License response signature invalid: ${sigResult.kind}`);
|
|
336
|
+
}
|
|
337
|
+
// Transition mode — one-shot stderr warning per session.
|
|
338
|
+
warnLicenseSigOnce(sigResult.kind);
|
|
339
|
+
}
|
|
340
|
+
|
|
299
341
|
if (data.valid) {
|
|
300
342
|
// Map plan name to tier using PLAN_TO_TIER_MAP
|
|
301
343
|
const tier: ToolTier = data.plan
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-license-pubkey.mjs at 2026-05-17T15:24:33.950Z.
|
|
2
|
+
// Source pem: packages/core/security/license-pubkey.pem
|
|
3
|
+
// RAW-bytes sha256: 18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475
|
|
4
|
+
// DO NOT EDIT — regenerate via `node scripts/bundle-license-pubkey.mjs` or
|
|
5
|
+
// the prepublishOnly hook (which runs on every npm publish).
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ed25519 public key for verifying signed /validate-key responses from
|
|
9
|
+
* massu.ai. Used by packages/core/src/security/license-response-verifier.ts
|
|
10
|
+
* to reject MITM / malicious-cloud-endpoint responses.
|
|
11
|
+
*
|
|
12
|
+
* P-H019 (plan-stage-c-high-batch / 1.10.5).
|
|
13
|
+
*
|
|
14
|
+
* To rotate: regenerate the keypair via Node crypto.generateKeyPairSync,
|
|
15
|
+
* update packages/core/security/license-pubkey.pem with the new public key,
|
|
16
|
+
* append the new RAW-bytes sha256 to KNOWN_PUBKEY_FINGERPRINTS in
|
|
17
|
+
* scripts/bundle-license-pubkey.mjs (DO NOT delete the old entry during
|
|
18
|
+
* the rotation grace window), then run `node scripts/bundle-license-pubkey.mjs`.
|
|
19
|
+
* Operator then updates the Supabase Edge Function env var
|
|
20
|
+
* `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` with the new private key.
|
|
21
|
+
*/
|
|
22
|
+
export const LICENSE_PUBKEY_ED25519: Uint8Array = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
|
|
23
|
+
|
|
24
|
+
/** Hex sha256 of LICENSE_PUBKEY_ED25519 (raw 32 bytes). */
|
|
25
|
+
export const LICENSE_PUBKEY_FINGERPRINT_HEX = '18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475';
|
|
26
|
+
|
|
27
|
+
/** Allowlist of historically-trusted license pubkey fingerprints. */
|
|
28
|
+
export const KNOWN_LICENSE_PUBKEY_FINGERPRINTS: ReadonlySet<string> = new Set([
|
|
29
|
+
"18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
|
|
30
|
+
]);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* P-H019 (plan-stage-c-high-batch / 1.10.5): Ed25519 verifier for signed
|
|
6
|
+
* /validate-key responses from massu.ai.
|
|
7
|
+
*
|
|
8
|
+
* **Bug class closed**: pre-fix, `packages/core/src/license.ts:280-318`
|
|
9
|
+
* accepted ANY `{ valid: true, plan: 'enterprise' }` JSON over HTTPS from
|
|
10
|
+
* whatever endpoint `config.cloud?.endpoint` pointed to. MITM, malicious
|
|
11
|
+
* cloud.endpoint, or local edit of the cached `license_cache` SQLite row
|
|
12
|
+
* could grant arbitrary tier.
|
|
13
|
+
*
|
|
14
|
+
* **Structural fix**: the server (validate-key Supabase Edge Function)
|
|
15
|
+
* signs every response with Ed25519. The client (this module, called
|
|
16
|
+
* from `license.ts`) verifies the signature against the bundled public
|
|
17
|
+
* key in `license-pubkey.generated.ts`. Invalid / missing signature →
|
|
18
|
+
* reject under strict mode, warn-and-accept under transition mode.
|
|
19
|
+
*
|
|
20
|
+
* **Transition mode** (default): when `MASSU_REQUIRE_SIGNED_LICENSE` env
|
|
21
|
+
* var is NOT `'true'`, unsigned/invalid-sig responses are accepted but
|
|
22
|
+
* a one-shot stderr warning is emitted. Lets existing customers keep
|
|
23
|
+
* working while operators provision the signing key.
|
|
24
|
+
*
|
|
25
|
+
* **Strict mode** (post-cutover): when `MASSU_REQUIRE_SIGNED_LICENSE=true`,
|
|
26
|
+
* unsigned/invalid-sig responses are REJECTED (caller drops to grace-period
|
|
27
|
+
* cached or 'free' tier). Operator flips this after Supabase Edge Function
|
|
28
|
+
* env var `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` is provisioned and
|
|
29
|
+
* cutover smoke test passes.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createVerify, createPublicKey } from 'crypto';
|
|
33
|
+
import {
|
|
34
|
+
LICENSE_PUBKEY_ED25519,
|
|
35
|
+
LICENSE_PUBKEY_FINGERPRINT_HEX,
|
|
36
|
+
KNOWN_LICENSE_PUBKEY_FINGERPRINTS,
|
|
37
|
+
} from './license-pubkey.generated.ts';
|
|
38
|
+
|
|
39
|
+
export interface SignedLicenseResponse {
|
|
40
|
+
/** Base64-encoded Ed25519 signature over the canonical-serialized payload. */
|
|
41
|
+
_signature?: string;
|
|
42
|
+
/** Signature algorithm tag (currently always `'ed25519'`). */
|
|
43
|
+
_signature_alg?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Sorted list of top-level keys (excluding the `_signature*` triplet) that
|
|
46
|
+
* were included in the signed payload. Lets the client reconstruct the
|
|
47
|
+
* canonical-serialization input deterministically.
|
|
48
|
+
*/
|
|
49
|
+
_signature_payload_keys?: readonly string[];
|
|
50
|
+
/** Hex sha256 of the LICENSE pubkey used for signing (so client can detect rotation). */
|
|
51
|
+
_signature_pubkey_fingerprint?: string;
|
|
52
|
+
// Plus the actual fields (plan, tier, validUntil, features, etc.)
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type VerificationResult =
|
|
57
|
+
| { kind: 'valid' }
|
|
58
|
+
| { kind: 'missing_signature' }
|
|
59
|
+
| { kind: 'bad_signature' }
|
|
60
|
+
| { kind: 'unknown_pubkey'; got: string }
|
|
61
|
+
| { kind: 'error'; reason: string };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Verify a server-signed /validate-key response. Returns a tagged union
|
|
65
|
+
* describing the verification outcome. Caller decides whether to accept
|
|
66
|
+
* based on strict/transition mode.
|
|
67
|
+
*/
|
|
68
|
+
export function verifyLicenseResponse(payload: SignedLicenseResponse): VerificationResult {
|
|
69
|
+
// Self-check: confirm the bundled key fingerprint matches the allowlist.
|
|
70
|
+
// CR-9 audit L2 pattern: a future build that swaps the pem to an unknown
|
|
71
|
+
// key refuses to load even if compiled in.
|
|
72
|
+
if (!KNOWN_LICENSE_PUBKEY_FINGERPRINTS.has(LICENSE_PUBKEY_FINGERPRINT_HEX)) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'error',
|
|
75
|
+
reason: `Bundled license pubkey fingerprint ${LICENSE_PUBKEY_FINGERPRINT_HEX} is not in the trusted allowlist. Possible build-time tamper.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sig = payload._signature;
|
|
80
|
+
const alg = payload._signature_alg;
|
|
81
|
+
const payloadKeys = payload._signature_payload_keys;
|
|
82
|
+
const sigPubkey = payload._signature_pubkey_fingerprint;
|
|
83
|
+
|
|
84
|
+
if (typeof sig !== 'string' || sig.length === 0) {
|
|
85
|
+
return { kind: 'missing_signature' };
|
|
86
|
+
}
|
|
87
|
+
if (alg !== 'ed25519') {
|
|
88
|
+
return { kind: 'error', reason: `Unsupported signature algorithm: ${alg}` };
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(payloadKeys) || payloadKeys.length === 0) {
|
|
91
|
+
return { kind: 'error', reason: 'Missing _signature_payload_keys' };
|
|
92
|
+
}
|
|
93
|
+
if (typeof sigPubkey === 'string' && sigPubkey !== LICENSE_PUBKEY_FINGERPRINT_HEX) {
|
|
94
|
+
// Server signed with a different pubkey than we have bundled — rotation
|
|
95
|
+
// mismatch. Caller should fail-open under transition mode (operator
|
|
96
|
+
// hasn't published the new bundle yet) but reject under strict mode.
|
|
97
|
+
return { kind: 'unknown_pubkey', got: sigPubkey };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Reconstruct the canonical serialization the server signed:
|
|
101
|
+
// JSON.stringify({ k1: payload[k1], k2: payload[k2], ... }, payloadKeysSorted)
|
|
102
|
+
// where payloadKeysSorted is the sorted list of NON-underscore-prefixed keys.
|
|
103
|
+
const canonicalObj: Record<string, unknown> = {};
|
|
104
|
+
for (const k of payloadKeys) {
|
|
105
|
+
if (typeof k !== 'string') continue;
|
|
106
|
+
canonicalObj[k] = payload[k];
|
|
107
|
+
}
|
|
108
|
+
const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
|
|
109
|
+
|
|
110
|
+
// Verify signature.
|
|
111
|
+
try {
|
|
112
|
+
// Construct a node KeyObject from the bundled raw Ed25519 pubkey bytes.
|
|
113
|
+
// We use createPublicKey with SubjectPublicKeyInfo DER encoding (44 bytes:
|
|
114
|
+
// 12-byte algorithm OID prefix + 32-byte raw key).
|
|
115
|
+
const spkiPrefix = Buffer.from([
|
|
116
|
+
0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65,
|
|
117
|
+
0x70, 0x03, 0x21, 0x00,
|
|
118
|
+
]);
|
|
119
|
+
const der = Buffer.concat([spkiPrefix, Buffer.from(LICENSE_PUBKEY_ED25519)]);
|
|
120
|
+
const pubkey = createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
121
|
+
|
|
122
|
+
// Ed25519 in node uses `verify` directly (no createVerify needed for ed25519).
|
|
123
|
+
// crypto.verify(algorithm, data, key, signature) — algorithm MUST be null for Ed25519.
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
125
|
+
const { verify } = require('crypto');
|
|
126
|
+
const ok = verify(
|
|
127
|
+
null,
|
|
128
|
+
Buffer.from(canonical, 'utf-8'),
|
|
129
|
+
pubkey,
|
|
130
|
+
Buffer.from(sig, 'base64'),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return ok ? { kind: 'valid' } : { kind: 'bad_signature' };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'error',
|
|
137
|
+
reason: `Signature verification threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* P-H019 strict-mode gate. Operator sets `MASSU_REQUIRE_SIGNED_LICENSE=true`
|
|
144
|
+
* AFTER the validate-key Edge Function is provisioned with
|
|
145
|
+
* `LICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64` and cutover smoke test passes.
|
|
146
|
+
*/
|
|
147
|
+
export function isLicenseSignatureRequired(): boolean {
|
|
148
|
+
return process.env.MASSU_REQUIRE_SIGNED_LICENSE === 'true';
|
|
149
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-
|
|
1
|
+
// AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T15:24:33.893Z.
|
|
2
2
|
// Source pem: packages/core/security/registry-pubkey.pem
|
|
3
3
|
// RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
|
|
4
4
|
// DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
|