/
certchecker.go
305 lines (271 loc) · 9.65 KB
/
certchecker.go
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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package certchecker contains implementation of CertChecker.
//
// CertChecker knows how to check certificate signatures and revocation status.
//
// Uses datastore entities managed by 'certconfig' package.
package certchecker
import (
"context"
"crypto/x509"
"fmt"
"time"
ds "go.chromium.org/gae/service/datastore"
"go.chromium.org/gae/service/info"
"github.com/TriggerMail/luci-go/common/clock"
"github.com/TriggerMail/luci-go/common/data/caching/lazyslot"
"github.com/TriggerMail/luci-go/common/retry/transient"
"github.com/TriggerMail/luci-go/server/caching"
"github.com/TriggerMail/luci-go/tokenserver/appengine/impl/certconfig"
)
// CN string => *CertChecker.
var certCheckerCache = caching.RegisterLRUCache(64)
const (
// RefetchCAPeriod is how often to check CA entity in the datastore.
//
// A big value here is acceptable, since CA is changing only when service
// config is changing (which happens infrequently).
RefetchCAPeriod = 5 * time.Minute
// RefetchCRLPeriod is how often to check CRL entities in the datastore.
//
// CRL changes pretty frequently, caching it for too long is harmful
// (increases delay between a cert is revoked by CA and no longer accepted by
// the token server).
RefetchCRLPeriod = 15 * time.Second
)
// ErrorReason is part of Error struct.
type ErrorReason int
const (
// NoSuchCA is returned by GetCertChecker or GetCA if requested CA is not
// defined in the config.
NoSuchCA ErrorReason = iota
// UnknownCA is returned by CheckCertificate if the cert was signed by an
// unexpected CA (i.e. a CA CertChecker is not configured with).
UnknownCA
// NotReadyCA is returned by CheckCertificate if the CA's CRL hasn't been
// fetched yet (and thus CheckCertificate can't verify certificate's
// revocation status).
NotReadyCA
// CertificateExpired is returned by CheckCertificate if the cert has
// expired already or not yet active.
CertificateExpired
// SignatureCheckError is returned by CheckCertificate if the certificate
// signature is not valid.
SignatureCheckError
// CertificateRevoked is returned by CheckCertificate if the certificate is
// in the CA's Certificate Revocation List.
CertificateRevoked
)
// Error is returned by CertChecker methods in case the certificate is invalid.
//
// Datastore errors and not wrapped in Error, but returned as is. You may use
// type cast to Error to distinguish certificate related errors from other kinds
// of errors.
type Error struct {
error // inner error with text description
Reason ErrorReason // enumeration that can be used in switches
}
// NewError instantiates Error.
//
// It is needed because initializing 'error' field on Error is not allowed
// outside of this package (it is lowercase - "unexported").
func NewError(e error, reason ErrorReason) error {
return Error{e, reason}
}
// IsCertInvalidError returns true for errors from CheckCertificate that
// indicate revoked or expired or otherwise invalid certificates.
//
// Such errors can be safely cast to Error.
func IsCertInvalidError(err error) bool {
_, ok := err.(Error)
return ok
}
// CertChecker knows how to check certificate signatures and revocation status.
//
// It is associated with single CA and assumes all certs needing a check are
// signed by that CA directly (i.e. there is no intermediate CAs).
//
// It caches CRL lists internally and must be treated as a heavy global object.
// Use GetCertChecker to grab a global instance of CertChecker for some CA.
//
// CertChecker is safe for concurrent use.
type CertChecker struct {
CN string // Common Name of the CA
CRL *certconfig.CRLChecker // knows how to query certificate revocation list
ca lazyslot.Slot // knows how to load CA cert and config
}
// CheckCertificate checks validity of a given certificate.
//
// It looks at the cert issuer, loads corresponding CertChecker and calls its
// CheckCertificate method. See CertChecker.CheckCertificate documentation for
// explanation of return values.
func CheckCertificate(c context.Context, cert *x509.Certificate) (*certconfig.CA, error) {
checker, err := GetCertChecker(c, cert.Issuer.CommonName)
if err != nil {
return nil, err
}
return checker.CheckCertificate(c, cert)
}
// GetCertChecker returns an instance of CertChecker for given CA.
//
// It caches CertChecker objects in local memory and reuses them between
// requests.
func GetCertChecker(c context.Context, cn string) (*CertChecker, error) {
checker, err := certCheckerCache.LRU(c).GetOrCreate(c, cn, func() (interface{}, time.Duration, error) {
// To avoid storing CertChecker for non-existent CAs in local memory forever,
// we do a datastore check when creating the checker. It happens once during
// the process lifetime.
switch exists, err := ds.Exists(c, ds.NewKey(c, "CA", cn, 0, nil)); {
case err != nil:
return nil, 0, transient.Tag.Apply(err)
case !exists.All():
return nil, 0, Error{
error: fmt.Errorf("no such CA %q", cn),
Reason: NoSuchCA,
}
}
return &CertChecker{
CN: cn,
CRL: certconfig.NewCRLChecker(cn, certconfig.CRLShardCount, refetchCRLPeriod(c)),
}, 0, nil
})
if err != nil {
return nil, err
}
return checker.(*CertChecker), nil
}
// GetCA returns CA entity with ParsedConfig and ParsedCert fields set.
func (ch *CertChecker) GetCA(c context.Context) (*certconfig.CA, error) {
value, err := ch.ca.Get(c, func(interface{}) (ca interface{}, exp time.Duration, err error) {
ca, err = ch.refetchCA(c)
if err == nil {
exp = refetchCAPeriod(c)
}
return
})
if err != nil {
return nil, err
}
ca, _ := value.(*certconfig.CA)
// nil 'ca' means 'refetchCA' could not find it in the datastore. May happen
// if CA entity was deleted after GetCertChecker call. It could have been also
// "soft-deleted" by setting Removed == true.
if ca == nil || ca.Removed {
return nil, Error{
error: fmt.Errorf("no such CA %q", ch.CN),
Reason: NoSuchCA,
}
}
return ca, nil
}
// CheckCertificate checks certificate's signature, validity period and
// revocation status.
//
// It returns nil error iff cert was directly signed by the CA, not expired yet,
// and its serial number is not in the CA's CRL.
//
// On success also returns *certconfig.CA instance used to check the
// certificate, since 'GetCA' may return another instance (in case certconfig.CA
// cache happened to expire between the calls).
func (ch *CertChecker) CheckCertificate(c context.Context, cert *x509.Certificate) (*certconfig.CA, error) {
// Has the cert expired already?
now := clock.Now(c)
if now.Before(cert.NotBefore) || now.After(cert.NotAfter) {
return nil, Error{
error: fmt.Errorf("certificate has expired"),
Reason: CertificateExpired,
}
}
// Grab CA cert from the datastore.
ca, err := ch.GetCA(c)
if err != nil {
return nil, err
}
// Did we fetch its CRL at least once?
if !ca.Ready {
return nil, Error{
error: fmt.Errorf("CRL of CA %q is not ready yet", ch.CN),
Reason: NotReadyCA,
}
}
// Verify the signature.
if cert.Issuer.CommonName != ca.ParsedCert.Subject.CommonName {
return nil, Error{
error: fmt.Errorf("can't check a signature made by %q", cert.Issuer.CommonName),
Reason: UnknownCA,
}
}
if err = cert.CheckSignatureFrom(ca.ParsedCert); err != nil {
return nil, Error{
error: err,
Reason: SignatureCheckError,
}
}
// Check the revocation list.
switch revoked, err := ch.CRL.IsRevokedSN(c, cert.SerialNumber); {
case err != nil:
return nil, err
case revoked:
return nil, Error{
error: fmt.Errorf("certificate with SN %s has been revoked", cert.SerialNumber),
Reason: CertificateRevoked,
}
}
return ca, nil
}
// refetchCAPeriod returns for how long to cache the CA in memory by default.
//
// On dev server we cache for a very short duration to simplify local testing.
func refetchCAPeriod(c context.Context) time.Duration {
if info.IsDevAppServer(c) {
return 100 * time.Millisecond
}
return RefetchCAPeriod
}
// refetchCRLPeriod returns for how long to cache the CRL in memory by default.
//
// On dev server we cache for a very short duration to simplify local testing.
func refetchCRLPeriod(c context.Context) time.Duration {
if info.IsDevAppServer(c) {
return 100 * time.Millisecond
}
return RefetchCRLPeriod
}
// refetchCA is called lazily whenever we need to fetch the CA entity.
//
// If CA entity has disappeared since CertChecker was created, it returns nil
// (that will be cached in ch.ca as usual). It acts as an indicator to GetCA to
// return NoSuchCA error, since returning a error here would just cause a retry
// of 'refetchCA' later.
func (ch *CertChecker) refetchCA(c context.Context) (*certconfig.CA, error) {
ca := &certconfig.CA{CN: ch.CN}
switch err := ds.Get(c, ca); {
case err == ds.ErrNoSuchEntity:
return nil, nil
case err != nil:
return nil, transient.Tag.Apply(err)
}
parsedConf, err := ca.ParseConfig()
if err != nil {
return nil, fmt.Errorf("can't parse stored config for %q - %s", ca.CN, err)
}
ca.ParsedConfig = parsedConf
parsedCert, err := x509.ParseCertificate(ca.Cert)
if err != nil {
return nil, fmt.Errorf("can't parse stored cert for %q - %s", ca.CN, err)
}
ca.ParsedCert = parsedCert
return ca, nil
}