Ключ подписи: можно взять ключ, который уже на флешке, — без архива НРД

На шаге 1 мастера ключа — второй путь «Ключ уже на флешке»: выбор флешки и
«Взять ключ с флешки». Флешка только читается (bj-keymedia-install stage-media):
ключ (*.vdk) и справочник (local.pse + local.gdbm; раскладка bj — профиль с
key.reg) копируются во временный каталог в раскладке архива, дальше — обычная
установка на ту же флешку под тем же именем профиля (резервная копия флешки,
пересборка справочника, профиль ИШ, перезапуск). Ключ без справочника — понятный
отказ: справочник есть только в архиве НРД. Устройство — только из обнаруженных
съёмных.

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-10-06 18:01:37 +03:00
parent 603c7b09bd
commit b58c4fb898
3 changed files with 107 additions and 5 deletions
+32
View File
@@ -88,6 +88,38 @@ if [ "$ACTION" = "certs-apply" ]; then
echo "{\"ok\":true,\"profile\":\"$PROFILE_NAME\",\"backup\":\"$BK\",\"igate\":\"$ACT\",\"rebuild\":$OUT,\"certs\":$(certs_list_json)}"; exit 0
fi
# ---- ключ уже на флешке: взять с неё ключ и справочник (только чтение) во временный каталог — дальше обычная
# установка (тот же носитель, то же имя профиля). Раскладка bj: <профиль>/key.reg + <профиль>/spr<N>/local.{pse,gdbm}
# + vdkeys/*.vdk; другая — ключ (*.vdk) и справочник (local.gdbm + local.pse) где угодно на носителе.
if [ "$ACTION" = "stage-media" ]; then
MNT="$TARGET_MNT"; RO_MNT=""
if [ -z "$MNT" ]; then
[ -n "$TARGET_DEV" ] || err "не выбран носитель"
RO_MNT="/run/bj-keymedia-ro-$$"; mkdir -p "$RO_MNT"
mount -o ro "$TARGET_DEV" "$RO_MNT" || { rmdir "$RO_MNT"; err "не смонтировать $TARGET_DEV"; }
MNT="$RO_MNT"
fi
done_ro(){ [ -n "$RO_MNT" ] && { umount "$RO_MNT" 2>/dev/null; rmdir "$RO_MNT" 2>/dev/null; }; }
VDKF=$(find "$MNT" -maxdepth 4 -iname '*.vdk' 2>/dev/null | sort | head -1)
GD=$(find "$MNT" -maxdepth 5 -iname 'local.gdbm' 2>/dev/null | sort | head -1)
[ -n "$VDKF" ] || { done_ro; err "на носителе нет ключа (файла *.vdk)"; }
[ -n "$GD" ] && [ -f "$(dirname "$GD")/local.pse" ] || { done_ro; err "на носителе есть ключ, но нет справочника сертификатов (local.pse и local.gdbm) — загрузите архив ключа от НРД: справочник есть в нём"; }
SPR=$(dirname "$GD"); PROFD=$(dirname "$SPR")
ID="media-$(date -u +%Y%m%d%H%M%S)"; ST="/var/lib/bj/media/iso/$ID"; mkdir -p "$ST/vdkeys"
if [ "$PROFD" != "$MNT" ] && [ -f "$PROFD/key.reg" ]; then
cp -a "$PROFD"/. "$ST"/ 2>/dev/null; PNAME=$(basename "$PROFD") # профиль bj: key.reg + spr<N>/
else
cp -a "$SPR" "$ST"/ 2>/dev/null; PNAME="" # справочник без профиля bj — имя по номеру
fi
cp -f "$VDKF" "$ST/vdkeys/"
rm -rf "$ST/vdkeys/vdkeys" 2>/dev/null
chown -R bj:bj "$ST" 2>/dev/null
done_ro
case "$PNAME" in *[!A-Za-z0-9_-]*) PNAME="" ;; esac
printf '{"ok":true,"staging":"%s","vdk":"%s","profile":"%s","spr":"%s"}\n' "$ID" "$(basename "$VDKF")" "$PNAME" "$(basename "$SPR")"
exit 0
fi
[ -n "$STAGING" ] && [ -d "$STAGING" ] || err "staging не найден"
case "$STAGING" in /var/lib/bj/media/iso/*) : ;; *) err "staging вне зоны" ;; esac
VDK=$(find "$STAGING" -iname '*.vdk' | head -1)
+55 -1
View File
@@ -47,6 +47,8 @@ type keyWizardState struct {
Steps []keyWizardStep // 1..5
Done bool // все шаги пройдены
Flash string
// ключ взят с флешки (а не из архива): на ту же флешку и под тем же именем профиля
MediaDevice, MediaProfile string
}
func newKeyWizardState() *keyWizardState {
@@ -62,6 +64,7 @@ func (s *keyWizardState) reset() {
s.Steps = defaultKeySteps()
s.Done = false
s.Flash = ""
s.MediaDevice, s.MediaProfile = "", ""
}
func defaultKeySteps() []keyWizardStep {
@@ -121,8 +124,10 @@ func (h *setupHandlers) keyWizView(want string, inWizard bool) *keyWizardData {
if want == "test3" || want == "prod" {
d.WantContour = want
}
if st.StagingID != "" && !st.Done { // список флешек — на шаге выбора носителя (архив уже загружен)
if !st.Done { // флешки: в начале — откуда взять ключ, после загрузки — куда записать
d.Drives = listFlashDrives()
}
if st.StagingID != "" && !st.Done {
d.SuggestContour, d.SuggestIssuer = keyArchiveContour(filepath.Join("/var/lib/bj/media/iso", st.StagingID))
}
return d
@@ -253,6 +258,7 @@ func (h *setupHandlers) registerKeyWizard(mux *http.ServeMux) {
mux.HandleFunc("/admin/setup/keywizard/upload", h.keyWizardUpload)
mux.HandleFunc("/admin/setup/keywizard/install", h.keyWizardInstall)
mux.HandleFunc("/admin/setup/keywizard/reset", h.keyWizardReset)
mux.HandleFunc("/admin/setup/keywizard/from-media", h.keyWizardFromMedia)
}
func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request) {
@@ -268,6 +274,54 @@ func (h *setupHandlers) renderKeyWizard(w http.ResponseWriter, r *http.Request)
render(w, h.tpl.a.keyWizard, *d)
}
// keyWizardFromMedia — POST /admin/setup/keywizard/from-media: ключ уже на флешке (подготовленной bj раньше
// или с ключом и справочником от НРД). Флешка только читается: ключ и справочник копируются во временный
// каталог — дальше обычная установка на эту же флешку под тем же именем профиля (с резервной копией флешки).
func (h *setupHandlers) keyWizardFromMedia(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
dev := strings.TrimSpace(r.FormValue("source_device"))
var drive *flashDrive
for _, d := range listFlashDrives() {
if d.Device == dev {
d := d
drive = &d
}
}
if drive == nil { // только обнаруженные съёмные: иначе root-воркер смонтировал бы что угодно из формы
h.keyWizFlash(w, r, "Флешка не найдена среди съёмных носителей — вставьте её и обновите страницу.")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Minute)
defer cancel()
res, err := runKeymediaOp(ctx, "stage-media", map[string]string{"target_dev": drive.Device, "target_mnt": drive.Mountpoint})
if err == nil && res["ok"] != true {
err = fmt.Errorf("%v", res["error"])
}
if err != nil {
h.keyWizFlash(w, r, "Ключ с флешки не взят: "+err.Error())
return
}
id, _ := res["staging"].(string)
if !strings.HasPrefix(id, "media-") {
h.keyWizFlash(w, r, "Ключ с флешки не взят: установщик не вернул каталог")
return
}
vdk, _ := res["vdk"].(string)
prof, _ := res["profile"].(string)
spr, _ := res["spr"].(string)
h.keyWiz.mu.Lock()
h.keyWiz.reset()
h.keyWiz.StagingID, h.keyWiz.VDK = id, vdk
h.keyWiz.MediaDevice, h.keyWiz.MediaProfile = drive.Device, prof
h.keyWiz.set(0, "ok", fmt.Sprintf("Ключ найден на флешке %s: %s, справочник %s%s", fallback(drive.Model, drive.Device), vdk, spr, prefixed(", профиль ", prof)))
h.keyWiz.set(1, "active", "")
h.keyWiz.mu.Unlock()
http.Redirect(w, r, strings.TrimRight(keyWizBack(r), "?&"), http.StatusSeeOther)
}
func (h *setupHandlers) keyWizardReset(w http.ResponseWriter, r *http.Request) {
h.keyWiz.mu.Lock()
h.keyWiz.reset()
@@ -58,7 +58,7 @@
{{if .Drives}}
{{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;margin-bottom:8px;cursor:pointer">
<input type="radio" name="target_device" value="{{$d.Device}}" {{if $d.IsKeymedia}}checked{{else if and (eq $i 0) (not (anyKeymedia $.Drives))}}checked{{end}} style="margin-top:3px">
<input type="radio" name="target_device" value="{{$d.Device}}" {{if $s.MediaDevice}}{{if eq $d.Device $s.MediaDevice}}checked{{end}}{{else if $d.IsKeymedia}}checked{{else if and (eq $i 0) (not (anyKeymedia $.Drives))}}checked{{end}} style="margin-top:3px">
<span>
<b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}
{{if $d.Label}}· метка «{{$d.Label}}»{{end}}<br>
@@ -83,7 +83,7 @@
<div>
<label style="font-weight:600;display:block;margin-bottom:6px">Имя профиля в справочнике (необязательно)</label>
<input type="text" name="profile_name" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
<input type="text" name="profile_name" value="{{$s.MediaProfile}}" placeholder="Авто из архива (напр. PrUser1046)" autocomplete="off"
pattern="[A-Za-z0-9_-]*" style="width:100%">
<span class="muted" style="font-size:12px">Пусто = имя берётся из архива автоматически.</span>
</div>
@@ -97,8 +97,8 @@
{{else}}
{{/* Начало — форма загрузки */}}
<div class="card">
<h2>Шаг 1 — загрузка архива</h2>
<p class="muted">Выберите .7z-архив с ключом от НРД и введите пароль архива.</p>
<h2>Шаг 1 — откуда взять ключ</h2>
<p class="muted"><b>Архив от НРД</b>: выберите .7z-архив с ключом и введите пароль архива.</p>
<form method="post" action="/admin/setup/keywizard/upload" enctype="multipart/form-data"
style="margin-top:12px;display:grid;gap:10px;max-width:560px">
{{template "keyWizRet" .}}
@@ -106,6 +106,22 @@
<input type="password" name="password" placeholder="Пароль архива (например 11)" autocomplete="off">
<button type="submit" class="btn btn-ok">Загрузить и распаковать</button>
</form>
<div style="margin-top:18px;padding-top:14px;border-top:1px solid var(--border)">
<p class="muted" style="margin:0 0 8px"><b>Ключ уже на флешке</b> (готовили раньше или получили с ключом и справочником): bj возьмёт ключ и справочник с флешки — она только читается — и подключит их к шлюзу НРД. Архив не нужен.</p>
{{if .Drives}}
<form method="post" action="/admin/setup/keywizard/from-media" style="display:grid;gap:8px;max-width:640px">
{{template "keyWizRet" .}}
{{range $i, $d := .Drives}}
<label style="display:flex;gap:10px;align-items:flex-start;padding:10px;border:1px solid var(--border,#ddd);border-radius:8px;cursor:pointer">
<input type="radio" name="source_device" value="{{$d.Device}}" {{if eq $i 0}}checked{{end}} style="margin-top:3px">
<span><b>{{fallbackTpl $d.Model "USB-носитель"}}</b> · {{$d.Size}} · {{$d.FSType}}{{if $d.Label}} · метка «{{$d.Label}}»{{end}}<br>
<span class="muted" style="font-size:12px">{{$d.Device}}{{if $d.IsKeymedia}} · <b style="color:var(--accent)">сейчас ключевой носитель ИШ</b>{{end}}</span></span>
</label>
{{end}}
<button type="submit" class="btn btn-secondary" onclick="this.textContent='Читаю флешку…'">Взять ключ с флешки</button>
</form>
{{else}}<p class="muted" style="font-size:13px;margin:0">Флешка не найдена — вставьте её в сервер и обновите страницу.</p>{{end}}
</div>
</div>
{{end}}
{{end}}