feat(crypto-service): gRPC-каркас сервиса криптографии (КриптоПро JCP)

- services/crypto-service/proto/crypto.proto — protobuf-контракт VerifyXMLDSig/SignXMLDSig/Health, package ru.zetit.bridgeandjoins.crypto.v1
- services/crypto-service/build.gradle.kts — Gradle Java 21 + protobuf-плагин + shadowJar
- services/crypto-service/src/main/java/.../CryptoServer.java — точка входа на UDS (Netty Epoll)
- services/crypto-service/src/main/java/.../CryptoServiceImpl.java — gRPC-биндинг
- services/crypto-service/src/main/java/.../{Verify,Sign,Health}Handler.java — заглушки операций
- services/crypto-service/src/main/java/.../KeystoreProvider.java — абстракция cryptopro/validata/vipnet/stub
- services/crypto-service/Dockerfile — Liberica JDK 21 → shadowJar → slim
- internal/cryptocli/client.go — Go-клиент по UDS, реализует m2mcore.CryptoVerifier (M1 stub)
- internal/cryptocli/client_test.go — тесты на доступность сокета и ErrNotImplemented
- deploy/docker-compose/docker-compose.yml — добавлен сервис crypto-service с UDS-volume

Реальная криптография КриптоПро JCP подключается после получения
лицензии и jar (положить в services/crypto-service/libs/jcp.jar) и
открытия Maven Central через прокси zetit (для grpc-java/santuario).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
fontvielle
2026-05-14 00:58:10 +03:00
parent a8cdeeb838
commit 1cf069b55b
15 changed files with 666 additions and 18 deletions
+14
View File
@@ -32,6 +32,20 @@ services:
volumes: volumes:
- bj-minio-data:/data - bj-minio-data:/data
crypto-service:
build:
context: ../../services/crypto-service
dockerfile: Dockerfile
container_name: bj-crypto-service
environment:
BJ_CRYPTO_SOCKET: /run/bj/crypto.sock
BJ_CRYPTO_PROVIDER: stub
volumes:
# UDS-сокет наружу как named volume, чтобы Go-сервисы
# (m2m-core, lk-gateway, nsd-adapter) могли его mount'ить.
- bj-crypto-sock:/run/bj
volumes: volumes:
bj-postgres-data: bj-postgres-data:
bj-minio-data: bj-minio-data:
bj-crypto-sock:
+1 -1
View File
@@ -16,7 +16,7 @@ PR-1 → PR-N. Каждая задача — самостоятельный ос
| PR-3 | `PR-3-lk-openapi.md` | выполнено | — (параллельно с PR-1) | | PR-3 | `PR-3-lk-openapi.md` | выполнено | — (параллельно с PR-1) |
| PR-4 | `PR-4-m2m-core-skeleton.md` | выполнено | PR-1 | | PR-4 | `PR-4-m2m-core-skeleton.md` | выполнено | PR-1 |
| PR-5 | `PR-5-nsd-adapter-skeleton.md` | выполнено (каркас) | PR-1, PR-4 | | PR-5 | `PR-5-nsd-adapter-skeleton.md` | выполнено (каркас) | PR-1, PR-4 |
| PR-6 | `PR-6-crypto-service-skeleton.md` | ждёт КриптоПро JCP | PR-1 | | PR-6 | `PR-6-crypto-service-skeleton.md` | выполнено (скелет) | PR-1 |
## Как запустить задачу ## Как запустить задачу
+29
View File
@@ -0,0 +1,29 @@
# internal/cryptocli — Go-клиент crypto-service
Реализация `m2mcore.CryptoVerifier` поверх gRPC по Unix Domain Socket.
## Состояние
На M1 — заглушка. Подключается к UDS-сокету crypto-service, проверяет
доступность и возвращает `ErrNotImplemented`. Этого достаточно, чтобы:
- m2m-core и другие сервисы могли инжектить клиент без условных веток;
- логи различали «сокета нет» (например, контейнер crypto-service не
запущен) от «сокет есть, но криптография не подключена» (нет лицензии
КриптоПро JCP).
## Когда станет полноценным
После генерации gRPC-стабов из `services/crypto-service/proto/crypto.proto`
(требует `protoc` + плагины), что в свою очередь требует доступа к
Maven Central / Go module proxy через прокси zetit.
## API
```go
cli := cryptocli.NewClient("/run/bj/crypto.sock")
info, err := cli.VerifyXMLDSig(ctx, signedXML)
if errors.Is(err, cryptocli.ErrNotImplemented) {
// M1: запасной путь (ручная проверка / откладывание).
}
```
+61
View File
@@ -0,0 +1,61 @@
// Package cryptocli — Go-клиент crypto-service по UDS. На M1 — заглушка,
// возвращает ErrNotImplemented. Реальная реализация — после
// генерации gRPC-стабов из proto (когда прокси откроет proto-tooling
// и Go-зависимости).
package cryptocli
import (
"context"
"errors"
"net"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
// ErrNotImplemented возвращается клиентом до подключения реального gRPC.
var ErrNotImplemented = errors.New("cryptocli: не реализовано (ждём gRPC-стабы из proto)")
// Client — клиент crypto-service по Unix Domain Socket.
type Client struct {
socketPath string
dialer net.Dialer
timeout time.Duration
}
// Option настраивает Client.
type Option func(*Client)
// WithTimeout задаёт таймаут на одну операцию.
func WithTimeout(t time.Duration) Option {
return func(c *Client) { c.timeout = t }
}
// NewClient создаёт клиента к UDS. На M1 — без реального gRPC.
// Используется как заглушка, реализующая m2mcore.CryptoVerifier.
func NewClient(socketPath string, opts ...Option) *Client {
c := &Client{socketPath: socketPath, timeout: 5 * time.Second}
for _, o := range opts {
o(c)
}
return c
}
// VerifyXMLDSig вызывает CryptoService.VerifyXMLDSig. На M1 — stub.
func (c *Client) VerifyXMLDSig(ctx context.Context, _ []byte) (m2mcore.CertInfo, error) {
// На M1 проверяем только доступность сокета и возвращаем
// ErrNotImplemented. Это позволяет m2m-core логировать «crypto-service
// доступен, но криптография ещё не подключена» отдельно от «сокета
// нет совсем».
dctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
conn, err := c.dialer.DialContext(dctx, "unix", c.socketPath)
if err != nil {
return m2mcore.CertInfo{}, err
}
_ = conn.Close()
return m2mcore.CertInfo{}, ErrNotImplemented
}
// Ensure Client реализует m2mcore.CryptoVerifier.
var _ m2mcore.CryptoVerifier = (*Client)(nil)
+52
View File
@@ -0,0 +1,52 @@
package cryptocli_test
import (
"context"
"errors"
"net"
"os"
"path/filepath"
"testing"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
)
func TestClientReturnsErrNotImplementedWhenSocketReachable(t *testing.T) {
dir := t.TempDir()
socketPath := filepath.Join(dir, "crypto.sock")
listener, err := net.Listen("unix", socketPath)
if err != nil {
t.Fatalf("listen unix: %v", err)
}
defer listener.Close()
defer os.Remove(socketPath)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
_ = conn.Close()
}
}()
cli := cryptocli.NewClient(socketPath, cryptocli.WithTimeout(time.Second))
_, err = cli.VerifyXMLDSig(context.Background(), []byte("<xml/>"))
if !errors.Is(err, cryptocli.ErrNotImplemented) {
t.Errorf("ожидалась ErrNotImplemented, получено %v", err)
}
}
func TestClientReturnsDialErrorWhenSocketMissing(t *testing.T) {
cli := cryptocli.NewClient("/nonexistent/crypto.sock", cryptocli.WithTimeout(200*time.Millisecond))
_, err := cli.VerifyXMLDSig(context.Background(), []byte("x"))
if err == nil {
t.Fatal("ожидалась ошибка диалинга на несуществующий сокет")
}
if errors.Is(err, cryptocli.ErrNotImplemented) {
t.Errorf("при отсутствующем сокете не должно быть ErrNotImplemented")
}
}
+18
View File
@@ -0,0 +1,18 @@
# Dockerfile для crypto-service.
# Базовый образ — Liberica JDK 21 (открытый дистрибутив BellSoft с
# поддержкой работы на территории РФ). Можно заменить на любой
# OpenJDK 21.
FROM bellsoft/liberica-openjdk-debian:21 AS build
WORKDIR /src
COPY . .
RUN ./gradlew --no-daemon shadowJar
FROM bellsoft/liberica-openjre-debian:21-slim
RUN useradd -r -u 1100 -g root bj-crypto && \
mkdir -p /run/bj && \
chown bj-crypto:root /run/bj
USER bj-crypto
COPY --from=build /src/build/libs/crypto-service-*-all.jar /opt/crypto-service.jar
ENV BJ_CRYPTO_SOCKET=/run/bj/crypto.sock \
BJ_CRYPTO_PROVIDER=stub
ENTRYPOINT ["java", "-jar", "/opt/crypto-service.jar"]
+59 -17
View File
@@ -1,24 +1,66 @@
# services/crypto-service — Java + КриптоПро JCP # services/crypto-service — крипто-сервис на Java + КриптоПро JCP
Изолированный сервис для криптографических операций по ГОСТ: gRPC-сервис криптографических операций по ГОСТ Р 34.10-2012:
- проверка подписи входящих квитанций ЭДО, ответов НРД и сообщений - проверка XMLDSig-подписи входящих квитанций НРД, ответов брокеров,
от брокеров; заявлений ЛК;
- серверная подпись действий оператора в `admin-ui` (ради соблюдения - серверная подпись действий оператора (admin-ui, SLA 2 мин);
SLA 2 мин — клиентский КриптоПро на АРМ исключён); - резервный канал в НРД через WS ONYX напрямую (когда ИШ не задействован
- резервный канал отправки в НРД через WS ONYX напрямую (когда ИШ не — упаковка и подпись на нашей стороне);
задействован — упаковка и подпись на нашей стороне);
- криптографические проверки целостности эталонов в MinIO. - криптографические проверки целостности эталонов в MinIO.
Стек: Java 21 (Liberica JDK), Apache Santuario с ГОСТ-патчем, КриптоПро Слушает Unix Domain Socket (`/run/bj/crypto.sock` по умолчанию,
JCP. КриптоПро CSP ставится на ВМ. Класс СКЗИ — **КС1** (программная переопределяется через `BJ_CRYPTO_SOCKET`). gRPC по UDS — без сетевого
защита, без HSM). хопа, минимальная задержка.
Канал общения с Go-ядром — gRPC по Unix Domain Socket (без сетевого хопа, Стек: Java 21 (Liberica JDK), Apache Santuario с ГОСТ-патчем,
минимальная задержка). КриптоПро JCP. Класс СКЗИ — **КС1** (программная защита, без HSM).
Архитектурно сохранена `Provider`-абстракция (Валидата JCP / VipNet и ## Состав
прочее) — для тиражирования другим компаниям. На M1 реализован только
КриптоПро.
Реализация — задача M1-M2 (заглушка gRPC) и M3-M4 (полный функционал). - `proto/crypto.proto` — protobuf-контракт: `VerifyXMLDSig`,
`SignXMLDSig`, `Health`. Go-package `cryptopb`, Java-package
`ru.zetit.bridgeandjoins.crypto.v1`.
- `build.gradle.kts` — Gradle сборка с протобуф-плагином и shadowJar.
- `src/main/java/.../CryptoServer.java` — точка входа, поднимает gRPC
на UDS (Netty Epoll). Перед стартом удаляет старый сокет.
- `src/main/java/.../CryptoServiceImpl.java` — gRPC-биндинг,
делегирует на хендлеры.
- `src/main/java/.../VerifyHandler.java`, `SignHandler.java`,
`HealthHandler.java` — реализации операций (на M1 заглушки).
- `src/main/java/.../KeystoreProvider.java` — provider-абстракция
(`cryptopro` | `validata` | `vipnet` | `stub`). Архитектурно
сохранена для тиражирования другим компаниям.
- `Dockerfile` — Liberica JDK 21 → shadowJar → slim-image, запуск под
`bj-crypto` (uid 1100).
## Состояние
На M1 — **только скелет**. Реальная криптография подключается, когда
заказчик предоставит:
- лицензию и jar КриптоПро JCP (положить в `libs/jcp.jar`);
- доступ к Maven Central через прокси zetit для скачивания
grpc-java, protobuf-java, Apache Santuario.
После этого `VerifyHandler` и `SignHandler` реализуются через
Apache Santuario с ГОСТ-патчем (см. TODO в коде).
## Запуск (после подключения зависимостей)
```bash
./gradlew shadowJar
java -jar build/libs/crypto-service-0.1.0-all.jar
```
Или в docker-compose:
```bash
podman-compose -f deploy/docker-compose/docker-compose.yml up crypto-service
```
## Go-клиент
`internal/cryptocli/` — реализация `m2mcore.CryptoVerifier` по UDS.
На M1 проверяет доступность сокета и возвращает `ErrNotImplemented`
(чтобы отличать «сокет недоступен» от «криптография не подключена»).
+83
View File
@@ -0,0 +1,83 @@
// build.gradle.kts — Gradle build для crypto-service.
//
// Артефакт: ./build/libs/crypto-service-0.1.0-all.jar (shadow jar).
// Запуск: java -jar crypto-service-0.1.0-all.jar (UDS из BJ_CRYPTO_SOCKET).
//
// Зависимости (Maven Central, подключаются автоматически):
// - grpc-java (server + protoc-gen-java)
// - protobuf-java
// - apache santuario (XMLDSig)
// - jcp (КриптоПро JCP) — внешний jar, поставляется заказчиком
// отдельно вместе с лицензией; положить в libs/.
//
// Соберётся, когда Maven Central доступен через прокси zetit
// (пока ждём админа).
plugins {
id("java")
id("application")
id("com.google.protobuf") version "0.9.4"
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = "ru.zetit.bridgeandjoins"
version = "0.1.0"
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
repositories {
mavenCentral()
flatDir { dirs("libs") } // для jcp.jar
}
dependencies {
val grpcVer = "1.62.2"
val protobufVer = "3.25.3"
implementation("io.grpc:grpc-netty-shaded:$grpcVer")
implementation("io.grpc:grpc-protobuf:$grpcVer")
implementation("io.grpc:grpc-stub:$grpcVer")
implementation("com.google.protobuf:protobuf-java:$protobufVer")
// XMLDSig (с ГОСТ-патчем поставляется в libs/ заказчиком).
implementation("org.apache.santuario:xmlsec:3.0.4")
// КриптоПро JCP — кладётся в services/crypto-service/libs/jcp.jar.
// implementation(files("libs/jcp.jar"))
compileOnly("javax.annotation:javax.annotation-api:1.3.2")
implementation("ch.qos.logback:logback-classic:1.5.6")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("io.grpc:grpc-testing:$grpcVer")
}
application {
mainClass.set("ru.zetit.bridgeandjoins.crypto.CryptoServer")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.3"
}
plugins {
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
}
generateProtoTasks {
all().forEach { task ->
task.plugins {
id("grpc")
}
}
}
}
tasks.test {
useJUnitPlatform()
}
@@ -0,0 +1,78 @@
syntax = "proto3";
package bridge_and_joins.crypto.v1;
option go_package = "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli/cryptopb;cryptopb";
option java_package = "ru.zetit.bridgeandjoins.crypto.v1";
option java_multiple_files = true;
// CryptoService — серверная криптография по ГОСТ через КриптоПро JCP.
// Слушает на Unix Domain Socket (по умолчанию /run/bj/crypto.sock).
service CryptoService {
// Проверка XMLDSig-подписи (ГОСТ или RSA). Возвращает сведения о
// подписанте: CN, ИНН (если есть), срок действия сертификата.
rpc VerifyXMLDSig(VerifyRequest) returns (VerifyResponse);
// Подпись XML по ГОСТ — для резервного канала WS ONYX и для
// серверной подписи действий оператора в admin-ui.
rpc SignXMLDSig(SignRequest) returns (SignResponse);
// Health-check.
rpc Health(HealthRequest) returns (HealthResponse);
}
message VerifyRequest {
// Целиком подписанный XML.
bytes payload = 1;
// Профиль ключей и сертификатов: "guest-gost" | "test3-gost" |
// "prod-gost" | "guest-rsa" | ... — определяет хранилище и trust store.
string profile = 2;
}
message VerifyResponse {
// Прошла ли проверка.
bool valid = 1;
// CN из сертификата подписанта.
string signer_cn = 2;
// ИНН из сертификата (если присутствует в OID 1.2.643.3.131.1.1).
string signer_inn = 3;
// Серийный номер сертификата (hex).
string serial = 4;
// Срок действия сертификата (unix epoch, секунды).
int64 not_before = 5;
int64 not_after = 6;
// Тексты ошибок проверки (если valid=false).
repeated string errors = 7;
}
message SignRequest {
// Канонизированный XML, который нужно подписать.
bytes payload = 1;
// Алиас ключа в JCP-keystore.
string key_alias = 2;
// Профиль (тот же что у Verify).
string profile = 3;
}
message SignResponse {
// Подписанный XML (с детачированной или встроенной подписью —
// зависит от профиля).
bytes signed_xml = 1;
}
message HealthRequest {}
message HealthResponse {
bool ok = 1;
string version = 2;
// Активный провайдер криптографии: "cryptopro" | "validata" | "vipnet" | "stub".
string provider = 3;
}
@@ -0,0 +1,62 @@
package ru.zetit.bridgeandjoins.crypto;
import io.grpc.Server;
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup;
import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerDomainSocketChannel;
import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;
/**
* Точка входа crypto-service.
*
* Слушает Unix Domain Socket по пути из переменной окружения
* BJ_CRYPTO_SOCKET (по умолчанию /run/bj/crypto.sock).
* Используется UDS вместо TCP — минимальная задержка и изоляция
* от сети.
*/
public final class CryptoServer {
private static final Logger LOG = Logger.getLogger(CryptoServer.class.getName());
private static final String DEFAULT_SOCKET = "/run/bj/crypto.sock";
private CryptoServer() {
// utility
}
public static void main(String[] args) throws IOException, InterruptedException {
String socketPath = System.getenv().getOrDefault("BJ_CRYPTO_SOCKET", DEFAULT_SOCKET);
String provider = System.getenv().getOrDefault("BJ_CRYPTO_PROVIDER", "stub");
Files.deleteIfExists(Path.of(socketPath));
KeystoreProvider keystore = KeystoreProvider.byName(provider);
VerifyHandler verifyHandler = new VerifyHandler(keystore);
SignHandler signHandler = new SignHandler(keystore);
HealthHandler healthHandler = new HealthHandler(provider);
CryptoServiceImpl service = new CryptoServiceImpl(verifyHandler, signHandler, healthHandler);
Server server = NettyServerBuilder
.forAddress(new DomainSocketAddress(socketPath))
.channelType(EpollServerDomainSocketChannel.class)
.bossEventLoopGroup(new EpollEventLoopGroup(1))
.workerEventLoopGroup(new EpollEventLoopGroup())
.addService(service)
.build()
.start();
LOG.info("crypto-service: UDS " + socketPath + ", provider=" + provider);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
LOG.info("crypto-service: graceful shutdown");
server.shutdown();
}));
server.awaitTermination();
}
}
@@ -0,0 +1,54 @@
package ru.zetit.bridgeandjoins.crypto;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import ru.zetit.bridgeandjoins.crypto.v1.CryptoServiceGrpc;
import ru.zetit.bridgeandjoins.crypto.v1.HealthRequest;
import ru.zetit.bridgeandjoins.crypto.v1.HealthResponse;
import ru.zetit.bridgeandjoins.crypto.v1.SignRequest;
import ru.zetit.bridgeandjoins.crypto.v1.SignResponse;
import ru.zetit.bridgeandjoins.crypto.v1.VerifyRequest;
import ru.zetit.bridgeandjoins.crypto.v1.VerifyResponse;
/**
* gRPC-биндинг: делегирует на хендлеры. Сам по себе не реализует
* криптографию.
*/
public final class CryptoServiceImpl extends CryptoServiceGrpc.CryptoServiceImplBase {
private final VerifyHandler verify;
private final SignHandler sign;
private final HealthHandler health;
public CryptoServiceImpl(VerifyHandler verify, SignHandler sign, HealthHandler health) {
this.verify = verify;
this.sign = sign;
this.health = health;
}
@Override
public void verifyXMLDSig(VerifyRequest request, StreamObserver<VerifyResponse> obs) {
try {
obs.onNext(verify.handle(request));
obs.onCompleted();
} catch (Exception e) {
obs.onError(Status.INTERNAL.withDescription(e.getMessage()).asException());
}
}
@Override
public void signXMLDSig(SignRequest request, StreamObserver<SignResponse> obs) {
try {
obs.onNext(sign.handle(request));
obs.onCompleted();
} catch (Exception e) {
obs.onError(Status.INTERNAL.withDescription(e.getMessage()).asException());
}
}
@Override
public void health(HealthRequest request, StreamObserver<HealthResponse> obs) {
obs.onNext(health.handle(request));
obs.onCompleted();
}
}
@@ -0,0 +1,24 @@
package ru.zetit.bridgeandjoins.crypto;
import ru.zetit.bridgeandjoins.crypto.v1.HealthRequest;
import ru.zetit.bridgeandjoins.crypto.v1.HealthResponse;
/** Хендлер Health: всегда ok=true с указанием активного провайдера. */
public final class HealthHandler {
private static final String VERSION = "0.1.0";
private final String provider;
public HealthHandler(String provider) {
this.provider = provider;
}
public HealthResponse handle(HealthRequest request) {
return HealthResponse.newBuilder()
.setOk(true)
.setVersion(VERSION)
.setProvider(provider)
.build();
}
}
@@ -0,0 +1,53 @@
package ru.zetit.bridgeandjoins.crypto;
/**
* Provider-абстракция над хранилищем ключей. Реальные реализации
* подключаются вместе с соответствующими jar-ами (КриптоПро JCP,
* Валидата JCP, ViPNet). На M1 — Stub.
*/
public interface KeystoreProvider {
/** Уникальное имя провайдера (используется в HealthResponse.provider). */
String name();
/** Возвращает true, если провайдер инициализирован и готов к работе. */
boolean ready();
/**
* Подбирает реализацию по имени из переменной окружения
* BJ_CRYPTO_PROVIDER. Неизвестное имя — Stub.
*/
static KeystoreProvider byName(String name) {
switch (name == null ? "" : name) {
case "cryptopro": return new CryptoProJcpProvider();
case "validata": return new ValidataJcpProvider();
case "vipnet": return new VipNetProvider();
default: return new StubProvider();
}
}
}
/** Заглушка по умолчанию. Возвращает ready()=false. */
final class StubProvider implements KeystoreProvider {
@Override public String name() { return "stub"; }
@Override public boolean ready() { return false; }
}
/** КриптоПро JCP. Реализация — после получения jar и лицензии. */
final class CryptoProJcpProvider implements KeystoreProvider {
// TODO(M2): инициализация Provider'а через Security.addProvider(new ru.CryptoPro.JCP.JCP());
@Override public String name() { return "cryptopro"; }
@Override public boolean ready() { return false; }
}
/** Валидата JCP. Заглушка. */
final class ValidataJcpProvider implements KeystoreProvider {
@Override public String name() { return "validata"; }
@Override public boolean ready() { return false; }
}
/** ViPNet. Заглушка. */
final class VipNetProvider implements KeystoreProvider {
@Override public String name() { return "vipnet"; }
@Override public boolean ready() { return false; }
}
@@ -0,0 +1,40 @@
package ru.zetit.bridgeandjoins.crypto;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import ru.zetit.bridgeandjoins.crypto.v1.SignRequest;
import ru.zetit.bridgeandjoins.crypto.v1.SignResponse;
/**
* Хендлер подписи XML. На M1 — заглушка, возвращает UNIMPLEMENTED.
* Реальная реализация подключается, когда лицензия КриптоПро JCP
* будет доступна.
*/
public final class SignHandler {
private final KeystoreProvider keystore;
public SignHandler(KeystoreProvider keystore) {
this.keystore = keystore;
}
public SignResponse handle(SignRequest request) {
// TODO(M2): реальная реализация:
// 1. Загружаем ключ по key_alias из keystore (через KeystoreProvider).
// 2. Канонизуем payload по xml-exc-c14n.
// 3. Создаём <Signature> элемент через Santuario с GOST-алгоритмом.
// 4. Возвращаем signed_xml.
//
// Сейчас — заглушка.
throw new StatusRuntimeException(Status.UNIMPLEMENTED
.withDescription("crypto-service: SignXMLDSig stub — провайдер "
+ keystore.name() + " не подключён (ждём PR-6/M2 КриптоПро JCP)"));
}
@SuppressWarnings("unused")
private SignResponse empty() {
return SignResponse.newBuilder().setSignedXml(ByteString.EMPTY).build();
}
}
@@ -0,0 +1,38 @@
package ru.zetit.bridgeandjoins.crypto;
import ru.zetit.bridgeandjoins.crypto.v1.VerifyRequest;
import ru.zetit.bridgeandjoins.crypto.v1.VerifyResponse;
/**
* Хендлер проверки XMLDSig-подписи. На M1 — заглушка, возвращающая
* valid=false с пояснением. Реальная реализация (Apache Santuario +
* КриптоПро JCP) подключается, когда лицензия JCP станет доступна.
*/
public final class VerifyHandler {
private final KeystoreProvider keystore;
public VerifyHandler(KeystoreProvider keystore) {
this.keystore = keystore;
}
public VerifyResponse handle(VerifyRequest request) {
// TODO(M2): реальная реализация через Apache Santuario.
//
// Шаги при подключении КриптоПро JCP:
// 1. Парсим XML, ищем элемент <Signature>.
// 2. Канонизуем по xml-exc-c14n.
// 3. Берём сертификат из KeyInfo, проверяем цепочку через
// trust store профиля.
// 4. Проверяем подпись (alg=GOST34112012-256/-512).
// 5. Извлекаем CN, ИНН из subject.
//
// Сейчас — заглушка.
return VerifyResponse.newBuilder()
.setValid(false)
.addErrors("crypto-service: VerifyXMLDSig stub — провайдер " + keystore.name()
+ " не подключён (ждём PR-6/M2 КриптоПро JCP)")
.build();
}
}