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
@@ -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();
}
}