cf1da4a0a9
На сервере без экрана окно Валидаты «Пожалуйста, двигайте мышью» никто не видел, ключ в тихом режиме падал с 0x80090022 (ИШ: «Не найден ключ ЭП (E0700009)»), пересборка справочника — тоже. bj-validata-rng: тихая проверка датчика (testcsp -silent -random), сеанс на виртуальном экране, движения из поля мастера повторяются в окне Валидаты (XTEST), прогресс — по полосе её окна. Установщик ключа проверяет датчик до записи на флешку. zpki.ini: Count = числу профилей, блоки подряд. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
199 lines
7.8 KiB
Go
199 lines
7.8 KiB
Go
package lkgateway
|
|
|
|
// Датчик случайных чисел СКЗИ Валидата (06.10.2026). Перед первой работой с ключом Валидата собирает случайность
|
|
// от движений мыши человека («Пожалуйста, двигайте мышью…»); дальше состояние датчика хранит служба ключей, и
|
|
// это делается один раз для сервера. На сервере без экрана окна никто не видит, и всё, что работает с ключом
|
|
// в «тихом» режиме — ИШ, пересборка справочника, проверка подписи, — падает с 0x80090022, а ИШ пишет «Не найден
|
|
// ключ ЭП (E0700009)», хотя флешка с ключом на месте (так было на чистом сервере .29).
|
|
//
|
|
// Окно Валидаты открывает root-сеанс bj-validata-rng на виртуальном экране; человек водит мышью по полю в мастере,
|
|
// браузер шлёт движения пачками сюда, мы кладём их в /var/lib/bj/rng/in — сеанс повторяет их в окне Валидаты и
|
|
// пишет прогресс по полосе самого окна в /var/lib/bj/rng/status.json. Случайность не подменяем ничем своим.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var rngDir = "/var/lib/bj/rng"
|
|
|
|
// rngReady — датчик уже инициализирован (запоминаем: обратно в «не готов» он сам не возвращается).
|
|
var rngReady struct {
|
|
sync.Mutex
|
|
ok bool
|
|
}
|
|
|
|
// rngView — ответ страницы: готов ли датчик и как идёт сеанс.
|
|
type rngView struct {
|
|
OK bool `json:"ok"`
|
|
Ready bool `json:"ready"`
|
|
State string `json:"state,omitempty"` // starting | running | done | failed | cancelled
|
|
Progress float64 `json:"progress"`
|
|
Window bool `json:"window,omitempty"` // окно Валидаты открылось — движения уже идут в него
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// rngSession — что пишет сеанс в status.json (at — время записи, unix).
|
|
type rngSession struct {
|
|
State string `json:"state"`
|
|
Progress float64 `json:"progress"`
|
|
Window bool `json:"window"`
|
|
Error string `json:"error"`
|
|
At int64 `json:"at"`
|
|
}
|
|
|
|
func rngReadSession() (rngSession, time.Duration) {
|
|
var s rngSession
|
|
b, err := os.ReadFile(filepath.Join(rngDir, "status.json"))
|
|
if err != nil || json.Unmarshal(b, &s) != nil {
|
|
return rngSession{}, 0
|
|
}
|
|
return s, time.Since(time.Unix(s.At, 0))
|
|
}
|
|
|
|
// rngLive — сеанс идёт (сеанс обновляет файл раз в полсекунды; «starting» пишется до запуска службы).
|
|
func rngLive(s rngSession, age time.Duration) bool {
|
|
return (s.State == "running" && age < 15*time.Second) || (s.State == "starting" && age < 60*time.Second)
|
|
}
|
|
|
|
// rngCheck — состояние датчика: идущий сеанс — из его файла, иначе тихая проверка у root-воркера.
|
|
func rngCheck(ctx context.Context) rngView {
|
|
s, age := rngReadSession()
|
|
if rngLive(s, age) {
|
|
return rngView{OK: true, State: s.State, Progress: s.Progress, Window: s.Window}
|
|
}
|
|
rngReady.Lock()
|
|
ready := rngReady.ok
|
|
rngReady.Unlock()
|
|
if !ready {
|
|
res, err := runKeymediaOp(ctx, "rng-status", nil)
|
|
if err != nil {
|
|
return rngView{Error: "проверка датчика не удалась: " + errStr(err)}
|
|
}
|
|
ready = res["ready"] == true
|
|
if ready {
|
|
rngReady.Lock()
|
|
rngReady.ok = true
|
|
rngReady.Unlock()
|
|
}
|
|
}
|
|
v := rngView{OK: true, Ready: ready}
|
|
if age < 10*time.Minute { // итог недавнего сеанса — показать, чем кончился
|
|
switch {
|
|
case ready && s.State == "done":
|
|
v.State, v.Progress = "done", 1
|
|
case !ready && (s.State == "failed" || s.State == "cancelled"):
|
|
v.State, v.Error = s.State, s.Error
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (h *setupHandlers) registerValidataRNG(mux *http.ServeMux) {
|
|
mux.HandleFunc("/admin/setup/rng/status", h.rngStatus)
|
|
mux.HandleFunc("/admin/setup/rng/start", h.rngStart)
|
|
mux.HandleFunc("/admin/setup/rng/moves", h.rngMoves)
|
|
mux.HandleFunc("/admin/setup/rng/cancel", h.rngCancel)
|
|
}
|
|
|
|
func rngJSON(w http.ResponseWriter, v rngView) { rngJSONCode(w, http.StatusOK, v) }
|
|
|
|
func rngJSONCode(w http.ResponseWriter, code int, v rngView) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
// GET /admin/setup/rng/status
|
|
func (h *setupHandlers) rngStatus(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
|
|
defer cancel()
|
|
rngJSON(w, rngCheck(ctx))
|
|
}
|
|
|
|
// POST /admin/setup/rng/start — открыть окно датчика Валидаты (уже готов — сразу ready).
|
|
func (h *setupHandlers) rngStart(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
|
|
defer cancel()
|
|
res, err := runKeymediaOp(ctx, "rng-start", nil)
|
|
if err != nil {
|
|
rngJSON(w, rngView{Error: "окно датчика не открылось: " + errStr(err)})
|
|
return
|
|
}
|
|
if res["ready"] == true {
|
|
rngReady.Lock()
|
|
rngReady.ok = true
|
|
rngReady.Unlock()
|
|
rngJSON(w, rngView{OK: true, Ready: true})
|
|
return
|
|
}
|
|
rngJSON(w, rngView{OK: true, State: "starting"})
|
|
}
|
|
|
|
// rngMovesMax — движений в одной пачке (браузер шлёт раз в четверть секунды, мышь — до 1000 событий в секунду).
|
|
const rngMovesMax = 1500
|
|
|
|
// POST /admin/setup/rng/moves — пачка движений мыши из поля мастера: {"m":[[x,y,dt],…]}, x и y — 0…1000
|
|
// по полю, dt — миллисекунды от предыдущего движения. Кладём файлом сеансу; в ответ — прогресс.
|
|
func (h *setupHandlers) rngMoves(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
s, age := rngReadSession()
|
|
if !rngLive(s, age) {
|
|
rngJSONCode(w, http.StatusConflict, rngCheck(r.Context())) // сеанса нет: браузер покажет, что дальше
|
|
return
|
|
}
|
|
var in struct {
|
|
M [][3]int `json:"m"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&in); err != nil {
|
|
http.Error(w, "движения: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if len(in.M) > rngMovesMax {
|
|
in.M = in.M[:rngMovesMax]
|
|
}
|
|
var b strings.Builder
|
|
for _, m := range in.M {
|
|
x, y, dt := m[0], m[1], m[2]
|
|
if x < 0 || x > 1000 || y < 0 || y > 1000 || dt < 0 || dt > 1000 {
|
|
continue
|
|
}
|
|
fmt.Fprintf(&b, "%d %d %d\n", x, y, dt)
|
|
}
|
|
if b.Len() > 0 {
|
|
dir := filepath.Join(rngDir, "in")
|
|
name := filepath.Join(dir, fmt.Sprintf("%019d.mv", time.Now().UnixNano()))
|
|
if err := os.WriteFile(name+".tmp", []byte(b.String()), 0o600); err == nil {
|
|
_ = os.Rename(name+".tmp", name)
|
|
}
|
|
}
|
|
rngJSON(w, rngView{OK: true, State: s.State, Progress: s.Progress, Window: s.Window})
|
|
}
|
|
|
|
// POST /admin/setup/rng/cancel
|
|
func (h *setupHandlers) rngCancel(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
|
defer cancel()
|
|
_, _ = runKeymediaOp(ctx, "rng-cancel", nil)
|
|
rngJSON(w, rngCheck(ctx))
|
|
}
|