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:
@@ -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:
|
||||
|
||||
@@ -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 |
|
||||
|
||||
## Как запустить задачу
|
||||
|
||||
|
||||
@@ -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: запасной путь (ручная проверка / откладывание).
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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`
|
||||
(чтобы отличать «сокет недоступен» от «криптография не подключена»).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+62
@@ -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();
|
||||
}
|
||||
}
|
||||
+54
@@ -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();
|
||||
}
|
||||
}
|
||||
+24
@@ -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();
|
||||
}
|
||||
}
|
||||
+53
@@ -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();
|
||||
}
|
||||
}
|
||||
+38
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user