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.
132 lines
4.2 KiB
Go
132 lines
4.2 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package license
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// tokenMagic is a 4-byte prefix used to detect mangled or wrong-format input
|
|
// before attempting signature verification. "LUXL" = Lux License.
|
|
var tokenMagic = [4]byte{'L', 'U', 'X', 'L'}
|
|
|
|
// tokenVersion identifies the framing layout.
|
|
//
|
|
// magic[4] = "LUXL"
|
|
// version uint8 = 1
|
|
// claims_len uint32 (big-endian)
|
|
// claims_bytes [claims_len]
|
|
// signature[64]
|
|
const tokenVersion uint8 = 1
|
|
|
|
// Errors returned by Verify / Parse.
|
|
var (
|
|
ErrBadMagic = errors.New("license: bad token magic")
|
|
ErrBadVersion = errors.New("license: unsupported token version")
|
|
ErrTruncated = errors.New("license: truncated token")
|
|
ErrBadSig = errors.New("license: signature verification failed")
|
|
ErrExpired = errors.New("license: token expired")
|
|
)
|
|
|
|
// Sign produces a fully framed, signed token from claims using priv.
|
|
//
|
|
// The Lux production signing key is held offline / in KMS. This function is
|
|
// public so customers and internal automation can mint test tokens; the
|
|
// runtime APIs (VerifyToken, LoadFromEnv, HasFeature, HasFeatureOrDie) never
|
|
// touch a private key.
|
|
func Sign(claims *Claims, priv ed25519.PrivateKey) ([]byte, error) {
|
|
if len(priv) != ed25519.PrivateKeySize {
|
|
return nil, fmt.Errorf("license: priv key size %d != %d", len(priv), ed25519.PrivateKeySize)
|
|
}
|
|
encoded, err := claims.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sig := ed25519.Sign(priv, encoded)
|
|
|
|
out := make([]byte, 0, 4+1+4+len(encoded)+ed25519.SignatureSize)
|
|
out = append(out, tokenMagic[:]...)
|
|
out = append(out, tokenVersion)
|
|
out = binary.BigEndian.AppendUint32(out, uint32(len(encoded)))
|
|
out = append(out, encoded...)
|
|
out = append(out, sig...)
|
|
return out, nil
|
|
}
|
|
|
|
// ParseToken splits a binary token into its claims payload and signature,
|
|
// without verifying the signature. Use VerifyToken for the verifying form.
|
|
func ParseToken(tok []byte) (claimsPayload, signature []byte, err error) {
|
|
if len(tok) < 4+1+4+ed25519.SignatureSize {
|
|
return nil, nil, ErrTruncated
|
|
}
|
|
if tok[0] != tokenMagic[0] || tok[1] != tokenMagic[1] || tok[2] != tokenMagic[2] || tok[3] != tokenMagic[3] {
|
|
return nil, nil, ErrBadMagic
|
|
}
|
|
if tok[4] != tokenVersion {
|
|
return nil, nil, ErrBadVersion
|
|
}
|
|
clen := binary.BigEndian.Uint32(tok[5:9])
|
|
if uint64(9)+uint64(clen)+uint64(ed25519.SignatureSize) != uint64(len(tok)) {
|
|
return nil, nil, ErrTruncated
|
|
}
|
|
claimsPayload = tok[9 : 9+clen]
|
|
signature = tok[9+clen:]
|
|
return claimsPayload, signature, nil
|
|
}
|
|
|
|
// VerifyToken parses tok, verifies its signature against IssuerPublicKey, and
|
|
// checks that the embedded expiry has not passed (using time.Now). On success
|
|
// it returns the parsed Claims.
|
|
func VerifyToken(tok []byte) (*Claims, error) {
|
|
warnIfDevKey()
|
|
return verifyTokenAt(tok, IssuerPublicKey, time.Now())
|
|
}
|
|
|
|
// VerifyTokenWithKey is the same as VerifyToken but allows an explicit public
|
|
// key. Used by tests and by anyone needing to verify tokens against a
|
|
// non-production signer (e.g. a per-tenant issuer).
|
|
func VerifyTokenWithKey(tok []byte, pub ed25519.PublicKey) (*Claims, error) {
|
|
return verifyTokenAt(tok, pub, time.Now())
|
|
}
|
|
|
|
func verifyTokenAt(tok []byte, pub ed25519.PublicKey, now time.Time) (*Claims, error) {
|
|
if len(pub) != ed25519.PublicKeySize {
|
|
return nil, fmt.Errorf("license: pub key size %d != %d", len(pub), ed25519.PublicKeySize)
|
|
}
|
|
payload, sig, err := ParseToken(tok)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ed25519.Verify(pub, payload, sig) {
|
|
return nil, ErrBadSig
|
|
}
|
|
c, err := DecodeClaims(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if c.Expired(now) {
|
|
return c, ErrExpired
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// EncodeBase64 returns the standard-base64 encoding of a binary token, useful
|
|
// for transporting tokens as environment variables.
|
|
func EncodeBase64(tok []byte) string {
|
|
return base64.StdEncoding.EncodeToString(tok)
|
|
}
|
|
|
|
// DecodeBase64 reverses EncodeBase64. Leading and trailing whitespace is
|
|
// tolerated so file-based tokens round-trip cleanly.
|
|
func DecodeBase64(s string) ([]byte, error) {
|
|
s = strings.TrimSpace(s)
|
|
return base64.StdEncoding.DecodeString(s)
|
|
}
|