diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml index d7d8eb9..0697f73 100644 --- a/deploy/docker-compose/docker-compose.yml +++ b/deploy/docker-compose/docker-compose.yml @@ -32,6 +32,20 @@ services: volumes: - 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: bj-postgres-data: bj-minio-data: + bj-crypto-sock: diff --git a/docs/tasks/README.md b/docs/tasks/README.md index ce8db11..cf55d81 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -16,7 +16,7 @@ PR-1 → PR-N. Каждая задача — самостоятельный ос | PR-3 | `PR-3-lk-openapi.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-6 | `PR-6-crypto-service-skeleton.md` | ждёт КриптоПро JCP | PR-1 | +| PR-6 | `PR-6-crypto-service-skeleton.md` | выполнено (скелет) | PR-1 | ## Как запустить задачу diff --git a/internal/cryptocli/README.md b/internal/cryptocli/README.md new file mode 100644 index 0000000..a5dfd2e --- /dev/null +++ b/internal/cryptocli/README.md @@ -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: запасной путь (ручная проверка / откладывание). +} +``` diff --git a/internal/cryptocli/client.go b/internal/cryptocli/client.go new file mode 100644 index 0000000..733d1f0 --- /dev/null +++ b/internal/cryptocli/client.go @@ -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) diff --git a/internal/cryptocli/client_test.go b/internal/cryptocli/client_test.go new file mode 100644 index 0000000..567eab0 --- /dev/null +++ b/internal/cryptocli/client_test.go @@ -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("")) + 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") + } +} diff --git a/services/crypto-service/Dockerfile b/services/crypto-service/Dockerfile new file mode 100644 index 0000000..f490c3a --- /dev/null +++ b/services/crypto-service/Dockerfile @@ -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"] diff --git a/services/crypto-service/README.md b/services/crypto-service/README.md index fafc470..2442d0d 100644 --- a/services/crypto-service/README.md +++ b/services/crypto-service/README.md @@ -1,24 +1,66 @@ -# services/crypto-service — Java + КриптоПро JCP +# services/crypto-service — крипто-сервис на Java + КриптоПро JCP -Изолированный сервис для криптографических операций по ГОСТ: +gRPC-сервис криптографических операций по ГОСТ Р 34.10-2012: -- проверка подписи входящих квитанций ЭДО, ответов НРД и сообщений - от брокеров; -- серверная подпись действий оператора в `admin-ui` (ради соблюдения - SLA 2 мин — клиентский КриптоПро на АРМ исключён); -- резервный канал отправки в НРД через WS ONYX напрямую (когда ИШ не - задействован — упаковка и подпись на нашей стороне); +- проверка XMLDSig-подписи входящих квитанций НРД, ответов брокеров, + заявлений ЛК; +- серверная подпись действий оператора (admin-ui, SLA 2 мин); +- резервный канал в НРД через WS ONYX напрямую (когда ИШ не задействован + — упаковка и подпись на нашей стороне); - криптографические проверки целостности эталонов в MinIO. -Стек: Java 21 (Liberica JDK), Apache Santuario с ГОСТ-патчем, КриптоПро -JCP. КриптоПро CSP ставится на ВМ. Класс СКЗИ — **КС1** (программная -защита, без HSM). +Слушает Unix Domain Socket (`/run/bj/crypto.sock` по умолчанию, +переопределяется через `BJ_CRYPTO_SOCKET`). gRPC по UDS — без сетевого +хопа, минимальная задержка. -Канал общения с 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` +(чтобы отличать «сокет недоступен» от «криптография не подключена»). diff --git a/services/crypto-service/build.gradle.kts b/services/crypto-service/build.gradle.kts new file mode 100644 index 0000000..d435dea --- /dev/null +++ b/services/crypto-service/build.gradle.kts @@ -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() +} diff --git a/services/crypto-service/proto/crypto.proto b/services/crypto-service/proto/crypto.proto new file mode 100644 index 0000000..8cec4a8 --- /dev/null +++ b/services/crypto-service/proto/crypto.proto @@ -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; +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServer.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServer.java new file mode 100644 index 0000000..18fa18e --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServer.java @@ -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(); + } +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServiceImpl.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServiceImpl.java new file mode 100644 index 0000000..bbad591 --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/CryptoServiceImpl.java @@ -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 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 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 obs) { + obs.onNext(health.handle(request)); + obs.onCompleted(); + } +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/HealthHandler.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/HealthHandler.java new file mode 100644 index 0000000..2e93e00 --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/HealthHandler.java @@ -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(); + } +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/KeystoreProvider.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/KeystoreProvider.java new file mode 100644 index 0000000..afe81ea --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/KeystoreProvider.java @@ -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; } +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/SignHandler.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/SignHandler.java new file mode 100644 index 0000000..5759b79 --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/SignHandler.java @@ -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. Создаём элемент через 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(); + } +} diff --git a/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/VerifyHandler.java b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/VerifyHandler.java new file mode 100644 index 0000000..4ee792c --- /dev/null +++ b/services/crypto-service/src/main/java/ru/zetit/bridgeandjoins/crypto/VerifyHandler.java @@ -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, ищем элемент . + // 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(); + } +}