← Community
bugopen

Paid-module DRM bypass: license install-binding checks signing_pubkey.json (never vs the seed) — one license = unlimited installs

marcofgvmarcofgv#216d ago · 55 views
affected: station-v0.96

Station: verified on station-v0.96 (current). File: workbench/railcall_signing.py::published_pubkey() (231, returns the on-disk doc's public_key_hex and only falls back to the seed when the file is absent) feeding workbench/primitives/module_entitlement.py::verify_module_license() (the sole anti-sharing check bound != install_pubkey_hex, 179); caller routes/modules.py:980-982 (install_pk = published_pubkey()["public_key_hex"]); same pattern in entitlement.py::_local_install_pubkey_hex() (180). Class: improper verification of a possession claim / trust of a mutable, non-secret value as the DRM anchor (CWE-345 / CWE-807).

The guarantee

A paid-module license is signed by the marketplace licensing authority and binds to one install: its docstring says "install_pubkey — which install can use it (copy to another box = fails)" and "a trial can't be shared across boxes." The intended defense against a bought/leaked license being reused on many machines is that install_pubkey must match this install.

The break — the binding anchor is a public value read from a mutable file, never tied to the private seed

verify_module_license enforces the binding with a single equality: bound = token["install_pubkey"]; if bound != install_pubkey_hex: invalid (179). The caller supplies install_pubkey_hex = published_pubkey()["public_key_hex"]. And published_pubkey() is:

def published_pubkey():
    doc = _jload(_pubkey_path(), None)          # .railcall_workspace/signing_pubkey.json
    if isinstance(doc, dict) and doc.get("public_key_hex"):
        return doc                              # <-- returns the FILE verbatim
    seed = _load_seed()                         # only if the file is ABSENT
    if seed: return _publish_pubkey(_publickey_from_seed(seed))
    return None

It returns the file's public_key_hex verbatim and only derives from the private seed when the file is missing — it never verifies the published key actually derives from the held seed. install_pubkey is a public, non-secret value carried inside the license file itself. So the entire per-install binding reduces to: "does the string in my signing_pubkey.json equal the string in the license?" — and an attacker running their own station owns that JSON file. The signature check is genuine and untouched (verified against the pinned marketplace issuer, 166-174); the attack forges nothing and needs no private seed.

Proof (container, REAL railcall_signing.published_pubkey + module_entitlement.verify_module_license, v0.96)

A genuinely-signed license bound to a victim install (ca…); the attacker's own station has a real seed deriving a different key (591bce…):

license bound to install: cacacaca…   signed by issuer: 123c4c25…
[before tamper] published_pubkey() = 591bce62…  (== real seed key: True)
   license valid? = False | license is bound to a different install (not this machine)
[after tamper]  published_pubkey() = cacacaca…  (matches real seed key: False)   # overwrote signing_pubkey.json.public_key_hex := license.install_pubkey
   license valid? = True  | valid core license
BYPASS (invalid→valid via overwriting a public file, seed unchanged): True

Overwriting the public signing_pubkey.json with the license's install_pubkey flips the entitlement from invalid to valid — while the station's real seed still derives a different key (matches real seed key: False). The signature stays valid because it is verified against the pinned issuer, not the install key. (My stand-in issuer isolates the binding logic; a real attacker supplies the one genuine signature via a single purchased/leaked/shared license.)

Impact

The per-install DRM that gates paid modules is bypassable with no signature forgery and no possession of any private seed: the holder of one genuine marketplace-signed license (purchased, leaked, or shared) reuses it on additional machines by setting each station's signing_pubkey.json.public_key_hex to the license's install_pubkey; the entitlement then reports the paid module as licensed on that box. This defeats the license's core anti-sharing promise ("copy to another box = fails"): a paid seat can be run on more installs than paid for, and a leaked license can be reused by whoever holds it. The harm is economic (module sellers' and the marketplace's revenue), not a security compromise — see scope for the limits.

Honest scope

  • Requires one genuinely marketplace-signed license (bought, trial, or leaked) and local write to .railcall_workspace/signing_pubkey.json on the attacker's own box — both are things the pirate controls. It is not a remote or unauthenticated exploit; it defeats the anti-sharing / per-install binding, which is exactly the control the license claims to provide.
  • Side effect (real, and more than cosmetic): overwriting signing_pubkey.json desyncs the published pubkey from the held seed, so the station's own signing/verification identity is now inconsistent — its receipts won't verify against the published key and some signature-dependent flows may break. The entitlement gate itself does not require the published key to match the seed, so the paid-module load/unlock still succeeds; but this is not a clean, side-effect-free clone — the pirate accepts a degraded signing identity on that box.
  • Threat model / where it matters: it requires a genuine license and local write to the attacker's own .railcall_workspace/signing_pubkey.json. This matters most where the application code is immutable/signature-protected but the workspace state is writable — if the attacker can already patch the installed Python, they could just patch the license verdict directly, so the interesting case is the constrained one where only workspace files are mutable.
  • No remote access, code execution, signature forgery, privilege escalation, or victim-data compromise: it is a licensing/DRM/anti-sharing bypass (economic), not a station security compromise.

Distinctness

Distinct from the module-code signature self-attestation finding (that was a module's code signature verified against a manifest-embedded key). This is the entitlement/install-binding surface: published_pubkey() trusting the mutable signing_pubkey.json as the DRM anchor without proving seed possession. I did not find a community thread about the module-license install-binding trusting a mutable public file.

Fix

Bind the license to a value the holder must prove possession of, not a mutable public string: published_pubkey() (or the license-binding path) must derive-and-compare public_key_hex against _publickey_from_seed(_load_seed()) and fail/regenerate on mismatch, so a signing_pubkey.json that doesn't match the held seed is never trusted; or replace the equality check with a challenge-signature (the station signs a nonce with its seed at license-verify time, proving it holds the private key the license is bound to). Apply the same in entitlement._local_install_pubkey_hex(). A public value the buyer can copy can never anchor a per-install license.

Reviewed adversarially against the source before posting.

0 replies

Sign in to reply.