From 29e19c78a9a4ac33088229343f70b89de745a3b7 Mon Sep 17 00:00:00 2001 From: fontvielle Date: Wed, 21 Oct 2026 23:27:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B5=D1=80=D1=82=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B0=D1=82=D1=8B:=20=D1=81=D1=80=D0=BE=D0=BA=20=D0=B4?= =?UTF-8?q?=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=81=D1=80=D0=BE=D0=BA=D1=83=20=D0=B7=D0=B0=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=20(PrivateKeyUsagePeriod)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В ГОСТ-сертификатах ключ подписи действует год–15 месяцев, а сам сертификат — до 15 лет; bj показывал и проверял срок сертификата (например, 06.10.2036 вместо 2027). Теперь справочник профиля ИШ и сертификаты с флешки берут срок ключа (2.5.29.16), если он раньше; срок сертификата показывается рядом. У УЦ — по-прежнему срок сертификата. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn --- deploy/linux/sbin/bj-keymedia-install | 21 ++++++++++++++++- internal/lkgateway/media.go | 23 +++++++++++++++++++ internal/lkgateway/nsdcerts.go | 13 ++++++++++- .../lkgateway/web/templates/admin_setup.html | 2 +- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/deploy/linux/sbin/bj-keymedia-install b/deploy/linux/sbin/bj-keymedia-install index 1e5b3e8..4e99b87 100755 --- a/deploy/linux/sbin/bj-keymedia-install +++ b/deploy/linux/sbin/bj-keymedia-install @@ -95,6 +95,25 @@ certs_list_json(){ python3 - "$PROFDIR/local.pse" "$PROFDIR/local.gdbm" <<'PY2' import sys,subprocess,json,re out=[]; seen=set() +MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'] +def key_not_after(b): + # срок закрытого ключа — расширение PrivateKeyUsagePeriod (2.5.29.16), [1] notAfter GeneralizedTime; + # у ГОСТ-сертификатов он короче срока самого сертификата (ключ подписи — год, сертификат — до 15 лет) + k=b.find(b'\x06\x03\x55\x1d\x10') + if k<0: return '' + j=k+5 + try: + if b[j]==0x01: j+=3 # critical BOOLEAN + if b[j]!=0x04: return '' + j+=2 if b[j+1]<0x80 else 2+(b[j+1]&0x7f) # OCTET STRING + if b[j]!=0x30: return '' + end=j+2+b[j+1]; j+=2 + while j=14: + return '%s %d %s:%s:%s %s GMT'%(MON[int(v[4:6])-1],int(v[6:8]),v[8:10],v[10:12],v[12:14],v[0:4]) + except Exception: pass + return '' for p in sys.argv[1:]: store=p.rsplit('.',1)[-1] try: d=open(p,'rb').read() @@ -112,7 +131,7 @@ for p in sys.argv[1:]: sn=g('serial').upper() if sn in seen: i+=4; continue seen.add(sn) - out.append({'kind':'x509','store':store,'subject':g('subject'),'subject_cn':cn(g('subject')),'issuer_cn':cn(g('issuer')),'serial':sn,'not_before':g('notBefore'),'not_after':g('notAfter'),'self_signed':g('subject')==g('issuer')}) + out.append({'kind':'x509','store':store,'subject':g('subject'),'subject_cn':cn(g('subject')),'issuer_cn':cn(g('issuer')),'serial':sn,'not_before':g('notBefore'),'not_after':g('notAfter'),'key_not_after':key_not_after(b),'self_signed':g('subject')==g('issuer')}) else: r=subprocess.run(['openssl','crl','-inform','DER','-noout','-issuer','-nextupdate','-nameopt','utf8,sep_comma_plus'],input=b,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL) if r.returncode==0: diff --git a/internal/lkgateway/media.go b/internal/lkgateway/media.go index 524c4ca..607a197 100644 --- a/internal/lkgateway/media.go +++ b/internal/lkgateway/media.go @@ -4,6 +4,7 @@ import ( "context" "crypto/sha256" "crypto/x509" + "crypto/x509/pkix" "encoding/asn1" "encoding/hex" "encoding/pem" @@ -242,10 +243,32 @@ func classifyCertFile(path string) *CertFile { cf.Serial = cert.SerialNumber.Text(16) cf.NotBefore = cert.NotBefore cf.NotAfter = cert.NotAfter + if k := privateKeyUsageEnd(cert.Extensions); !k.IsZero() && k.Before(cf.NotAfter) { + cf.NotAfter = k // подписывать можно до конца срока ключа, а не сертификата + } cf.INN = extractCertINN(cert) return cf } +// privateKeyUsageEnd — конец срока закрытого ключа (расширение PrivateKeyUsagePeriod, 2.5.29.16). В ГОСТ- +// сертификатах ключ подписи действует год–15 месяцев, а сам сертификат — до 15 лет; подписывать после +// срока ключа нельзя. Нулевое время — расширения нет. +func privateKeyUsageEnd(exts []pkix.Extension) time.Time { + for _, e := range exts { + if !e.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 16}) { + continue + } + var p struct { + NotBefore time.Time `asn1:"generalized,optional,tag:0"` + NotAfter time.Time `asn1:"generalized,optional,tag:1"` + } + if _, err := asn1.Unmarshal(e.Value, &p); err == nil { + return p.NotAfter + } + } + return time.Time{} +} + // extractCertINN — ИНН из OID 1.2.643.3.131.1.1 в Subject. func extractCertINN(c *x509.Certificate) string { innOID := asn1.ObjectIdentifier{1, 2, 643, 3, 131, 1, 1} diff --git a/internal/lkgateway/nsdcerts.go b/internal/lkgateway/nsdcerts.go index d8c03cc..b46d2c9 100644 --- a/internal/lkgateway/nsdcerts.go +++ b/internal/lkgateway/nsdcerts.go @@ -233,8 +233,12 @@ type NSDStoreCert struct { IssuerCN string `json:"issuer_cn"` Serial string `json:"serial"` NotBefore string `json:"not_before"` - NotAfter string `json:"not_after"` + NotAfter string `json:"not_after"` // до какой даты действует: срок ключа подписи, если он раньше срока сертификата SelfSigned bool `json:"self_signed"` + // KeyNotAfter — срок закрытого ключа (PrivateKeyUsagePeriod); CertNotAfter — срок самого сертификата, + // если он позже ключа (в ГОСТ-сертификате ключ подписи действует год, сертификат — до 15 лет) + KeyNotAfter string `json:"key_not_after,omitempty"` + CertNotAfter string `json:"cert_not_after,omitempty"` } func nsdStoreFromResult(res map[string]any) []NSDStoreCert { @@ -244,6 +248,13 @@ func nsdStoreFromResult(res map[string]any) []NSDStoreCert { for i := range st { st[i].NotBefore = nsdPrettyDate(st[i].NotBefore) st[i].NotAfter = nsdPrettyDate(st[i].NotAfter) + st[i].KeyNotAfter = nsdPrettyDate(st[i].KeyNotAfter) + k, kerr := time.Parse("02.01.2006", st[i].KeyNotAfter) + c, cerr := time.Parse("02.01.2006", st[i].NotAfter) + // у УЦ срок ключа — только для выпуска новых сертификатов; проверять им можно весь срок сертификата + if kerr == nil && !st[i].SelfSigned && (cerr != nil || k.Before(c)) { + st[i].CertNotAfter, st[i].NotAfter = st[i].NotAfter, st[i].KeyNotAfter + } } return st } diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 15b9db9..20c02a5 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -314,7 +314,7 @@ {{if .NSDCerts.LastLog}}
{{.NSDCerts.LastLog}}
{{end}} {{if .NSDCerts.Store}}
Содержимое справочника профиля ({{len .NSDCerts.Store}} записей) - {{range .NSDCerts.Store}}{{end}}
ВладелецИздательСерийныйДоХранилище
{{if eq .Kind "crl"}}CRL{{else}}{{.SubjectCN}}{{if .SelfSigned}} (УЦ){{end}}{{end}}{{.IssuerCN}}{{.Serial}}{{.NotAfter}}{{.Store}}
{{end}} + {{range .NSDCerts.Store}}{{end}}
ВладелецИздательСерийныйДоХранилище
{{if eq .Kind "crl"}}CRL{{else}}{{.SubjectCN}}{{if .SelfSigned}} (УЦ){{end}}{{end}}{{.IssuerCN}}{{.Serial}}{{.NotAfter}}{{if .CertNotAfter}}
ключ; сертификат до {{.CertNotAfter}}
{{end}}
{{.Store}}
{{end}}

Свои сертификаты можно положить в /var/lib/bj/.bj/nsd-certs/*.cer — они добавляются по тем же правилам и никогда не удаляются автоматически.