fdf0ddd498
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
230 lines
8.2 KiB
Go
230 lines
8.2 KiB
Go
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) }
|
|
}
|