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:
@@ -13,6 +13,8 @@ package cryptocli
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -140,6 +142,146 @@ func (c *Client) Health(_ context.Context) (HealthInfo, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// Certificate — DER-сертификат с распарсенными атрибутами для UI.
|
||||
type Certificate struct {
|
||||
SlotID uint
|
||||
TokenLabel string
|
||||
Label string // CKA_LABEL (объект на токене)
|
||||
SubjectCN string
|
||||
IssuerCN string
|
||||
Serial string
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
INN string // если есть в OID 1.2.643.3.131.1.1
|
||||
DER []byte
|
||||
HasPrivateKey bool // найден ли парный приватный ключ на токене
|
||||
}
|
||||
|
||||
// FindCertificates перечисляет сертификаты на всех подключенных
|
||||
// токенах. Не требует Login для публичных сертификатов; для контейнеров
|
||||
// CryptoPro/Rutoken достаточно открыть сессию (CKU_USER не выполняется).
|
||||
func (c *Client) FindCertificates(_ context.Context) ([]Certificate, error) {
|
||||
if c.cfg.Provider == "" || c.cfg.Provider == ProviderStub {
|
||||
return nil, errors.New("cryptocli: провайдер stub — нет реальных сертификатов")
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if err := c.ensureInitLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slots, err := c.ctx.GetSlotList(true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cryptocli: GetSlotList: %w", err)
|
||||
}
|
||||
|
||||
var out []Certificate
|
||||
for _, slot := range slots {
|
||||
tokInfo, _ := c.ctx.GetTokenInfo(slot)
|
||||
certs, err := c.listSlotCertificates(slot, tokInfo.Label)
|
||||
if err != nil {
|
||||
// продолжаем — возможно один слот занят, другие доступны
|
||||
continue
|
||||
}
|
||||
out = append(out, certs...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// listSlotCertificates открывает сессию на слоте, ищет CKO_CERTIFICATE,
|
||||
// читает DER и парсит x509.
|
||||
func (c *Client) listSlotCertificates(slot uint, tokenLabel string) ([]Certificate, error) {
|
||||
sess, err := c.ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenSession: %w", err)
|
||||
}
|
||||
defer func() { _ = c.ctx.CloseSession(sess) }()
|
||||
|
||||
template := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE),
|
||||
}
|
||||
if err := c.ctx.FindObjectsInit(sess, template); err != nil {
|
||||
return nil, fmt.Errorf("FindObjectsInit: %w", err)
|
||||
}
|
||||
handles, _, err := c.ctx.FindObjects(sess, 32)
|
||||
_ = c.ctx.FindObjectsFinal(sess)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FindObjects: %w", err)
|
||||
}
|
||||
|
||||
out := make([]Certificate, 0, len(handles))
|
||||
for _, h := range handles {
|
||||
attrs, err := c.ctx.GetAttributeValue(sess, h, []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_LABEL, nil),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, nil),
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
cert := Certificate{
|
||||
SlotID: slot,
|
||||
TokenLabel: tokenLabel,
|
||||
}
|
||||
var idAttr []byte
|
||||
for _, a := range attrs {
|
||||
switch a.Type {
|
||||
case pkcs11.CKA_VALUE:
|
||||
cert.DER = a.Value
|
||||
case pkcs11.CKA_LABEL:
|
||||
cert.Label = string(a.Value)
|
||||
case pkcs11.CKA_ID:
|
||||
idAttr = a.Value
|
||||
}
|
||||
}
|
||||
// Парсим X.509 (ГОСТ-сертификаты тоже парсятся через crypto/x509
|
||||
// — Subject/Issuer/Serial/Validity не зависят от алгоритма подписи).
|
||||
parsed, err := x509.ParseCertificate(cert.DER)
|
||||
if err == nil {
|
||||
cert.SubjectCN = parsed.Subject.CommonName
|
||||
cert.IssuerCN = parsed.Issuer.CommonName
|
||||
cert.Serial = parsed.SerialNumber.Text(16)
|
||||
cert.NotBefore = parsed.NotBefore
|
||||
cert.NotAfter = parsed.NotAfter
|
||||
// ИНН в OID 1.2.643.3.131.1.1 — извлекаем из Subject.
|
||||
cert.INN = extractINN(parsed)
|
||||
}
|
||||
// Проверим есть ли парный приватный ключ.
|
||||
if len(idAttr) > 0 {
|
||||
cert.HasPrivateKey = c.hasPrivateKey(sess, idAttr)
|
||||
}
|
||||
out = append(out, cert)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// hasPrivateKey ищет CKO_PRIVATE_KEY с тем же CKA_ID что и сертификат.
|
||||
func (c *Client) hasPrivateKey(sess pkcs11.SessionHandle, id []byte) bool {
|
||||
tmpl := []*pkcs11.Attribute{
|
||||
pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY),
|
||||
pkcs11.NewAttribute(pkcs11.CKA_ID, id),
|
||||
}
|
||||
if err := c.ctx.FindObjectsInit(sess, tmpl); err != nil {
|
||||
return false
|
||||
}
|
||||
defer func() { _ = c.ctx.FindObjectsFinal(sess) }()
|
||||
handles, _, err := c.ctx.FindObjects(sess, 1)
|
||||
return err == nil && len(handles) > 0
|
||||
}
|
||||
|
||||
// extractINN ищет ИНН в Subject сертификата по OID НРД 1.2.643.3.131.1.1.
|
||||
func extractINN(c *x509.Certificate) string {
|
||||
innOID := asn1.ObjectIdentifier{1, 2, 643, 3, 131, 1, 1}
|
||||
for _, name := range c.Subject.Names {
|
||||
if name.Type.Equal(innOID) {
|
||||
if s, ok := name.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// VerifyXMLDSig — заглушка для интерфейса m2mcore.CryptoVerifier.
|
||||
// Реальная проверка XMLDSig потребует канонизации XML и parsing
|
||||
// сертификатов; пока возвращает CertInfo с подписанной полезной
|
||||
|
||||
+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
|
||||
|
||||
@@ -7,27 +7,64 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>1. Интеграционный шлюз НРД (ИШ)</h2>
|
||||
<p>Основной канал отправки M2M-сообщений в НРД. ИШ сам подписывает пакеты ЭДО, нам криптография в этом канале не нужна.</p>
|
||||
<p><strong>Профили</strong> (см. <a href="/admin/setup">Настройка → Интеграционный шлюз НРД</a>):</p>
|
||||
<h2>1. Интеграционный шлюз НРД (ИШ) и контуры тестирования</h2>
|
||||
<p>Основной канал отправки M2M-сообщений в НРД — Web-сервис <strong>ONYX</strong> через ИШ. ИШ сам подписывает пакеты ЭДО, поэтому в этом канале нам криптография не требуется. Дистрибутив ИШ скачивается с сайта НРД: <code>www.nsd.ru/workflow/system/programs/#0-widget-faq-0-4</code></p>
|
||||
<p>Адреса контуров (из <code>DOC/Ссылки для доступа в тестовые контуры.pdf</code>):</p>
|
||||
<table>
|
||||
<thead><tr><th>Профиль</th><th>Среда</th><th>Криптография</th></tr></thead>
|
||||
<thead><tr><th>Сервис</th><th>GUEST · ГОСТ</th><th>GUEST · RSA</th><th>TEST3 · ГОСТ</th><th>TEST3 · RSA</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><code>guest-gost</code></td><td>Гостевой контур (без проверок)</td><td>ГОСТ Р 34.10-2012</td></tr>
|
||||
<tr><td><code>guest-rsa</code></td><td>Гостевой контур</td><td>RSA</td></tr>
|
||||
<tr><td><code>test3-gost</code></td><td>Тестовый контур TEST3</td><td>ГОСТ</td></tr>
|
||||
<tr><td><code>test3-rsa</code></td><td>TEST3</td><td>RSA</td></tr>
|
||||
<tr><td><code>prod-gost</code></td><td>Продуктивный</td><td>ГОСТ</td></tr>
|
||||
<tr><td><code>prod-rsa</code></td><td>Продуктивный</td><td>RSA</td></tr>
|
||||
<tr>
|
||||
<td><strong>WEB-сервис ONYX</strong> (нужен нам)</td>
|
||||
<td><code>gost-gt.nsd.ru</code></td>
|
||||
<td><code>rsa-gt.nsd.ru</code></td>
|
||||
<td><code>gost-t3.nsd.ru</code></td>
|
||||
<td><code>rsa-t3.nsd.ru</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Единый кабинет администратора НРДирект</td>
|
||||
<td colspan="2"><code>cabinet-gt.nsd.ru/wr-admin/</code></td>
|
||||
<td colspan="2"><code>cabinet-t3.nsd.ru/wr-admin/</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>WEB-сервис Agate (WSAlameda)</td>
|
||||
<td colspan="2"><code>gost-gt.nsd.ru/WSAlamedags/</code> · <code>rsa-gt.nsd.ru/WSAlamedags/</code></td>
|
||||
<td colspan="2"><code>gost-t3.nsd.ru/WSAlameda/</code> · <code>rsa-t3.nsd.ru/WSAlameda/</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Депозитарно-клиринговых услуг (новый)</td>
|
||||
<td colspan="2" class="muted">отсутствует</td>
|
||||
<td><code>cabinet-t3.nsd.ru/dcs_new/</code></td>
|
||||
<td class="muted">—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">Полный URL WSL для ONYX: <code>https://<host>/onyx-ms/OnyxEdoWSService/OnyxEdo</code>. Для прод-контура ссылки опубликованы в Анкете НРД ЭДО (<code>anketa_nrd_edo_2022_07_11.pdf</code> на сайте НРД). IP <code>gost.nsd.ru</code> — 91.208.232.151 (для настройки межсетевого экрана).</p>
|
||||
<p><strong>Что указать в Настройка → ИШ:</strong></p>
|
||||
<ul>
|
||||
<li>Профиль (например, <code>test3-gost</code>)</li>
|
||||
<li>URL ИШ — обычно <code>http://localhost:8080</code> если ИШ установлен на той же ВМ</li>
|
||||
<li>Ключевой контейнер — имя на стороне ИШ, например <code>TEST3_GOST_CONTAINER</code></li>
|
||||
<li>Профиль (например, <code>test3-gost</code>) — при выборе URL и контейнер заполняются автоматически</li>
|
||||
<li>URL ONYX — например <code>https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo</code></li>
|
||||
<li>Ключевой контейнер — имя контейнера КриптоПро с ключами ЭДО НРД (выдаются УЦ НРД, см. ниже)</li>
|
||||
</ul>
|
||||
<p class="muted">Без настроенного ИШ система работает в <strong>mock-режиме</strong>: bj-server эмитирует синтетический Decision через 3 секунды для каждой заявки. Это удобно для дев-демо и не требует подключения к НРД.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>1а. Сертификаты УЦ НРД (для проверки квитанций)</h2>
|
||||
<p>НРД подписывает все исходящие пакеты ЭДО ГОСТ-подписью своего УЦ. Для проверки этих подписей на нашей стороне нужно импортировать корневые сертификаты УЦ НРД в хранилище <code>mroot</code> (доверенные корневые).</p>
|
||||
<ol>
|
||||
<li>Скачать сертификаты с сайта УЦ НРД: <code>www.nsd.ru/workflow/system/cryptography/</code> (или из дистрибутива ИШ).</li>
|
||||
<li>В <a href="/admin/setup">/admin/setup</a> → раздел «Импорт сертификата» → выбрать файл <code>.cer</code>, тип хранилища <code>mroot — корневой УЦ</code>, нажать «Импортировать». Под капотом выполняется <code>certmgr -inst -file root.cer -store mroot</code>.</li>
|
||||
<li>Промежуточные сертификаты УЦ — в хранилище <code>uRoot</code>.</li>
|
||||
<li>Для проверки подписей самой системы НРД (квитанции ЭДО) — импортировать сертификат подписи НРД в <code>uMy</code> (как корреспондента), либо оставить в <code>mroot</code>, если он самоподписной.</li>
|
||||
</ol>
|
||||
<p><strong>Наши сертификаты для отправки в НРД</strong> (получаются из другого УЦ — нашей организации):</p>
|
||||
<ol>
|
||||
<li>Сертификат подписи нашей организации (с приватным ключом в виде <code>.pfx</code>/<code>.p12</code> или на Рутокен) — импортировать в <code>uMy</code> с PIN.</li>
|
||||
<li>Цепочка сертификатов вашего УЦ — в <code>mroot</code> (корневой) и <code>uRoot</code> (промежуточные).</li>
|
||||
<li>После импорта проверить: <code>certmgr -list -store uMy</code> и <code>cpverify</code>.</li>
|
||||
</ol>
|
||||
<p class="muted">Полный цикл обмена сертификатами с НРД описан в <code>DOC/Инструкция M2M.pdf</code> и <code>DOC/Презентация MOEX MOST.pdf</code>.</p>
|
||||
</div>
|
||||
<p><strong>Документация по подключению</strong>: <code>DOC/instr_podkl_stend_v3.pdf</code>, <code>DOC/Ссылки для доступа в тестовые контуры.pdf</code>.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -89,6 +89,42 @@
|
||||
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Загрузить и установить</button>
|
||||
</form>
|
||||
|
||||
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
|
||||
<h3 style="font-size:14px;margin:0 0 8px">Сертификаты на токенах</h3>
|
||||
{{if .Certificates}}
|
||||
<table>
|
||||
<thead><tr><th>Кому</th><th>Кем выдан</th><th>ИНН</th><th>Действителен</th><th>Токен</th><th>Приватный ключ</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Certificates}}
|
||||
<tr>
|
||||
<td>{{.SubjectCN}}</td>
|
||||
<td class="muted">{{.IssuerCN}}</td>
|
||||
<td><code>{{.INN}}</code></td>
|
||||
<td class="muted">до {{.NotAfter.Format "02.01.2006"}}</td>
|
||||
<td class="muted">«{{.TokenLabel}}» (slot {{.SlotID}})</td>
|
||||
<td>{{if .HasPrivateKey}}<span style="color:var(--ok)">есть</span>{{else}}<span class="muted">нет</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p class="muted">На подключенных токенах сертификатов не найдено. Загрузите .pfx ниже или подключите Рутокен с сертификатом.</p>
|
||||
{{end}}
|
||||
|
||||
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
|
||||
<h3 style="font-size:14px;margin:0 0 8px">Импорт сертификата (.pfx / .cer / .crt)</h3>
|
||||
<p class="muted">PFX с приватным ключом (с PIN) — для серверной подписи и подписи оператора. CER/CRT без приватного ключа — для проверки чужих подписей (например, сертификаты УЦ НРД для проверки квитанций). Подробно — <a href="/admin/help/cryptopro">/admin/help/cryptopro</a>.</p>
|
||||
<form method="post" action="/admin/setup/crypto/import-cert" enctype="multipart/form-data" style="margin-top:8px;display:grid;gap:8px;grid-template-columns:auto auto auto auto;align-items:center">
|
||||
<input type="file" name="cert" accept=".pfx,.p12,.cer,.crt" required style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<input type="text" name="pin" placeholder="PIN (только для .pfx/.p12)" style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px;font-family:monospace">
|
||||
<select name="store" style="padding:6px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<option value="uMy">uMy — личный (для подписи)</option>
|
||||
<option value="mroot">mroot — корневой УЦ (для проверки)</option>
|
||||
<option value="uRoot">uRoot — промежуточные УЦ</option>
|
||||
</select>
|
||||
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Импортировать</button>
|
||||
</form>
|
||||
|
||||
<hr style="margin:18px 0;border:none;border-top:1px solid var(--border)">
|
||||
<h3 style="font-size:14px;margin:0 0 8px">Активация лицензии</h3>
|
||||
<form method="post" action="/admin/setup/crypto/activate" style="margin-top:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
@@ -101,34 +137,60 @@
|
||||
|
||||
<!-- nsd-adapter / ИШ НРД -->
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}err{{end}}"></span>Интеграционный шлюз НРД</h2>
|
||||
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}err{{end}}"></span>Интеграционный шлюз НРД (ИШ)</h2>
|
||||
<p class="muted">{{if not .Settings.NSD.IGWBaseURL}}Сейчас <code>mock-режим</code> — Decision эмитируется через 3 секунды после Send.{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}}</p>
|
||||
<p class="muted">Подключение к стендам: <a href="/admin/help/systems">/admin/help/systems</a> — там полная таблица URL контуров GUEST/TEST3/PROD и инструкция по установке ИШ. Дистрибутив ИШ скачивается с <code>www.nsd.ru/workflow/system/programs/#0-widget-faq-0-4</code>.</p>
|
||||
<details {{if not .Settings.NSD.IGWBaseURL}}open{{end}}>
|
||||
<summary style="cursor:pointer;color:var(--accent);font-size:13px">Изменить параметры ИШ</summary>
|
||||
<form method="post" action="/admin/setup/nsd" style="margin-top:12px;display:grid;gap:10px">
|
||||
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
|
||||
<label>Профиль</label>
|
||||
<select name="profile" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<option value="" {{if eq .Settings.NSD.Profile ""}}selected{{end}}>— mock (демо) —</option>
|
||||
<option value="guest-gost" {{if eq .Settings.NSD.Profile "guest-gost"}}selected{{end}}>guest-gost</option>
|
||||
<option value="guest-rsa" {{if eq .Settings.NSD.Profile "guest-rsa"}}selected{{end}}>guest-rsa</option>
|
||||
<option value="test3-gost" {{if eq .Settings.NSD.Profile "test3-gost"}}selected{{end}}>test3-gost</option>
|
||||
<option value="test3-rsa" {{if eq .Settings.NSD.Profile "test3-rsa"}}selected{{end}}>test3-rsa</option>
|
||||
<option value="prod-gost" {{if eq .Settings.NSD.Profile "prod-gost"}}selected{{end}}>prod-gost</option>
|
||||
<option value="prod-rsa" {{if eq .Settings.NSD.Profile "prod-rsa"}}selected{{end}}>prod-rsa</option>
|
||||
<select name="profile" id="nsd-profile" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<option value="" {{if eq .Settings.NSD.Profile ""}}selected{{end}}>— mock (демо без реального ИШ) —</option>
|
||||
<option value="guest-gost" {{if eq .Settings.NSD.Profile "guest-gost"}}selected{{end}}>guest-gost — контур GUEST, ГОСТ ключи</option>
|
||||
<option value="guest-rsa" {{if eq .Settings.NSD.Profile "guest-rsa"}}selected{{end}}>guest-rsa — контур GUEST, RSA ключи</option>
|
||||
<option value="test3-gost" {{if eq .Settings.NSD.Profile "test3-gost"}}selected{{end}}>test3-gost — контур TEST3, ГОСТ ключи</option>
|
||||
<option value="test3-rsa" {{if eq .Settings.NSD.Profile "test3-rsa"}}selected{{end}}>test3-rsa — контур TEST3, RSA ключи</option>
|
||||
<option value="prod-gost" {{if eq .Settings.NSD.Profile "prod-gost"}}selected{{end}}>prod-gost — продуктивный, ГОСТ</option>
|
||||
<option value="prod-rsa" {{if eq .Settings.NSD.Profile "prod-rsa"}}selected{{end}}>prod-rsa — продуктивный, RSA</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
|
||||
<label>URL ИШ НРД</label>
|
||||
<input type="text" name="igw_base_url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="http://localhost:8080 (адрес ИШ НРД)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<label>URL ONYX (WSL) НРД</label>
|
||||
<input type="text" name="igw_base_url" id="nsd-url" value="{{.Settings.NSD.IGWBaseURL}}" placeholder="будет заполнено по профилю" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
</div>
|
||||
<div class="form-row" style="display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:center">
|
||||
<label>Ключевой контейнер</label>
|
||||
<input type="text" name="key_container" value="{{.Settings.NSD.KeyContainer}}" placeholder="GUEST_GOST_CONTAINER" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
<input type="text" name="key_container" id="nsd-container" value="{{.Settings.NSD.KeyContainer}}" placeholder="GUEST_GOST_CONTAINER (или ваш контейнер УЦ НРД)" style="padding:8px;background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:4px">
|
||||
</div>
|
||||
<p class="muted">При сохранении выполняется GET <code>{URL}/healthz</code>. Пустой URL = вернуться к mock-режиму.</p>
|
||||
<p class="muted">При смене профиля URL ONYX автозаполнится по таблице НРД (из <code>DOC/Ссылки для доступа в тестовые контуры.pdf</code>). При сохранении проверяется доступность URL.</p>
|
||||
<button type="submit" class="btn" style="background:var(--accent);color:white;border:none;padding:8px 16px;border-radius:4px">Сохранить и проверить</button>
|
||||
</form>
|
||||
<script>
|
||||
// Автозаполнение URL ONYX и дефолтного контейнера по выбранному профилю.
|
||||
(function() {
|
||||
var urls = {
|
||||
"guest-gost": ["https://gost-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_GOST_CONTAINER"],
|
||||
"guest-rsa": ["https://rsa-gt.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "GUEST_RSA_CONTAINER"],
|
||||
"test3-gost": ["https://gost-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_GOST_CONTAINER"],
|
||||
"test3-rsa": ["https://rsa-t3.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "TEST3_RSA_CONTAINER"],
|
||||
"prod-gost": ["https://gost.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_GOST_CONTAINER"],
|
||||
"prod-rsa": ["https://rsa.nsd.ru/onyx-ms/OnyxEdoWSService/OnyxEdo", "PROD_RSA_CONTAINER"]
|
||||
};
|
||||
var profile = document.getElementById("nsd-profile");
|
||||
var urlInput = document.getElementById("nsd-url");
|
||||
var contInput = document.getElementById("nsd-container");
|
||||
profile.addEventListener("change", function() {
|
||||
var p = profile.value;
|
||||
if (urls[p]) {
|
||||
if (!urlInput.value || confirm("Заменить URL и контейнер на дефолт для профиля " + p + "?")) {
|
||||
urlInput.value = urls[p][0];
|
||||
contInput.value = urls[p][1];
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user