Files
Bridge-and-Join-s/internal/cryptocli/client.go
T
zuevav 9737c787f9 feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура M2M (живой обмен с НРД через ИШ):
- обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение
  ответа; INFO → ждём Decision; идемпотентность поллера
- fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO
- сырой XML ответа НРД в карточке заявки (для пересылки в ТП)
- тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes,
  4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта
- редирект из теста сразу в карточку заявки

Мастер установки ключа Валидаты на флешку (admin/setup/keywizard):
- пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник
  сертификатов (CRL) → перезапуск+проверка ИШ → готово
- привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен,
  bj-server остаётся в песочнице
- сохранение структуры профиля архива (spr<N>), перечисление съёмных USB

Прочее:
- пакет-доказательство для ТП НРД + форма регистрации участника M2M
- эталонные образцы робота (DOC/m2m_robot_samples)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 00:03:21 +03:00

306 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package cryptocli — gRPC-клиент к crypto-service по Unix Domain
// Socket. Сам Go-процесс не выполняет криптографию — всё делает
// Java-сайдкар (services/crypto-service) поверх АПК «Валидата
// Клиент L».
//
// На дев-стендах без поднятого сайдкара (стандартный путь
// /run/bj/crypto.sock не существует) клиент возвращает понятную
// ошибку «провайдер недоступен» и lk-gateway работает в stub-режиме:
// XMLDSig-подписи проходят без проверки (только для демо).
package cryptocli
import (
"context"
"errors"
"fmt"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli/cryptopb"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
// Provider — тип СКЗИ-провайдера (информативный — реальный выбор
// делает crypto-service через переменную BJ_CRYPTO_PROVIDER).
type Provider string
const (
ProviderStub Provider = "stub"
ProviderValidata Provider = "validata"
)
// DefaultModulePath сохранена для обратной совместимости с UI;
// текущий путь интеграции — не PKCS#11-модуль, а UDS-сокет
// crypto-service. Возвращаемое значение информативное.
func DefaultModulePath(p Provider) string {
if p == ProviderValidata {
return "/opt/Validata/VDCSP/lib/amd64 (через сайдкар, не PKCS#11)"
}
return ""
}
// Config — конфигурация клиента.
type Config struct {
// SocketPath — путь к UDS-сокету crypto-service.
// Пустое значение = /run/bj/crypto.sock.
SocketPath string
// Provider — желаемый провайдер; информативно (см. выше).
Provider Provider
// ModulePath — сохраняется для UI; в gRPC-режиме не используется.
ModulePath string
// Timeout — таймаут одной gRPC-операции.
Timeout time.Duration
}
// Client — gRPC-клиент к crypto-service.
type Client struct {
cfg Config
mu sync.Mutex
conn *grpc.ClientConn
api cryptopb.CryptoServiceClient
}
// New создаёт клиент. Само соединение поднимается лениво при первом
// вызове.
func New(cfg Config) *Client {
if cfg.Timeout == 0 {
cfg.Timeout = 5 * time.Second
}
if cfg.SocketPath == "" {
cfg.SocketPath = "/run/bj/crypto.sock"
}
return &Client{cfg: cfg}
}
// Close закрывает gRPC-соединение.
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn != nil {
err := c.conn.Close()
c.conn = nil
c.api = nil
return err
}
return nil
}
// ensureConn устанавливает gRPC-канал к UDS-сокету при первом
// использовании. Используем встроенный в grpc-go резолвер unix:.
func (c *Client) ensureConn() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.api != nil {
return nil
}
target := "unix:" + c.cfg.SocketPath
conn, err := grpc.NewClient(target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return fmt.Errorf("cryptocli: dial %s: %w", c.cfg.SocketPath, err)
}
c.conn = conn
c.api = cryptopb.NewCryptoServiceClient(conn)
return nil
}
// Health — gRPC Health-вызов. Если сокет недоступен (сайдкар не
// поднят) — вернёт «провайдер недоступен» с явной ошибкой.
func (c *Client) Health(ctx context.Context) (HealthInfo, error) {
if c.cfg.Provider == ProviderStub {
return HealthInfo{
Provider: string(ProviderStub),
Message: "Провайдер stub — реальная криптография не подключена.",
}, nil
}
if err := c.ensureConn(); err != nil {
return HealthInfo{}, err
}
cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
resp, err := c.api.Health(cctx, &cryptopb.HealthRequest{})
if err != nil {
return HealthInfo{}, fmt.Errorf("cryptocli: Health: %w", err)
}
return HealthInfo{
Provider: resp.GetProvider(),
Message: resp.GetVersion(),
ModulePath: c.cfg.SocketPath,
}, nil
}
// Certificate — упрощённое описание сертификата (для совместимости с
// прежним UI). В gRPC-режиме crypto-service возвращает информацию о
// подписанте через VerifyResponse; полный список сертификатов
// (FindCertificates) пока не реализован — для UI возвращаем пустой
// список.
type Certificate struct {
SlotID uint
TokenLabel string
Label string
SubjectCN string
IssuerCN string
Serial string
NotBefore time.Time
NotAfter time.Time
INN string
DER []byte
HasPrivateKey bool
}
// FindCertificates пока возвращает пустой список — список ключей
// управляется самой Валидатой через её собственный справочник (zcs),
// а bj-server о конкретных сертификатах узнаёт по результатам
// Verify/Sign-операций. Эту функцию переопределим позже отдельным
// gRPC-методом ListCertificates если потребуется.
func (c *Client) FindCertificates(_ context.Context) ([]Certificate, error) {
return nil, nil
}
// Shutdown — отправляет команду «выйти с exit-code 2» сайдкару.
// systemd с Restart=on-failure поднимет его обратно. Возвращает
// ошибку если соединение разорвалось (что нормально и означает что
// сайдкар уже завершается).
func (c *Client) Shutdown(ctx context.Context) error {
if c.cfg.Provider == ProviderStub {
return errors.New("provider=stub: некуда отправлять Shutdown")
}
if err := c.ensureConn(); err != nil {
return err
}
cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
_, err := c.api.Shutdown(cctx, &cryptopb.ShutdownRequest{})
// Закрываем соединение, чтобы не держать ссылку на падающий процесс.
_ = c.Close()
return err
}
// ActivateResult — результат переключения профиля Валидаты.
type ActivateResult struct {
OK bool
Provider string
Profile string
Message string
}
// Activate переключает crypto-service на указанный профиль pki1.conf.
// Пустая строка = minimal mode (без профиля).
func (c *Client) Activate(ctx context.Context, profile string) (ActivateResult, error) {
if c.cfg.Provider == ProviderStub {
return ActivateResult{
OK: false,
Provider: string(ProviderStub),
Message: "Провайдер stub — переключение профиля недоступно.",
}, nil
}
if err := c.ensureConn(); err != nil {
return ActivateResult{}, err
}
cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
resp, err := c.api.Activate(cctx, &cryptopb.ActivateRequest{Profile: profile})
if err != nil {
return ActivateResult{}, fmt.Errorf("cryptocli: Activate: %w", err)
}
return ActivateResult{
OK: resp.GetOk(),
Provider: resp.GetProvider(),
Profile: resp.GetProfile(),
Message: resp.GetMessage(),
}, nil
}
// VerifyXMLDSig — проксирует в crypto-service.VerifyXMLDSig.
// Реализует m2mcore.CryptoVerifier — поэтому возвращает CertInfo,
// заполненный из gRPC-ответа.
func (c *Client) VerifyXMLDSig(ctx context.Context, payload []byte) (m2mcore.CertInfo, error) {
if c.cfg.Provider == ProviderStub {
return m2mcore.CertInfo{
SignerCN: "stub-verifier",
}, nil
}
if err := c.ensureConn(); err != nil {
return m2mcore.CertInfo{}, err
}
cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
resp, err := c.api.VerifyXMLDSig(cctx, &cryptopb.VerifyRequest{
Payload: payload,
})
if err != nil {
return m2mcore.CertInfo{}, fmt.Errorf("cryptocli: VerifyXMLDSig: %w", err)
}
if !resp.GetValid() {
var msg string
if errs := resp.GetErrors(); len(errs) > 0 {
msg = errs[0]
} else {
msg = "подпись недействительна"
}
return m2mcore.CertInfo{}, errors.New("cryptocli: " + msg)
}
return m2mcore.CertInfo{
SignerCN: resp.GetSignerCn(),
SignerINN: resp.GetSignerInn(),
Serial: resp.GetSerial(),
NotBefore: time.Unix(resp.GetNotBefore(), 0),
NotAfter: time.Unix(resp.GetNotAfter(), 0),
}, nil
}
// SignXMLDSig — проксирует в crypto-service.SignXMLDSig. Возвращает
// DER-байты CMS detached signature (готовы к включению в XMLDSig-обёртку
// или к самостоятельной отправке как .p7s).
//
// keyAlias — alias ключа из ПСП Валидаты (пустой = ключ по умолчанию
// активного профиля). profile — имя профиля в pki1.conf, пустой = тот
// что инициализирован.
func (c *Client) SignXMLDSig(ctx context.Context, payload []byte, keyAlias, profile string) ([]byte, error) {
if c.cfg.Provider == ProviderStub {
return nil, errors.New("provider=stub: подпись недоступна")
}
if err := c.ensureConn(); err != nil {
return nil, err
}
cctx, cancel := context.WithTimeout(ctx, c.cfg.Timeout)
defer cancel()
resp, err := c.api.SignXMLDSig(cctx, &cryptopb.SignRequest{
Payload: payload,
KeyAlias: keyAlias,
Profile: profile,
})
if err != nil {
return nil, fmt.Errorf("cryptocli: SignXMLDSig: %w", err)
}
return resp.GetSignedXml(), nil
}
// HealthInfo — что показывает /admin/setup → СКЗИ.
type HealthInfo struct {
Provider string
ModulePath string // в gRPC-режиме — UDS-сокет
CryptokiVersion string // не используется
ManufacturerID string // не используется
LibraryVersion string // не используется
Tokens []TokenInfo
Message string
}
// TokenInfo — для совместимости с UI; в gRPC-режиме пустой.
type TokenInfo struct {
SlotID uint
Label string
Manufacturer string
Model string
SerialNumber string
Error string
}
// Ensure Client реализует m2mcore.CryptoVerifier.
var _ m2mcore.CryptoVerifier = (*Client)(nil)