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:
@@ -0,0 +1,448 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------- //
|
||||
// Хелперы
|
||||
// --------------------------------------------------------------------- //
|
||||
|
||||
// runCmd — запускает команду, прокидывает stdout/stderr построчно в log.
|
||||
// Возвращает ошибку с последними строками stderr для удобства отображения.
|
||||
func runCmd(logFn func(string), name string, args ...string) error {
|
||||
logFn(fmt.Sprintf("$ %s %s", name, strings.Join(args, " ")))
|
||||
cmd := exec.Command(name, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
for _, line := range strings.Split(strings.TrimRight(string(out), "\n"), "\n") {
|
||||
if line != "" {
|
||||
logFn(line)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFileIfChanged — пишет файл только если содержимое отличается. Возвращает
|
||||
// true если файл был создан/изменён (для решения «нужен ли daemon-reload»).
|
||||
func writeFileIfChanged(path string, content string, mode os.FileMode) (bool, error) {
|
||||
existing, err := os.ReadFile(path)
|
||||
if err == nil && string(existing) == content {
|
||||
return false, nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), mode); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------- //
|
||||
// Шаги
|
||||
// --------------------------------------------------------------------- //
|
||||
|
||||
func stepInstallDeps(s *State, log func(string)) (StepStatus, error) {
|
||||
log("Обновляю apt-кеш...")
|
||||
if err := runCmd(log, "apt-get", "update", "-qq"); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
|
||||
deps := []string{
|
||||
"libgtk-3-0", "libpcsclite1", "libccid", "pcscd",
|
||||
"libcurl4", "libkrb5-3", "libgssapi-krb5-2",
|
||||
"libsasl2-modules", "libsasl2-modules-gssapi-mit",
|
||||
"execstack", "p7zip-full",
|
||||
}
|
||||
if hasAPTPackage("libldap-2.4-2") {
|
||||
deps = append(deps, "libldap-2.4-2")
|
||||
} else {
|
||||
deps = append(deps, "libldap-2.5-0")
|
||||
log("libldap-2.4-2 не найден → ставлю 2.5-0, для zpki будет --force-depends")
|
||||
}
|
||||
|
||||
args := append([]string{"install", "-y", "--no-install-recommends"}, deps...)
|
||||
if err := runCmd(log, "apt-get", args...); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepInstallValidataDebs(s *State, log func(string)) (StepStatus, error) {
|
||||
zpki, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zpki-*.amd64.deb"))
|
||||
zsdk, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ClientL_Other", "zsdk-*.amd64.deb"))
|
||||
if len(zpki) == 0 {
|
||||
return StepFailed, fmt.Errorf("zpki-*.amd64.deb не найден в %s/ClientL_Other/", s.artifactsDir)
|
||||
}
|
||||
useForce := !hasAPTPackage("libldap-2.4-2")
|
||||
for _, deb := range append(zpki, zsdk...) {
|
||||
args := []string{"-i", deb}
|
||||
if useForce {
|
||||
args = append([]string{"--force-depends"}, args...)
|
||||
}
|
||||
if err := runCmd(log, "dpkg", args...); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat("/opt/Validata/VDCSP/lib/amd64"); err != nil {
|
||||
return StepFailed, fmt.Errorf("/opt/Validata не появился после установки")
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepExecstack(s *State, log func(string)) (StepStatus, error) {
|
||||
target := "/opt/Validata/VDCSP/lib/amd64/libvdcsp.so"
|
||||
// Проверка состояния
|
||||
out, err := exec.Command("execstack", "-q", target).Output()
|
||||
if err == nil && strings.HasPrefix(strings.TrimSpace(string(out)), "-") {
|
||||
log("executable-stack уже снят")
|
||||
return StepSkipped, nil
|
||||
}
|
||||
return StepDone, runCmd(log, "execstack", "-c", target)
|
||||
}
|
||||
|
||||
func stepCreateBJUser(s *State, log func(string)) (StepStatus, error) {
|
||||
if _, err := exec.LookPath("id"); err == nil {
|
||||
if exec.Command("id", "bj").Run() == nil {
|
||||
log("Пользователь bj уже существует")
|
||||
} else {
|
||||
if err := runCmd(log, "useradd", "--system", "--create-home",
|
||||
"--home-dir", "/var/lib/bj", "--shell", "/bin/bash", "bj"); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
}
|
||||
dirs := []struct {
|
||||
Path string
|
||||
Mode os.FileMode
|
||||
}{
|
||||
{"/var/lib/bj/usb", 0o755},
|
||||
{"/var/lib/bj/.Validata", 0o700},
|
||||
{"/var/lib/bj/.Validata/vdkeys", 0o700},
|
||||
{"/var/lib/bj/profiles", 0o755},
|
||||
{"/var/log/bj", 0o755},
|
||||
{"/var/lib/bj/.bj", 0o700},
|
||||
}
|
||||
for _, d := range dirs {
|
||||
if err := os.MkdirAll(d.Path, d.Mode); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj", "/var/log/bj")
|
||||
}
|
||||
|
||||
func stepPcscdDropin(s *State, log func(string)) (StepStatus, error) {
|
||||
const dropin = `[Unit]
|
||||
Requires=
|
||||
After=
|
||||
Sockets=
|
||||
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/usr/sbin/pcscd --foreground
|
||||
`
|
||||
changed, err := writeFileIfChanged("/etc/systemd/system/pcscd.service.d/no-autoexit.conf", dropin, 0o644)
|
||||
if err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if !changed {
|
||||
log("Drop-in уже актуален")
|
||||
return StepSkipped, nil
|
||||
}
|
||||
log("Создан /etc/systemd/system/pcscd.service.d/no-autoexit.conf")
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepBJCryptoDropins(s *State, log func(string)) (StepStatus, error) {
|
||||
files := map[string]string{
|
||||
"/etc/systemd/system/bj-crypto.service.d/validata-paths.conf": `[Service]
|
||||
WorkingDirectory=/opt/Validata/VDCSP/etc
|
||||
ReadWritePaths=/opt/Validata/VDCSP/etc
|
||||
ReadWritePaths=/var/lib/bj
|
||||
`,
|
||||
"/etc/systemd/system/bj-crypto.service.d/usb-access.conf": `[Service]
|
||||
ReadOnlyPaths=/media
|
||||
ReadOnlyPaths=/var/lib/bj/usb
|
||||
`,
|
||||
"/etc/systemd/system/bj-crypto.service.d/share-crysvc.conf": `[Service]
|
||||
PrivateTmp=true
|
||||
BindPaths=/tmp/.crysvc.sock:/tmp/.crysvc.sock
|
||||
`,
|
||||
}
|
||||
for path, content := range files {
|
||||
if _, err := writeFileIfChanged(path, content, 0o644); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepBJServerDropin(s *State, log func(string)) (StepStatus, error) {
|
||||
const dropin = `[Service]
|
||||
ReadWritePaths=/opt/Validata/VDCSP/etc
|
||||
`
|
||||
_, err := writeFileIfChanged("/etc/systemd/system/bj-server.service.d/pki1conf.conf", dropin, 0o644)
|
||||
if err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepSPKIIni(s *State, log func(string)) (StepStatus, error) {
|
||||
const path = "/opt/Validata/VDCSP/etc/spki.ini"
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log("Файл уже существует")
|
||||
return StepSkipped, nil
|
||||
}
|
||||
const content = `[store]
|
||||
count = 0
|
||||
|
||||
[Parameters]
|
||||
PkiLdapTimeout = 10
|
||||
PkiHttpTimeout = 60
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepPKI1Prep(s *State, log func(string)) (StepStatus, error) {
|
||||
const path = "/opt/Validata/VDCSP/etc/pki1.conf"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
log("Файл pki1.conf отсутствует — Валидата создаст при первом запуске")
|
||||
return StepSkipped, nil
|
||||
}
|
||||
if err := runCmd(log, "chgrp", "bj", path); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := runCmd(log, "chmod", "g+w", path); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
|
||||
existing, _ := os.ReadFile(path)
|
||||
if !strings.Contains(string(existing), "# --- bj-server: BEGIN ---") {
|
||||
appended := string(existing) + "\n# --- bj-server: BEGIN ---\n# Секции профилей дописываются bj-server при импорте через /admin/setup.\n# --- bj-server: END ---\n"
|
||||
if err := os.WriteFile(path, []byte(appended), 0o664); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepUSBMount(s *State, log func(string)) (StepStatus, error) {
|
||||
files := map[string]string{
|
||||
"/etc/udev/rules.d/99-bj-usb.rules": `# Авто-mount USB-флешек в /var/lib/bj/usb/<UUID> с владельцем bj.
|
||||
ACTION=="add", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \
|
||||
ENV{ID_FS_TYPE}!="", \
|
||||
ENV{SYSTEMD_WANTS}="bj-usb-mount@$env{ID_FS_UUID}.service"
|
||||
ACTION=="remove", KERNEL=="sd?[0-9]", SUBSYSTEMS=="usb", \
|
||||
ENV{ID_FS_TYPE}!="", \
|
||||
ENV{SYSTEMD_WANTS}="bj-usb-umount@$env{ID_FS_UUID}.service"
|
||||
`,
|
||||
"/etc/systemd/system/bj-usb-mount@.service": `[Unit]
|
||||
Description=Mount USB %i to /var/lib/bj/usb/%i for bj
|
||||
DefaultDependencies=no
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/bash -c 'mkdir -p /var/lib/bj/usb/%i && /usr/bin/mount -o uid=$(id -u bj),gid=$(id -g bj),fmask=0133,dmask=0022 UUID=%i /var/lib/bj/usb/%i'
|
||||
ExecStop=/usr/bin/umount /var/lib/bj/usb/%i || true
|
||||
`,
|
||||
"/etc/systemd/system/bj-usb-umount@.service": `[Unit]
|
||||
Description=Umount USB %i from /var/lib/bj/usb/%i
|
||||
DefaultDependencies=no
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/bash -c '/usr/bin/umount /var/lib/bj/usb/%i 2>/dev/null; /usr/bin/rmdir /var/lib/bj/usb/%i 2>/dev/null; true'
|
||||
`,
|
||||
}
|
||||
anyChanged := false
|
||||
for path, content := range files {
|
||||
ch, err := writeFileIfChanged(path, content, 0o644)
|
||||
if err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
anyChanged = anyChanged || ch
|
||||
}
|
||||
if anyChanged {
|
||||
_ = runCmd(log, "udevadm", "control", "--reload-rules")
|
||||
_ = runCmd(log, "udevadm", "trigger")
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepInstallBJServer(s *State, log func(string)) (StepStatus, error) {
|
||||
src := filepath.Join(s.artifactsDir, "bj-server")
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return StepSkipped, nil // нет бинаря — может ставится через rpm/deb
|
||||
}
|
||||
if err := os.MkdirAll("/opt/bj", 0o755); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0755", src, "/opt/bj/bj-server"); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepInstallCryptoJar(s *State, log func(string)) (StepStatus, error) {
|
||||
src := filepath.Join(s.artifactsDir, "crypto-service.jar")
|
||||
if _, err := os.Stat(src); err != nil {
|
||||
return StepSkipped, nil
|
||||
}
|
||||
if err := os.MkdirAll("/opt/bj", 0o755); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := runCmd(log, "install", "-o", "bj", "-g", "bj", "-m", "0644", src, "/opt/bj/crypto-service.jar"); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepSystemdUnits(s *State, log func(string)) (StepStatus, error) {
|
||||
units := map[string]string{
|
||||
"/etc/systemd/system/bj-crypto.service": `[Unit]
|
||||
Description=Bridge-and-Join-s — Crypto sidecar (Java + Валидата Клиент L)
|
||||
Before=bj-server.service
|
||||
After=network-online.target pcscd.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bj
|
||||
Group=bj
|
||||
RuntimeDirectory=bj
|
||||
RuntimeDirectoryMode=0750
|
||||
Environment=BJ_CRYPTO_SOCKET=/run/bj/crypto.sock
|
||||
Environment=BJ_CRYPTO_PROVIDER=validata
|
||||
Environment=LD_LIBRARY_PATH=/opt/Validata/VDCSP/lib/amd64
|
||||
ExecStart=/usr/bin/java -Djava.library.path=/opt/Validata/VDCSP/lib/amd64 -jar /opt/bj/crypto-service.jar
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=append:/var/log/bj/crypto-service.log
|
||||
StandardError=append:/var/log/bj/crypto-service.err
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/run/bj /var/log/bj
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`,
|
||||
"/etc/systemd/system/bj-server.service": `[Unit]
|
||||
Description=Bridge-and-Join-s — единый сервис M2M-переводов
|
||||
After=network-online.target bj-crypto.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=bj
|
||||
Group=bj
|
||||
WorkingDirectory=/var/lib/bj
|
||||
ExecStart=/opt/bj/bj-server
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=BJ_HTTP_ADDR=:8080
|
||||
Environment=BJ_SETUP_PATH=/var/lib/bj/.bj/setup.json
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/bj /var/log/bj
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`,
|
||||
}
|
||||
for path, content := range units {
|
||||
if _, err := writeFileIfChanged(path, content, 0o644); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, runCmd(log, "systemctl", "daemon-reload")
|
||||
}
|
||||
|
||||
func stepInstallISH(s *State, log func(string)) (StepStatus, error) {
|
||||
matches, _ := filepath.Glob(filepath.Join(s.artifactsDir, "ish", "igate_*.deb"))
|
||||
if len(matches) == 0 {
|
||||
log("Дистрибутив ИШ не найден — пропускаю (можно установить позже)")
|
||||
return StepSkipped, nil
|
||||
}
|
||||
if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil {
|
||||
// допустим, что зависимости подтянутся
|
||||
_ = runCmd(log, "apt-get", "-f", "install", "-y")
|
||||
if err := runCmd(log, "dpkg", "-i", matches[0]); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepSaveConfig(s *State, log func(string)) (StepStatus, error) {
|
||||
cfg := s.Snapshot().Config
|
||||
if cfg.OrgINN == "" && cfg.AdminEmail == "" && cfg.LicenseKey == "" {
|
||||
return StepSkipped, nil
|
||||
}
|
||||
b, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := os.MkdirAll("/var/lib/bj/.bj", 0o700); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := os.WriteFile("/var/lib/bj/.bj/setup.json", b, 0o600); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
return StepDone, runCmd(log, "chown", "-R", "bj:bj", "/var/lib/bj/.bj")
|
||||
}
|
||||
|
||||
func stepStartServices(s *State, log func(string)) (StepStatus, error) {
|
||||
// disable+stop socket activation для pcscd
|
||||
_ = runCmd(log, "systemctl", "stop", "pcscd.socket")
|
||||
_ = runCmd(log, "systemctl", "disable", "pcscd.socket")
|
||||
|
||||
for _, svc := range []string{"pcscd", "bj-crypto", "bj-server"} {
|
||||
if err := runCmd(log, "systemctl", "enable", svc); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
if err := runCmd(log, "systemctl", "restart", svc); err != nil {
|
||||
return StepFailed, err
|
||||
}
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
func stepHealthCheck(s *State, log func(string)) (StepStatus, error) {
|
||||
var bad []string
|
||||
for _, svc := range []string{"pcscd", "vdcrysvc", "bj-crypto", "bj-server"} {
|
||||
if err := exec.Command("systemctl", "is-active", "--quiet", svc).Run(); err != nil {
|
||||
bad = append(bad, svc)
|
||||
} else {
|
||||
log(svc + ": active")
|
||||
}
|
||||
}
|
||||
if len(bad) > 0 {
|
||||
return StepFailed, fmt.Errorf("сервисы не запустились: %s", strings.Join(bad, ", "))
|
||||
}
|
||||
return StepDone, nil
|
||||
}
|
||||
|
||||
// hasAPTPackage — проверяет наличие пакета в apt-cache (доступен ли для установки).
|
||||
func hasAPTPackage(name string) bool {
|
||||
out, err := exec.Command("apt-cache", "show", name).CombinedOutput()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(out), "Package: "+name)
|
||||
}
|
||||
Reference in New Issue
Block a user