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)) }