Files
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

352 lines
9.6 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command lux-license issues, inspects, and verifies Lux commercial license
// tokens. The runtime API used by Lux binaries lives in the parent package;
// this CLI is for operators (Lux key custodians) and customers.
//
// lux-license issue --customer "Acme Inc" --expiry 2027-12-31 --scope gpu,dex
// lux-license inspect <base64-token>
// lux-license verify <base64-token>
// lux-license keygen --out ./issuer.key
//
// `issue` requires a private key (--private-key path or --dev for the embedded
// development key). All other commands are read-only and require no key.
package main
import (
"bufio"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/luxfi/license"
)
const usage = `lux-license — issue, inspect, and verify Lux commercial license tokens.
Subcommands:
issue Sign a new license token. Requires --private-key or --dev.
inspect Decode and print a token (without verifying signature).
verify Verify a token's signature and expiry. Exit 0 on success.
keygen Generate a new Ed25519 issuer keypair.
help Print this message.
Run 'lux-license <subcommand> -h' for subcommand flags.
`
func main() {
if len(os.Args) < 2 {
fmt.Fprint(os.Stderr, usage)
os.Exit(2)
}
switch os.Args[1] {
case "issue":
cmdIssue(os.Args[2:])
case "inspect":
cmdInspect(os.Args[2:])
case "verify":
cmdVerify(os.Args[2:])
case "keygen":
cmdKeygen(os.Args[2:])
case "help", "-h", "--help":
fmt.Print(usage)
default:
fmt.Fprintf(os.Stderr, "unknown subcommand %q\n%s", os.Args[1], usage)
os.Exit(2)
}
}
// issue ----------------------------------------------------------------------
func cmdIssue(args []string) {
fs := flag.NewFlagSet("issue", flag.ExitOnError)
customer := fs.String("customer", "", "customer ID (required)")
licenseID := fs.String("license-id", "", "unique license ID (default: auto-generated)")
scope := fs.String("scope", "", "comma-separated scopes (e.g. gpu,dex,fpga)")
feat := fs.String("features", "", "comma-separated key=value features (e.g. tier=enterprise,max_nodes=10)")
notes := fs.String("notes", "", "free-form notes")
expiry := fs.String("expiry", "", "expiry date YYYY-MM-DD or RFC3339 (empty = never)")
keyPath := fs.String("private-key", "", "path to Ed25519 private key (32-byte seed, hex or base64)")
useDev := fs.Bool("dev", false, "sign with the embedded development key (testing only)")
out := fs.String("out", "-", "output file path ('-' = stdout)")
if err := fs.Parse(args); err != nil {
os.Exit(2)
}
if *customer == "" {
exit("--customer is required")
}
if (*keyPath == "") == (!*useDev) {
exit("exactly one of --private-key or --dev is required")
}
priv, err := loadPrivateKey(*keyPath, *useDev)
if err != nil {
exit("load private key: %v", err)
}
id := *licenseID
if id == "" {
id = generateLicenseID()
}
c := &license.Claims{
CustomerID: *customer,
LicenseID: id,
IssuedAt: time.Now().UTC().Truncate(time.Second),
Scope: splitTrim(*scope, ","),
Features: parseFeatures(*feat),
Notes: *notes,
}
if *expiry != "" {
t, err := parseDate(*expiry)
if err != nil {
exit("--expiry %q: %v", *expiry, err)
}
c.Expiry = t
}
tok, err := license.Sign(c, priv)
if err != nil {
exit("sign: %v", err)
}
encoded := license.EncodeBase64(tok)
if *out == "-" {
fmt.Println(encoded)
} else {
if err := os.WriteFile(*out, []byte(encoded+"\n"), 0o600); err != nil {
exit("write %s: %v", *out, err)
}
fmt.Fprintf(os.Stderr, "wrote %s\n", *out)
}
fmt.Fprintf(os.Stderr, "issued license_id=%s customer=%q scopes=%v expiry=%s\n",
c.LicenseID, c.CustomerID, c.Scope, formatExpiry(c.Expiry))
}
// inspect --------------------------------------------------------------------
func cmdInspect(args []string) {
fs := flag.NewFlagSet("inspect", flag.ExitOnError)
if err := fs.Parse(args); err != nil {
os.Exit(2)
}
tok, err := readTokenArg(fs.Args())
if err != nil {
exit("%v", err)
}
payload, _, err := license.ParseToken(tok)
if err != nil {
exit("parse: %v", err)
}
c, err := license.DecodeClaims(payload)
if err != nil {
exit("decode: %v", err)
}
printClaims(c, false)
}
// verify ---------------------------------------------------------------------
func cmdVerify(args []string) {
fs := flag.NewFlagSet("verify", flag.ExitOnError)
if err := fs.Parse(args); err != nil {
os.Exit(2)
}
tok, err := readTokenArg(fs.Args())
if err != nil {
exit("%v", err)
}
c, err := license.VerifyToken(tok)
if err != nil {
exit("verify: %v", err)
}
printClaims(c, true)
}
// keygen ---------------------------------------------------------------------
func cmdKeygen(args []string) {
fs := flag.NewFlagSet("keygen", flag.ExitOnError)
out := fs.String("out", "", "private key output path (required)")
pubOut := fs.String("pub-out", "", "public key output path (default: <out>.pub)")
if err := fs.Parse(args); err != nil {
os.Exit(2)
}
if *out == "" {
exit("--out is required")
}
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
exit("generate: %v", err)
}
seed := priv.Seed()
if err := os.WriteFile(*out, []byte(hex.EncodeToString(seed)+"\n"), 0o600); err != nil {
exit("write %s: %v", *out, err)
}
pubPath := *pubOut
if pubPath == "" {
pubPath = *out + ".pub"
}
if err := os.WriteFile(pubPath, []byte(hex.EncodeToString(pub)+"\n"), 0o644); err != nil {
exit("write %s: %v", pubPath, err)
}
fmt.Fprintf(os.Stderr, "wrote private key %s (mode 0600)\n", *out)
fmt.Fprintf(os.Stderr, "wrote public key %s\n", pubPath)
fmt.Fprintln(os.Stderr, "Embed the public key in luxfi/license/pubkey.go:IssuerPublicKey before issuing customer tokens.")
fmt.Println(hex.EncodeToString(pub))
}
// helpers --------------------------------------------------------------------
func exit(format string, args ...any) {
fmt.Fprintf(os.Stderr, "error: "+format+"\n", args...)
os.Exit(1)
}
func splitTrim(s, sep string) []string {
if s == "" {
return nil
}
parts := strings.Split(s, sep)
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
func parseFeatures(s string) map[string]string {
if s == "" {
return nil
}
out := make(map[string]string)
for _, kv := range splitTrim(s, ",") {
idx := strings.Index(kv, "=")
if idx <= 0 {
exit("--features: %q is not key=value", kv)
}
out[strings.TrimSpace(kv[:idx])] = strings.TrimSpace(kv[idx+1:])
}
return out
}
func parseDate(s string) (time.Time, error) {
for _, layout := range []string{"2006-01-02", time.RFC3339, time.DateOnly} {
if t, err := time.Parse(layout, s); err == nil {
// Date-only inputs land at start of day UTC; promote to end of day to
// avoid "issued today, expires today, no usable hours" surprise.
if layout == "2006-01-02" || layout == time.DateOnly {
t = t.Add(24*time.Hour - time.Second)
}
return t, nil
}
}
return time.Time{}, fmt.Errorf("expected YYYY-MM-DD or RFC3339")
}
func generateLicenseID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
exit("rand: %v", err)
}
return "lic_" + hex.EncodeToString(b)
}
func loadPrivateKey(path string, dev bool) (ed25519.PrivateKey, error) {
if dev {
return license.DevPrivateKey(), nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
s := strings.TrimSpace(string(data))
// Accept hex or base64; in either form must decode to a 32-byte seed or a
// 64-byte expanded private key.
if seed, err := hex.DecodeString(s); err == nil && (len(seed) == ed25519.SeedSize || len(seed) == ed25519.PrivateKeySize) {
return seedOrFull(seed)
}
if seed, err := base64.StdEncoding.DecodeString(s); err == nil && (len(seed) == ed25519.SeedSize || len(seed) == ed25519.PrivateKeySize) {
return seedOrFull(seed)
}
return nil, errors.New("private key must be hex or base64 encoding of a 32-byte seed (or 64-byte expanded key)")
}
func seedOrFull(b []byte) (ed25519.PrivateKey, error) {
switch len(b) {
case ed25519.SeedSize:
return ed25519.NewKeyFromSeed(b), nil
case ed25519.PrivateKeySize:
return ed25519.PrivateKey(b), nil
default:
return nil, fmt.Errorf("invalid key length %d", len(b))
}
}
func readTokenArg(args []string) ([]byte, error) {
var raw string
switch {
case len(args) == 0 || args[0] == "-":
b, err := io.ReadAll(bufio.NewReader(os.Stdin))
if err != nil {
return nil, fmt.Errorf("read stdin: %w", err)
}
raw = string(b)
case len(args) == 1:
// Treat as a file path if it exists; otherwise a literal base64 token.
if data, err := os.ReadFile(args[0]); err == nil {
raw = string(data)
} else {
raw = args[0]
}
default:
return nil, errors.New("expected a single token argument or '-' for stdin")
}
tok, err := license.DecodeBase64(raw)
if err != nil {
return nil, fmt.Errorf("decode base64: %w", err)
}
return tok, nil
}
func printClaims(c *license.Claims, verified bool) {
fmt.Printf("Customer ID: %s\n", c.CustomerID)
fmt.Printf("License ID: %s\n", c.LicenseID)
fmt.Printf("Issued At: %s\n", c.IssuedAt.Format(time.RFC3339))
fmt.Printf("Expiry: %s\n", formatExpiry(c.Expiry))
if len(c.Scope) > 0 {
fmt.Printf("Scopes: %s\n", strings.Join(c.Scope, ", "))
}
if len(c.Features) > 0 {
fmt.Printf("Features:\n")
for k, v := range c.Features {
fmt.Printf(" %s = %s\n", k, v)
}
}
if c.Notes != "" {
fmt.Printf("Notes: %s\n", c.Notes)
}
if verified {
fmt.Println("Status: OK (signature valid, not expired)")
}
}
func formatExpiry(t time.Time) string {
if t.IsZero() {
return "never"
}
return t.Format(time.RFC3339)
}