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

259 lines
5.9 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package main exposes a stable C ABI (see capi/include/lux_license.h) over
// the Go API in github.com/luxfi/license. Build as a shared library or static
// archive:
//
// go build -buildmode=c-shared -o liblux_license.so ./capi
// go build -buildmode=c-archive -o liblux_license.a ./capi
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"os"
"sync"
"unsafe"
"github.com/luxfi/license"
)
// claimsHandle holds the parsed Claims and any C-string pointers we have
// returned to the caller. We allocate C-strings lazily via C.CString and free
// them all when the handle is freed; this guarantees the simple-by-default
// invariant that "any pointer returned by lux_license_* is valid until
// lux_license_free(handle)".
type claimsHandle struct {
c *license.Claims
cstrsMu sync.Mutex
cstrs []unsafe.Pointer
}
// dupAndTrack copies a Go string into a C-allocated NUL-terminated buffer and
// remembers the pointer for cleanup. Caller treats the result as read-only.
func (h *claimsHandle) dupAndTrack(s string) *C.char {
p := C.CString(s)
h.cstrsMu.Lock()
h.cstrs = append(h.cstrs, unsafe.Pointer(p))
h.cstrsMu.Unlock()
return p
}
func (h *claimsHandle) freeAll() {
h.cstrsMu.Lock()
for _, p := range h.cstrs {
C.free(p)
}
h.cstrs = nil
h.cstrsMu.Unlock()
}
// Handle table: opaque IDs are C-allocated 1-byte stubs (so the pointer is in
// C memory, never a Go heap address that the GC might move). We index from
// `uintptr(cptr) -> *claimsHandle` via sync.Map.
var handles sync.Map // uintptr -> *claimsHandle
func registerHandle(h *claimsHandle) unsafe.Pointer {
stub := C.malloc(1)
if stub == nil {
return nil
}
handles.Store(uintptr(stub), h)
return stub
}
func lookupHandle(p unsafe.Pointer) *claimsHandle {
if p == nil {
return nil
}
v, ok := handles.Load(uintptr(p))
if !ok {
return nil
}
return v.(*claimsHandle)
}
func unregisterHandle(p unsafe.Pointer) *claimsHandle {
if p == nil {
return nil
}
v, ok := handles.LoadAndDelete(uintptr(p))
if !ok {
return nil
}
C.free(p)
return v.(*claimsHandle)
}
// Last error: cgo callbacks come in on locked OS threads, so per-thread state
// requires either TLS or a global. We use a global with a mutex; the C ABI
// promises only that lux_license_last_error reflects the most recent failure
// from this thread. In practice, license operations are infrequent and the
// global serialization is invisible.
var (
lastErrMu sync.Mutex
lastErrC *C.char
)
func setLastError(err error) {
lastErrMu.Lock()
defer lastErrMu.Unlock()
if lastErrC != nil {
C.free(unsafe.Pointer(lastErrC))
lastErrC = nil
}
if err == nil {
return
}
lastErrC = C.CString(err.Error())
}
func clearLastError() { setLastError(nil) }
// ---------------------------------------------------------------------------
// Exported API
// ---------------------------------------------------------------------------
//export lux_license_load_from_env
func lux_license_load_from_env() unsafe.Pointer {
clearLastError()
c, err := license.LoadFromEnv()
if err != nil {
setLastError(err)
return nil
}
return registerHandle(&claimsHandle{c: c})
}
//export lux_license_verify_base64
func lux_license_verify_base64(tokenB64 *C.char) unsafe.Pointer {
clearLastError()
if tokenB64 == nil {
setLastError(fmt.Errorf("lux_license_verify_base64: NULL token"))
return nil
}
tok, err := license.DecodeBase64(C.GoString(tokenB64))
if err != nil {
setLastError(fmt.Errorf("decode base64: %w", err))
return nil
}
c, err := license.VerifyToken(tok)
if err != nil {
setLastError(err)
return nil
}
return registerHandle(&claimsHandle{c: c})
}
//export lux_license_free
func lux_license_free(p unsafe.Pointer) {
h := unregisterHandle(p)
if h == nil {
return
}
h.freeAll()
}
//export lux_license_has_feature
func lux_license_has_feature(p unsafe.Pointer, scope *C.char) C.int {
h := lookupHandle(p)
if h == nil || scope == nil {
return 0
}
if h.c.HasScope(C.GoString(scope)) {
return 1
}
return 0
}
//export lux_license_customer_id
func lux_license_customer_id(p unsafe.Pointer) *C.char {
h := lookupHandle(p)
if h == nil {
return nil
}
return h.dupAndTrack(h.c.CustomerID)
}
//export lux_license_license_id
func lux_license_license_id(p unsafe.Pointer) *C.char {
h := lookupHandle(p)
if h == nil {
return nil
}
return h.dupAndTrack(h.c.LicenseID)
}
//export lux_license_scope_count
func lux_license_scope_count(p unsafe.Pointer) C.int {
h := lookupHandle(p)
if h == nil {
return 0
}
return C.int(len(h.c.Scope))
}
//export lux_license_scope
func lux_license_scope(p unsafe.Pointer, i C.int) *C.char {
h := lookupHandle(p)
if h == nil {
return nil
}
idx := int(i)
if idx < 0 || idx >= len(h.c.Scope) {
return nil
}
return h.dupAndTrack(h.c.Scope[idx])
}
//export lux_license_issued_at_unix
func lux_license_issued_at_unix(p unsafe.Pointer) C.int64_t {
h := lookupHandle(p)
if h == nil || h.c.IssuedAt.IsZero() {
return 0
}
return C.int64_t(h.c.IssuedAt.Unix())
}
//export lux_license_expiry_unix
func lux_license_expiry_unix(p unsafe.Pointer) C.int64_t {
h := lookupHandle(p)
if h == nil || h.c.Expiry.IsZero() {
return 0
}
return C.int64_t(h.c.Expiry.Unix())
}
//export lux_license_require_feature_or_die
func lux_license_require_feature_or_die(scope *C.char) {
if scope == nil {
fmt.Fprintln(os.Stderr, "FATAL: lux_license_require_feature_or_die called with NULL scope")
os.Exit(1)
}
license.HasFeatureOrDie(C.GoString(scope))
}
//export lux_license_last_error
func lux_license_last_error() *C.char {
lastErrMu.Lock()
defer lastErrMu.Unlock()
if lastErrC == nil {
// Empty string sentinel: return a static empty C string. Allocate once.
if emptyErr == nil {
emptyErr = C.CString("")
}
return emptyErr
}
return lastErrC
}
var emptyErr *C.char
// main is required by cgo's c-shared / c-archive build modes but never called.
func main() {}