chore: initial commit
CLI to mint RS256 customer JWTs for the YAMA License Server. Thin shell over licensing.Issue() in the YAMA Go server; consumed via local replace directive during development. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
134
main.go
Normal file
134
main.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// yama-issue-token mints customer JWTs (RS256) for the YAMA License Server.
|
||||
//
|
||||
// The tool is a thin shell around licensing.Issue() — all validation
|
||||
// (tier rules, ttl floor, sub required) is enforced there, so this binary
|
||||
// stays in sync as the licensing package evolves.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/licensing"
|
||||
)
|
||||
|
||||
const usage = `yama-issue-token — sign a customer JWT for the YAMA License Server.
|
||||
|
||||
Usage:
|
||||
yama-issue-token -priv <license_priv.pem> -sub <customer-id> [options]
|
||||
|
||||
Required:
|
||||
-priv RSA private key PEM (PKCS#1 or PKCS#8). Paired with the public
|
||||
key the License Server loads via YAMA_LICENSE_PUBLIC_KEY.
|
||||
-sub Customer ID — unique string, lands in the JWT "sub" claim.
|
||||
Used by the License Server to track per-customer quotas.
|
||||
|
||||
Options:
|
||||
-tier trial | paid (default: trial)
|
||||
-max Max concurrent devices (trial default: 20;
|
||||
paid: REQUIRED, no default)
|
||||
-days Token TTL in days (default: 365; minimum: 1 hour)
|
||||
-out Write token to this file (0600) instead of stdout
|
||||
|
||||
Output:
|
||||
The signed JWT is printed to stdout (single line, no trailing whitespace),
|
||||
so you can pipe it cleanly. Issuance details (sub/tier/max/expires) go to
|
||||
stderr and are visible even when stdout is redirected.
|
||||
|
||||
Examples:
|
||||
# Trial customer, 5 devices, 30 days, print to terminal
|
||||
yama-issue-token -priv license_priv.pem -sub acme-trial -tier trial -max 5 -days 30
|
||||
|
||||
# Paid customer, 200 devices, 1 year, write to file
|
||||
yama-issue-token -priv license_priv.pem -sub acme-corp -tier paid -max 200 -out token-acme.jwt
|
||||
|
||||
# Pipe into env var (Linux/macOS shell)
|
||||
export YAMA_LICENSE_TOKEN=$(yama-issue-token -priv ... -sub ... -tier paid -max 100)
|
||||
`
|
||||
|
||||
func main() {
|
||||
var (
|
||||
priv = flag.String("priv", "", "")
|
||||
sub = flag.String("sub", "", "")
|
||||
tier = flag.String("tier", "trial", "")
|
||||
max = flag.Int("max", 0, "")
|
||||
days = flag.Int("days", 365, "")
|
||||
out = flag.String("out", "", "")
|
||||
)
|
||||
flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
|
||||
flag.Parse()
|
||||
|
||||
if *priv == "" || *sub == "" {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if *days <= 0 {
|
||||
fail("error: -days must be > 0 (got %d)\n", *days)
|
||||
}
|
||||
|
||||
key, err := loadRSAPrivateKey(*priv)
|
||||
if err != nil {
|
||||
fail("error: %v\n", err)
|
||||
}
|
||||
|
||||
ttl := time.Duration(*days) * 24 * time.Hour
|
||||
tok, err := licensing.Issue(key, *sub, *tier, *max, ttl)
|
||||
if err != nil {
|
||||
fail("error: %v\n", err)
|
||||
}
|
||||
|
||||
// Resolve the effective max (licensing.Issue defaults trial→20 silently).
|
||||
effectiveMax := *max
|
||||
if effectiveMax == 0 && *tier == licensing.TierTrial {
|
||||
effectiveMax = licensing.TrialMaxDevices
|
||||
}
|
||||
fmt.Fprintf(os.Stderr,
|
||||
"issued: sub=%s tier=%s max_devices=%d ttl=%dd expires=%s\n",
|
||||
*sub, *tier, effectiveMax, *days,
|
||||
time.Now().Add(ttl).UTC().Format(time.RFC3339))
|
||||
|
||||
if *out != "" {
|
||||
if err := os.WriteFile(*out, []byte(tok+"\n"), 0o600); err != nil {
|
||||
fail("error writing %s: %v\n", *out, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "wrote: %s (0600)\n", *out)
|
||||
return
|
||||
}
|
||||
fmt.Println(tok)
|
||||
}
|
||||
|
||||
// loadRSAPrivateKey accepts both `openssl genrsa` output (PKCS#1, header
|
||||
// `-----BEGIN RSA PRIVATE KEY-----`) and `openssl genpkey -algorithm RSA`
|
||||
// output (PKCS#8, header `-----BEGIN PRIVATE KEY-----`).
|
||||
func loadRSAPrivateKey(path string) (*rsa.PrivateKey, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("no PEM block found in %s", path)
|
||||
}
|
||||
if k, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
|
||||
return k, nil
|
||||
}
|
||||
if pk, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil {
|
||||
rsaKey, ok := pk.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("PKCS#8 key is not RSA")
|
||||
}
|
||||
return rsaKey, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse %s as RSA private key (tried PKCS#1 and PKCS#8)", path)
|
||||
}
|
||||
|
||||
func fail(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user