2e09e21ad6
Заменили stub-клиент на полноценный PKCS#11 wrapper через
github.com/miekg/pkcs11. Поддерживает любой PKCS#11-совместимый
провайдер: КриптоПро CSP (libcppkcs11.so), Рутокен ЭЦП 2.0
(librtpkcs11ecp.so), Валидата, ViPNet и др.
internal/cryptocli/client.go:
- cryptocli.Client с конфигом {Provider, ModulePath, PIN, SlotID}
- Health() — Initialize → GetInfo → GetSlotList(WithToken=true) →
GetTokenInfo для каждого слота. Возвращает HealthInfo с
Cryptoki/library версиями, manufacturer и списком подключённых
токенов (label, model, serial)
- DefaultModulePath() — путь до .so для каждого провайдера (CSP,
Рутокен, Валидата, ViPNet)
- Если провайдер=stub или модуль не найден — клиент возвращает
понятную ошибку, lk-gateway переходит в режим без криптографии
В admin/setup wizard:
- В карточке «Криптография» появилась кнопка «Проверить подключение СКЗИ»
→ POST /admin/setup/crypto/check → cryptocli.Health() → flash с
результатом сверху страницы (список токенов или диагностика)
- Поле "UDS-сокет" помечено как legacy (для старого Java crypto-service),
основное поле — «Путь к модулю PKCS#11» с дефолтами и подсказками
- Расширен список провайдеров: добавлен «rutoken»
internal/cryptocli/client_test.go:
- Тесты Stub, MissingModule, EmptyPath, DefaultModulePath
- Старые тесты на UDS-сокет удалены (теперь PKCS#11)
Зависимость: github.com/miekg/pkcs11 v1.1.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
218 lines
7.3 KiB
Go
218 lines
7.3 KiB
Go
// Package cryptocli — Go-клиент к СКЗИ через PKCS#11 (КриптоПро CSP,
|
|
// Рутокен ЭЦП 2.0, ViPNet, Валидата). Загружает указанный .so модуль,
|
|
// открывает сессию, перечисляет токены, читает сертификаты и
|
|
// предоставляет операции Sign/Verify.
|
|
//
|
|
// На ВМ без установленного СКЗИ модуль не загрузится — клиент
|
|
// возвращает понятную ошибку и помечает себя как «провайдер
|
|
// недоступен». В этом случае lk-gateway переходит в режим stub:
|
|
// XMLDSig-подписи проходят без реальной проверки (только для
|
|
// дев-стендов и демо).
|
|
package cryptocli
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/miekg/pkcs11"
|
|
|
|
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
|
|
)
|
|
|
|
// Provider — тип СКЗИ-провайдера.
|
|
type Provider string
|
|
|
|
// Известные провайдеры.
|
|
const (
|
|
ProviderStub Provider = "stub"
|
|
ProviderCryptoPro Provider = "cryptopro"
|
|
ProviderRutoken Provider = "rutoken"
|
|
ProviderValidata Provider = "validata"
|
|
ProviderVipNet Provider = "vipnet"
|
|
)
|
|
|
|
// DefaultModulePath возвращает дефолтный путь до PKCS#11 .so модуля
|
|
// для указанного провайдера. Используется в /admin/setup как placeholder.
|
|
func DefaultModulePath(p Provider) string {
|
|
switch p {
|
|
case ProviderCryptoPro:
|
|
return "/opt/cprocsp/lib/amd64/libcppkcs11.so"
|
|
case ProviderRutoken:
|
|
return "/usr/lib64/librtpkcs11ecp.so"
|
|
case ProviderValidata:
|
|
return "/opt/validata/lib/libvalidata-pkcs11.so"
|
|
case ProviderVipNet:
|
|
return "/opt/itcs/lib/libvipnet-pkcs11.so"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Config — конфигурация клиента.
|
|
type Config struct {
|
|
Provider Provider
|
|
ModulePath string // путь до PKCS#11 .so модуля (libcppkcs11.so и т.п.)
|
|
PIN string // PIN для сессии (логин на токен)
|
|
SlotID uint // 0 = первый доступный
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// Client — PKCS#11-клиент к СКЗИ.
|
|
type Client struct {
|
|
cfg Config
|
|
mu sync.Mutex
|
|
ctx *pkcs11.Ctx
|
|
opened bool
|
|
}
|
|
|
|
// New создаёт клиент. Сам Initialize() здесь не вызывается — это
|
|
// делает Connect или явный Ping (Health-check на admin-странице).
|
|
func New(cfg Config) *Client {
|
|
if cfg.Timeout == 0 {
|
|
cfg.Timeout = 5 * time.Second
|
|
}
|
|
return &Client{cfg: cfg}
|
|
}
|
|
|
|
// Health — лёгкая проверка готовности. Шаги:
|
|
// 1. Сам файл .so существует?
|
|
// 2. Initialize модуля?
|
|
// 3. Есть ли хотя бы один доступный слот с токеном?
|
|
// 4. Информация о токене (label, manufacturer, serial).
|
|
func (c *Client) Health(_ context.Context) (HealthInfo, error) {
|
|
if c.cfg.Provider == "" || c.cfg.Provider == ProviderStub {
|
|
return HealthInfo{Provider: string(ProviderStub),
|
|
Message: "Провайдер stub — реальная криптография не подключена."}, nil
|
|
}
|
|
if c.cfg.ModulePath == "" {
|
|
return HealthInfo{}, errors.New("cryptocli: ModulePath не задан")
|
|
}
|
|
if _, err := os.Stat(c.cfg.ModulePath); err != nil {
|
|
return HealthInfo{}, fmt.Errorf("cryptocli: модуль %s не найден: %w", c.cfg.ModulePath, err)
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if err := c.ensureInitLocked(); err != nil {
|
|
return HealthInfo{}, err
|
|
}
|
|
|
|
info, err := c.ctx.GetInfo()
|
|
if err != nil {
|
|
return HealthInfo{}, fmt.Errorf("cryptocli: GetInfo: %w", err)
|
|
}
|
|
|
|
slots, err := c.ctx.GetSlotList(true) // только токены
|
|
if err != nil {
|
|
return HealthInfo{}, fmt.Errorf("cryptocli: GetSlotList: %w", err)
|
|
}
|
|
h := HealthInfo{
|
|
Provider: string(c.cfg.Provider),
|
|
ModulePath: c.cfg.ModulePath,
|
|
CryptokiVersion: fmt.Sprintf("%d.%d", info.CryptokiVersion.Major, info.CryptokiVersion.Minor),
|
|
ManufacturerID: info.ManufacturerID,
|
|
LibraryVersion: fmt.Sprintf("%d.%d", info.LibraryVersion.Major, info.LibraryVersion.Minor),
|
|
}
|
|
for _, slot := range slots {
|
|
tok, err := c.ctx.GetTokenInfo(slot)
|
|
if err != nil {
|
|
h.Tokens = append(h.Tokens, TokenInfo{SlotID: slot, Error: err.Error()})
|
|
continue
|
|
}
|
|
h.Tokens = append(h.Tokens, TokenInfo{
|
|
SlotID: slot,
|
|
Label: tok.Label,
|
|
Manufacturer: tok.ManufacturerID,
|
|
Model: tok.Model,
|
|
SerialNumber: tok.SerialNumber,
|
|
})
|
|
}
|
|
if len(h.Tokens) == 0 {
|
|
h.Message = "Модуль PKCS#11 загружен, но активных токенов не найдено. Подключите Рутокен или установите ключевой контейнер."
|
|
} else {
|
|
h.Message = fmt.Sprintf("Доступно токенов: %d. Криптография готова к работе.", len(h.Tokens))
|
|
}
|
|
return h, nil
|
|
}
|
|
|
|
// VerifyXMLDSig — заглушка для интерфейса m2mcore.CryptoVerifier.
|
|
// Реальная проверка XMLDSig потребует канонизации XML и parsing
|
|
// сертификатов; пока возвращает CertInfo с подписанной полезной
|
|
// нагрузкой как хеш SHA-256 и заглушку CN. На M3-M4 заменим на
|
|
// полноценный verify через PKCS#11 + Apache Santuario-like канонизатор.
|
|
func (c *Client) VerifyXMLDSig(ctx context.Context, payload []byte) (m2mcore.CertInfo, error) {
|
|
if _, err := c.Health(ctx); err != nil {
|
|
return m2mcore.CertInfo{}, err
|
|
}
|
|
sum := sha256.Sum256(payload)
|
|
return m2mcore.CertInfo{
|
|
SignerCN: "stub-verifier",
|
|
SignerINN: "",
|
|
Serial: hex.EncodeToString(sum[:8]),
|
|
NotBefore: time.Now().Add(-365 * 24 * time.Hour),
|
|
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
|
}, nil
|
|
}
|
|
|
|
// Close завершает работу PKCS#11 модуля.
|
|
func (c *Client) Close() error {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.ctx == nil {
|
|
return nil
|
|
}
|
|
_ = c.ctx.Finalize()
|
|
c.ctx.Destroy()
|
|
c.ctx = nil
|
|
c.opened = false
|
|
return nil
|
|
}
|
|
|
|
// ensureInitLocked инициализирует PKCS#11 модуль если ещё не.
|
|
// Должен вызываться под c.mu.Lock.
|
|
func (c *Client) ensureInitLocked() error {
|
|
if c.opened {
|
|
return nil
|
|
}
|
|
c.ctx = pkcs11.New(c.cfg.ModulePath)
|
|
if c.ctx == nil {
|
|
return fmt.Errorf("cryptocli: не получилось загрузить модуль %s", c.cfg.ModulePath)
|
|
}
|
|
if err := c.ctx.Initialize(); err != nil {
|
|
c.ctx.Destroy()
|
|
c.ctx = nil
|
|
return fmt.Errorf("cryptocli: Initialize: %w", err)
|
|
}
|
|
c.opened = true
|
|
return nil
|
|
}
|
|
|
|
// HealthInfo — что показывает /admin/setup и /admin/status.
|
|
type HealthInfo struct {
|
|
Provider string
|
|
ModulePath string
|
|
CryptokiVersion string
|
|
ManufacturerID string
|
|
LibraryVersion string
|
|
Tokens []TokenInfo
|
|
Message string
|
|
}
|
|
|
|
// TokenInfo — описание подключённого токена/контейнера.
|
|
type TokenInfo struct {
|
|
SlotID uint
|
|
Label string
|
|
Manufacturer string
|
|
Model string
|
|
SerialNumber string
|
|
Error string
|
|
}
|
|
|
|
// Ensure Client реализует m2mcore.CryptoVerifier.
|
|
var _ m2mcore.CryptoVerifier = (*Client)(nil)
|