9737c787f9
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
193 lines
5.1 KiB
Go
193 lines
5.1 KiB
Go
// 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 <prefix>")
|
|
fmt.Fprintln(os.Stderr, "bj-license issue -tenant <name> -plan free|pro|enterprise -days <n> -features a,b -key <priv> [-keyid id] [-max-nodes n] [-note txt]")
|
|
fmt.Fprintln(os.Stderr, "bj-license verify -key-file <license.key> -pub <pubkey.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)
|
|
}
|