// 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)