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:
zuevav
2026-06-19 00:03:21 +03:00
parent 6e503433d4
commit 9737c787f9
110 changed files with 10771 additions and 1690 deletions
+25 -42
View File
@@ -12,12 +12,14 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// caCertsDir — куда складываются скачанные сертификаты УЦ.
const caCertsDir = "/var/lib/bj/ca-certs"
// defaultNSDCAURLs — список URL для авто-загрузки сертификатов УЦ НРД.
// Эти URL пользователь может скорректировать в /admin/setup → «Сертификаты
// УЦ» (раздел появляется после первого сохранения настроек). На сайте НРД
@@ -31,8 +33,8 @@ var defaultNSDCAURLs = []string{
// нужные ссылки в UI после того, как уточните URL у НРД.
}
// FetchCACertificates скачивает все URL из настроек, парсит .cer, и при
// успехе вызывает certmgr -inst -store mroot. Если передан rc — на каждое
// FetchCACertificates скачивает все URL из настроек, парсит .cer и
// сохраняет файл в /var/lib/bj/ca-certs/. Если передан rc — на каждое
// фактическое изменение сертификата (новый или изменился SHA-256)
// публикуется новость в ленту через rc.AddNews. На сертификаты,
// истекающие в ближайшие 14 дней — отдельная новость-предупреждение.
@@ -68,13 +70,13 @@ func FetchCACertificates(ctx context.Context, s CACertsSettings, rc *RuntimeConf
fc.IssuerCN = cert.Issuer.CommonName
fc.NotAfter = cert.NotAfter
fc.SHA256 = hex.EncodeToString(sha256Bytes(der))
// УЦ-сертификаты с самоподписью (Issuer == Subject) идут в mroot,
// промежуточные — в uRoot.
store := "uRoot"
// Корневые (Issuer == Subject) и промежуточные складываем рядом,
// в общую папку /var/lib/bj/ca-certs/.
kind := "intermediate"
if cert.Subject.CommonName == cert.Issuer.CommonName {
store = "mroot"
kind = "root"
}
fc.Store = store
fc.Store = kind
// Дедуп: если sha256 совпадает с уже импортированным — пропускаем
// сам импорт (но фиксируем что проверили).
@@ -91,7 +93,7 @@ func FetchCACertificates(ctx context.Context, s CACertsSettings, rc *RuntimeConf
continue
}
// Импорт через certmgr.
// Сохраняем DER на диск в /var/lib/bj/ca-certs/<sha>.cer.
isNew := true
for _, old := range s.FetchedCerts {
if old.URL == u && old.Error == "" {
@@ -99,22 +101,22 @@ func FetchCACertificates(ctx context.Context, s CACertsSettings, rc *RuntimeConf
break
}
}
if err := importCertToStore(ctx, der, store); err != nil {
fc.Error = "certmgr: " + err.Error()
fmt.Fprintf(&logBuf, "%s — certmgr упал: %s\n", u, err)
if err := saveCertToDir(der, fc.SHA256); err != nil {
fc.Error = "save: " + err.Error()
fmt.Fprintf(&logBuf, "%s — сохранить не удалось: %s\n", u, err)
if rc != nil {
_ = rc.AddNews(NewsItem{
ID: "ca-error-" + fc.SHA256[:12],
At: now,
Kind: "system",
Title: "Не удалось импортировать сертификат УЦ",
Title: "Не удалось сохранить сертификат УЦ",
Body: "URL: " + u + "\nCN: " + fc.SubjectCN + "\nОшибка: " + err.Error(),
URL: u,
})
}
} else {
fmt.Fprintf(&logBuf, "%s — импортирован в %s (CN=%s, sha256=%s...)\n",
u, store, fc.SubjectCN, fc.SHA256[:12])
fmt.Fprintf(&logBuf, "%s — сохранён (%s, CN=%s, sha256=%s...)\n",
u, kind, fc.SubjectCN, fc.SHA256[:12])
if rc != nil {
kindTitle := "Обновлён сертификат УЦ"
if isNew {
@@ -125,8 +127,8 @@ func FetchCACertificates(ctx context.Context, s CACertsSettings, rc *RuntimeConf
At: now,
Kind: "feature",
Title: kindTitle + ": " + fc.SubjectCN,
Body: fmt.Sprintf("Хранилище: %s\nИздатель: %s\nДействителен до: %s\nSHA-256: %s…\nURL источника: %s",
store, fc.IssuerCN, fc.NotAfter.Format("02.01.2006"), fc.SHA256[:16], u),
Body: fmt.Sprintf("Тип: %s\nИздатель: %s\nДействителен до: %s\nSHA-256: %s…\nURL источника: %s",
kind, fc.IssuerCN, fc.NotAfter.Format("02.01.2006"), fc.SHA256[:16], u),
URL: u,
ValidTo: fc.NotAfter,
})
@@ -198,29 +200,13 @@ func downloadAndParseCert(ctx context.Context, rawURL string) ([]byte, error) {
return data, nil
}
// importCertToStore вызывает certmgr -inst -store <store> -file <tmp>.
func importCertToStore(ctx context.Context, der []byte, store string) error {
const certmgr = "/opt/cprocsp/bin/amd64/certmgr"
if _, err := os.Stat(certmgr); err != nil {
return fmt.Errorf("certmgr не найден (КриптоПро CSP не установлен?): %w", err)
}
tmp, err := os.CreateTemp("", "bj-ca-*.cer")
if err != nil {
// saveCertToDir сохраняет DER-байты в /var/lib/bj/ca-certs/<sha>.cer.
func saveCertToDir(der []byte, sha256hex string) error {
if err := os.MkdirAll(caCertsDir, 0o755); err != nil {
return err
}
defer os.Remove(tmp.Name())
if _, err := tmp.Write(der); err != nil {
tmp.Close()
return err
}
tmp.Close()
cmd := exec.CommandContext(ctx, certmgr, "-inst", "-store", store, "-file", tmp.Name())
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%w / %s", err, strings.TrimSpace(string(out)))
}
return nil
dst := filepath.Join(caCertsDir, sha256hex+".cer")
return os.WriteFile(dst, der, 0o644)
}
// StartCACertsAutoUpdater запускает горутину, которая раз в сутки
@@ -312,7 +298,4 @@ func (h *setupHandlers) fetchCACertsNow(w http.ResponseWriter, r *http.Request)
// caCertsTemplateString — компактный URL для отображения в UI.
func caCertsTemplateString(s CACertsSettings) string {
return strings.Join(s.URLs, "\n")
}
// доп. защита от пустых импортов (linter)
var _ = filepath.Join
}