mirror of
https://github.com/luxfi/license.git
synced 2026-10-30 16:21:14 +00:00
- 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.
378 lines
10 KiB
Go
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
|
|
}
|