forked from gokalkan/gokalkan
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcert_summary.go
More file actions
237 lines (199 loc) · 8.15 KB
/
Copy pathcert_summary.go
File metadata and controls
237 lines (199 loc) · 8.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
package gokalkan
import (
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"regexp"
"strings"
"time"
"github.com/ncanode-kz/gokalkan/ckalkan"
)
type (
// RevocationType - способ проверки отозванности сертификата.
RevocationType string
// KeyUsage - упрощённое (в терминах NCA) назначение ключа сертификата.
// Это НЕ побитовая семантика стандартного X.509 KeyUsage, а бизнес-классификация,
// которую использует kz.gov.pki-экосистема: сертификат либо для аутентификации,
// либо для подписи.
KeyUsage string
)
const (
RevocationTypeOCSP RevocationType = "OCSP"
RevocationTypeCRL RevocationType = "CRL"
KeyUsageUnknown KeyUsage = "UNKNOWN"
KeyUsageAuth KeyUsage = "AUTH"
KeyUsageSign KeyUsage = "SIGN"
)
// Revocation - результат одной проверки отозванности (OCSP или CRL).
type Revocation struct {
Type RevocationType
Revoked bool
Reason string
// RevokedAt - точное время отзыва, если revoked=true и его удалось
// извлечь из текстового отчёта нативной библиотеки (X509ValidateCertificate
// возвращает секцию "Revocation Time: DD.MM.YYYY HH:MM:SS +HH:MM" при
// проверке через OCSP - см. revokedAtRe). Zero value, если не найдено.
RevokedAt time.Time
}
// CertificateInfo - расширенная информация о сертификате, аналог CertificateInfo
// в NCANode (Java): помимо разбора полей (см. Summary/X509CertificateGetSummary)
// содержит результат проверки отозванности и агрегированную валидность.
type CertificateInfo struct {
Valid bool
Revocations []Revocation
NotBefore time.Time
NotAfter time.Time
KeyUsage KeyUsage
// KeyUser - набор меток назначения сертификата: "INDIVIDUAL" или "ORGANIZATION",
// и для организации дополнительно роль подписанта ("CEO", "SIGN", "SIGN_FINANCE",
// "HR", "EMPLOYEE"). Кроме "INDIVIDUAL"/"ORGANIZATION"/"CEO" (сверено с NCANode)
// остальные названия ролей - по аналогии, не сверялись с эталоном напрямую.
KeyUser []string
SerialNumber string
SignAlg string
PublicKey string
// Signature - "сырое" значение подписи сертификата (BIT STRING из
// Certificate.signatureValue), в base64. Извлекается обычным ASN.1-разбором
// внешней структуры X.509 (не зависит от алгоритма подписи, GOST в том числе).
Signature string
Subject CertSubject
Organization *CertOrganization
Issuer CertIssuer
}
// GetCertificateInfo возвращает расширенную информацию о сертификате, включая
// результат проверки отозванности. checkOCSP включает проверку через OCSP
// (URL берётся из опций клиента, см. WithOCSP/OptsTest/OptsProd). crlPath,
// если не пустой, включает проверку по локальному файлу CRL (см. ValidateCertCRL).
func (cli *Client) GetCertificateInfo(cert string, checkOCSP bool, crlPath string) (*CertificateInfo, error) {
summary, err := cli.X509CertificateGetSummary(cert)
if err != nil {
return nil, err
}
info := &CertificateInfo{
Valid: true,
NotBefore: summary.NotBefore,
NotAfter: summary.NotAfter,
SerialNumber: strings.ToLower(summary.SerialNumber),
PublicKey: summary.PublicKey,
Subject: summary.Subject,
Organization: summary.Organization,
Issuer: summary.Issuer,
}
now := time.Now()
if now.Before(info.NotBefore) || now.After(info.NotAfter) {
info.Valid = false
}
info.KeyUser = buildKeyUser(summary)
rawKeyUsage, err := cli.kc.X509CertificateGetInfo(cert, ckalkan.CertPropKeyUsage)
if err != nil {
return nil, err
}
info.KeyUsage = parseKeyUsage(rawKeyUsage)
rawSignAlg, err := cli.kc.X509CertificateGetInfo(cert, ckalkan.CertPropSignatureAlg)
if err != nil {
return nil, err
}
info.SignAlg = cleanupValue(rawSignAlg, "=")
if sig, err := extractRawSignature(cert); err == nil {
info.Signature = sig
}
if checkOCSP {
raw, verr := cli.ValidateCertOCSP(cert)
rev := Revocation{Type: RevocationTypeOCSP, Revoked: verr != nil}
if verr != nil {
rev.Reason = verr.Error()
rev.RevokedAt = parseRevokedAt(raw)
info.Valid = false
}
info.Revocations = append(info.Revocations, rev)
}
if crlPath != "" {
_, verr := cli.ValidateCertCRL(cert, crlPath)
rev := Revocation{Type: RevocationTypeCRL, Revoked: verr != nil}
if verr != nil {
rev.Reason = verr.Error()
info.Valid = false
}
info.Revocations = append(info.Revocations, rev)
}
return info, nil
}
// parseKeyUsage сводит битовую маску X.509 KeyUsage к упрощённой классификации
// AUTH/SIGN, принятой в NCA-экосистеме: наличие nonRepudiation (contentCommitment,
// бит 1) означает сертификат для ЭЦП (SIGN), наличие keyEncipherment (бит 2) без
// nonRepudiation - сертификат для аутентификации/шифрования (AUTH).
func parseKeyUsage(raw string) KeyUsage {
value := cleanupValue(raw, "=")
bits := strings.Fields(value)
set := make(map[string]bool, len(bits))
for _, b := range bits {
set[b] = true
}
switch {
case set["nonRepudiation"]:
return KeyUsageSign
case set["keyEncipherment"]:
return KeyUsageAuth
default:
return KeyUsageUnknown
}
}
func buildKeyUser(summary *Summary) []string {
if summary.Type == CertTypeIndividual {
return []string{"INDIVIDUAL"}
}
users := []string{"ORGANIZATION"}
if summary.Organization != nil {
switch summary.Organization.SubjectRole {
case CertSubjectRoleCEO:
users = append(users, "CEO")
case CertSubjectRoleSign:
users = append(users, "SIGN")
case CertSubjectRoleSignFinance:
users = append(users, "SIGN_FINANCE")
case CertSubjectRoleHR:
users = append(users, "HR")
case CertSubjectRoleEmployee:
users = append(users, "EMPLOYEE")
}
}
return users
}
//nolint:gochecknoglobals
var revokedAtRe = regexp.MustCompile(`Revocation Time: (\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}:\d{2} \S+)`)
// parseRevokedAt извлекает точное время отзыва из текстового отчёта
// X509ValidateCertificate (см. ValidateCertOCSP) - эмпирически подтверждено,
// что при статусе OCSP "revoked" отчёт содержит строку вида
// "Revocation Time: 08.05.2026 14:57:29 ALMT" (либо, реже, с числовым
// оффсетом вместо аббревиатуры - см. parseKalkanTime). Возвращает нулевое
// время, если строка не найдена или не распозналась.
func parseRevokedAt(raw string) time.Time {
m := revokedAtRe.FindStringSubmatch(raw)
if m == nil {
return time.Time{}
}
t, err := parseKalkanTime(m[1])
if err != nil {
return time.Time{}
}
return t
}
type derCertificate struct {
TBSCertificate asn1.RawValue
SignatureAlgorithm asn1.RawValue
SignatureValue asn1.BitString
}
// extractRawSignature вытаскивает Certificate.signatureValue напрямую из DER,
// без разбора алгоритмо-специфичных полей внутри TBSCertificate - поэтому
// работает одинаково для GOST- и RSA-сертификатов.
func extractRawSignature(certPEM string) (string, error) {
der := []byte(certPEM)
if block, _ := pem.Decode([]byte(certPEM)); block != nil {
der = block.Bytes
}
var c derCertificate
if _, err := asn1.Unmarshal(der, &c); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(c.SignatureValue.Bytes), nil
}