Runtime, offline-verifiable license tokens for gating commercial features in Lux binaries.
The API is open source (BSD-3-Clause). The tokens it verifies are signed by Lux Industries' private Ed25519 key and embed the entitlements (customer, expiry, scopes, features) for a particular deployment. Customers who hold a valid token can run commercial-only Lux features; everyone else gets the same code, but the gated features refuse to start.
Modelled after HashiCorp Vault Enterprise's licensing package: detect a
token from a well-known set of locations, verify its signature against an
embedded public key, check expiry, and expose a single HasFeature(scope)
predicate that the rest of the binary calls.
import "github.com/luxfi/license"
func main() {
license.HasFeatureOrDie("gpu") // aborts with a clear stderr message if the
// user has no token, an expired token, or
// a token without the "gpu" scope.
runGPUPipeline()
}
Some Lux components — CUDA / Metal-accelerated EVM, the order-book DEX engine, FHE / homomorphic primitives, FPGA SHA-3 — are commercial features. The OSS build is fully functional for the non-commercial paths; the commercial paths check for a valid license at startup and refuse to run without one.
This is the "honor-system + tamper detection" middle ground:
- Honor-system: no phone-home, no DRM, no kernel modules. A motivated attacker with the source can patch out the check.
- Tamper detection: tokens are signed; expired or tampered tokens fail loudly. Removing the check requires modifying our open source code, which is a license violation customers don't want to commit to.
This mirrors HashiCorp's Vault Enterprise model, which has been adequate deterrence in practice.
Tokens are framed binary (not JWT, not CBOR — stdlib only):
LUXL magic [4]
1 version uint8
claims_len uint32 big-endian
claims_bytes [claims_len] deterministic encoding of Claims
signature [64] Ed25519(SHA-512(claims_bytes))
The Claims payload is itself a deterministic, length-prefixed binary layout
(see claims.go). Encoding sorts scopes and feature keys ascending so that a
given Claims struct always serialises identically and round-trips byte-for-
byte through Encode / DecodeClaims.
Tokens are exchanged with customers as standard base64 (so they fit in env
vars and .env files).
| Field | Type | Meaning |
|---|---|---|
CustomerID |
string | Human-readable customer identifier ("Acme Inc") |
LicenseID |
string | Unique ID for this token; used for revocation logs |
IssuedAt |
time.Time | When the token was signed |
Expiry |
time.Time (or zero) | After this time, VerifyToken returns ErrExpired |
Scope |
[]string | Authorized feature flags (see scopes below) |
Features |
map[string]string | Free-form attributes (tier, max_nodes, etc.) |
Notes |
string | Free-form note from the issuer |
| Scope | Gates |
|---|---|
gpu |
CUDA / Metal acceleration in luxfi/evm, luxfi/cevm |
cuda |
CUDA-specific kernels (subset of gpu) |
fhe-mlx |
OpenFHE Metal NTT acceleration |
dex |
Commercial order-book engine in luxfi/dex |
fpga |
FPGA SHA-3 / signature kernels in luxfi/fpga |
Adding a new scope is a string change. There is no enum on the Lux side.
// Discover and verify a license from env / disk. Cached for the lifetime
// of the process.
claims, err := license.LoadFromEnv()
// Non-fatal check: false on missing/invalid license or missing scope.
ok := license.HasFeature("gpu")
// Fatal check: prints a clear error to stderr and os.Exit(1)s if the
// license is missing or does not entitle the named scope.
license.HasFeatureOrDie("gpu")priv := loadIssuerPrivateKey() // from KMS / Vault / hardware token
tok, err := license.Sign(&license.Claims{
CustomerID: "Acme Inc",
LicenseID: "lic_abc123",
IssuedAt: time.Now().UTC(),
Expiry: time.Now().Add(365 * 24 * time.Hour),
Scope: []string{"gpu", "dex"},
Features: map[string]string{"tier": "enterprise", "max_nodes": "10"},
}, priv)
fmt.Println(license.EncodeBase64(tok))| Error | Meaning |
|---|---|
license.ErrNoLicense |
No token in env / file / default path |
license.ErrBadMagic |
Input is not a Lux license token |
license.ErrBadVersion |
Unknown token version (future tokens, old binary) |
license.ErrTruncated |
Token bytes incomplete |
license.ErrBadSig |
Signature does not match IssuerPublicKey |
license.ErrExpired |
Token signature is valid but Expiry has passed |
The capi/ subpackage exposes a stable C ABI:
#include "lux_license.h"
LuxLicenseClaims* c = lux_license_load_from_env();
if (c == NULL) {
fprintf(stderr, "%s\n", lux_license_last_error());
return 1;
}
if (lux_license_has_feature(c, "gpu") != 1) {
fprintf(stderr, "no gpu entitlement\n");
return 1;
}
fprintf(stdout, "licensed to %s\n", lux_license_customer_id(c));
lux_license_free(c);C++17+ RAII wrapper:
#include "lux_license.hpp"
try {
auto lic = lux::License::load_from_env();
if (!lic.has_feature("gpu")) {
throw std::runtime_error("gpu not licensed");
}
} catch (const lux::license_error& e) {
std::cerr << "license: " << e.what() << "\n";
return 1;
}Or the one-liner mirroring Go's HasFeatureOrDie:
lux::License::require_feature_or_die("gpu");cd capi
go build -buildmode=c-archive -o liblux_license.a .
# or
go build -buildmode=c-shared -o liblux_license.so .The build emits liblux_license.a (or .so) plus a generated
liblux_license.h (a superset of the stable header in include/; consumers
should include include/lux_license.h for forward compatibility).
cmd/lux-license is a thin issuer / inspector:
# Generate an Ed25519 issuer keypair (paste the pub key into pubkey.go).
lux-license keygen --out ./issuer.key
# Issue a token signed by the new key.
lux-license issue \
--customer "Acme Inc" \
--expiry 2027-12-31 \
--scope gpu,dex,fpga \
--features tier=enterprise,max_nodes=10 \
--notes "Primary commercial license" \
--private-key ./issuer.key \
> acme-license.txt
# Inspect (no signature check) — useful for support tickets.
lux-license inspect acme-license.txt
# Verify against the embedded public key.
lux-license verify acme-license.txt--dev is accepted in place of --private-key to sign with the embedded
development key (testing only; tokens will refuse to verify on any build that
has swapped in a production key).
LoadFromEnv consults these locations in order, returning the first one that
contains a valid token:
$LUX_LICENSE— base64-encoded token, in-band.$LUX_LICENSE_FILE— path to a binary or base64 token file.$HOME/.lux/license.jwt— default path for installer-managed tokens.$HOME/.lux/license— alternate default.
The standing convention in our codebases is to drop the LUX_ prefix on
env vars (so we don't double-namespace inside lux/* binaries). These two
are an explicit exception: LICENSE and LICENSE_FILE are catastrophically
overloaded across the Unix ecosystem (every Java app, Adobe / IBM / Oracle
installer, etc.), and customer ops staff need a name they can grep for in
their own shell rc files without false positives. The LUX_ prefix exists
purely to occupy a clean namespace in the customer's environment.
The Lux production signing key is never stored in this repository. Suggested custodianship:
- Generate on an air-gapped machine, or inside HashiCorp Vault's transit
engine, or inside the Lux KMS (
~/work/lux/kms). - Distribute the public key to all build pipelines that produce Lux
binaries (paste into
pubkey.go:IssuerPublicKey). - Treat the private key like a code-signing key: split-knowledge / dual control, hardware token, audit log of every issuance.
The current IssuerPublicKey is a development key derived deterministically
from a fixed seed. The library emits a one-time warning to stderr the first
time VerifyToken is called on any build that hasn't replaced it. CI should
fail if license.IsUsingDevKey() returns true in a release build.
This repository ships only the library + CLI. Each commercial-gated binary adds its own one-line opt-in:
// in cmd/cevm/main.go, before any GPU work
license.HasFeatureOrDie("gpu")// in cmd/dexd/main.go
license.HasFeatureOrDie("dex")// in cmd/fpgad/main.go
license.HasFeatureOrDie("fpga")// in src/main.cpp
lux::License::require_feature_or_die("gpu");Those one-liners are the only edits to the downstream codebases — no imports of issuer logic, no key material, no business of license discovery duplicated elsewhere.
The wire-up is intentionally not in this PR; it's tracked in separate follow-up changes (one per downstream repository) so each commercial gate ships with its own CHANGELOG entry and customer-visible message.
go test ./... # 22 Go tests
CGO_ENABLED=1 go test ./... # 22 Go + 1 cgo round-tripThe cgo round-trip test builds liblux_license.a, links a small C program
against it (capi/testdata/test_capi.c), issues a dev-signed token, and
verifies the C side accepts the valid token and rejects a tampered one.
.
├── LICENSE BSD-3
├── README.md this file
├── go.mod module github.com/luxfi/license
├── doc.go package doc
├── claims.go Claims struct + deterministic encoding
├── token.go Sign / VerifyToken / framing
├── pubkey.go embedded Ed25519 public key + dev key
├── runtime.go LoadFromEnv / HasFeature / HasFeatureOrDie
├── license_test.go 22 Go tests
├── cmd/lux-license/main.go issuer + inspector + verifier CLI
└── capi/
├── lux_license.go cgo bridge (package main)
├── include/lux_license.h stable C ABI header
├── include/lux_license.hpp C++17 RAII wrapper
├── testdata/test_capi.c round-trip test driver
└── internal/captest/captest_test.go cgo round-trip Go test
BSD 3-Clause. See LICENSE.
The library is BSD. The tokens it verifies are issued by Lux Industries Inc. under a separate commercial license; possessing this library does not grant any right to commercial Lux features.