feat(update): обновления с сервера лицензий по лицензии установки

- по умолчанию https://lic.zetit.ru/updates; клиент артефактории передаёт лицензионный
  токен установки (Authorization: Bearer), сервер отдаёт модули по признакам лицензии
- компоненты получили признак лицензии (ядро и криптосервис — updates, ЛК — web-cabinet,
  НПР — npr, экспорт QUIK — quik-export)
- в bj зашит открытый ключ подписи релизов (закрытый хранится вне сервера лицензий)
- проверено сквозной установкой bj-lk 0.2.0 из канала beta

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-15 20:43:56 +03:00
parent 2908697363
commit 8099e50ab2
4 changed files with 41 additions and 14 deletions
+17 -5
View File
@@ -27,13 +27,16 @@ import (
var BuildVersion = "0.1.0"
// DefaultUpdatePublicKey — публичный ключ артефактории, зашитый в релиз.
// Пустой в исходниках; подставляется при официальной сборке. Если задан в
// Открытая часть ключа подписи релизов. Если задан в
// настройках (UpdateSettings.PublicKey) — приоритет у настроек (для проверки
// в bj; root-установщик верит только /etc/bj/update.pub).
var DefaultUpdatePublicKey = ""
var DefaultUpdatePublicKey = "GHC/gJdQOAEVuBmq8pRpfG+CoIwtcjpNlSf6bPFtr/Y=" // ~/keys/release-signing.pub (закрытый ключ хранится вне сервера лицензий)
const updateStagingDir = "/var/lib/bj/update-staging"
// DefaultUpdateURL — сервер обновлений по умолчанию (встроен в сервер лицензий).
const DefaultUpdateURL = "https://lic.zetit.ru/updates"
type Updater struct{ rc *RuntimeConfig }
func NewUpdater(rc *RuntimeConfig) *Updater { return &Updater{rc: rc} }
@@ -55,14 +58,23 @@ func (u *Updater) updateClient() (*release.Client, error) {
if pub == "" {
pub = DefaultUpdatePublicKey
}
if cfg.BaseURL == "" || pub == "" {
return nil, fmt.Errorf("обновления не настроены (нужны URL артефактории и публичный ключ)")
base := cfg.BaseURL
if base == "" {
base = DefaultUpdateURL
}
if pub == "" {
return nil, fmt.Errorf("обновления не настроены: в этой сборке нет открытого ключа подписи релизов")
}
channel := cfg.Channel
if channel == "" {
channel = "stable"
}
return release.NewClient(cfg.BaseURL, channel, pub)
cl, err := release.NewClient(base, channel, pub)
if err != nil {
return nil, err
}
cl.Token = strings.TrimSpace(u.rc.Snapshot().License.Key) // лицензионный токен установки
return cl, nil
}
// CheckForUpdate — манифест и состояние каждого компонента.
@@ -947,10 +947,10 @@
<div class="card">
<h2><span class="dot {{if .Settings.Update.BaseURL}}ok{{else}}warn{{end}}"></span>Источник обновлений</h2>
<p class="muted">Артефактория раздаёт подписанные релизы. Обновления проверяются по подписи Ed25519 и sha256 — без валидной подписи установка не выполняется.</p>
<p class="muted">Обновления раздаёт сервер лицензий по лицензии этой установки: модуль скачивается, только если он входит в лицензию. Каждый релиз проверяется по подписи Ed25519 и контрольным суммам, без действительной подписи установка не выполняется.</p>
<form method="post" action="/admin/setup/update" style="margin-top:12px;display:grid;gap:10px;max-width:680px">
<label>URL артефактории</label>
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://updates.example.com">
<input type="text" name="base_url" value="{{.Settings.Update.BaseURL}}" placeholder="https://lic.zetit.ru/updates (по умолчанию)">
<label>Канал</label>
<select name="channel">
<option value="stable" {{if eq .Settings.Update.Channel "stable"}}selected{{end}}>stable — стабильный</option>
+15 -2
View File
@@ -20,6 +20,15 @@ type Client struct {
Channel string // "stable" | "beta"
Pub ed25519.PublicKey // публичный ключ издателя (зашит в bj-server)
HTTP *http.Client
// Token — лицензионный токен установки (Authorization: Bearer). Сервер обновлений на
// lic.zetit.ru отдаёт файлы только действующим лицензиям, модули — по признакам лицензии.
Token string
}
func (c *Client) authorize(req *http.Request) {
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
}
}
// NewClient собирает клиент. pubB64 — публичный ключ в base64 (зашитый).
@@ -51,13 +60,15 @@ func (c *Client) FetchSigned(ctx context.Context) (*SignedManifest, *Manifest, e
if err != nil {
return nil, nil, err
}
c.authorize(req)
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("release: запрос манифеста: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("release: манифест HTTP %d", resp.StatusCode)
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return nil, nil, fmt.Errorf("release: манифест HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(msg)))
}
var sm SignedManifest
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&sm); err != nil {
@@ -76,13 +87,15 @@ func (c *Client) DownloadArtifact(ctx context.Context, a Artifact, destDir strin
if err != nil {
return "", err
}
c.authorize(req)
resp, err := c.HTTP.Do(req)
if err != nil {
return "", fmt.Errorf("release: скачивание %s: %w", a.Name, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("release: %s HTTP %d", a.Name, resp.StatusCode)
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return "", fmt.Errorf("release: %s HTTP %d: %s", a.Name, resp.StatusCode, strings.TrimSpace(string(msg)))
}
tmp, err := os.CreateTemp(destDir, "."+a.File+".dl-*")
+7 -5
View File
@@ -21,15 +21,17 @@ type Component struct {
Unit string // служба systemd, перезапускаемая после установки ("" — без перезапуска)
Exec bool // исполняемый файл
Core bool // ядро: ставится всегда; модули — только если уже установлены на этом сервере
// Feature — признак лицензии, без которого сервер обновлений не отдаёт файл компонента.
Feature string
}
// Components — все компоненты bj. Порядок — порядок показа и установки (ядро перезапускается последним).
var Components = []Component{
{Name: "bj-server", Title: "Ядро bj: переводы M2M, DEPO, QUIK МНП, ЛК ЕСИА", Path: "/opt/bj/bj-server", Unit: "bj-server", Exec: true, Core: true},
{Name: "crypto-service.jar", Title: "Криптосервис", Path: "/opt/bj/crypto-service.jar", Unit: "bj-crypto"},
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true},
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true},
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true},
{Name: "bj-server", Title: "Ядро bj: переводы M2M, DEPO, QUIK МНП, ЛК ЕСИА", Path: "/opt/bj/bj-server", Unit: "bj-server", Exec: true, Core: true, Feature: "updates"},
{Name: "crypto-service.jar", Title: "Криптосервис", Path: "/opt/bj/crypto-service.jar", Unit: "bj-crypto", Feature: "updates"},
{Name: "bj-lk", Title: "Личный кабинет клиента", Path: "/opt/bj/bj-lk", Unit: "bj-lk", Exec: true, Feature: "web-cabinet"},
{Name: "bj-npr", Title: "Риск-монитор НПР / УДС", Path: "/opt/bj/bj-npr", Unit: "bj-npr", Exec: true, Feature: "npr"},
{Name: "bj-quikexport", Title: "Экспорт QUIK", Path: "/opt/bj/bj-quikexport", Unit: "bj-quikexport", Exec: true, Feature: "quik-export"},
}
// ComponentByName — компонент по имени артефакта.