Files
Bridge-and-Join-s/internal/lkgateway/certexpiry.go
T
fontvielle ad6b52f5fa Сроки сертификатов — только по нашей организации
В справочнике ИШ — сертификаты всех участников обмена (на 9 — 351: ВТБ,
Сбербанк, ТБанк…). «Свой» был любой не НРД/биржа/НКЦ/УЦ, и приходило
«Сертификат ключа организации истекает сегодня» по сотруднику Сбербанка.
Теперь свой — ИНН в subject равен ИНН нашей организации; уведомления о
сроках — только по своим (сертификаты НРД приезжают с обновлением bj);
чужие сертификаты не показываются в шаге «Сертификаты».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
2026-10-22 12:09:04 +03:00

241 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"))
}
// 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) }
}