9737c787f9
Инфраструктура 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>
306 lines
11 KiB
Go
306 lines
11 KiB
Go
// 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)
|