Files
Bridge-and-Join-s/internal/lkgateway/certexpiry.go
T

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) }
}