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:
+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