Мастер по порядку без пропусков; шаг 5 — ключ организации и автообновление сертификатов НРД; в образ кладутся все файлы ядра

- мастер запоминает пройденный шаг и продолжает со следующего, выполненные шаги показываются с пояснением;
- шаг 5: мастер установки ключа + описание автоматического ведения сертификатов НРД (контуры не смешиваются);
- образ: bj-installer и crypto-service.jar в комплекте; установщик пропускает недостающие файлы, кроме ядра.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-24 18:10:10 +03:00
parent 9ed5b037a8
commit ce98ca137f
4 changed files with 55 additions and 91 deletions
+6 -1
View File
@@ -634,7 +634,12 @@ func (in *installer) componentsFromBundle() (string, error) {
default:
src := filepath.Join(in.bundle, art.File)
if _, err := os.Stat(src); err != nil {
return "", fmt.Errorf("в релизе нет файла %s", art.File)
if c.Name == "bj-server" {
return "", fmt.Errorf("в релизе нет файла %s", art.File)
}
// в образ кладётся не всё: недостающее поставит первое обновление после ввода ключа
skipped = append(skipped, c.Title+" (поставится обновлением)")
continue
}
body, err := os.ReadFile(src)
if err != nil {
+1 -1
View File
@@ -31,7 +31,7 @@ echo "$CHANNEL" > "$WORK/bj/channel"; echo "$VERSION" > "$WORK/bj/version"
# ядро bj: подписанный манифест + bj-server, bj-component-install
[ -s "$BUNDLE/manifest.json" ] || { echo "в $BUNDLE нет manifest.json"; exit 1; }
cp "$BUNDLE/manifest.json" "$WORK/bj/bundle/"
FILES=$(python3 -c 'import json,sys,base64;s=json.load(open(sys.argv[1]));m=json.loads(base64.b64decode(s["payload"]));print(" ".join(a["file"] for a in m["artifacts"] if a["name"] in ("bj-server","bj-component-install")))' "$BUNDLE/manifest.json")
FILES=$(python3 -c 'import json,sys,base64;s=json.load(open(sys.argv[1]));m=json.loads(base64.b64decode(s["payload"]));print(" ".join(a["file"] for a in m["artifacts"] if a["name"] in ("bj-server","bj-component-install","bj-installer","crypto-service.jar")))' "$BUNDLE/manifest.json")
[ -n "$FILES" ] || { echo "в релизе нет компонентов ядра"; exit 1; }
for f in $FILES; do cp "$BUNDLE/$f" "$WORK/bj/bundle/"; done
+26 -44
View File
@@ -238,30 +238,21 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
d.Done.NSD = s.NSD.IGWBaseURL != "" && s.NSD.Profile != ""
d.Done.TestRun = s.LastTest != nil || (!d.DevMode && d.Done.NSD) // вне режима разработки тестовой заявки нет
// Определяем текущий шаг.
step := 1
// Мастер идёт по порядку: каждый шаг показывается, даже если он уже выполнен (установщиком,
// лицензией) — на нём написано, что сделано, и пользователь сам нажимает «Далее». Без ?step
// продолжаем со следующего за последним пройденным шагом.
progress := wizardProgress()
step := progress + 1
if v := r.URL.Query().Get("step"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 1 && n <= 7 {
step = n
if n-1 > progress {
setWizardProgress(n - 1)
}
}
} else {
// Авто: первый незавершённый.
switch {
case !d.Done.License:
step = 1
case !d.Done.Postgres:
step = 2
case !d.Done.Depo:
step = 3
case !d.Done.Crypto:
step = 4
case !d.Done.Certs:
step = 5
case !d.Done.NSD:
step = 6
default:
step = 7
}
}
if step > 7 {
step = 7
}
d.Step = step
if step == 1 && d.Done.License {
@@ -273,35 +264,26 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) {
}
cancel()
}
d.AutoDone = wizardAutoDone(&d)
render(w, h.tpl.a.wizard, d)
}
// wizardAutoDone — пояснения к шагам до текущего, которые уже выполнены: мастер переходит к первому
// незавершённому шагу сам, и без пояснения непонятно, почему предыдущие отмечены галочкой.
func wizardAutoDone(d *WizardData) []string {
var out []string
if d.Step > 1 && d.Done.License {
out = append(out, "1. Лицензия — активирована: "+d.Lic.Tenant+".")
// wizardProgressFile — номер последнего пройденного шага мастера (переход «Далее» на шаг N
// означает, что шаги до N пройдены).
const wizardProgressFile = "/var/lib/bj/.bj/wizard-progress"
func wizardProgress() int {
b, err := os.ReadFile(wizardProgressFile)
if err != nil {
return 0
}
if d.Step > 2 && d.Done.Postgres {
t := "2. База bj — готова"
if d.PGSummary != "" {
t += " (" + d.PGSummary + ")"
}
out = append(out, t+": её создал установщик, настраивать ничего не нужно.")
n, _ := strconv.Atoi(strings.TrimSpace(string(b)))
return n
}
func setWizardProgress(n int) {
if err := os.WriteFile(wizardProgressFile, []byte(strconv.Itoa(n)+"\n"), 0o600); err != nil {
log.Printf("wizard: прогресс мастера не сохранён: %v", err)
}
if d.Step > 3 && d.Done.Depo {
if !d.DepoLicensed {
out = append(out, "3. Учёт DEPO — не нужен: модуль обмена с DEPO и QUIK не входит в лицензию. Когда модуль добавят, шаг станет доступен.")
} else {
out = append(out, "3. Учёт DEPO — подключение уже сохранено ("+d.Settings.Fancy.Server+").")
}
}
if d.Step > 4 && d.Done.Crypto && d.ValidataVer != "" {
out = append(out, "4. Валидата — установлена вместе с bj (версия "+d.ValidataVer+").")
}
return out
}
// dsnSummary — «host:port, база name» из строки подключения PostgreSQL, без логина и пароля.
@@ -41,11 +41,7 @@
<div class="wiz-card">
{{if .AutoDone}}<div class="card" style="border-left:3px solid var(--ok)">
<p style="margin:0 0 6px;font-weight:600">Уже готово — эти шаги пропущены, делать там ничего не нужно:</p>
{{range .AutoDone}}<p style="margin:2px 0;font-size:14px">✓ {{.}}</p>{{end}}
<p class="muted" style="margin:6px 0 0;font-size:12px">Открыть любой шаг можно кнопками «← К шагу» внизу.</p>
</div>{{end}}
{{/* ============= ШАГ 1: Лицензия ============= */}}
{{if eq .Step 1}}
@@ -57,7 +53,7 @@
{{if .Done.License}}<p>✓ Лицензия активна: <strong>{{.Lic.Tenant}}</strong>, план {{.Lic.Plan}}, модули: {{if .Lic.Features}}{{range $i, $f := .Lic.Features}}{{if $i}}, {{end}}{{$f}}{{end}}{{else}}только ядро{{end}}; {{.Lic.Message}}</p>{{end}}
{{with .Update}}{{if .HasUpdate}}
<div style="border:1px solid var(--accent);border-radius:8px;padding:12px 14px;margin-top:12px;background:var(--accent-weak)">
<p style="margin:0 0 6px;font-weight:600">Есть обновление: версия {{.Available}}</p>
<p style="margin:0 0 6px;font-weight:600">На сервере лицензий есть что поставить (выпуск {{.Available}})</p>
<p class="muted" style="margin:0 0 10px;font-size:13px">Образ установил ту версию bj, с которой был собран. На сервере лицензий есть более новая — с исправлениями ядра, установщика и служб. Поставьте её сейчас, до настройки: bj перезапустится примерно на 10 секунд, затем обновите страницу.<br>{{.Message}}</p>
<form method="post" action="/admin/setup/update/apply" style="margin:0">
<input type="hidden" name="return_to" value="/admin/wizard?step=1">
@@ -107,7 +103,7 @@
{{/* ============= ШАГ 1: PostgreSQL ============= */}}
{{if eq .Step 2}}
<div class="card">
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>Шаг 2. PostgreSQL</h2>
<h2><span class="dot {{if .Done.Postgres}}ok{{else}}err{{end}}"></span>Шаг 2. База данных bj (PostgreSQL)</h2>
<p>База самого bj: журнал заявок и поручений, история обмена с НРД и DEPO, пользователи.</p>
{{if .Done.Postgres}}<p>✓ База готова{{if .PGSummary}}: {{.PGSummary}}{{end}}. Её создал установщик — ничего делать не нужно, переходите к следующему шагу.</p>{{end}}
@@ -137,7 +133,7 @@
</details>
<div style="margin-top:20px;display:flex;justify-content:space-between">
<span></span>
<a href="/admin/wizard?step=1" class="btn" style="background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8);text-decoration:none">← К шагу 1</a>
{{if .Done.Postgres}}<a href="/admin/wizard?step=3" class="btn" style="text-decoration:none">К шагу 3 →</a>{{else}}<button class="btn" disabled style="opacity:0.5;cursor:not-allowed">К шагу 2 → (сначала настройте PostgreSQL или нажмите «in-memory режим»)</button>{{end}}
</div>
{{if not .Done.Postgres}}<p style="margin-top:8px"><a href="/admin/wizard?step=3&skip=postgres" style="font-size:13px">Пропустить (буду работать в режиме in-memory — без сохранения сделок)</a></p>{{end}}
@@ -151,7 +147,7 @@
<p>СКЗИ «Валидата Клиент L» проверяет подписи НРД на квитанциях и сообщениях и нужно для работы с ключами и сертификатами (шаг 5).</p>
{{if .ValidataVer}}
<p>✓ Валидата установлена вместе с bj: версия {{.ValidataVer}}, каталог <code>/opt/Validata/VDCSP</code>. Ничего настраивать на этом шаге не нужно.</p>
<p class="muted" style="font-size:13px">bj работает с Валидатой через свою службу криптографии <code>bj-crypto</code>{{if eq .CryptoSvc "active"}} — она запущена{{else}} — она запустится, когда на шаге 5 будут загружены ключи и сертификаты{{end}}.</p>
<p class="muted" style="font-size:13px">bj работает с Валидатой через свою службу криптографии <code>bj-crypto</code>{{if eq .CryptoSvc "active"}} — она запущена{{else}} — сейчас она не запущена: служба ставится обновлением с сервера лицензий (шаг 1, кнопка «Обновить сейчас»), а работает с ключом, установленным на шаге 5{{end}}.</p>
{{else}}
<p style="color:var(--err)">Валидата на этом сервере не найдена.</p>
<div class="help-block">
@@ -174,41 +170,22 @@
{{if eq .Step 5}}
<div class="card">
<h2><span class="dot {{if .Done.Certs}}ok{{else}}err{{end}}"></span>Шаг 5. Сертификаты</h2>
<p>Импортируйте сертификаты вашей организации и сертификаты УЦ НРД (для проверки квитанций).</p>
<p>Здесь две части: ключ и сертификат вашей организации (ставите вы) и сертификаты НРД и удостоверяющих центров (bj ведёт их сам).</p>
<div class="help-block">
<strong>Что говорят документы НРД (<code>DOC/Инструккия M2M.pdf</code>, стр. 11, 16-19):</strong>
<ul style="margin:6px 0 6px 16px">
<li>Наши пакеты должны быть подписаны сертификатом <strong>УЦ МБ</strong> (Удостоверяющий центр Московской Биржи).</li>
<li>В режиме <strong>ИШ НРД</strong>: подписывает <em>сам ИШ</em> — наш ключ настраивается <em>в ИШ</em>, не здесь. Bj-server нужен только для проверки квитанций НРД и (опц.) расшифровки 4BROKER01.</li>
<li>В режиме <strong>прямого ONYX без ИШ</strong>: bj-server подписывает сам — нужен наш ключ с приватной частью.</li>
</ul>
<strong>Что куда загружать:</strong>
<ul style="margin:6px 0 6px 16px">
<li>Корневой сертификат <strong>УЦ МБ</strong> (<a href="https://ca.moex.com/" target="_blank">ca.moex.com</a>) — для проверки цепочки нашей подписи и подписей контрагентов.</li>
<li>Корневой и подписной <strong>УЦ НРД</strong> (<a href="https://www.nsd.ru/workflow/system/cryptography/" target="_blank">nsd.ru/workflow/system/cryptography/</a>) — для проверки квитанций от НРД.</li>
<li>Наш сертификат с приватным ключом <em>(только если без ИШ)</em> — для подписи пакетов и расшифровки 4BROKER01.</li>
</ul>
<p class="muted" style="margin-top:6px">Полный регламент PKI — в «Правилах ЭДО НРД» и «Руководстве по установке ИШ» (<a href="https://www.nsd.ru/ru/documents/workflow/" target="_blank">nsd.ru/ru/documents/workflow/</a>) — в наших PDF этого не описано.</p>
<h3 style="margin-top:14px">1. Ключ и сертификат организации</h3>
<div class="help-block"><strong>Что это.</strong> Ключ ЭП, которым подписываются сообщения в НРД, и его сертификат (выдаёт УЦ Московской биржи). Архив ключа с паролем вам передаёт НРД/УЦ.<br>
<strong>Как поставить.</strong> Мастер установки ключа: загрузить архив → запись на носитель → справочник сертификатов → проверка подписи Валидатой.</div>
<a href="/admin/setup/keywizard" class="btn" style="text-decoration:none;display:inline-block">Открыть мастер установки ключа →</a>
<h3 style="margin-top:22px">2. Сертификаты НРД и УЦ — автоматически</h3>
<p>✓ <strong>Автоматическое обновление {{if .Settings.NSDCerts.Manual}}<span style="color:var(--warn)">выключено</span> (включается в «Настройка → Криптография»){{else}}включено{{end}}.</strong> Делать на этом шаге ничего не нужно.</p>
<div class="help-block" style="font-size:13px">
<strong>Как это работает.</strong> Действующие сертификаты НРД и удостоверяющих центров Московской биржи поставляются вместе с bj и приезжают с обновлениями. Каждые 6 часов bj сверяет их со справочником сертификатов ИШ: добавляет недостающие, а заменённые НРД убирает строго после срока, который НРД объявил в уведомлении. Перед каждым изменением делается резервная копия и проверка подписи, при ошибке всё откатывается.<br>
<strong>Тестовые и боевые.</strong> Сертификат попадает только в справочник своего контура: боевые (издатель «Корневой УЦ») — только к боевому ключу, тестовые (издатель «Тестовый УЦ») — только к тестовому. Поэтому смешать контуры нельзя.<br>
<strong>Откуда берутся.</strong> НРД и Московская биржа публикуют сертификаты в постоянных местах: комплект ГОСТ МБ, НКЦ и НРД — <code>https://fs.moex.com/cdp/sert/GOST.zip</code>, сертификаты УЦ — <code>https://fs.moex.com/cdp/</code>. Когда там появляется новый сертификат, мы добавляем его в комплект bj, и он приезжает к вам с обновлением.<br>
Состояние, ручная проверка и выключение — «Настройка → Криптография → Сертификаты НРД в справочнике ИШ».
</div>
<h3 style="margin-top:18px">Авто-загрузка сертификатов УЦ НРД</h3>
<p class="muted">Самый простой способ — добавить прямые URL <code>.cer</code>-файлов УЦ НРД (с <a href="https://www.nsd.ru/workflow/system/cryptography/" target="_blank" rel="noopener">nsd.ru/workflow/system/cryptography/</a>) и включить авто-обновление. Раз в сутки система перепроверит и сохранит изменённые сертификаты в <code>/var/lib/bj/ca-certs/</code>.</p>
<form method="post" action="/admin/setup/cacerts" style="margin-top:8px;display:grid;gap:10px">
<textarea name="urls" rows="3" placeholder="https://www.nsd.ru/path/to/root-ca.cer&#10;https://www.nsd.ru/path/to/sub-ca.cer" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace;font-size:12px">{{range .Settings.CACerts.URLs}}{{.}}
{{end}}</textarea>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" name="auto_update" {{if .Settings.CACerts.AutoUpdate}}checked{{end}}>
<span>Авто-обновление раз в сутки</span>
</label>
<div style="display:flex;gap:8px">
<button type="submit" class="btn">Сохранить</button>
</div>
</form>
<form method="post" action="/admin/setup/cacerts/fetch" style="margin-top:8px">
<button type="submit" class="btn" style="background:var(--ok)">⬇ Скачать и импортировать сейчас</button>
{{if not .Settings.CACerts.LastFetch.IsZero}}<span class="muted" style="margin-left:10px">Последнее обновление: {{.Settings.CACerts.LastFetch.Format "02.01.2006 15:04:05"}}</span>{{end}}
</form>
{{if .Settings.NSDCerts.LastRows}}<p class="muted" style="font-size:13px">Последняя сверка: {{.Settings.NSDCerts.LastRun.Format "02.01.2006 15:04"}}{{if .Settings.NSDCerts.LastProfile}}, профиль ИШ {{.Settings.NSDCerts.LastProfile}}{{end}}.</p>{{else}}<p class="muted" style="font-size:13px">Первая сверка пройдёт, когда будут настроены ключ и шлюз НРД (шаг 6).</p>{{end}}
{{if .Certs}}
<h3 style="margin-top:18px">Установленные сертификаты ({{len .Certs}})</h3>
@@ -227,7 +204,7 @@
</tbody>
</table>
{{else}}
<p class="muted" style="margin-top:12px">Пока сертификаты не импортированы.</p>
<p class="muted" style="margin-top:12px">Ключ организации ещё не установлен. Можно пропустить этот шаг и вернуться к нему, когда будет архив ключа.</p>
{{end}}
<div style="margin-top:20px;display:flex;justify-content:space-between">
@@ -268,7 +245,7 @@
<input type="text" name="key_container" value="{{.Settings.NSD.KeyContainer}}" placeholder="nrd-edo" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%">
</div>
<hr style="border:none;border-top:1px solid var(--border);margin:4px 0">
<p class="muted" style="margin:0">Депозитарные реквизиты организации в НРД. {{if .Settings.Fancy.Server}}Их можно взять из базы DEPO (Fancy): кнопка «Определить из DEPO» подставит найденное в пустые поля — проверьте и сохраните.{{else}}Если учёт ведётся в Fancy DEPO — сначала настройте подключение к базе в разделе <a href="/admin/setup#sec-fancy">Обмен Fancy</a>, тогда реквизиты подставятся из DEPO автоматически; иначе введите их из договора с НРД.{{end}}</p>
<p class="muted" style="margin:0">Депозитарные реквизиты организации в НРД. {{if .Settings.Fancy.Server}}Их можно взять из базы DEPO (Fancy): кнопка «Определить из DEPO» подставит найденное в пустые поля — проверьте и сохраните.{{else}}Если учёт ведётся в Fancy DEPO — сначала подключите базу DEPO на <a href="/admin/wizard?step=3">шаге 3</a>, тогда реквизиты подставятся из DEPO автоматически; иначе введите их из договора с НРД.{{end}}</p>
{{if .Settings.Fancy.Server}}<button type="submit" class="btn" formaction="/admin/setup/fancy/detect" formmethod="post" style="justify-self:start;background:var(--card);color:var(--text);border:1px solid var(--border-strong,#c9cdd8)">Определить из DEPO</button>{{end}}
{{if .Suggest}}{{if .Suggest.AccountID}}<div class="help-block" style="margin:0"><strong>Найдено в DEPO:</strong> счёт {{.Suggest.AccountID}}, код {{.Suggest.DeponentCode}}, раздел {{.Suggest.SectionID}}{{if .Suggest.OrgName}}; организация {{.Suggest.OrgName}}, ИНН {{.Suggest.OrgINN}}{{end}}.{{range .Suggest.Notes}}<br><span class="muted">⚠ {{.}}</span>{{end}}</div>{{end}}{{end}}
<div><label>Депозитарный код</label><input type="text" name="deponent_code" value="{{if .Settings.NSD.DeponentCode}}{{.Settings.NSD.DeponentCode}}{{else if .Suggest}}{{.Suggest.DeponentCode}}{{end}}" placeholder="MC0000000000" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;width:100%"></div>
@@ -339,7 +316,7 @@
<h3 style="margin-top:18px">Итоговая сводка</h3>
<table>
<tr><td class="muted">PostgreSQL</td><td>{{if .Done.Postgres}}<span style="color:var(--ok)">настроен</span>{{else}}<span class="muted">in-memory</span>{{end}}</td></tr>
<tr><td class="muted">Крипто-провайдер</td><td>{{if .Done.Crypto}}<span style="color:var(--ok)">{{.Settings.Crypto.Provider}}</span>{{else}}<span style="color:var(--err)">не настроен</span>{{end}}</td></tr>
<tr><td class="muted">Крипто-провайдер</td><td>{{if .Done.Crypto}}<span style="color:var(--ok)">Валидата Клиент L{{if .ValidataVer}} {{.ValidataVer}}{{end}}</span>{{else}}<span style="color:var(--err)">не настроен</span>{{end}}</td></tr>
<tr><td class="muted">Сертификатов установлено</td><td>{{len .Certs}}</td></tr>
<tr><td class="muted">ИШ НРД</td><td>{{if .Done.NSD}}<span style="color:var(--ok)">{{.Settings.NSD.Profile}}</span>{{else}}<span class="muted">{{if devOn}}mock-режим{{else}}не настроен{{end}}</span>{{end}}</td></tr>
{{if devOn}}<tr><td class="muted">Тестовый прогон</td><td>{{if .Done.TestRun}}<span style="color:var(--ok)">пройден</span>{{else}}<span class="muted">не запускался</span>{{end}}</td></tr>{{end}}