Files
zeekayandHanzo Dev d2c8a33038 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers: slices.Contains and range over int. omitzero is not applied,
since it changes what the JSON says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:14:48 -07:00

261 lines
6.8 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package license
import (
"encoding/binary"
"errors"
"fmt"
"io"
"slices"
"sort"
"time"
)
// Claims is the entitlement set carried inside a license token.
//
// Wire layout (big-endian, length-prefixed, deterministic field order):
//
// version uint8 (=1)
// customer_id str (uint16-len-prefixed utf-8)
// license_id str
// issued_at int64 (unix seconds)
// expiry int64 (unix seconds; 0 = never expires)
// scope_count uint16
// scope[i] str (sorted ascending, no duplicates)
// feat_count uint16
// feat[i].key str (sorted ascending)
// feat[i].val str (free-form; integers/bools are encoded as decimal/"true"/"false")
// notes str
//
// The encoding is deterministic so that a token round-trips byte-for-byte and
// can be re-verified against its embedded signature.
type Claims struct {
CustomerID string
LicenseID string
IssuedAt time.Time
Expiry time.Time // zero value = no expiry
Scope []string
Features map[string]string
Notes string
}
const claimsVersion uint8 = 1
// HasScope returns true if scope is among the claim's authorized scopes.
func (c *Claims) HasScope(scope string) bool {
return slices.Contains(c.Scope, scope)
}
// Expired reports whether the claim's Expiry is non-zero and in the past
// relative to now.
func (c *Claims) Expired(now time.Time) bool {
return !c.Expiry.IsZero() && now.After(c.Expiry)
}
// Encode serialises the claims into the deterministic wire format used for
// signing and verification.
func (c *Claims) Encode() ([]byte, error) {
if c.CustomerID == "" {
return nil, errors.New("license: claims.CustomerID is required")
}
if c.LicenseID == "" {
return nil, errors.New("license: claims.LicenseID is required")
}
if c.IssuedAt.IsZero() {
return nil, errors.New("license: claims.IssuedAt is required")
}
// Sort scopes and features to keep the encoding deterministic.
scopes := append([]string(nil), c.Scope...)
sort.Strings(scopes)
for i := 1; i < len(scopes); i++ {
if scopes[i] == scopes[i-1] {
return nil, fmt.Errorf("license: duplicate scope %q", scopes[i])
}
}
featKeys := make([]string, 0, len(c.Features))
for k := range c.Features {
featKeys = append(featKeys, k)
}
sort.Strings(featKeys)
w := newEncoder()
w.writeUint8(claimsVersion)
w.writeString(c.CustomerID)
w.writeString(c.LicenseID)
w.writeInt64(c.IssuedAt.Unix())
if c.Expiry.IsZero() {
w.writeInt64(0)
} else {
w.writeInt64(c.Expiry.Unix())
}
if len(scopes) > 0xFFFF {
return nil, errors.New("license: too many scopes (max 65535)")
}
w.writeUint16(uint16(len(scopes)))
for _, s := range scopes {
w.writeString(s)
}
if len(featKeys) > 0xFFFF {
return nil, errors.New("license: too many features (max 65535)")
}
w.writeUint16(uint16(len(featKeys)))
for _, k := range featKeys {
w.writeString(k)
w.writeString(c.Features[k])
}
w.writeString(c.Notes)
return w.bytes(), nil
}
// DecodeClaims parses the binary claims payload (without the surrounding
// signature framing). Use ParseToken / VerifyToken for the framed, signed form.
func DecodeClaims(b []byte) (*Claims, error) {
r := newDecoder(b)
v, err := r.readUint8()
if err != nil {
return nil, fmt.Errorf("license: read version: %w", err)
}
if v != claimsVersion {
return nil, fmt.Errorf("license: unsupported claims version %d", v)
}
var c Claims
if c.CustomerID, err = r.readString(); err != nil {
return nil, fmt.Errorf("license: read customer_id: %w", err)
}
if c.LicenseID, err = r.readString(); err != nil {
return nil, fmt.Errorf("license: read license_id: %w", err)
}
iat, err := r.readInt64()
if err != nil {
return nil, fmt.Errorf("license: read issued_at: %w", err)
}
c.IssuedAt = time.Unix(iat, 0).UTC()
exp, err := r.readInt64()
if err != nil {
return nil, fmt.Errorf("license: read expiry: %w", err)
}
if exp != 0 {
c.Expiry = time.Unix(exp, 0).UTC()
}
scopeCount, err := r.readUint16()
if err != nil {
return nil, fmt.Errorf("license: read scope_count: %w", err)
}
c.Scope = make([]string, 0, scopeCount)
for i := range scopeCount {
s, err := r.readString()
if err != nil {
return nil, fmt.Errorf("license: read scope[%d]: %w", i, err)
}
c.Scope = append(c.Scope, s)
}
featCount, err := r.readUint16()
if err != nil {
return nil, fmt.Errorf("license: read feat_count: %w", err)
}
if featCount > 0 {
c.Features = make(map[string]string, featCount)
}
for i := range featCount {
k, err := r.readString()
if err != nil {
return nil, fmt.Errorf("license: read feat[%d].key: %w", i, err)
}
v, err := r.readString()
if err != nil {
return nil, fmt.Errorf("license: read feat[%d].val: %w", i, err)
}
c.Features[k] = v
}
if c.Notes, err = r.readString(); err != nil {
return nil, fmt.Errorf("license: read notes: %w", err)
}
if r.remaining() != 0 {
return nil, fmt.Errorf("license: trailing %d bytes after claims", r.remaining())
}
return &c, nil
}
// ---------------------------------------------------------------------------
// internal encoder / decoder (length-prefixed primitives, big-endian)
type encoder struct{ buf []byte }
func newEncoder() *encoder { return &encoder{buf: make([]byte, 0, 256)} }
func (e *encoder) bytes() []byte { return e.buf }
func (e *encoder) writeUint8(v uint8) { e.buf = append(e.buf, v) }
func (e *encoder) writeUint16(v uint16) { e.buf = binary.BigEndian.AppendUint16(e.buf, v) }
func (e *encoder) writeInt64(v int64) { e.buf = binary.BigEndian.AppendUint64(e.buf, uint64(v)) }
func (e *encoder) writeString(s string) {
if len(s) > 0xFFFF {
// Caller responsibility; truncate would be silent corruption.
panic(fmt.Sprintf("license: string too long (%d bytes, max 65535)", len(s)))
}
e.buf = binary.BigEndian.AppendUint16(e.buf, uint16(len(s)))
e.buf = append(e.buf, s...)
}
type decoder struct {
buf []byte
pos int
}
func newDecoder(b []byte) *decoder { return &decoder{buf: b} }
func (d *decoder) remaining() int { return len(d.buf) - d.pos }
func (d *decoder) readUint8() (uint8, error) {
if d.remaining() < 1 {
return 0, io.ErrUnexpectedEOF
}
v := d.buf[d.pos]
d.pos++
return v, nil
}
func (d *decoder) readUint16() (uint16, error) {
if d.remaining() < 2 {
return 0, io.ErrUnexpectedEOF
}
v := binary.BigEndian.Uint16(d.buf[d.pos:])
d.pos += 2
return v, nil
}
func (d *decoder) readInt64() (int64, error) {
if d.remaining() < 8 {
return 0, io.ErrUnexpectedEOF
}
v := int64(binary.BigEndian.Uint64(d.buf[d.pos:]))
d.pos += 8
return v, nil
}
func (d *decoder) readString() (string, error) {
l, err := d.readUint16()
if err != nil {
return "", err
}
if d.remaining() < int(l) {
return "", io.ErrUnexpectedEOF
}
s := string(d.buf[d.pos : d.pos+int(l)])
d.pos += int(l)
return s, nil
}