// Command bj-license — инструмент издателя: генерация ключей подписи, // выпуск годовых лицензий и проверка. // // bj-license keygen -out ./keys/license // bj-license issue -tenant "ООО Ромашка" -plan pro -days 365 \ // -features updates,web-cabinet -key ./keys/license.priv -keyid main // bj-license verify -key-file license.key -pub ./keys/license.pub package main import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/hex" "fmt" "os" "strings" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/license" ) // newUUID — UUID v4 без внешних зависимостей. func newUUID() string { var b [16]byte _, _ = rand.Read(b[:]) b[6] = (b[6] & 0x0f) | 0x40 // версия 4 b[8] = (b[8] & 0x3f) | 0x80 // вариант h := hex.EncodeToString(b[:]) return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32] } func main() { if len(os.Args) < 2 { usage() } switch os.Args[1] { case "keygen": keygen(os.Args[2:]) case "issue": issue(os.Args[2:]) case "verify": verify(os.Args[2:]) default: usage() } } func usage() { fmt.Fprintln(os.Stderr, "bj-license keygen -out ") fmt.Fprintln(os.Stderr, "bj-license issue -tenant -plan free|pro|enterprise -days -features a,b -key [-keyid id] [-max-nodes n] [-note txt]") fmt.Fprintln(os.Stderr, "bj-license verify -key-file -pub ") os.Exit(2) } func keygen(args []string) { out := "license" for i := 0; i < len(args)-1; i++ { if args[i] == "-out" { out = args[i+1] } } pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { fatal("keygen: %v", err) } if err := os.WriteFile(out+".priv", []byte(base64.StdEncoding.EncodeToString(priv.Seed())+"\n"), 0o600); err != nil { fatal("write priv: %v", err) } if err := os.WriteFile(out+".pub", []byte(base64.StdEncoding.EncodeToString(pub)+"\n"), 0o644); err != nil { fatal("write pub: %v", err) } fmt.Printf("Приватный ключ лицензий: %s.priv (СЕКРЕТ)\n", out) fmt.Printf("Публичный ключ (зашить в bj-server):\n %s\n", base64.StdEncoding.EncodeToString(pub)) } func issue(args []string) { a := parseArgs(args) tenant := a["tenant"] keyPath := a["key"] if tenant == "" || keyPath == "" { fatal("issue: требуются -tenant и -key") } plan := license.Plan(orDefault(a["plan"], "pro")) days := atoiDefault(a["days"], 365) keyID := orDefault(a["keyid"], "main") priv, err := license.LoadPrivateKey(keyPath) if err != nil { fatal("load key: %v", err) } now := time.Now().UTC() var feats []string if a["features"] != "" { feats = strings.Split(a["features"], ",") } l := &license.License{ Schema: license.CurrentSchema, ID: newUUID(), Tenant: tenant, Product: "bj-server", Plan: plan, IssuedAt: now, ExpiresAt: now.AddDate(0, 0, days), Features: feats, MaxNodes: atoiDefault(a["max-nodes"], 0), Note: a["note"], } tok, err := license.Sign(l, priv, keyID) if err != nil { fatal("sign: %v", err) } fmt.Printf("Лицензия выпущена: tenant=%q plan=%s до %s (%d дней)\n", tenant, plan, l.ExpiresAt.Format("02.01.2006"), days) fmt.Printf("ID: %s\n", l.ID) fmt.Println("Ключ для клиента (вставить в bj-server → Лицензия):") fmt.Println(tok.Encode()) } func verify(args []string) { a := parseArgs(args) if a["key-file"] == "" || a["pub"] == "" { fatal("verify: требуются -key-file и -pub") } raw, err := os.ReadFile(a["key-file"]) if err != nil { fatal("read key-file: %v", err) } pubB, err := os.ReadFile(a["pub"]) if err != nil { fatal("read pub: %v", err) } pub, err := license.ParsePublicKey(strings.TrimSpace(string(pubB))) if err != nil { fatal("pub: %v", err) } tok, err := license.DecodeToken(string(raw)) if err != nil { fatal("decode: %v", err) } l, err := license.Verify(tok, pub) if err != nil { fatal("verify: %v", err) } now := time.Now().UTC() fmt.Printf("Подпись валидна. tenant=%q plan=%s\n", l.Tenant, l.Plan) fmt.Printf("Действует: %s — %s (осталось %d дней)\n", l.IssuedAt.Format("02.01.2006"), l.ExpiresAt.Format("02.01.2006"), l.DaysLeft(now)) if err := l.Valid(now); err != nil { fmt.Printf("СТАТУС: %v\n", err) } else { fmt.Printf("СТАТУС: активна, обновления %v\n", l.AllowsUpdates()) } } // --- helpers --- func parseArgs(args []string) map[string]string { m := map[string]string{} for i := 0; i < len(args); i++ { if strings.HasPrefix(args[i], "-") && i+1 < len(args) { m[strings.TrimPrefix(args[i], "-")] = args[i+1] i++ } } return m } func orDefault(s, def string) string { if s == "" { return def } return s } func atoiDefault(s string, def int) int { if s == "" { return def } var n int _, err := fmt.Sscanf(s, "%d", &n) if err != nil { return def } return n } func fatal(format string, a ...any) { fmt.Fprintf(os.Stderr, "bj-license: "+format+"\n", a...) os.Exit(1) }