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,110 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client — клиент артефактории: скачивает подписанный манифест и артефакты,
|
||||
// проверяет подпись зашитым публичным ключом и sha256 каждого файла.
|
||||
type Client struct {
|
||||
BaseURL string // напр. https://updates.example.com
|
||||
Channel string // "stable" | "beta"
|
||||
Pub ed25519.PublicKey // публичный ключ издателя (зашит в bj-server)
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewClient собирает клиент. pubB64 — публичный ключ в base64 (зашитый).
|
||||
func NewClient(baseURL, channel, pubB64 string) (*Client, error) {
|
||||
pub, err := ParsePublicKey(pubB64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release: публичный ключ: %w", err)
|
||||
}
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
Channel: channel,
|
||||
Pub: pub,
|
||||
HTTP: &http.Client{Timeout: 60 * time.Second},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FetchManifest скачивает /v1/<channel>/manifest.json и проверяет подпись.
|
||||
// Возвращает доверенный Manifest (или ошибку, если подпись не сошлась).
|
||||
func (c *Client) FetchManifest(ctx context.Context) (*Manifest, error) {
|
||||
url := fmt.Sprintf("%s/v1/%s/manifest.json", c.BaseURL, c.Channel)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("release: запрос манифеста: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("release: манифест HTTP %d", resp.StatusCode)
|
||||
}
|
||||
var sm SignedManifest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&sm); err != nil {
|
||||
return nil, fmt.Errorf("release: разбор манифеста: %w", err)
|
||||
}
|
||||
return Verify(&sm, c.Pub) // здесь проверяется подпись
|
||||
}
|
||||
|
||||
// DownloadArtifact скачивает артефакт в destDir, проверяет sha256/размер по
|
||||
// манифесту, выставляет +x при необходимости. Возвращает путь к файлу.
|
||||
// Скачивает во временный файл и переименовывает атомарно.
|
||||
func (c *Client) DownloadArtifact(ctx context.Context, a Artifact, destDir string) (string, error) {
|
||||
url := fmt.Sprintf("%s/v1/%s/files/%s", c.BaseURL, c.Channel, a.File)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(destDir, "."+a.File+".dl-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName) // если что-то пойдёт не так — подчистим
|
||||
|
||||
if _, err := io.Copy(tmp, resp.Body); err != nil {
|
||||
tmp.Close()
|
||||
return "", err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Проверка целостности по манифесту (подпись манифеста уже проверена).
|
||||
if err := VerifyArtifact(tmpName, a); err != nil {
|
||||
return "", err
|
||||
}
|
||||
mode := os.FileMode(0o644)
|
||||
if a.Exec {
|
||||
mode = 0o755
|
||||
}
|
||||
if err := os.Chmod(tmpName, mode); err != nil {
|
||||
return "", err
|
||||
}
|
||||
final := filepath.Join(destDir, a.File)
|
||||
if err := os.Rename(tmpName, final); err != nil { // атомарно в пределах destDir
|
||||
return "", err
|
||||
}
|
||||
return final, nil
|
||||
}
|
||||
Reference in New Issue
Block a user