Files
license/license_test.go
zeekay f77da86cda Initial luxfi/license — Ed25519-signed offline license tokens for commercial Lux features.
- Stdlib-only deterministic binary token format (no JWT / CBOR dep).
- Go API: VerifyToken, LoadFromEnv, HasFeature, HasFeatureOrDie.
- Stable C ABI + C++17 RAII wrapper for non-Go commercial binaries (luxcpp/cevm).
- cmd/lux-license: keygen / issue / inspect / verify.
- Embedded development key; warns once at runtime until production key swap.
- 22 Go tests + 1 cgo round-trip test, all passing.
2026-05-15 17:08:32 -07:00

378 lines
10 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package license
import (
"bytes"
"crypto/ed25519"
"errors"
"os"
"path/filepath"
"testing"
"time"
)
// helper ----------------------------------------------------------------------
func newTestClaims(scopes ...string) *Claims {
return &Claims{
CustomerID: "Acme Inc",
LicenseID: "lic_test_0001",
IssuedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
Expiry: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC),
Scope: scopes,
Features: map[string]string{"max_nodes": "10", "tier": "enterprise"},
Notes: "primary commercial license",
}
}
func newTestClaimsExpired() *Claims {
return &Claims{
CustomerID: "Acme Inc",
LicenseID: "lic_test_expired",
IssuedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
Expiry: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Scope: []string{"gpu"},
}
}
// claims encoding -------------------------------------------------------------
func TestClaimsEncodeDecodeRoundTrip(t *testing.T) {
c := newTestClaims("dex", "fpga", "gpu") // intentionally unsorted input; encoder sorts
enc, err := c.Encode()
if err != nil {
t.Fatalf("encode: %v", err)
}
dec, err := DecodeClaims(enc)
if err != nil {
t.Fatalf("decode: %v", err)
}
if dec.CustomerID != c.CustomerID || dec.LicenseID != c.LicenseID {
t.Fatalf("ids mismatch: %+v vs %+v", dec, c)
}
if !dec.IssuedAt.Equal(c.IssuedAt) || !dec.Expiry.Equal(c.Expiry) {
t.Fatalf("times mismatch: %+v vs %+v", dec, c)
}
wantScopes := []string{"dex", "fpga", "gpu"} // sorted
if !equalStrings(dec.Scope, wantScopes) {
t.Fatalf("scopes: got %v want %v", dec.Scope, wantScopes)
}
if dec.Features["max_nodes"] != "10" || dec.Features["tier"] != "enterprise" {
t.Fatalf("features mismatch: %v", dec.Features)
}
if dec.Notes != c.Notes {
t.Fatalf("notes: got %q want %q", dec.Notes, c.Notes)
}
}
func TestClaimsEncodeIsDeterministic(t *testing.T) {
c1 := newTestClaims("gpu", "dex", "fpga")
c2 := newTestClaims("fpga", "gpu", "dex") // different input order
a, err := c1.Encode()
if err != nil {
t.Fatalf("encode 1: %v", err)
}
b, err := c2.Encode()
if err != nil {
t.Fatalf("encode 2: %v", err)
}
if !bytes.Equal(a, b) {
t.Fatalf("encodings differ for permuted input")
}
}
func TestClaimsEncodeRejectsDuplicateScope(t *testing.T) {
c := newTestClaims("gpu", "gpu")
if _, err := c.Encode(); err == nil {
t.Fatalf("expected duplicate-scope error")
}
}
func TestClaimsEncodeRequiresIDs(t *testing.T) {
c := newTestClaims("gpu")
c.CustomerID = ""
if _, err := c.Encode(); err == nil {
t.Fatalf("expected error for empty CustomerID")
}
c = newTestClaims("gpu")
c.LicenseID = ""
if _, err := c.Encode(); err == nil {
t.Fatalf("expected error for empty LicenseID")
}
c = newTestClaims("gpu")
c.IssuedAt = time.Time{}
if _, err := c.Encode(); err == nil {
t.Fatalf("expected error for zero IssuedAt")
}
}
// signing / verification ------------------------------------------------------
func TestSignVerifyRoundTrip(t *testing.T) {
priv := DevPrivateKey()
pub := DevPublicKey()
c := newTestClaims("gpu", "dex")
tok, err := Sign(c, priv)
if err != nil {
t.Fatalf("sign: %v", err)
}
got, err := VerifyTokenWithKey(tok, pub)
if err != nil {
t.Fatalf("verify: %v", err)
}
if got.CustomerID != c.CustomerID || got.LicenseID != c.LicenseID {
t.Fatalf("got %+v want %+v", got, c)
}
if !got.HasScope("gpu") || !got.HasScope("dex") || got.HasScope("nope") {
t.Fatalf("scope check incorrect: %v", got.Scope)
}
}
func TestVerifyDetectsTamperedClaims(t *testing.T) {
priv := DevPrivateKey()
pub := DevPublicKey()
tok, err := Sign(newTestClaims("gpu"), priv)
if err != nil {
t.Fatalf("sign: %v", err)
}
// Flip a byte inside the claims payload (skip the 9-byte framing header).
tok[20] ^= 0xFF
if _, err := VerifyTokenWithKey(tok, pub); !errors.Is(err, ErrBadSig) {
t.Fatalf("expected ErrBadSig, got %v", err)
}
}
func TestVerifyDetectsTamperedSignature(t *testing.T) {
priv := DevPrivateKey()
pub := DevPublicKey()
tok, err := Sign(newTestClaims("gpu"), priv)
if err != nil {
t.Fatalf("sign: %v", err)
}
tok[len(tok)-1] ^= 0xFF
if _, err := VerifyTokenWithKey(tok, pub); !errors.Is(err, ErrBadSig) {
t.Fatalf("expected ErrBadSig, got %v", err)
}
}
func TestVerifyExpiredToken(t *testing.T) {
priv := DevPrivateKey()
pub := DevPublicKey()
tok, err := Sign(newTestClaimsExpired(), priv)
if err != nil {
t.Fatalf("sign: %v", err)
}
c, err := VerifyTokenWithKey(tok, pub)
if !errors.Is(err, ErrExpired) {
t.Fatalf("expected ErrExpired, got %v", err)
}
// Expired claims are still returned so callers can show details in errors.
if c == nil || c.LicenseID != "lic_test_expired" {
t.Fatalf("expected claims to be returned alongside ErrExpired, got %+v", c)
}
}
func TestVerifyRejectsBadMagic(t *testing.T) {
tok := make([]byte, 80)
copy(tok, "ZZZZ")
if _, err := VerifyTokenWithKey(tok, DevPublicKey()); !errors.Is(err, ErrBadMagic) {
t.Fatalf("expected ErrBadMagic, got %v", err)
}
}
func TestVerifyRejectsTruncated(t *testing.T) {
if _, err := VerifyTokenWithKey([]byte("LUXL"), DevPublicKey()); !errors.Is(err, ErrTruncated) {
t.Fatalf("expected ErrTruncated, got %v", err)
}
}
func TestVerifyRejectsWrongVersion(t *testing.T) {
priv := DevPrivateKey()
pub := DevPublicKey()
tok, _ := Sign(newTestClaims("gpu"), priv)
tok[4] = 99
if _, err := VerifyTokenWithKey(tok, pub); !errors.Is(err, ErrBadVersion) {
t.Fatalf("expected ErrBadVersion, got %v", err)
}
}
func TestVerifyRejectsSignatureFromWrongKey(t *testing.T) {
otherSeed := bytes.Repeat([]byte{0x42}, ed25519.SeedSize)
otherPriv := ed25519.NewKeyFromSeed(otherSeed)
tok, err := Sign(newTestClaims("gpu"), otherPriv)
if err != nil {
t.Fatalf("sign: %v", err)
}
if _, err := VerifyTokenWithKey(tok, DevPublicKey()); !errors.Is(err, ErrBadSig) {
t.Fatalf("expected ErrBadSig from wrong issuer, got %v", err)
}
}
func TestEncodeDecodeBase64RoundTrip(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("gpu"), priv)
enc := EncodeBase64(tok)
dec, err := DecodeBase64(" " + enc + "\n")
if err != nil {
t.Fatalf("decode: %v", err)
}
if !bytes.Equal(dec, tok) {
t.Fatalf("base64 roundtrip mismatch")
}
}
// runtime / env --------------------------------------------------------------
func TestLoadFromEnvViaEnvVar(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("gpu"), priv)
t.Setenv(EnvVarToken, EncodeBase64(tok))
t.Setenv(EnvVarFile, "")
t.Setenv("HOME", t.TempDir()) // ensure default-path lookup misses
ResetCache()
c, err := LoadFromEnv()
if err != nil {
t.Fatalf("LoadFromEnv: %v", err)
}
if c.LicenseID != "lic_test_0001" {
t.Fatalf("got %q", c.LicenseID)
}
}
func TestLoadFromEnvViaFile(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("dex"), priv)
dir := t.TempDir()
path := filepath.Join(dir, "license.bin")
if err := os.WriteFile(path, tok, 0o600); err != nil {
t.Fatalf("write: %v", err)
}
t.Setenv(EnvVarToken, "")
t.Setenv(EnvVarFile, path)
t.Setenv("HOME", t.TempDir())
ResetCache()
c, err := LoadFromEnv()
if err != nil {
t.Fatalf("LoadFromEnv: %v", err)
}
if !c.HasScope("dex") {
t.Fatalf("expected dex scope, got %v", c.Scope)
}
}
func TestLoadFromEnvViaBase64File(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("fpga"), priv)
dir := t.TempDir()
path := filepath.Join(dir, "license.txt")
if err := os.WriteFile(path, []byte(EncodeBase64(tok)+"\n"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
t.Setenv(EnvVarToken, "")
t.Setenv(EnvVarFile, path)
t.Setenv("HOME", t.TempDir())
ResetCache()
c, err := LoadFromEnv()
if err != nil {
t.Fatalf("LoadFromEnv: %v", err)
}
if !c.HasScope("fpga") {
t.Fatalf("expected fpga scope")
}
}
func TestLoadFromEnvViaDefaultPath(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("cuda"), priv)
home := t.TempDir()
luxDir := filepath.Join(home, ".lux")
if err := os.MkdirAll(luxDir, 0o700); err != nil {
t.Fatalf("mkdir: %v", err)
}
if err := os.WriteFile(filepath.Join(luxDir, "license.jwt"), []byte(EncodeBase64(tok)), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
t.Setenv(EnvVarToken, "")
t.Setenv(EnvVarFile, "")
t.Setenv("HOME", home)
ResetCache()
c, err := LoadFromEnv()
if err != nil {
t.Fatalf("LoadFromEnv: %v", err)
}
if !c.HasScope("cuda") {
t.Fatalf("expected cuda scope")
}
}
func TestLoadFromEnvMissing(t *testing.T) {
t.Setenv(EnvVarToken, "")
t.Setenv(EnvVarFile, "")
t.Setenv("HOME", t.TempDir())
ResetCache()
_, err := LoadFromEnv()
if !errors.Is(err, ErrNoLicense) {
t.Fatalf("expected ErrNoLicense, got %v", err)
}
}
func TestHasFeature(t *testing.T) {
priv := DevPrivateKey()
tok, _ := Sign(newTestClaims("gpu", "fhe-mlx"), priv)
t.Setenv(EnvVarToken, EncodeBase64(tok))
t.Setenv(EnvVarFile, "")
t.Setenv("HOME", t.TempDir())
ResetCache()
if !HasFeature("gpu") {
t.Fatalf("expected gpu = true")
}
if !HasFeature("fhe-mlx") {
t.Fatalf("expected fhe-mlx = true")
}
if HasFeature("missing") {
t.Fatalf("expected missing = false")
}
}
func TestHasFeatureWithoutLicense(t *testing.T) {
t.Setenv(EnvVarToken, "")
t.Setenv(EnvVarFile, "")
t.Setenv("HOME", t.TempDir())
ResetCache()
if HasFeature("gpu") {
t.Fatalf("expected gpu = false when no license present")
}
}
// pubkey ----------------------------------------------------------------------
func TestDevKeyMatchesSeed(t *testing.T) {
derived := DevPublicKey()
if !bytes.Equal(derived, IssuerPublicKey) {
t.Fatalf("IssuerPublicKey is not the derived dev key.\n got = %x\n expected = %x",
IssuerPublicKey, derived)
}
}
func TestIsUsingDevKey(t *testing.T) {
if !IsUsingDevKey() {
t.Fatalf("expected IsUsingDevKey=true (production key not yet installed)")
}
}
// helpers ---------------------------------------------------------------------
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}