feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура 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>
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// 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 <prefix>")
|
||||
fmt.Fprintln(os.Stderr, "bj-release build -dir <artifacts> -version <v> -channel <c> -key <priv> -keyid <id> -out <manifest.json> [-notes <txt>]")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func keygen(args []string) {
|
||||
fs := flag.NewFlagSet("keygen", flag.ExitOnError)
|
||||
out := fs.String("out", "signing", "префикс файлов ключей (создаст <out>.priv и <out>.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 (по умолчанию <dir>/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)
|
||||
}
|
||||
Reference in New Issue
Block a user