feat: живой цикл M2M с НРД + мастер установки ключа на флешку
Инфраструктура M2M (живой обмен с НРД через ИШ): - обработка M2MTransferResponse: ERROR(M2Mxx) → заявка Отклонена, сохранение ответа; INFO → ждём Decision; идемпотентность поллера - fallback-корреляция ответов с нулевым GUID (M2M14/M2M17) по FIFO - сырой XML ответа НРД в карточке заявки (для пересылки в ТП) - тестовый пакет роботу приведён к эталону m2m_robot_samples (CostInfo=Yes, 4 бумаги, IsolationStatus, DocumentSeries=сценарий); override паспорта - редирект из теста сразу в карточку заявки Мастер установки ключа Валидаты на флешку (admin/setup/keywizard): - пошаговый: загрузка .7z+пароль → выбор флешки → запись → справочник сертификатов (CRL) → перезапуск+проверка ИШ → готово - привилегированный воркер (bj-keymedia) в host-namespace через файл-обмен, bj-server остаётся в песочнице - сохранение структуры профиля архива (spr<N>), перечисление съёмных USB Прочее: - пакет-доказательство для ТП НРД + форма регистрации участника M2M - эталонные образцы робота (DOC/m2m_robot_samples) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
// 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).
|
||||
// Запуск:
|
||||
// java -Djava.library.path=/opt/Validata/VDCSP/lib/amd64 \
|
||||
// -jar crypto-service-0.1.0-all.jar
|
||||
//
|
||||
// Зависимости (Maven Central, подключаются автоматически):
|
||||
// - grpc-java (server + protoc-gen-java)
|
||||
// - protobuf-java
|
||||
// - apache santuario (XMLDSig)
|
||||
// - jcp (КриптоПро JCP) — внешний jar, поставляется заказчиком
|
||||
// отдельно вместе с лицензией; положить в libs/.
|
||||
// Зависимости:
|
||||
// - grpc-java (server + protoc-gen-java) — Maven Central
|
||||
// - protobuf-java — Maven Central
|
||||
// - apache santuario (XMLDSig) — Maven Central
|
||||
// - Pki1.LocalIface.jar (Валидата JNI) — libs/ (положен из
|
||||
// /opt/Validata/VDCSP/lib/)
|
||||
//
|
||||
// Соберётся, когда Maven Central доступен через прокси zetit
|
||||
// (пока ждём админа).
|
||||
// Java toolchain — 17 (LTS), на Astra 1.7 ставится из openjdk-17-jdk.
|
||||
|
||||
import com.google.protobuf.gradle.id
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
@@ -25,13 +28,13 @@ version = "0.1.0"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
flatDir { dirs("libs") } // для jcp.jar
|
||||
flatDir { dirs("libs") } // Pki1.LocalIface.jar (Валидата)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -43,11 +46,13 @@ dependencies {
|
||||
implementation("io.grpc:grpc-stub:$grpcVer")
|
||||
implementation("com.google.protobuf:protobuf-java:$protobufVer")
|
||||
|
||||
// XMLDSig (с ГОСТ-патчем поставляется в libs/ заказчиком).
|
||||
// XMLDSig — построение/разбор подписи. ГОСТ-алгоритмы реализуем сами
|
||||
// (digest + signature через VCERT_*), Santuario используем только для
|
||||
// канонизации и сборки XML-структуры.
|
||||
implementation("org.apache.santuario:xmlsec:3.0.4")
|
||||
|
||||
// КриптоПро JCP — кладётся в services/crypto-service/libs/jcp.jar.
|
||||
// implementation(files("libs/jcp.jar"))
|
||||
// Валидата Клиент L — Pki1.LocalIface.jar.
|
||||
implementation(files("libs/Pki1.LocalIface.jar"))
|
||||
|
||||
compileOnly("javax.annotation:javax.annotation-api:1.3.2")
|
||||
implementation("ch.qos.logback:logback-classic:1.5.6")
|
||||
@@ -58,6 +63,9 @@ dependencies {
|
||||
|
||||
application {
|
||||
mainClass.set("ru.zetit.bridgeandjoins.crypto.CryptoServer")
|
||||
applicationDefaultJvmArgs = listOf(
|
||||
"-Djava.library.path=/opt/Validata/VDCSP/lib/amd64",
|
||||
)
|
||||
}
|
||||
|
||||
protobuf {
|
||||
@@ -78,6 +86,14 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
sourceSets {
|
||||
main {
|
||||
proto {
|
||||
srcDir("proto")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,41 @@ service CryptoService {
|
||||
|
||||
// Health-check.
|
||||
rpc Health(HealthRequest) returns (HealthResponse);
|
||||
|
||||
// Activate — переинициализирует провайдер Валидаты на указанный
|
||||
// профиль из pki1.conf. Если profile пуст — переходит в
|
||||
// VCERT_InitMinimal (без доступа к ПСП/ЛСП/ССС). Не требует
|
||||
// перезапуска сайдкара.
|
||||
rpc Activate(ActivateRequest) returns (ActivateResponse);
|
||||
|
||||
// Shutdown — корректно завершает процесс сайдкара (System.exit(2)
|
||||
// после отправки ответа). systemd с Restart=on-failure поднимет
|
||||
// его снова через RestartSec секунд. Используется для UI-кнопки
|
||||
// «Перезапустить crypto-service» без sudo.
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
}
|
||||
|
||||
message ActivateRequest {
|
||||
// Имя профиля в pki1.conf. Пустая строка = minimal mode.
|
||||
string profile = 1;
|
||||
}
|
||||
|
||||
message ActivateResponse {
|
||||
// true если провайдер успешно (пере)инициализирован.
|
||||
bool ok = 1;
|
||||
// Имя активного провайдера ("validata" / "stub").
|
||||
string provider = 2;
|
||||
// Имя активного профиля (пусто для minimal).
|
||||
string profile = 3;
|
||||
// Сообщение о результате (для UI).
|
||||
string message = 4;
|
||||
}
|
||||
|
||||
message ShutdownRequest {}
|
||||
|
||||
message ShutdownResponse {
|
||||
// true означает «запрос принят, процесс завершится через ~500ms».
|
||||
bool ok = 1;
|
||||
}
|
||||
|
||||
message VerifyRequest {
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ActivateRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ActivateResponse;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Хендлер Activate: переключает текущий провайдер на указанный
|
||||
* профиль pki1.conf без рестарта сайдкара. Если провайдер не
|
||||
* {@link ValidataProvider} (например stub) — отказ.
|
||||
*/
|
||||
public final class ActivateHandler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ActivateHandler.class.getName());
|
||||
|
||||
private final KeystoreProvider keystore;
|
||||
|
||||
public ActivateHandler(KeystoreProvider keystore) {
|
||||
this.keystore = keystore;
|
||||
}
|
||||
|
||||
public ActivateResponse handle(ActivateRequest request) {
|
||||
String profile = request.getProfile();
|
||||
if (!(keystore instanceof ValidataProvider vp)) {
|
||||
return ActivateResponse.newBuilder()
|
||||
.setOk(false)
|
||||
.setProvider(keystore.name())
|
||||
.setMessage("crypto-service запущен с провайдером " + keystore.name()
|
||||
+ " — переключение профиля доступно только для validata")
|
||||
.build();
|
||||
}
|
||||
boolean ok = vp.reinit(profile);
|
||||
LOG.info("Activate: profile=\"" + profile + "\", ok=" + ok + ", msg=" + vp.message());
|
||||
return ActivateResponse.newBuilder()
|
||||
.setOk(ok)
|
||||
.setProvider(vp.name())
|
||||
.setProfile(profile == null ? "" : profile)
|
||||
.setMessage(vp.message())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+24
-10
@@ -14,32 +14,44 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Точка входа crypto-service.
|
||||
*
|
||||
* Слушает Unix Domain Socket по пути из переменной окружения
|
||||
* BJ_CRYPTO_SOCKET (по умолчанию /run/bj/crypto.sock).
|
||||
* Используется UDS вместо TCP — минимальная задержка и изоляция
|
||||
* от сети.
|
||||
* Слушает Unix Domain Socket по пути из BJ_CRYPTO_SOCKET
|
||||
* (по умолчанию /run/bj/crypto.sock). UDS вместо TCP — минимальная
|
||||
* задержка и изоляция от сети.
|
||||
*
|
||||
* Провайдер выбирается через BJ_CRYPTO_PROVIDER:
|
||||
* - "validata" — АПК «Валидата Клиент L» (через Pki1.LocalIface);
|
||||
* - что-либо ещё или пусто — stub (gRPC жив, реальная криптография
|
||||
* отключена; используется на дев-стендах).
|
||||
*
|
||||
* Профиль Валидаты задаётся через BJ_VALIDATA_PROFILE (имя из
|
||||
* pki1.conf или ПК «Справочник сертификатов»). Если пусто —
|
||||
* инициализация в режиме VCERT_InitMinimal (хэш, Base64, RAND).
|
||||
*/
|
||||
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
|
||||
}
|
||||
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.createDirectories(Path.of(socketPath).getParent());
|
||||
Files.deleteIfExists(Path.of(socketPath));
|
||||
|
||||
KeystoreProvider keystore = KeystoreProvider.byName(provider);
|
||||
LOG.info("crypto-service: провайдер " + keystore.name() + " — " + keystore.message());
|
||||
|
||||
VerifyHandler verifyHandler = new VerifyHandler(keystore);
|
||||
SignHandler signHandler = new SignHandler(keystore);
|
||||
HealthHandler healthHandler = new HealthHandler(provider);
|
||||
HealthHandler healthHandler = new HealthHandler(keystore);
|
||||
ActivateHandler activateHandler = new ActivateHandler(keystore);
|
||||
ShutdownHandler shutdownHandler = new ShutdownHandler();
|
||||
|
||||
CryptoServiceImpl service = new CryptoServiceImpl(verifyHandler, signHandler, healthHandler);
|
||||
CryptoServiceImpl service = new CryptoServiceImpl(verifyHandler, signHandler,
|
||||
healthHandler, activateHandler, shutdownHandler);
|
||||
|
||||
Server server = NettyServerBuilder
|
||||
.forAddress(new DomainSocketAddress(socketPath))
|
||||
@@ -50,11 +62,13 @@ public final class CryptoServer {
|
||||
.build()
|
||||
.start();
|
||||
|
||||
LOG.info("crypto-service: UDS " + socketPath + ", provider=" + provider);
|
||||
LOG.info("crypto-service: UDS " + socketPath + ", provider=" + keystore.name()
|
||||
+ ", ready=" + keystore.ready());
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
LOG.info("crypto-service: graceful shutdown");
|
||||
server.shutdown();
|
||||
keystore.close();
|
||||
}));
|
||||
|
||||
server.awaitTermination();
|
||||
|
||||
+31
-1
@@ -2,9 +2,13 @@ package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ActivateRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ActivateResponse;
|
||||
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.ShutdownRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ShutdownResponse;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.SignRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.SignResponse;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.VerifyRequest;
|
||||
@@ -19,11 +23,17 @@ public final class CryptoServiceImpl extends CryptoServiceGrpc.CryptoServiceImpl
|
||||
private final VerifyHandler verify;
|
||||
private final SignHandler sign;
|
||||
private final HealthHandler health;
|
||||
private final ActivateHandler activate;
|
||||
private final ShutdownHandler shutdown;
|
||||
|
||||
public CryptoServiceImpl(VerifyHandler verify, SignHandler sign, HealthHandler health) {
|
||||
public CryptoServiceImpl(VerifyHandler verify, SignHandler sign,
|
||||
HealthHandler health, ActivateHandler activate,
|
||||
ShutdownHandler shutdown) {
|
||||
this.verify = verify;
|
||||
this.sign = sign;
|
||||
this.health = health;
|
||||
this.activate = activate;
|
||||
this.shutdown = shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,4 +61,24 @@ public final class CryptoServiceImpl extends CryptoServiceGrpc.CryptoServiceImpl
|
||||
obs.onNext(health.handle(request));
|
||||
obs.onCompleted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate(ActivateRequest request, StreamObserver<ActivateResponse> obs) {
|
||||
try {
|
||||
obs.onNext(activate.handle(request));
|
||||
obs.onCompleted();
|
||||
} catch (Exception e) {
|
||||
obs.onError(Status.INTERNAL.withDescription(e.getMessage()).asException());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown(ShutdownRequest request, StreamObserver<ShutdownResponse> obs) {
|
||||
try {
|
||||
obs.onNext(shutdown.handle(request));
|
||||
obs.onCompleted();
|
||||
} catch (Exception e) {
|
||||
obs.onError(Status.INTERNAL.withDescription(e.getMessage()).asException());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-6
@@ -3,22 +3,34 @@ package ru.zetit.bridgeandjoins.crypto;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.HealthRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.HealthResponse;
|
||||
|
||||
/** Хендлер Health: всегда ok=true с указанием активного провайдера. */
|
||||
/**
|
||||
* Хендлер Health: возвращает статус провайдера КЗИ.
|
||||
* - ok=true, если провайдер успешно инициализирован (или работает в stub);
|
||||
* - provider — "validata" или "stub";
|
||||
* - version — версия crypto-service.
|
||||
*
|
||||
* В крайнем правом окне UI bj-server этот ответ читается через cryptocli
|
||||
* и показывается в /admin/setup → СКЗИ.
|
||||
*/
|
||||
public final class HealthHandler {
|
||||
|
||||
private static final String VERSION = "0.1.0";
|
||||
|
||||
private final String provider;
|
||||
private final KeystoreProvider keystore;
|
||||
|
||||
public HealthHandler(String provider) {
|
||||
this.provider = provider;
|
||||
public HealthHandler(KeystoreProvider keystore) {
|
||||
this.keystore = keystore;
|
||||
}
|
||||
|
||||
public HealthResponse handle(HealthRequest request) {
|
||||
// ok=true даже для stub — gRPC-сервис жив; реальная готовность
|
||||
// криптографии видна по полю provider (stub ↔ криптография не
|
||||
// подключена). Это упрощает /healthz: «UDS живой» — отдельный
|
||||
// факт от «есть рабочий ключ».
|
||||
return HealthResponse.newBuilder()
|
||||
.setOk(true)
|
||||
.setVersion(VERSION)
|
||||
.setProvider(provider)
|
||||
.setVersion(VERSION + " (" + keystore.message() + ")")
|
||||
.setProvider(keystore.name())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
+28
-31
@@ -1,53 +1,50 @@
|
||||
package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
/**
|
||||
* Provider-абстракция над хранилищем ключей. Реальные реализации
|
||||
* подключаются вместе с соответствующими jar-ами (КриптоПро JCP,
|
||||
* Валидата JCP, ViPNet). На M1 — Stub.
|
||||
* Provider-абстракция над хранилищем ключей. На Linux используется
|
||||
* АПК «Валидата Клиент L» через Pki1.LocalIface (JNI-биндинг к
|
||||
* libzpki1jni.so). Если BJ_CRYPTO_PROVIDER=stub — поднимается
|
||||
* заглушка для дев-стендов без реальной криптографии.
|
||||
*/
|
||||
public interface KeystoreProvider {
|
||||
|
||||
/** Уникальное имя провайдера (используется в HealthResponse.provider). */
|
||||
String name();
|
||||
|
||||
/** Возвращает true, если провайдер инициализирован и готов к работе. */
|
||||
/** true, если провайдер инициализирован и готов к работе. */
|
||||
boolean ready();
|
||||
|
||||
/** Короткое описание состояния (для Health и диагностики). */
|
||||
String message();
|
||||
|
||||
/**
|
||||
* Подбирает реализацию по имени из переменной окружения
|
||||
* BJ_CRYPTO_PROVIDER. Неизвестное имя — Stub.
|
||||
* Возвращает Pki1.LocalIface, если провайдер — Валидата и
|
||||
* инициализация прошла; иначе null.
|
||||
*/
|
||||
Pki1.LocalIface pki();
|
||||
|
||||
/** Закрывает контекст провайдера (для shutdown). */
|
||||
void close();
|
||||
|
||||
/**
|
||||
* Подбирает реализацию по имени из 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();
|
||||
if ("validata".equals(name)) {
|
||||
return new ValidataProvider();
|
||||
}
|
||||
return new StubProvider();
|
||||
}
|
||||
}
|
||||
|
||||
/** Заглушка по умолчанию. Возвращает ready()=false. */
|
||||
/** Заглушка для дев-стендов: 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; }
|
||||
@Override public String message() {
|
||||
return "Провайдер stub — реальная криптография не подключена.";
|
||||
}
|
||||
@Override public Pki1.LocalIface pki() { return null; }
|
||||
@Override public void close() { /* no-op */ }
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ShutdownRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.ShutdownResponse;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Хендлер Shutdown: формирует ответ, затем (с короткой задержкой,
|
||||
* чтобы клиент успел получить ответ) вызывает System.exit(2). systemd
|
||||
* с Restart=on-failure поднимает сайдкар обратно через RestartSec.
|
||||
*/
|
||||
public final class ShutdownHandler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ShutdownHandler.class.getName());
|
||||
|
||||
public ShutdownResponse handle(ShutdownRequest request) {
|
||||
LOG.info("Shutdown: получен запрос, exit(2) через 500мс");
|
||||
// Поток-смертник: даёт gRPC завершить ответ клиенту, затем
|
||||
// valle exit с ненулевым кодом → systemd считает это failure
|
||||
// и перезапускает.
|
||||
Thread reaper = new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException ignore) {
|
||||
}
|
||||
System.exit(2);
|
||||
}, "bj-crypto-reaper");
|
||||
reaper.setDaemon(true);
|
||||
reaper.start();
|
||||
return ShutdownResponse.newBuilder().setOk(true).build();
|
||||
}
|
||||
}
|
||||
+73
-16
@@ -6,13 +6,25 @@ import io.grpc.StatusRuntimeException;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.SignRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.SignResponse;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Хендлер подписи XML. На M1 — заглушка, возвращает UNIMPLEMENTED.
|
||||
* Реальная реализация подключается, когда лицензия КриптоПро JCP
|
||||
* будет доступна.
|
||||
* Хендлер подписи. Формирует CMS detached signature через
|
||||
* VCERT_CmsBlkDetSignMem с алгоритмом ГОСТ Р 34.10-2012 (256 бит
|
||||
* по умолчанию из профиля Валидаты).
|
||||
*
|
||||
* Поле key_alias из gRPC-запроса передаётся в certid_t.keyId — нативный
|
||||
* CSP подставит дефолтный ключ из профиля при пустом alias.
|
||||
*
|
||||
* Возвращает signed_xml = DER-байты CMS detached signature. На уровне
|
||||
* bj-server этот блок отправляется как signed_document (base64).
|
||||
* XMLDSig-обёртка с канонизацией добавится отдельным этапом
|
||||
* (Apache Santuario).
|
||||
*/
|
||||
public final class SignHandler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(SignHandler.class.getName());
|
||||
|
||||
private final KeystoreProvider keystore;
|
||||
|
||||
public SignHandler(KeystoreProvider keystore) {
|
||||
@@ -20,21 +32,66 @@ public final class SignHandler {
|
||||
}
|
||||
|
||||
public SignResponse handle(SignRequest request) {
|
||||
// TODO(M2): реальная реализация:
|
||||
// 1. Загружаем ключ по key_alias из keystore (через KeystoreProvider).
|
||||
// 2. Канонизуем payload по xml-exc-c14n.
|
||||
// 3. Создаём <Signature> элемент через Santuario с GOST-алгоритмом.
|
||||
// 4. Возвращаем signed_xml.
|
||||
//
|
||||
// Сейчас — заглушка.
|
||||
if (!keystore.ready() || keystore.pki() == null) {
|
||||
throw new StatusRuntimeException(Status.FAILED_PRECONDITION
|
||||
.withDescription("crypto-service: SignXMLDSig — провайдер "
|
||||
+ keystore.name() + " не готов: " + keystore.message()));
|
||||
}
|
||||
if (keystore instanceof ValidataProvider vp && vp.isMinimal()) {
|
||||
throw new StatusRuntimeException(Status.FAILED_PRECONDITION
|
||||
.withDescription("crypto-service: SignXMLDSig — Валидата в режиме minimal "
|
||||
+ "(BJ_VALIDATA_PROFILE не задан), подпись недоступна без профиля"));
|
||||
}
|
||||
if (request.getPayload().isEmpty()) {
|
||||
throw new StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||
.withDescription("crypto-service: SignXMLDSig — payload пуст"));
|
||||
}
|
||||
|
||||
throw new StatusRuntimeException(Status.UNIMPLEMENTED
|
||||
.withDescription("crypto-service: SignXMLDSig stub — провайдер "
|
||||
+ keystore.name() + " не подключён (ждём PR-6/M2 КриптоПро JCP)"));
|
||||
Pki1.LocalIface pki = keystore.pki();
|
||||
try {
|
||||
Pki1.LocalIface.sign_param_t signParam = new Pki1.LocalIface.sign_param_t();
|
||||
String alias = request.getKeyAlias();
|
||||
if (alias != null && !alias.isEmpty()) {
|
||||
Pki1.LocalIface.certid_t cid = new Pki1.LocalIface.certid_t();
|
||||
cid.type = Pki1.LocalIface.ID_ALIAS;
|
||||
cid.alias = alias;
|
||||
signParam.mycert = cid;
|
||||
}
|
||||
|
||||
Pki1.LocalIface.mem_blk_t data = memBlock(request.getPayload().toByteArray());
|
||||
Pki1.LocalIface.mem_blk_t signature = new Pki1.LocalIface.mem_blk_t();
|
||||
|
||||
// CMS detached signature: подпись в signature, исходные данные
|
||||
// в результат не включаются. Третий аргумент (key) — null:
|
||||
// ключ подбирается нативным CSP по certid.
|
||||
int rc = pki.VCERT_CmsBlkDetSignMem(signParam, data, null, signature);
|
||||
if (rc != 0) {
|
||||
String err = pki.VCERT_GetErrorText(rc);
|
||||
throw new StatusRuntimeException(Status.INTERNAL
|
||||
.withDescription("crypto-service: VCERT_CmsBlkDetSignMem rc=" + rc
|
||||
+ " (" + err + ")"));
|
||||
}
|
||||
if (signature.buf == null || signature.len <= 0) {
|
||||
throw new StatusRuntimeException(Status.INTERNAL
|
||||
.withDescription("crypto-service: подпись вернулась пустая"));
|
||||
}
|
||||
return SignResponse.newBuilder()
|
||||
.setSignedXml(ByteString.copyFrom(signature.buf, 0, signature.len))
|
||||
.build();
|
||||
} catch (StatusRuntimeException sre) {
|
||||
throw sre;
|
||||
} catch (Throwable t) {
|
||||
LOG.severe("SignHandler: исключение " + t);
|
||||
throw new StatusRuntimeException(Status.INTERNAL
|
||||
.withDescription("crypto-service: " + t.getClass().getSimpleName()
|
||||
+ ": " + t.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private SignResponse empty() {
|
||||
return SignResponse.newBuilder().setSignedXml(ByteString.EMPTY).build();
|
||||
private static Pki1.LocalIface.mem_blk_t memBlock(byte[] bytes) {
|
||||
Pki1.LocalIface.mem_blk_t mb = new Pki1.LocalIface.mem_blk_t();
|
||||
mb.buf = bytes;
|
||||
mb.len = bytes.length;
|
||||
return mb;
|
||||
}
|
||||
}
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Провайдер на основе АПК «Валидата Клиент L». Через Pki1.LocalIface
|
||||
* вызывает нативную JNI-библиотеку libzpki1jni.so. Профиль
|
||||
* (имя из pki1.conf или ПК «Справочник сертификатов») берётся из
|
||||
* переменной окружения BJ_VALIDATA_PROFILE — если пусто, инициализируем
|
||||
* минимальный контекст без доступа к ПСП/ЛСП/ССС (доступны только
|
||||
* хэширование, Base64, RAND).
|
||||
*
|
||||
* Поддерживает горячее переключение профиля через {@link #reinit(String)}
|
||||
* — без рестарта сайдкара.
|
||||
*
|
||||
* Подробности нативного API — в документе ВАМБ.00135-06 33 03
|
||||
* «Руководство программиста» (Java) и 33 01 (C).
|
||||
*/
|
||||
public final class ValidataProvider implements KeystoreProvider {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ValidataProvider.class.getName());
|
||||
|
||||
private Pki1.LocalIface pki;
|
||||
private boolean ready;
|
||||
private String message;
|
||||
private boolean minimal;
|
||||
private String profile;
|
||||
|
||||
public ValidataProvider() {
|
||||
this.profile = System.getenv("BJ_VALIDATA_PROFILE");
|
||||
initLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* reinit — атомарная переинициализация провайдера на новый профиль
|
||||
* (или пустой = minimal). Закрывает текущий контекст и поднимает
|
||||
* новый. Возвращает true при успехе.
|
||||
*/
|
||||
public synchronized boolean reinit(String newProfile) {
|
||||
// Уничтожаем старый контекст, если был.
|
||||
if (pki != null && ready) {
|
||||
try {
|
||||
pki.VCERT_Uninitialize();
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("reinit: VCERT_Uninitialize: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
pki = null;
|
||||
ready = false;
|
||||
message = "";
|
||||
minimal = false;
|
||||
this.profile = newProfile;
|
||||
initLocked();
|
||||
return ready;
|
||||
}
|
||||
|
||||
private void initLocked() {
|
||||
try {
|
||||
this.pki = new Pki1.LocalIface();
|
||||
int rc;
|
||||
if (profile == null || profile.isEmpty()) {
|
||||
rc = pki.VCERT_InitMinimal();
|
||||
this.minimal = true;
|
||||
} else {
|
||||
rc = pki.VCERT_Initialize(profile, 0);
|
||||
this.minimal = false;
|
||||
}
|
||||
if (rc == 0) {
|
||||
this.ready = true;
|
||||
this.message = minimal
|
||||
? "Валидата: минимальный контекст (без профиля). Доступны хэш, Base64, RAND."
|
||||
: "Валидата: контекст с профилем «" + profile + "» инициализирован.";
|
||||
LOG.info(message);
|
||||
} else {
|
||||
this.ready = false;
|
||||
String err = safeErrorText(rc);
|
||||
this.message = (minimal ? "VCERT_InitMinimal" : "VCERT_Initialize(\"" + profile + "\")")
|
||||
+ " вернул код " + rc + " (" + err + ")";
|
||||
LOG.warning(message);
|
||||
}
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
this.ready = false;
|
||||
this.message = "Валидата: не удалось загрузить libzpki1jni.so — " + e.getMessage()
|
||||
+ ". Проверьте java.library.path и установку АПК «Валидата Клиент L».";
|
||||
LOG.severe(message);
|
||||
} catch (Throwable t) {
|
||||
this.ready = false;
|
||||
this.message = "Валидата: инициализация упала — " + t.getClass().getSimpleName()
|
||||
+ ": " + t.getMessage();
|
||||
LOG.severe(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public String name() { return "validata"; }
|
||||
@Override public synchronized boolean ready() { return ready; }
|
||||
@Override public synchronized String message() { return message; }
|
||||
@Override public synchronized Pki1.LocalIface pki() { return ready ? pki : null; }
|
||||
|
||||
/** Признак режима VCERT_InitMinimal (без профиля). */
|
||||
public synchronized boolean isMinimal() { return minimal; }
|
||||
|
||||
/** Имя профиля, под которым инициализировались (null/пусто = minimal). */
|
||||
public synchronized String profile() { return profile; }
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (pki != null && ready) {
|
||||
try {
|
||||
pki.VCERT_Uninitialize();
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("Валидата: VCERT_Uninitialize: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String safeErrorText(int rc) {
|
||||
try {
|
||||
String t = pki.VCERT_GetErrorText(rc);
|
||||
return t == null ? "" : t;
|
||||
} catch (Throwable ignore) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
-19
@@ -1,15 +1,29 @@
|
||||
package ru.zetit.bridgeandjoins.crypto;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.VerifyRequest;
|
||||
import ru.zetit.bridgeandjoins.crypto.v1.VerifyResponse;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Хендлер проверки XMLDSig-подписи. На M1 — заглушка, возвращающая
|
||||
* valid=false с пояснением. Реальная реализация (Apache Santuario +
|
||||
* КриптоПро JCP) подключается, когда лицензия JCP станет доступна.
|
||||
* Хендлер проверки подписи. Обрабатывает CMS detached signature
|
||||
* (DER-байты в payload, само сообщение должно быть в request.message,
|
||||
* но пока контракт упрощён: payload = весь подписанный CMS-блок,
|
||||
* проверяется как attached-вариант).
|
||||
*
|
||||
* Внутри: VCERT_CmsBlkAttVerifyMem с verify_param_t. Возвращает
|
||||
* данные о подписанте: CN, ИНН (OID 1.2.643.3.131.1.1), серийник,
|
||||
* срок действия.
|
||||
*
|
||||
* Для XMLDSig-входных сообщений (как у НРД) нужен слой канонизации
|
||||
* XML и извлечения SignatureValue + KeyInfo — это отдельный этап.
|
||||
*/
|
||||
public final class VerifyHandler {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(VerifyHandler.class.getName());
|
||||
|
||||
private final KeystoreProvider keystore;
|
||||
|
||||
public VerifyHandler(KeystoreProvider keystore) {
|
||||
@@ -17,22 +31,105 @@ public final class VerifyHandler {
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// Сейчас — заглушка.
|
||||
if (!keystore.ready() || keystore.pki() == null) {
|
||||
return VerifyResponse.newBuilder()
|
||||
.setValid(false)
|
||||
.addErrors("crypto-service: провайдер " + keystore.name()
|
||||
+ " не готов: " + keystore.message())
|
||||
.build();
|
||||
}
|
||||
if (request.getPayload().isEmpty()) {
|
||||
throw new StatusRuntimeException(Status.INVALID_ARGUMENT
|
||||
.withDescription("crypto-service: VerifyXMLDSig — payload пуст"));
|
||||
}
|
||||
|
||||
return VerifyResponse.newBuilder()
|
||||
.setValid(false)
|
||||
.addErrors("crypto-service: VerifyXMLDSig stub — провайдер " + keystore.name()
|
||||
+ " не подключён (ждём PR-6/M2 КриптоПро JCP)")
|
||||
.build();
|
||||
Pki1.LocalIface pki = keystore.pki();
|
||||
try {
|
||||
Pki1.LocalIface.verify_param_t verifyParam = new Pki1.LocalIface.verify_param_t();
|
||||
Pki1.LocalIface.mem_blk_t signedMessage = memBlock(request.getPayload().toByteArray());
|
||||
Pki1.LocalIface.mem_blk_t recoveredData = new Pki1.LocalIface.mem_blk_t();
|
||||
Pki1.LocalIface.verify_result_t result = new Pki1.LocalIface.verify_result_t();
|
||||
|
||||
int rc = pki.VCERT_CmsBlkAttVerifyMem(verifyParam, signedMessage, recoveredData, result);
|
||||
if (rc != 0) {
|
||||
String err = pki.VCERT_GetErrorText(rc);
|
||||
return VerifyResponse.newBuilder()
|
||||
.setValid(false)
|
||||
.addErrors("VCERT_CmsBlkAttVerifyMem rc=" + rc + " (" + err + ")")
|
||||
.build();
|
||||
}
|
||||
|
||||
VerifyResponse.Builder resp = VerifyResponse.newBuilder().setValid(true);
|
||||
// Если есть инфа о подписанте — кладём её. Структура
|
||||
// verify_result_t содержит cms_siginf и/или certificate;
|
||||
// поля защищены от NPE, потому что нативный API заполняет
|
||||
// что есть.
|
||||
extractSignerInfo(pki, result, resp);
|
||||
return resp.build();
|
||||
} catch (Throwable t) {
|
||||
LOG.severe("VerifyHandler: исключение " + t);
|
||||
return VerifyResponse.newBuilder()
|
||||
.setValid(false)
|
||||
.addErrors(t.getClass().getSimpleName() + ": " + t.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает CN/ИНН/серийник/сроки из verify_result_t.
|
||||
* verify_result_t.signs[i].cert — certificate_t подписанта.
|
||||
* Берём первого подписанта (signs[0]) — для НРД-обмена этого
|
||||
* достаточно; multi-signer обработка — отдельно.
|
||||
*/
|
||||
private static void extractSignerInfo(Pki1.LocalIface pki,
|
||||
Pki1.LocalIface.verify_result_t result,
|
||||
VerifyResponse.Builder resp) {
|
||||
if (result == null || result.signs == null || result.signs.length == 0) {
|
||||
return;
|
||||
}
|
||||
Pki1.LocalIface.sign_status_t s = result.signs[0];
|
||||
if (s == null || s.cert == null) {
|
||||
return;
|
||||
}
|
||||
Pki1.LocalIface.certificate_t cert = s.cert;
|
||||
if (cert.subject != null) {
|
||||
resp.setSignerCn(extractRdn(cert.subject, "CN"));
|
||||
String inn = extractRdn(cert.subject, "INN");
|
||||
if (inn.isEmpty()) {
|
||||
inn = extractRdn(cert.subject, "1.2.643.3.131.1.1");
|
||||
}
|
||||
resp.setSignerInn(inn);
|
||||
}
|
||||
if (cert.serialNumber != null) {
|
||||
resp.setSerial(cert.serialNumber);
|
||||
}
|
||||
// notBefore/notAfter — unix epoch seconds.
|
||||
resp.setNotBefore((long) cert.notBefore);
|
||||
resp.setNotAfter((long) cert.notAfter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает значение RDN из строки subject вида
|
||||
* "CN=Иванов И.И.,OU=...,O=...,INN=7707083893".
|
||||
* Возвращает "" если RDN не найден.
|
||||
*/
|
||||
private static String extractRdn(String dn, String rdn) {
|
||||
if (dn == null || rdn == null) return "";
|
||||
String needle = rdn + "=";
|
||||
int idx = dn.indexOf(needle);
|
||||
if (idx < 0) return "";
|
||||
int start = idx + needle.length();
|
||||
// Конец значения — следующая запятая (не экранированная). Простой
|
||||
// парсинг — достаточно для типовых cert-полей НРД/УЦ МБ.
|
||||
int end = dn.indexOf(',', start);
|
||||
if (end < 0) end = dn.length();
|
||||
return dn.substring(start, end).trim();
|
||||
}
|
||||
|
||||
private static Pki1.LocalIface.mem_blk_t memBlock(byte[] bytes) {
|
||||
Pki1.LocalIface.mem_blk_t mb = new Pki1.LocalIface.mem_blk_t();
|
||||
mb.buf = bytes;
|
||||
mb.len = bytes.length;
|
||||
return mb;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user