P-H019 Ed25519 license signing — closes the deferred follow-up from Stage C (parent plan plan-2026-05-16-prelaunch-audit, sub-plan plan-stage-c-high-batch). Pre-fix packages/core/src/license.ts:280-318 accepted ANY {valid:true,plan:'enterprise'} JSON over HTTPS from whatever config.cloud?.endpoint pointed to. MITM, malicious cloud.endpoint, or local SQLite edit of license_cache could grant arbitrary tier.
Added
packages/core/security/license-pubkey.pem— Ed25519 public key (fingerprint18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475) for verifying signed validate-key responses. Counterpart private key is operator-provisioned in the Supabase Edge Function env varLICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64(NEVER in repo).scripts/bundle-license-pubkey.mjs— bundler that writespackages/core/src/security/license-pubkey.generated.tsfrom the on-disk pem. Mirrorsscripts/bundle-pubkey.mjspattern for the adapter-registry key. Wired intopackages/core/package.jsonprepublishOnlyso every npm publish ships the bundled key in lockstep.packages/core/src/security/license-response-verifier.ts— Ed25519 verifier with strict/transition mode gate (MASSU_REQUIRE_SIGNED_LICENSE=true|unset). Returns tagged unionvalid | missing_signature | bad_signature | unknown_pubkey | error. Pubkey fingerprint allowlist check rejects bundled keys that don't appear inKNOWN_LICENSE_PUBKEY_FINGERPRINTS.packages/core/src/security/license-pubkey.generated.ts— bundledLICENSE_PUBKEY_ED25519Uint8Array +LICENSE_PUBKEY_FINGERPRINT_HEX+KNOWN_LICENSE_PUBKEY_FINGERPRINTSSet, all consumed by the verifier.packages/core/src/tests/license-response-signature.test.ts— 6-case drift-guard: bundled-fingerprint allowlist, missing-signature rejection, unsupported-algorithm rejection, garbage-signature rejection, unknown-pubkey rejection, strict-mode env-var read.docs/runbooks/license-response-signing-key-rotation.md— operator runbook for initial provisioning, smoke-test, strict-mode cutover, and rotation procedure.
Fixed
packages/core/src/license.ts:280-318—validateLicensenow callsverifyLicenseResponse(data)after the fetch returns. Under strict mode (MASSU_REQUIRE_SIGNED_LICENSE=true), unsigned/invalid responses throw (caller drops to grace-period cache or free tier). Under transition mode (default), responses are accepted with a one-shot stderr warning per process lifetime so the customer's terminal isn't spammed every session.website/supabase/functions/validate-key/index.ts:7-67— Edge Function now wraps every successful response withsignature/_signature_alg/_signature_payload_keys/_signature_pubkey_fingerprintfields. Canonical-serialization: top-level non--prefixed keys are sorted;JSON.stringify(obj, keysSorted)produces the deterministic input to Ed25519. Signing key is cached per Edge-Function instance for performance. IfLICENSE_RESPONSE_SIGNING_PRIVATE_KEY_B64env var unset, responses go out UNSIGNED (transition mode) so the deploy can ship before operator provisions the key.packages/core/package.json:27—prepublishOnlyextended to also runbundle-license-pubkey.mjsalongside the existing registry-pubkey bundler, ensuring every npm publish ships the freshly-bundled license pubkey.