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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user