License package — Threat model

This page expands §10 of the design spec. Each row covers one threat class, the mitigation implemented in the package, and the residual risk.

Threat matrix

#ThreatMitigation in this packageResidual risk / out of scope
1License forgery — attacker constructs a valid-looking license without the private keyEd25519 signature (256-bit security, deterministic). Verify rejects any body whose signature does not match.Compromised private key. Mitigation: key rotation (Step 7 in workflow.md), air-gapped issuance (future: HSM).
2Field tampering — attacker modifies Subject, NotAfter, or any other field after issuanceSignature covers the entire canonical JSON body. A single changed byte invalidates the signature.None within this package.
3Replay across audiences — attacker uses a rshell license in memscan-serveraud field is signed. WithAudience(...) rejects the license if the caller-provided audience is not in the signed list.An operator who issues with an empty aud (wildcard). The package warns but does not reject.
4Cross-binary reuse — attacker copies a license from one binary to anotherBinarySHA256 pins the exact on-disk hash. IdentitySHA256 pins the embedded identity bytes. WithBinaryPinning() enables both checks.Attacker simultaneously replaces both the binary and its embedded identity blob. Mitigation: pack + code-sign the binary.
5Stale-cache substitution — attacker replaces the local revocation-list cache with an older copyRevocation list carries a monotonic Sequence. Verify rejects any fetched list whose sequence regresses below LastSeenSequence in the state file.State file deletion. The package resets state to zero and logs a warning; the attacker gains one grace period window.
6Revocation server downtime — attacker takes the revocation server offline to prevent revocation deliverySigned ExpiresAt in the revocation list. After LastFetchOk + gracePeriod expires the binary stops running (causeRevocationStale).Operator must choose a grace period short enough to match the security model. Default: none (operator configures).
7Password brute-force — attacker iterates passwords against the binding hashArgon2id (64 MiB, t=3, p=4, 32-byte output). ~100 ms per attempt on 2024 hardware; GPU acceleration still bottlenecked by memory requirements.Offline attack against the full PEM file. Mitigation: keep license files off publicly accessible paths.
8Side-channel binding discrimination — attacker determines which constraint failed to guide enumerationErrLicenseInvalid is a single opaque sentinel. The internal 17-value cause enum is never serialised into the error or the PEM file.Timing side-channels between fast checks (audience) and slow checks (argon2id). The package does not add artificial delays. In practice argon2id dwarfs all other step timings.
9Clock rollback — attacker sets the system clock backward to bypass NotAfterTrustedFloor = max(observed heartbeat times, observed revocation ServerTime). Verify rejects if now < TrustedFloor - skew.Air-gapped machine that never contacts the online checks retains only LastSeenLocal monotonicity. No TPM anchor.
10Algorithm confusion — attacker substitutes a signature generated by a different algorithmDomain separation: the signed payload is `"maldev-license-v1\x00"
11Key rotation gap — operator removes old public key before old licenses expireTrusted is a map; old keys remain valid as long as the operator keeps them present. The workflow documents the retention rule.Human error. There is no automated key-expiry warning.
12Heartbeat nonce replay — attacker captures a heartbeat reply and replays itHeartbeat reply carries a random 16-byte nonce echo. Verify performs subtle.ConstantTimeCompare(reply.NonceEcho, nonce). A captured reply has the wrong nonce for the next call.TOCTOU: an attacker racing the live binary's heartbeat call. Mitigation: TLS on the heartbeat endpoint.
13Machine ID spoofing — attacker clones the MachineGuid / /etc/machine-id of a licensed hosthostid.Local() mixes multiple OS sources via sha256. All sources must match for the fingerprint to agree.Attacker with root/SYSTEM can rewrite all sources. Full OS compromise is out of scope.
14State file HMAC forgery — attacker writes a crafted state file to reset TrustedFloorHMAC key is `HKDF(license_signature
15DoS via oversized input — attacker supplies a 100 MB PEM file to exhaust memoryMaxLicenseSize = 16 KB. Verify rejects oversized input before any JSON parsing.None within this package.
16JSON parse panic — attacker supplies malformed bytes crafted to trigger a decoder panicjsonUnmarshalStrict uses json.NewDecoder with DisallowUnknownFields. Standard library decoder; no known panic paths on adversarial input.Future Go stdlib vulnerabilities. Mitigated by updating the Go toolchain.
17Sealed payload decryption — attacker reads the sealed config blob in the licenseseal.Seal uses X25519 ephemeral key agreement + ChaCha20-Poly1305 AEAD. Only the holder of the recipient private key can open it.Recipient private key compromise. Out of scope for this package.

Explicit non-goals (documented limitations)

The following threats are out of scope for v1. Each has a documented mitigation path:

ThreatWhy out of scopeMitigation path
Binary anti-tamper / anti-debugThe evasion/ packages cover this independentlyPack with cmd/packer + code-signing
Verify bypass via binary patchingAny sufficiently motivated attacker with code execution can patch return nil into the functionUse cmd/packer obfuscation; this is the fundamental limitation of software licensing
Seat counting (max N machines simultaneously)Requires a stateful server with session accountingv2: DB-backed server with session table
Perfect clock anti-rollbackWould require TPM/enclave endorsement keysv2: TPM binding; current code is best-effort offline
Sub-license / delegated issuanceSignature chains with inherited constraintsv2 scope
HSM / PKCS#11 issuanceKey stored in YubiKey or hardware modulev2: license/hsm sub-package

See also