// Command bj-release — инструмент издателя: генерация ключей подписи, // сборка манифеста релиза из каталога артефактов и его подпись Ed25519. // // Использование: // // bj-release keygen -out ./keys/signing // → создаёт signing.priv (base64 seed) и signing.pub (base64 pubkey) // // bj-release build -dir ./dist -version 1.2.0 -channel stable \ // -key ./keys/signing.priv -keyid main -out ./dist/manifest.json // → хеширует все файлы в ./dist, собирает Manifest, подписывает, // пишет SignedManifest в manifest.json // // Манифест подписывается целиком; клиент (bj-server auto-update) проверяет // подпись зашитым публичным ключом ДО доверия версиям/хешам. package main import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/json" "flag" "fmt" "os" "path/filepath" "sort" "strings" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release" ) func main() { if len(os.Args) < 2 { usage() } switch os.Args[1] { case "keygen": keygen(os.Args[2:]) case "build": build(os.Args[2:]) default: usage() } } func usage() { fmt.Fprintln(os.Stderr, "bj-release keygen -out ") fmt.Fprintln(os.Stderr, "bj-release build -dir -version -channel -key -keyid -out [-notes ]") os.Exit(2) } func keygen(args []string) { fs := flag.NewFlagSet("keygen", flag.ExitOnError) out := fs.String("out", "signing", "префикс файлов ключей (создаст .priv и .pub)") _ = fs.Parse(args) pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { fatal("keygen: %v", err) } seed := priv.Seed() if err := os.WriteFile(*out+".priv", []byte(base64.StdEncoding.EncodeToString(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("Публичный ключ: %s.pub\n", *out) fmt.Printf("Публичный ключ (зашить в bj-server):\n %s\n", base64.StdEncoding.EncodeToString(pub)) } func build(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) dir := fs.String("dir", "./dist", "каталог с артефактами") version := fs.String("version", "", "версия релиза, напр. 1.2.0") channel := fs.String("channel", "stable", "канал: stable|beta") keyPath := fs.String("key", "", "путь к приватному ключу (base64 seed)") keyID := fs.String("keyid", "main", "идентификатор ключа") out := fs.String("out", "", "путь для записи manifest.json (по умолчанию /manifest.json)") notes := fs.String("notes", "", "заметки к релизу") _ = fs.Parse(args) if *version == "" || *keyPath == "" { fatal("build: требуются -version и -key") } if *out == "" { *out = filepath.Join(*dir, "manifest.json") } priv, err := release.LoadPrivateKey(*keyPath) if err != nil { fatal("load key: %v", err) } // Имена артефактов, которые издаём (логическое имя → ставить +x). known := map[string]bool{ "bj-server": true, // Go-бинарь "crypto-service.jar": false, // Java сайдкар "install-validata.sh": true, "install.sh": true, "configure-ish.sql": false, } entries, err := os.ReadDir(*dir) if err != nil { fatal("read dir: %v", err) } var arts []release.Artifact for _, e := range entries { if e.IsDir() || e.Name() == "manifest.json" { continue } full := filepath.Join(*dir, e.Name()) sha, size, err := release.HashFile(full) if err != nil { fatal("hash %s: %v", e.Name(), err) } exec, ok := known[e.Name()] if !ok { // неизвестный файл — включаем, +x по расширению exec = strings.HasSuffix(e.Name(), ".sh") } arts = append(arts, release.Artifact{ Name: e.Name(), File: e.Name(), Version: *version, SHA256: sha, Size: size, Exec: exec, }) } sort.Slice(arts, func(i, j int) bool { return arts[i].Name < arts[j].Name }) if len(arts) == 0 { fatal("build: в каталоге %s нет артефактов", *dir) } m := &release.Manifest{ Schema: release.CurrentSchema, Version: *version, Channel: *channel, ReleasedAt: time.Now().UTC(), Notes: *notes, Artifacts: arts, } sm, err := release.Sign(m, priv, *keyID) if err != nil { fatal("sign: %v", err) } b, err := json.MarshalIndent(sm, "", " ") if err != nil { fatal("marshal: %v", err) } if err := os.WriteFile(*out, b, 0o644); err != nil { fatal("write manifest: %v", err) } fmt.Printf("Манифест %s: версия %s, канал %s, артефактов %d, подписан ключом %s\n", *out, *version, *channel, len(arts), *keyID) for _, a := range arts { fmt.Printf(" %-22s %10d B %s\n", a.Name, a.Size, a.SHA256[:16]) } } func fatal(format string, a ...any) { fmt.Fprintf(os.Stderr, "bj-release: "+format+"\n", a...) os.Exit(1) }