Skip to content

Commit a510b2f

Browse files
committed
docs: describe the security boundary and how to harden a plugin
The SDK ships no anti-tamper, anti-debug, obfuscation or integrity-checking code, and that is a deliberate scope decision rather than an oversight. Nothing in the documentation said so, so a reader could reasonably finish the README believing it was a DRM layer, wire `licensedFlag()` into a single `processBlock` branch, and ship. A new "Security and hardening" section sits between the device fingerprint and 3.x migration sections, keeping the trust material together: how the binding is computed, then what it does and does not buy you. It states what the SDK guarantees (only Moonbase can mint a token, the product and device bindings, the bounded grace period, the store being a cache rather than a credential, fail-closed defaults), why hardening is deliberately left to the consumer, and seven principles for doing it. The rationale for the boundary is the part worth keeping: hardening only works when it lives inside your binary and is specific to it, so anything general enough to ship in an open-source header would be public, identical in every plugin using it, and one published bypass would apply to all of them. Guidance is positive throughout. The section does not enumerate the SDK's weak points or publish attack recipes. Much of the reasoning already existed in code comments that consumers never read, notably the on-disk cache warning in device_id_resolver.hpp and the fail-closed default in LicenseGate.h.
1 parent 960ba54 commit a510b2f

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,65 @@ prints all of the above as JSON, which is what
323323
[the parity workflow](.github/workflows/fingerprint-parity.yml) compares against
324324
`@moonbase.sh/licensing` on every OS.
325325

326+
## Security and hardening
327+
328+
This SDK answers one question and answers it soundly: *is this license valid, for
329+
this product, on this machine, right now?* What it cannot do is defend the code
330+
that asks. Validation runs inside your binary, on a machine the user controls,
331+
and the result eventually becomes a branch. Keeping a token honest and keeping a
332+
shipped binary honest are two different problems; the SDK solves the first and
333+
leaves the second to you, deliberately.
334+
335+
### What the SDK guarantees
336+
337+
- **Only Moonbase can mint a license.** Every token is an RS256 JWT verified
338+
against the public key you embed, with the algorithm pinned.
339+
- **A license is bound to your product.** The `aud` claim must contain your
340+
`product_id`, and if you set `account_id` the issuer must match too.
341+
- **A license is bound to one machine, and the binding is recomputed rather than
342+
read.** The `sig` claim is checked against a device id derived from the
343+
machine's own hardware on every validation, never from a file, so there is no
344+
stored value to edit. See [Device fingerprint](#device-fingerprint).
345+
- **Offline tolerance is bounded.** `online_validation_grace_period` caps how
346+
long an online-activated license runs without a successful server check.
347+
Transient failures fall back to the local result inside that window;
348+
definitive rejections propagate immediately regardless of it.
349+
- **The stored file is a cache, not a credential.** The SDK re-derives every
350+
field from the signed token rather than trusting what it read. Do the same:
351+
branch on what the validator returned, never on fields read out of the store.
352+
- **The defaults fail closed.** Nothing is persisted unless you supply a store,
353+
and the JUCE module's `LicenseGate` starts silent.
354+
355+
### What it deliberately leaves to you
356+
357+
There is no anti-debugging, obfuscation, integrity self-checking or tamper
358+
detection anywhere in this SDK, and there will not be. Hardening works only when
359+
it lives inside your binary and is specific to it: anything general enough to
360+
ship in an open-source header would be public, identical in every plugin using
361+
it, and one published bypass would apply to all of them. The SDK stops at the
362+
boundary where it can still keep the promises it makes.
363+
364+
### Principles
365+
366+
- **Ask more than once, in more than one place.** A single call site that
367+
decides everything is a single thing to change.
368+
- **Gate what the customer pays for**, not the window or the menu item.
369+
- **Fail closed.** The failure you did not anticipate should be the safe one.
370+
- **Separate detection from response.** Nothing requires a rejection to be
371+
immediate, adjacent to the check, or loud.
372+
- **Take the free wins.** Release builds, stripped symbols, code signing.
373+
- **Use the server you are already talking to.** Wire
374+
[revocation](#revoking-an-activation) to a real "deactivate" affordance so seat
375+
limits mean something.
376+
- **Price it honestly.** The goal is raising cost, not eliminating piracy, and
377+
every measure is paid for by legitimate users: the studio behind a locked-down
378+
network, the engineer who swapped a motherboard. Spending their goodwill to
379+
inconvenience people who were never going to buy is a poor trade.
380+
381+
For where this meets audio code, see [JUCE Plugins](#juce-plugins) and the
382+
`licensedFlag()` / `LicenseGate` helpers in
383+
[`docs/juce-module.md`](docs/juce-module.md#gating).
384+
326385
## Migrating from 3.x
327386

328387
Device ids computed by 3.x do not follow the spec, so **by default every device

0 commit comments

Comments
 (0)