feat(admin): импорт сертификатов через UI + список сертификатов на токенах + URL контуров НРД
После реальной установки КриптоПро CSP добавлены следующие функциональности: cryptocli/client.go: - FindCertificates() — перечисляет CKO_CERTIFICATE объекты на всех подключенных слотах через PKCS#11, парсит X.509, извлекает CN, ИНН (OID 1.2.643.3.131.1.1), серийник, срок действия. Для каждого сертификата проверяет наличие парного приватного ключа (CKO_PRIVATE_KEY с тем же CKA_ID). - Тип Certificate с полями: SubjectCN, IssuerCN, INN, Serial, NotBefore, NotAfter, DER, HasPrivateKey, TokenLabel, SlotID. internal/lkgateway/setup.go: - handler importCertificate (POST /admin/setup/crypto/import-cert, multipart). Принимает .pfx/.p12 (с PIN) или .cer/.crt. Запускает certmgr -inst -pfx или -inst с выбором хранилища (uMy/mroot/uRoot). - listCertsForUI() — вспомогательный метод renderSetup для подгрузки актуального списка сертификатов с подключенных токенов при каждом открытии страницы. internal/lkgateway/web/templates/admin_setup.html: - секция «Сертификаты на токенах» с таблицей (Кому/Кем выдан/ИНН/срок/ токен/есть-ли-приватный-ключ). - форма «Импорт сертификата (.pfx/.cer/.crt)» с полями для PIN и выбора хранилища. - блок «Интеграционный шлюз НРД»: добавлен JS автозаполнения URL ONYX и контейнера по выбору профиля (guest/test3/prod × gost/rsa) — значения из DOC/Ссылки для доступа в тестовые контуры.pdf. internal/lkgateway/web/templates/admin_help_systems.html: - секция «Интеграционный шлюз НРД и контуры тестирования» дополнена полной таблицей URL-ов сервисов GUEST/TEST3 (ONYX, Agate, DCS, Единый кабинет, Корпоративные действия). IP gost.nsd.ru для настройки межсетевого экрана. - новая секция «Сертификаты УЦ НРД (для проверки квитанций)» с пошаговой инструкцией: куда импортировать корневой сертификат УЦ НРД, куда промежуточные, куда наши сертификаты из стороннего УЦ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+106
-12
@@ -19,6 +19,23 @@ import (
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
|
||||
)
|
||||
|
||||
// crypto-сертификаты на текущих токенах (для отображения на странице).
|
||||
func (h *setupHandlers) listCertsForUI() []cryptocli.Certificate {
|
||||
s := h.rc.Snapshot()
|
||||
if s.Crypto.Provider == "" || s.Crypto.Provider == "stub" || s.Crypto.JCPPath == "" {
|
||||
return nil
|
||||
}
|
||||
cli := cryptocli.New(cryptocli.Config{
|
||||
Provider: cryptocli.Provider(s.Crypto.Provider),
|
||||
ModulePath: s.Crypto.JCPPath,
|
||||
})
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
certs, _ := cli.FindCertificates(ctx)
|
||||
return certs
|
||||
}
|
||||
|
||||
// setupHandlers — обработчики /admin/setup/*.
|
||||
type setupHandlers struct {
|
||||
rc *RuntimeConfig
|
||||
@@ -46,6 +63,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
|
||||
mux.HandleFunc("/admin/setup/crypto/check", h.checkCrypto)
|
||||
mux.HandleFunc("/admin/setup/crypto/activate", h.activateLicense)
|
||||
mux.HandleFunc("/admin/setup/crypto/install", h.installCryptoPro)
|
||||
mux.HandleFunc("/admin/setup/crypto/import-cert", h.importCertificate)
|
||||
mux.HandleFunc("/admin/setup/nsd", h.saveNSD)
|
||||
mux.HandleFunc("/admin/setup/lk", h.saveLK)
|
||||
mux.HandleFunc("/admin/setup/test-run", h.testRun)
|
||||
@@ -137,6 +155,80 @@ func (h *setupHandlers) installCryptoPro(w http.ResponseWriter, r *http.Request)
|
||||
setupFlash(w, r, "КриптоПро CSP установлен. Файлов rpm: "+fmt.Sprint(len(rpms))+". Теперь введите серийник и нажмите «Активировать лицензию». Вывод rpm: "+strings.TrimSpace(output))
|
||||
}
|
||||
|
||||
// importCertificate — POST /admin/setup/crypto/import-cert (multipart).
|
||||
// Принимает .pfx (PKCS#12 — приватный ключ + сертификат + опц. PIN) или
|
||||
// .cer/.crt (только публичный сертификат). Импортирует через certmgr
|
||||
// КриптоПро. Сертификат добавляется в хранилище uMy (либо mroot для
|
||||
// корневых).
|
||||
func (h *setupHandlers) importCertificate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(64 << 20); err != nil {
|
||||
setupFlash(w, r, "Импорт сертификата: ошибка чтения формы: "+err.Error())
|
||||
return
|
||||
}
|
||||
file, header, err := r.FormFile("cert")
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Импорт сертификата: выберите файл .pfx/.cer/.crt")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
pin := strings.TrimSpace(r.FormValue("pin"))
|
||||
store := strings.TrimSpace(r.FormValue("store")) // "uMy" по умолчанию, "mroot" для корневых
|
||||
|
||||
dir := "/tmp/bj-certs"
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
safeName := filepath.Base(header.Filename)
|
||||
dst := filepath.Join(dir, safeName)
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Импорт сертификата: не получилось создать "+dst+": "+err.Error())
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
out.Close()
|
||||
setupFlash(w, r, "Импорт сертификата: ошибка записи: "+err.Error())
|
||||
return
|
||||
}
|
||||
out.Close()
|
||||
|
||||
certmgr := "/opt/cprocsp/bin/amd64/certmgr"
|
||||
if _, err := os.Stat(certmgr); err != nil {
|
||||
setupFlash(w, r, "Импорт сертификата: certmgr не найден. Сначала установите КриптоПро CSP.")
|
||||
return
|
||||
}
|
||||
if store == "" {
|
||||
store = "uMy"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
lower := strings.ToLower(safeName)
|
||||
var cmdOut string
|
||||
switch {
|
||||
case strings.HasSuffix(lower, ".pfx") || strings.HasSuffix(lower, ".p12"):
|
||||
// PKCS#12 — импорт через certmgr -inst с PIN
|
||||
args := []string{"-inst", "-pfx", "-file", dst, "-store", store}
|
||||
if pin != "" {
|
||||
args = append(args, "-pin", pin)
|
||||
}
|
||||
cmdOut, err = runCmd(ctx, certmgr, args...)
|
||||
case strings.HasSuffix(lower, ".cer") || strings.HasSuffix(lower, ".crt"):
|
||||
// Голый сертификат — импорт в хранилище без приватного ключа
|
||||
cmdOut, err = runCmd(ctx, certmgr, "-inst", "-file", dst, "-store", store)
|
||||
default:
|
||||
setupFlash(w, r, "Импорт сертификата: неизвестное расширение, нужен .pfx/.p12/.cer/.crt")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
setupFlash(w, r, "Импорт сертификата: certmgr упал: "+err.Error()+" / вывод: "+strings.TrimSpace(cmdOut))
|
||||
return
|
||||
}
|
||||
setupFlash(w, r, "Сертификат «"+safeName+"» импортирован в хранилище "+store+". Вывод certmgr: "+strings.TrimSpace(cmdOut))
|
||||
}
|
||||
|
||||
// runCmdInDir выполняет команду в указанной рабочей директории.
|
||||
func runCmdInDir(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
@@ -219,12 +311,13 @@ 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
|
||||
Flash string
|
||||
Error string
|
||||
Settings Settings
|
||||
Readiness []Readiness
|
||||
ReadyCount int
|
||||
TotalCount int
|
||||
Certificates []cryptocli.Certificate
|
||||
Flash string
|
||||
Error string
|
||||
}
|
||||
|
||||
func (h *setupHandlers) renderSetup(w http.ResponseWriter, _ *http.Request, flash string) {
|
||||
@@ -237,12 +330,13 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, _ *http.Request, flas
|
||||
}
|
||||
}
|
||||
data := SetupData{
|
||||
page: nowPage("Настройка", "setup"),
|
||||
Settings: s,
|
||||
Readiness: r,
|
||||
ReadyCount: ready,
|
||||
TotalCount: len(r),
|
||||
Flash: flash,
|
||||
page: nowPage("Настройка", "setup"),
|
||||
Settings: s,
|
||||
Readiness: r,
|
||||
ReadyCount: ready,
|
||||
TotalCount: len(r),
|
||||
Certificates: h.listCertsForUI(),
|
||||
Flash: flash,
|
||||
}
|
||||
if errVal := errMsgFromQuery(_q(w)); errVal != "" {
|
||||
data.Error = errVal
|
||||
|
||||
Reference in New Issue
Block a user