da302c6d69
После запуска (вместе с первой проверкой сроков) из ленты и главной уходят уведомления «Сертификат ключа организации…» по сертификатам не нашей организации и «Сертификат НРД/биржи в справочнике ИШ…» — на 9 их 22, все по чужим (Сбербанк и др.). Свои остаются. RuntimeConfig.RemoveNews. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
273 lines
11 KiB
Go
273 lines
11 KiB
Go
package lkgateway
|
||
|
||
// Сроки сертификатов в справочнике ИШ (05.10.2026). Своих сертификатов bj напрямую не видит (ключи — в
|
||
// Валидате и ИШ), но знает содержимое справочника активного профиля ИШ: его раз в 6 часов читает сверка
|
||
// сертификатов НРД (nsdcerts.go, NSDCerts.LastStore). В справочнике — сертификаты всех участников
|
||
// обмена (на 9 — 351: ВТБ, Сбербанк, ТБанк…). Свой — ИНН в subject равен ИНН нашей организации (22.10.2026;
|
||
// раньше «своим» был любой не НРД/биржа/НКЦ/УЦ). По нему же отмечается шаг «Сертификаты».
|
||
//
|
||
// Уведомления: за 30, 14, 7, 3 и 1 день до окончания и в день окончания — только по сертификатам нашей
|
||
// организации (сертификаты НРД приезжают с обновлением bj).
|
||
|
||
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 (НРД/биржа/НКЦ), other (другие участники обмена),
|
||
// ca (УЦ) или "" (не сертификат). ourINN — ИНН нашей организации; пусто (ещё не задан) — своим
|
||
// считается любой, кроме НРД/биржи/НКЦ/УЦ, как до 22.10.2026.
|
||
func storeCertKind(c NSDStoreCert, bundle map[string]bool, ourINN string) string {
|
||
if c.Kind != "x509" {
|
||
return ""
|
||
}
|
||
if c.SelfSigned || strings.Contains(c.SubjectCN, "УЦ") {
|
||
return "ca"
|
||
}
|
||
inn := subjectINN(c.Subject)
|
||
if bundle[normSerial(c.Serial)] || counterpartINN[inn] != "" {
|
||
return "counterpart"
|
||
}
|
||
if ourINN = strings.TrimLeft(strings.TrimSpace(ourINN), "0"); ourINN != "" && inn != ourINN {
|
||
return "other"
|
||
}
|
||
return "own"
|
||
}
|
||
|
||
// ourOrgINN — ИНН нашей организации для отбора своих сертификатов.
|
||
func ourOrgINN(s Settings) string { return orgINN(s) }
|
||
|
||
// ownStoreCerts — свои сертификаты в справочнике ИШ.
|
||
func ownStoreCerts(s Settings) []NSDStoreCert {
|
||
bundle, our := bundleSerials(), ourOrgINN(s)
|
||
var out []NSDStoreCert
|
||
for _, c := range s.NSDCerts.LastStore {
|
||
if storeCertKind(c, bundle, our) == "own" {
|
||
out = append(out, c)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// storeCertsForUI — сертификаты справочника ИШ (кроме УЦ) для шага «Сертификаты»: свой — с отметкой «ключ есть».
|
||
func storeCertsForUI(s Settings) []cryptocli.Certificate {
|
||
bundle, our := bundleSerials(), ourOrgINN(s)
|
||
var out []cryptocli.Certificate
|
||
seen := map[string]bool{}
|
||
for _, c := range s.NSDCerts.LastStore {
|
||
k := storeCertKind(c, bundle, our)
|
||
if k == "" || k == "ca" || k == "other" || 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
|
||
our := ourOrgINN(s)
|
||
for _, c := range s.NSDCerts.LastStore {
|
||
kind := storeCertKind(c, bundle, our)
|
||
if kind != "own" { // только сертификаты нашей организации
|
||
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"))
|
||
}
|
||
|
||
var certNewsSerialRe = regexp.MustCompile(`серийный номер ([0-9A-Fa-f]+)`)
|
||
|
||
// certNewsCleanup — убрать из ленты уведомления о сроках чужих сертификатов (до 22.10.2026 «своим» считался
|
||
// любой сертификат участника в справочнике ИШ) и сертификатов НРД/биржи: остаются только по своим.
|
||
func certNewsCleanup(rc *RuntimeConfig) {
|
||
s := rc.Snapshot()
|
||
if strings.TrimSpace(ourOrgINN(s)) == "" || len(s.NSDCerts.LastStore) == 0 {
|
||
return // не знаем, чьи сертификаты наши, — ничего не трогаем
|
||
}
|
||
own := map[string]bool{}
|
||
for _, c := range ownStoreCerts(s) {
|
||
own[normSerial(c.Serial)] = true
|
||
}
|
||
n := rc.RemoveNews(func(it NewsItem) bool {
|
||
if it.Kind != "system" || !strings.HasPrefix(it.Title, "Сертификат ") {
|
||
return false
|
||
}
|
||
if strings.Contains(it.Title, "в справочнике ИШ") {
|
||
return true // сертификаты НРД/биржи — больше не уведомляем
|
||
}
|
||
if !strings.HasPrefix(it.Title, "Сертификат ключа организации") {
|
||
return false
|
||
}
|
||
m := certNewsSerialRe.FindStringSubmatch(it.Body)
|
||
return m != nil && !own[normSerial(m[1])]
|
||
})
|
||
if n > 0 {
|
||
log.Printf("cert-expiry: из ленты убрано %d уведомлений о сроках чужих сертификатов", n)
|
||
}
|
||
}
|
||
|
||
// 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:
|
||
certNewsCleanup(rc)
|
||
certExpiryTick(rc)
|
||
t.Reset(6 * time.Hour)
|
||
}
|
||
}
|
||
}()
|
||
return func() { close(stop) }
|
||
}
|