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 с подписанной полезной
|
||||
|
||||
Reference in New Issue
Block a user