Первичная настройка, часть 1: префикс торгового счёта DEPO — настройка с подсказкой по учёту (был зашит «ВТНКЦ» в 7 запросах); шаг 8 — адрес ИШ вместо ONYX, профили prod-gost/prod-rsa (пункт prod уводил bj в имитацию), без неиспользуемого контейнера, перезапуск bj после смены контура; реквизиты НРД на шаге 4; точная проверка процедур bj2depo и таблиц Fancy M2M; QUIK — объекты M2M и справочник получателей, без зашитого IP; сертификаты из справочника ИШ (FindCertificates — заглушка), уведомления о сроке за 30/14/7/3/1 день; тексты мастера, нумерация, режим разработки; уникальные id оповещений с русскими заголовками
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
package lkgateway
|
||||
|
||||
// Сроки сертификатов в справочнике ИШ (05.10.2026). Своих сертификатов bj напрямую не видит (ключи — в
|
||||
// Валидате и ИШ), но знает содержимое справочника активного профиля ИШ: его раз в 6 часов читает сверка
|
||||
// сертификатов НРД (nsdcerts.go, NSDCerts.LastStore). Свой сертификат — тот, что не из комплекта НРД,
|
||||
// не самоподписанный (не УЦ) и выдан не НРД, бирже или НКЦ. По нему же отмечается шаг «Сертификаты».
|
||||
//
|
||||
// Уведомления: за 30, 14, 7, 3 и 1 день до окончания и в день окончания — для своего сертификата и для
|
||||
// сертификатов НРД, если им нет замены с более поздним сроком.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli"
|
||||
)
|
||||
|
||||
// ИНН организаций, чьи сертификаты лежат в справочнике рядом с нашим (без ведущих нулей).
|
||||
var counterpartINN = map[string]string{"7702165310": "НРД", "7702077840": "Московская Биржа", "7750004023": "НКЦ"}
|
||||
|
||||
var subjectINNRe = regexp.MustCompile(`(?:^|,)\s*INN=0*(\d{10,12})`)
|
||||
|
||||
func subjectINN(subject string) string {
|
||||
if m := subjectINNRe.FindStringSubmatch(subject); m != nil {
|
||||
return m[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// storeNotAfter — срок из справочника («02.01.2006»): сертификат действует весь последний день.
|
||||
func storeNotAfter(c NSDStoreCert) time.Time {
|
||||
t, err := time.ParseInLocation("02.01.2006", strings.TrimSpace(c.NotAfter), time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t.Add(24*time.Hour - time.Second)
|
||||
}
|
||||
|
||||
func certDaysLeft(t time.Time) int {
|
||||
if t.IsZero() {
|
||||
return 1 << 20
|
||||
}
|
||||
d := time.Until(t)
|
||||
if d < 0 {
|
||||
return -1
|
||||
}
|
||||
return int(d.Hours() / 24)
|
||||
}
|
||||
|
||||
func bundleSerials() map[string]bool {
|
||||
out := map[string]bool{}
|
||||
bundle, _ := loadNSDCertBundle()
|
||||
for _, b := range bundle {
|
||||
out[normSerial(b.Serial)] = true
|
||||
if b.ReplacesSerial != "" {
|
||||
out[normSerial(b.ReplacesSerial)] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// storeCertKind — own (свой), counterpart (НРД/биржа/НКЦ), ca (УЦ) или "" (не сертификат).
|
||||
func storeCertKind(c NSDStoreCert, bundle map[string]bool) string {
|
||||
if c.Kind != "x509" {
|
||||
return ""
|
||||
}
|
||||
if c.SelfSigned || strings.Contains(c.SubjectCN, "УЦ") {
|
||||
return "ca"
|
||||
}
|
||||
if bundle[normSerial(c.Serial)] || counterpartINN[subjectINN(c.Subject)] != "" {
|
||||
return "counterpart"
|
||||
}
|
||||
return "own"
|
||||
}
|
||||
|
||||
// ownStoreCerts — свои сертификаты в справочнике ИШ.
|
||||
func ownStoreCerts(s Settings) []NSDStoreCert {
|
||||
bundle := bundleSerials()
|
||||
var out []NSDStoreCert
|
||||
for _, c := range s.NSDCerts.LastStore {
|
||||
if storeCertKind(c, bundle) == "own" {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// storeCertsForUI — сертификаты справочника ИШ (кроме УЦ) для шага «Сертификаты»: свой — с отметкой «ключ есть».
|
||||
func storeCertsForUI(s Settings) []cryptocli.Certificate {
|
||||
bundle := bundleSerials()
|
||||
var out []cryptocli.Certificate
|
||||
seen := map[string]bool{}
|
||||
for _, c := range s.NSDCerts.LastStore {
|
||||
k := storeCertKind(c, bundle)
|
||||
if k == "" || k == "ca" || seen[c.Serial] {
|
||||
continue
|
||||
}
|
||||
seen[c.Serial] = true
|
||||
out = append(out, cryptocli.Certificate{SubjectCN: c.SubjectCN, IssuerCN: c.IssuerCN, Serial: c.Serial,
|
||||
NotAfter: storeNotAfter(c), INN: subjectINN(c.Subject), HasPrivateKey: k == "own"})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var certExpiryMarks = []int{30, 14, 7, 3, 1, 0}
|
||||
|
||||
var certExpiry = struct {
|
||||
sync.Mutex
|
||||
file string
|
||||
}{file: "/var/lib/bj/.bj/cert-expiry.json"}
|
||||
|
||||
// certExpiryTick — разослать уведомления о сроках (каждая отметка — один раз на сертификат).
|
||||
func certExpiryTick(rc *RuntimeConfig) {
|
||||
s := rc.Snapshot()
|
||||
if len(s.NSDCerts.LastStore) == 0 {
|
||||
return
|
||||
}
|
||||
certExpiry.Lock()
|
||||
defer certExpiry.Unlock()
|
||||
sent := map[string][]int{}
|
||||
if raw, err := os.ReadFile(certExpiry.file); err == nil {
|
||||
_ = json.Unmarshal(raw, &sent)
|
||||
}
|
||||
bundle := bundleSerials()
|
||||
// сертификат НРД, которому уже есть замена с более поздним сроком, не тревожим
|
||||
latest := map[string]time.Time{}
|
||||
for _, c := range s.NSDCerts.LastStore {
|
||||
if t := storeNotAfter(c); c.Kind == "x509" && t.After(latest[c.SubjectCN]) {
|
||||
latest[c.SubjectCN] = t
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
for _, c := range s.NSDCerts.LastStore {
|
||||
kind := storeCertKind(c, bundle)
|
||||
if kind != "own" && kind != "counterpart" {
|
||||
continue
|
||||
}
|
||||
until := storeNotAfter(c)
|
||||
if until.IsZero() || (kind == "counterpart" && latest[c.SubjectCN].After(until)) {
|
||||
continue
|
||||
}
|
||||
days := certDaysLeft(until)
|
||||
key := normSerial(c.Serial)
|
||||
mark := -1
|
||||
for _, m := range certExpiryMarks {
|
||||
if days <= m && !containsInt(sent[key], m) {
|
||||
mark = m // самая близкая к сроку из ещё не отправленных
|
||||
}
|
||||
}
|
||||
if mark < 0 {
|
||||
continue
|
||||
}
|
||||
for _, m := range certExpiryMarks {
|
||||
if m >= mark && !containsInt(sent[key], m) {
|
||||
sent[key] = append(sent[key], m)
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
title, body := certExpiryText(c, kind, days, until)
|
||||
log.Printf("cert-expiry: %s", title)
|
||||
raiseSystemAlert(rc, title, body)
|
||||
}
|
||||
if changed {
|
||||
if raw, err := json.MarshalIndent(sent, "", " "); err == nil {
|
||||
_ = os.WriteFile(certExpiry.file, raw, 0o600)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containsInt(a []int, v int) bool {
|
||||
for _, x := range a {
|
||||
if x == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func certExpiryText(c NSDStoreCert, kind string, days int, until time.Time) (string, string) {
|
||||
when := fmt.Sprintf("через %d дн.", days)
|
||||
switch {
|
||||
case days < 0:
|
||||
when = "ИСТЁК"
|
||||
case days == 0:
|
||||
when = "истекает сегодня"
|
||||
}
|
||||
who := fmt.Sprintf("«%s» (издатель «%s», серийный номер %s)", c.SubjectCN, c.IssuerCN, c.Serial)
|
||||
if kind == "own" {
|
||||
title := "Сертификат ключа организации " + when
|
||||
if days > 0 {
|
||||
title = "Сертификат ключа организации истекает " + when
|
||||
}
|
||||
return title, fmt.Sprintf("Сертификат %s действует до %s включительно. После этой даты подписать пакеты для НРД будет нечем — переводы M2M остановятся. "+
|
||||
"Закажите новый ключ в Удостоверяющем центре Московской биржи заранее (выпуск занимает время) и установите его мастером установки ключа: Настройка → Криптография.",
|
||||
who, until.Format("02.01.2006"))
|
||||
}
|
||||
org := pickStr(counterpartINN[subjectINN(c.Subject)], "НРД")
|
||||
title := "Сертификат " + org + " в справочнике ИШ " + when
|
||||
if days > 0 {
|
||||
title = "Сертификат " + org + " в справочнике ИШ истекает " + when
|
||||
}
|
||||
return title, fmt.Sprintf("Сертификат %s действует до %s, замены с более поздним сроком в справочнике нет. Новые сертификаты НРД приезжают с обновлением bj — проверьте, что bj обновлён (Настройка → Обновления), и сверку сертификатов НРД (Настройка → Криптография).",
|
||||
who, until.Format("02.01.2006"))
|
||||
}
|
||||
|
||||
// StartCertExpiryWatch — проверка сроков раз в 6 часов (первая — через 3 минуты после запуска).
|
||||
func StartCertExpiryWatch(rc *RuntimeConfig) func() {
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
t := time.NewTimer(3 * time.Minute)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-t.C:
|
||||
certExpiryTick(rc)
|
||||
t.Reset(6 * time.Hour)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() { close(stop) }
|
||||
}
|
||||
Reference in New Issue
Block a user