Skip to content

fix(license): close dev-key privilege-escalation — clamp dev-signed licenses to trial - #177

Merged
SyncTekLLC merged 2 commits into
mainfrom
fix/license-dev-key-tier-clamp
Jul 22, 2026
Merged

fix(license): close dev-key privilege-escalation — clamp dev-signed licenses to trial#177
SyncTekLLC merged 2 commits into
mainfrom
fix/license-dev-key-tier-clamp

Conversation

@SyncTekLLC

Copy link
Copy Markdown
Contributor

Security fix (release-gating)

Confirmed vulnerability: the embedded dev signing key ships in the wheel and production trusts it, while the validator enforced only subject=="dev-trial" — not tier/seats/expires_at. A license forged with the shipped dev key as {subject:"dev-trial", tier:"enterprise", seats:999} validated → unlimited enterprise, offline, permanent. Pre-fix probe: EXPLOIT SUCCEEDED (tier=enterprise seats=999).

Fix

_enforce_dev_trial_limits() in validator.py — a dev-key-signed license can never exceed a trial (fail-closed hard-rejects):

  • tier must be "trial" (paid tiers refused loudly)
  • seats must be a genuine int in 1..4 (the trial tier's own max_simulators) — rejects float/bool/string/negative (hardening)
  • expires_at ≤ now + 30d (anchored to now, not attacker-controlled issued_at)
  • subject must be "dev-trial" (preserved)
    Plus a structural guard: if the dev verify key is ever added to TRUSTED_PUBLIC_KEYS, it's demoted from the prod path so the clamp still runs.

Case B note: the private key can't be dropped — cli.py::_issue_dev_license self-issues offline trials at runtime (--offline-dev). So the clamp is the load-bearing control; residual by-design risk is unlimited offline trials (never a paid tier). Follow-up (out of scope): gate self-issuance behind a build flag to drop the key from customer wheels.

Verification

  • Independent separate-model SoD: APPROVE — escalation fully closed on all 8 validate-call sites; 12 adversarial probe variants, none escalate; boundaries sound; legit flows unaffected.
  • Regression tests: test_license_dev_key_clamp.py 32 passed (incl. the structural guard proven two ways, type/range hardening, and legit-trial still validates). Full license suite 115 passed. ruff clean.
  • Post-fix probe: forged enterprise license REJECTED (license_invalid).

Do not release simdrive until this is merged.

SyncTekLLC and others added 2 commits July 21, 2026 23:15
…fix)

The private dev SIGNING key ships inside the distributed package
(public_key.DEV_SIGNING_KEY_HEX) so the client can self-issue offline
trials at runtime (license/cli.py::_issue_dev_license). Because the
private key is shipped, a dev-key *signature* is forgeable and is NOT a
trust boundary. The validator only checked `subject == "dev-trial"` and
then read tier/seats/expires_at straight from the attacker-controlled
payload, so anyone who unpacks the wheel could sign
`subject="dev-trial", tier="enterprise", seats=999, expires_at=<far future>`
and obtain unlimited enterprise access. Confirmed by probe: pre-fix the
forged payload validated as tier=enterprise seats=999.

Fix (load-bearing control = the validator, not the signature):
- validator._enforce_dev_trial_limits() hard-rejects (fail closed,
  license_invalid) any dev-key license that is not a genuine trial:
  subject must be "dev-trial", tier must be "trial" (paid tiers refused
  loudly, not downgraded), seats <= trial cap (4 = trial max_simulators),
  and expires_at within 30 days of effective-now (legit trial is 14d;
  anchored to now, not the attacker-controlled issued_at). The prod-key
  path is untouched.
- Correct the misleading docstrings in validator.py / public_key.py that
  claimed the signature alone prevented forgery.
- Add tests/test_license_dev_key_clamp.py: forged enterprise/999-seat/
  far-future dev-key payloads are rejected; a legit seats=1 14-day dev
  trial still validates; prod-signed licenses (all tiers, 999 seats,
  1-year expiry) are unaffected. 18 new tests; 110 license tests green.

**Scope:** simdrive/src/simdrive/license/validator.py (dev-key branch +
helper + constants) and doc corrections in public_key.py. No change to
the signer, the cloud issuance routes, or the prod-key validation path.

**Case B (key removal):** NOT removed. A shipped runtime path DOES sign
with the dev key (license/cli.py::_issue_dev_license self-issues offline
trials via SIMDRIVE_OFFLINE_DEV / `trial start --offline-dev`), so the
private key must stay in the package. The tier/seats/expiry clamp is
therefore the load-bearing control. Residual, by-design risk: a user can
mint unlimited offline 14-day *trials* — capped to trial entitlements,
never a paid tier.

**Not done:** did not change the 14-day offline-trial feature itself, nor
add a build-time check that the private key is absent from paid builds
(not applicable while offline-dev ships). Flagged for SoD: whether
offline-dev self-issuance should be gated behind a build flag so the
private key can be dropped from customer wheels entirely — a larger
product decision beyond this security fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…key in trusted set (SoD follow-ups)

Implements the 3 non-blocking SoD follow-ups on the dev-key license-forge
fix (cf4f56d), with regression coverage:

1/2. _enforce_dev_trial_limits now requires seats to be a genuine positive
     int within the trial cap: rejects float (4.9 no longer truncates to 4),
     str, bool (int subclass), None, and non-positive (<1) values.
3.   validate_license structural guard: if the selected verify key equals
     the DEV verify key (e.g. mistakenly added to TRUSTED_PUBLIC_KEYS),
     signed_by_prod is demoted to False so the dev-trial clamp always runs.

Tests (test_license_dev_key_clamp.py): seats type/range hardening; the
structural guard via both trusted_keys= and a monkeypatched
TRUSTED_PUBLIC_KEYS default path (forged tier=enterprise dev-key sig still
REJECTED); legit dev trial still validates.

**Scope:** validator.py hardening carried over from the SoD-approved fix was
pre-applied; this commit adds the regression tests plus the commit itself.
**Not done:** no push / no PR (per task); the unrelated journey test module
that imports `anthropic` is untouched and out of scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_enforce_dev_trial_limits(payload, last_known_server_time)
log.debug(
"license validated via dev key (offline trial)",
extra={"subject": payload.get("subject", "")},
@SyncTekLLC
SyncTekLLC merged commit 013a4f0 into main Jul 22, 2026
6 of 7 checks passed
@SyncTekLLC
SyncTekLLC deleted the fix/license-dev-key-tier-clamp branch July 22, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants