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