// 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 -sub [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) }