diff --git a/internal/lkgateway/flashcontainers.go b/internal/lkgateway/flashcontainers.go new file mode 100644 index 0000000..821aca7 --- /dev/null +++ b/internal/lkgateway/flashcontainers.go @@ -0,0 +1,190 @@ +package lkgateway + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/user" + "path/filepath" + "strings" + "time" +) + +// FlashContainer — найденный на смонтированной флешке контейнер КриптоПро. +// КриптоПро CSP под Linux ожидает контейнер в виде папки .000 с +// файлами header.key/masks.key/name.key/primary.key/primary2.key. +type FlashContainer struct { + // Mountpoint — путь смонтированной флешки, например /run/media/user/USB. + Mountpoint string + // Path — полный путь до папки .000. + Path string + // Name — имя контейнера (без суффикса .000). + Name string + // Files — список файлов в контейнере (для дисплея). + Files []string + // AlreadyImported — true, если папка .000 уже есть в локальном + // хранилище /var/opt/cprocsp/keys//. + AlreadyImported bool +} + +// scanFlashContainers ищет контейнеры формата .000 на типичных +// точках монтирования USB-носителей в Linux: /run/media//* и +// /media//* и /media/*. Возвращает список найденных контейнеров. +func scanFlashContainers() []FlashContainer { + u, err := user.Current() + if err != nil { + return nil + } + roots := []string{ + filepath.Join("/run/media", u.Username), + filepath.Join("/media", u.Username), + "/media", + "/mnt", + } + localKeysDir := filepath.Join("/var/opt/cprocsp/keys", u.Username) + + var out []FlashContainer + for _, root := range roots { + entries, err := os.ReadDir(root) + if err != nil { + continue + } + for _, e := range entries { + if !e.IsDir() { + continue + } + mountpoint := filepath.Join(root, e.Name()) + out = append(out, findContainersAt(mountpoint, localKeysDir)...) + } + } + return out +} + +func findContainersAt(mountpoint, localKeysDir string) []FlashContainer { + var out []FlashContainer + // Ищем папки .000 на верхнем уровне и на 1 уровне вглубь. + _ = filepath.Walk(mountpoint, func(p string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + // Глубже 2 уровней не лезем (на флешке могут быть личные папки). + rel, _ := filepath.Rel(mountpoint, p) + if strings.Count(rel, string(filepath.Separator)) > 2 { + return filepath.SkipDir + } + if !info.IsDir() || !strings.HasSuffix(strings.ToLower(p), ".000") { + return nil + } + // Проверяем, что внутри лежат файлы вида *.key. + entries, _ := os.ReadDir(p) + var files []string + hasKey := false + for _, ent := range entries { + files = append(files, ent.Name()) + if strings.HasSuffix(strings.ToLower(ent.Name()), ".key") { + hasKey = true + } + } + if !hasKey { + return nil + } + name := strings.TrimSuffix(filepath.Base(p), ".000") + fc := FlashContainer{ + Mountpoint: mountpoint, + Path: p, + Name: name, + Files: files, + } + // Проверка: уже скопирован в локальное хранилище? + if _, err := os.Stat(filepath.Join(localKeysDir, name+".000")); err == nil { + fc.AlreadyImported = true + } + out = append(out, fc) + return filepath.SkipDir + }) + return out +} + +// copyContainerToLocal копирует папку .000 с флешки в локальное +// хранилище КриптоПро /var/opt/cprocsp/keys//.000. После +// этого контейнер виден как \\.\HDIMAGE\ и работает даже без +// вставленной флешки. +func copyContainerToLocal(srcDir string) (string, error) { + u, err := user.Current() + if err != nil { + return "", err + } + localKeysDir := filepath.Join("/var/opt/cprocsp/keys", u.Username) + if err := os.MkdirAll(localKeysDir, 0o700); err != nil { + return "", fmt.Errorf("создать %s: %w", localKeysDir, err) + } + base := filepath.Base(srcDir) + dstDir := filepath.Join(localKeysDir, base) + if _, err := os.Stat(dstDir); err == nil { + return "", fmt.Errorf("контейнер %s уже существует в локальном хранилище", dstDir) + } + if err := os.MkdirAll(dstDir, 0o700); err != nil { + return "", fmt.Errorf("создать %s: %w", dstDir, err) + } + entries, err := os.ReadDir(srcDir) + if err != nil { + return "", err + } + for _, e := range entries { + if e.IsDir() { + continue + } + src, err := os.Open(filepath.Join(srcDir, e.Name())) + if err != nil { + return "", err + } + dst, err := os.OpenFile(filepath.Join(dstDir, e.Name()), + os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + src.Close() + return "", err + } + if _, err := io.Copy(dst, src); err != nil { + src.Close() + dst.Close() + return "", err + } + src.Close() + dst.Close() + } + return dstDir, nil +} + +// copyContainer — POST /admin/setup/crypto/copy-container. +// Параметр src — путь до папки .000 на флешке. +func (h *setupHandlers) copyContainer(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + src := strings.TrimSpace(r.FormValue("src")) + if src == "" { + setupFlash(w, r, "Копирование контейнера: не указан путь") + return + } + // Минимальная защита: ожидаем .000 в конце пути. + if !strings.HasSuffix(strings.ToLower(src), ".000") { + setupFlash(w, r, "Копирование контейнера: путь должен заканчиваться на .000") + return + } + if _, err := os.Stat(src); err != nil { + setupFlash(w, r, "Копирование контейнера: исходная папка недоступна: "+err.Error()) + return + } + dst, err := copyContainerToLocal(src) + if err != nil { + setupFlash(w, r, "Копирование контейнера: "+err.Error()) + return + } + // Дадим CSP несколько мс «заметить» новый контейнер (не критично). + _, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond) + cancel() + setupFlash(w, r, "Контейнер скопирован в "+dst+". Теперь он виден как \\\\.\\HDIMAGE\\"+strings.TrimSuffix(filepath.Base(dst), ".000")+" и работает без вставленной флешки. Импортируйте сертификат: certmgr -inst -cont '\\\\.\\HDIMAGE\\"+strings.TrimSuffix(filepath.Base(dst), ".000")+"' -store uMy.") +} diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 205b979..8c8b4f5 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -74,6 +74,9 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service mux.HandleFunc("/admin/setup/cacerts", h.saveCACerts) mux.HandleFunc("/admin/setup/cacerts/fetch", h.fetchCACertsNow) + // Копирование контейнера КриптоПро с флешки в локальное хранилище. + mux.HandleFunc("/admin/setup/crypto/copy-container", h.copyContainer) + // Пошаговый мастер настройки для нетехнических пользователей. mux.HandleFunc("/admin/wizard", h.renderWizard) } @@ -84,6 +87,7 @@ type WizardData struct { Step int Settings Settings Certs []cryptocli.Certificate + FlashContainers []FlashContainer Flash string CryptoProInstalled bool CryptoProVersion string @@ -106,10 +110,11 @@ func (h *setupHandlers) renderWizard(w http.ResponseWriter, r *http.Request) { } s := h.rc.Snapshot() d := WizardData{ - page: nowPage("Мастер настройки", "wizard"), - Settings: s, - Certs: h.listCertsForUI(), - Flash: r.URL.Query().Get("flash"), + page: nowPage("Мастер настройки", "wizard"), + Settings: s, + Certs: h.listCertsForUI(), + FlashContainers: scanFlashContainers(), + Flash: r.URL.Query().Get("flash"), } d.Done.Postgres = s.Postgres.DSN != "" d.Done.Crypto = s.Crypto.Provider != "" && s.Crypto.Provider != "stub" && s.Crypto.JCPPath != "" @@ -401,13 +406,14 @@ func (h *setupHandlers) checkCrypto(w http.ResponseWriter, r *http.Request) { // SetupData — данные для шаблона admin_setup.html. type SetupData struct { page - Settings Settings - Readiness []Readiness - ReadyCount int - TotalCount int - Certificates []cryptocli.Certificate - Flash string - Error string + Settings Settings + Readiness []Readiness + ReadyCount int + TotalCount int + Certificates []cryptocli.Certificate + FlashContainers []FlashContainer + Flash string + Error string } func (h *setupHandlers) renderSetup(w http.ResponseWriter, _ *http.Request, flash string) { @@ -420,13 +426,14 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, _ *http.Request, flas } } data := SetupData{ - page: nowPage("Настройка", "setup"), - Settings: s, - Readiness: r, - ReadyCount: ready, - TotalCount: len(r), - Certificates: h.listCertsForUI(), - Flash: flash, + page: nowPage("Настройка", "setup"), + Settings: s, + Readiness: r, + ReadyCount: ready, + TotalCount: len(r), + Certificates: h.listCertsForUI(), + FlashContainers: scanFlashContainers(), + Flash: flash, } if errVal := errMsgFromQuery(_q(w)); errVal != "" { data.Error = errVal diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 4f0a9a3..385065f 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -147,6 +147,37 @@ + +
+

Контейнеры на USB-носителях (флешка/Рутокен)

+ {{if .FlashContainers}} +

Найдено {{len .FlashContainers}} контейнер(а) формата name.000 на смонтированных USB-носителях. Кнопка ниже копирует папку в /var/opt/cprocsp/keys/$USER/ — после этого контейнер виден как \\.\HDIMAGE\name и работает без вставленной флешки.

+ + + + {{range .FlashContainers}} + + + + + + + + {{end}} + +
НосительИмя контейнераФайлыСтатус
{{.Mountpoint}}{{.Name}}{{len .Files}} файлов{{if .AlreadyImported}}уже в HDIMAGE{{else}}только на флешке{{end}} + {{if not .AlreadyImported}} +
+ + +
+ {{end}} +
+ {{else}} +

Подключённые USB-носители с контейнерами КриптоПро (папки name.000 с *.key) не обнаружены. Поиск идёт в /run/media/$USER/, /media/$USER/, /media/, /mnt/. Вставьте флешку и обновите страницу.

+ {{end}} +
+

Сертификаты УЦ (НРД и др.) — авто-загрузка

diff --git a/internal/lkgateway/web/templates/admin_wizard.html b/internal/lkgateway/web/templates/admin_wizard.html index a96c948..56c560d 100644 --- a/internal/lkgateway/web/templates/admin_wizard.html +++ b/internal/lkgateway/web/templates/admin_wizard.html @@ -150,13 +150,23 @@

Импортируйте сертификаты вашей организации и сертификаты УЦ НРД (для проверки квитанций).

- Какие сертификаты нужны? -
    -
  1. Ваш сертификат организации с приватным ключом (.pfx / .p12 на диске или контейнер на Рутокене) — для подписи отправляемых пакетов.
  2. -
  3. Корневой сертификат вашего УЦ (.cer) — в хранилище mroot.
  4. -
  5. Корневой и подписной сертификаты УЦ НРД — для проверки квитанций. Скачиваются с nsd.ru/workflow/system/cryptography/
  6. -
- Где взять? Сертификат вашей организации — у вашего УЦ (Контур, СКБ Контур, ИнфоТеКС, КриптоПро УЦ, …). Сертификаты УЦ НРД — на сайте НРД (см. выше). + Что говорят документы НРД (DOC/Инструккия M2M.pdf, стр. 11, 16-19): +
    +
  • Наши пакеты должны быть подписаны сертификатом УЦ МБ (Удостоверяющий центр Московской Биржи).
  • +
  • В режиме ИШ НРД: подписывает сам ИШ — наш ключ настраивается в ИШ, не здесь. Bj-server нужен только для проверки квитанций НРД и (опц.) расшифровки 4BROKER01.
  • +
  • В режиме прямого ONYX без ИШ: bj-server подписывает сам — нужен наш ключ с приватной частью.
  • +
+ Что куда загружать (по режиму): + + + + + + + + +
ЧтоЗачемКуда
Корневой сертификат УЦ МБ (ca.moex.com)проверка цепочки нашей подписи и подписей контрагентовmroot
Корневой и подписной УЦ НРД (nsd.ru/workflow/system/cryptography/)проверка квитанций от НРДmroot + uRoot
Наш сертификат + ключ (только если без ИШ)подпись отправляемых пакетов + расшифровка 4BROKER01uMy — с приватным ключом
Сертификаты с Рутокенасами появятся в таблице ниже после подключения USBне грузить
+

Полный регламент PKI — в «Правилах ЭДО НРД» и «Руководстве по установке ИШ» (nsd.ru/ru/documents/workflow/) — в наших PDF этого не описано.

Импорт сертификата

@@ -181,6 +191,35 @@ +

Контейнеры на подключённых носителях (флешка/Рутокен)

+ {{if .FlashContainers}} +

Найдено {{len .FlashContainers}} контейнер(а) формата name.000 на смонтированных USB-носителях. Нажмите «Скопировать в локальное хранилище» — папка будет перенесена в /var/opt/cprocsp/keys/$USER/, после чего контейнер виден как \\.\HDIMAGE\name и работает даже без вставленной флешки.

+ + + + {{range .FlashContainers}} + + + + + + + + {{end}} + +
НосительИмя контейнераФайлыСтатус
{{.Mountpoint}}{{.Name}}{{len .Files}} файлов{{if .AlreadyImported}}уже в HDIMAGE{{else}}только на флешке{{end}} + {{if not .AlreadyImported}} +
+ + +
+ {{end}} +
+

После копирования: импортировать сертификат из контейнера командой certmgr -inst -cont '\\.\HDIMAGE\{имя}' -store uMy — это пропишет сертификат в видимое хранилище. (UI-кнопку для этого добавим следующим шагом.)

+ {{else}} +

Подключённые USB-носители с контейнерами КриптоПро формата name.000 не обнаружены. Поиск идёт в /run/media/$USER/, /media/$USER/, /media/, /mnt/. Вставьте флешку с контейнером и обновите страницу — контейнер появится в этой таблице автоматически.

+ {{end}} +

Авто-загрузка сертификатов УЦ НРД

Самый простой способ — добавить прямые URL .cer-файлов УЦ НРД (с nsd.ru/workflow/system/cryptography/) и включить авто-обновление. Раз в сутки система перепроверит и переустановит изменённые сертификаты.