Skip to content

fix(security): ship real offline license + policy signing keys (SEC-H1)#701

Merged
remyluslosius merged 2 commits into
mainfrom
fix/sec-h1-real-signing-keys
Jun 27, 2026
Merged

fix(security): ship real offline license + policy signing keys (SEC-H1)#701
remyluslosius merged 2 commits into
mainfrom
fix/sec-h1-real-signing-keys

Conversation

@remyluslosius

Copy link
Copy Markdown
Contributor

SEC-H1 (P0 GA blocker)

The embedded "current" trust anchors for license JWTs and admin-policy envelopes were byte-for-byte the public halves of the committed test private keys in internal/{license,policy}/testdata/. Anyone with repo access could forge a license of any tier/feature/quota (bypassing all internal/license gating) or sign admin-policy envelopes. The verification code was correct; the trust root was the defect.

What changed

  • Real keys shipped. internal/license/keys/license-pubkey-current.pem and internal/policy/keys/policy-pubkey-current.pem are now the real Ed25519 public keys generated offline. The private keys live only in the operator vault, never in the repo or CI. (Verified: each embedded public key corresponds to its vault private key; neither equals the testdata key.)
  • Regression guard. A new keys_guard_test.go in each package fails the build if an embedded current public key equals the testdata key. Proven to fail loudly on the test key and pass on the real key, so this can never silently regress.
  • Test decoupling. The license validator suite assumed the embedded key was the test key (it signs with the testdata private key). mustRing now builds the ring from the testdata key, making verification-logic tests independent of the shipped production key; the embedded key is covered separately by the guard.
  • Specced. system-license-validation C-09/AC-14 and system-policy C-10/AC-13 (both -> v1.1.0). Annotation coverage stays 100%.

Verification

  • go test ./internal/license/... ./internal/policy/... green
  • Guard negative-tested: swapping the test key back in fails both guards
  • specter check (114 specs), specter check --test (0 errors), specter coverage --strictness annotation (100%, T1 37/37)
  • gofmt/go vet/go build clean

Follow-ups (not in this PR)

  • cmd/owlicgen still defaults to the test private key for dev minting; production must pass --key <vault path>. Could harden to refuse the testdata key.
  • No production policy-signing tool exists yet (policy envelopes are signed only in tests); issuing real signed admin policies needs one.

The embedded "current" trust anchors for license JWTs and admin-policy
envelopes were byte-for-byte the public halves of the committed test
private keys (internal/{license,policy}/testdata). Anyone with repo
access could forge licenses of any tier/feature or sign admin policies.
The verification code was correct; the trust root was the defect.

- Replace internal/license/keys/license-pubkey-current.pem and
  internal/policy/keys/policy-pubkey-current.pem with the real
  Ed25519 public keys generated offline (private keys live only in the
  operator vault, never in the repo or CI).
- Add a regression guard in each package (keys_guard_test.go) that fails
  the build if an embedded current public key equals the testdata key.
  Verified to fail loudly on the test key and pass on the real key.
- Decouple the license validator tests from the embedded key: mustRing
  now builds the ring from the testdata key (what signJWT signs with),
  so verification-logic tests are independent of the shipped production
  key. The embedded key is covered separately by the new guard.
- Spec it: system-license-validation C-09/AC-14 and system-policy
  C-10/AC-13 (both bumped to 1.1.0); annotation coverage stays 100%.

Follow-ups (not in this change): cmd/owlicgen still defaults to the test
private key for dev minting (production must pass --key <vault>); a
production policy-signing tool does not exist yet.
Shipping the real signing keys broke every test that signs an artifact
with the testdata key and verifies it against the embedded trust anchor
(it used to be that same test key). These are DB-touching tests, so they
skipped locally without OPENWATCH_TEST_DSN and only failed in CI.

Add an internal/-scoped SetVerificationKeyForTesting(pub) to both the
license and policy packages (returns a restore func; never on a
production path) and install the testdata public key as the active
verifier from the helpers that sign with the testdata private key:
- policy: setupKeys (covers the 6 loader tests)
- server: mintTestLicenseJWT (4 license/premium tests),
  mintSignedAlertThresholds (signoff)

So the binary embeds the real offline key while tests verify their own
test-signed artifacts. Verified with OPENWATCH_TEST_DSN: full
internal/{server,policy,license} suites pass.
@remyluslosius remyluslosius merged commit 38828f6 into main Jun 27, 2026
13 checks passed
@remyluslosius remyluslosius deleted the fix/sec-h1-real-signing-keys branch June 27, 2026 05:22
remyluslosius added a commit that referenced this pull request Jun 27, 2026
Stage 1 docs freeze for rc.17.
- packaging/version.env -> 0.2.0-rc.17
- CHANGELOG.md: roll [Unreleased] into a dated [0.2.0-rc.17] section; add the
  two operator-facing security entries from this cycle (real offline
  license/policy signing keys + build guard; OIDC/notification SSRF hardening).
- refresh the rc version string in the SERVICE_DOWN / DISK_FULL runbooks.

Bundles since rc.16: #701 (SEC-H1 real signing keys + regression guards),
#702 (owlicgen removed from the product), #703 (SEC-H2 OIDC SSRF guard +
non-gating transactionlog perf test). Verified: changelog format gate green,
version injects, specter check + coverage 100%.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant