go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/certchecker/certchecker.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package certchecker contains implementation of CertChecker. 16 // 17 // CertChecker knows how to check certificate signatures and revocation status. 18 // 19 // Uses datastore entities managed by 'certconfig' package. 20 package certchecker 21 22 import ( 23 "context" 24 "crypto/x509" 25 "fmt" 26 "time" 27 28 "go.chromium.org/luci/common/clock" 29 "go.chromium.org/luci/common/data/caching/lazyslot" 30 "go.chromium.org/luci/common/retry/transient" 31 ds "go.chromium.org/luci/gae/service/datastore" 32 "go.chromium.org/luci/server/caching" 33 34 "go.chromium.org/luci/tokenserver/appengine/impl/certconfig" 35 ) 36 37 // CN string => *CertChecker. 38 var certCheckerCache = caching.RegisterLRUCache[string, *CertChecker](64) 39 40 const ( 41 // RefetchCAPeriod is how often to check CA entity in the datastore. 42 // 43 // A big value here is acceptable, since CA is changing only when service 44 // config is changing (which happens infrequently). 45 RefetchCAPeriod = 5 * time.Minute 46 47 // RefetchCRLPeriod is how often to check CRL entities in the datastore. 48 // 49 // CRL changes pretty frequently, caching it for too long is harmful 50 // (increases delay between a cert is revoked by CA and no longer accepted by 51 // the token server). 52 RefetchCRLPeriod = 15 * time.Second 53 ) 54 55 // ErrorReason is part of Error struct. 56 type ErrorReason int 57 58 const ( 59 // NoSuchCA is returned by GetCertChecker or GetCA if requested CA is not 60 // defined in the config. 61 NoSuchCA ErrorReason = iota 62 63 // UnknownCA is returned by CheckCertificate if the cert was signed by an 64 // unexpected CA (i.e. a CA CertChecker is not configured with). 65 UnknownCA 66 67 // NotReadyCA is returned by CheckCertificate if the CA's CRL hasn't been 68 // fetched yet (and thus CheckCertificate can't verify certificate's 69 // revocation status). 70 NotReadyCA 71 72 // CertificateExpired is returned by CheckCertificate if the cert has 73 // expired already or not yet active. 74 CertificateExpired 75 76 // SignatureCheckError is returned by CheckCertificate if the certificate 77 // signature is not valid. 78 SignatureCheckError 79 80 // CertificateRevoked is returned by CheckCertificate if the certificate is 81 // in the CA's Certificate Revocation List. 82 CertificateRevoked 83 ) 84 85 // Error is returned by CertChecker methods in case the certificate is invalid. 86 // 87 // Datastore errors and not wrapped in Error, but returned as is. You may use 88 // type cast to Error to distinguish certificate related errors from other kinds 89 // of errors. 90 type Error struct { 91 error // inner error with text description 92 Reason ErrorReason // enumeration that can be used in switches 93 } 94 95 // NewError instantiates Error. 96 // 97 // It is needed because initializing 'error' field on Error is not allowed 98 // outside of this package (it is lowercase - "unexported"). 99 func NewError(e error, reason ErrorReason) error { 100 return Error{e, reason} 101 } 102 103 // IsCertInvalidError returns true for errors from CheckCertificate that 104 // indicate revoked or expired or otherwise invalid certificates. 105 // 106 // Such errors can be safely cast to Error. 107 func IsCertInvalidError(err error) bool { 108 _, ok := err.(Error) 109 return ok 110 } 111 112 // CertChecker knows how to check certificate signatures and revocation status. 113 // 114 // It is associated with single CA and assumes all certs needing a check are 115 // signed by that CA directly (i.e. there is no intermediate CAs). 116 // 117 // It caches CRL lists internally and must be treated as a heavy global object. 118 // Use GetCertChecker to grab a global instance of CertChecker for some CA. 119 // 120 // CertChecker is safe for concurrent use. 121 type CertChecker struct { 122 CN string // Common Name of the CA 123 CRL *certconfig.CRLChecker // knows how to query certificate revocation list 124 125 ca lazyslot.Slot // knows how to load CA cert and config 126 } 127 128 // CheckCertificate checks validity of a given certificate. 129 // 130 // It looks at the cert issuer, loads corresponding CertChecker and calls its 131 // CheckCertificate method. See CertChecker.CheckCertificate documentation for 132 // explanation of return values. 133 func CheckCertificate(c context.Context, cert *x509.Certificate) (*certconfig.CA, error) { 134 checker, err := GetCertChecker(c, cert.Issuer.CommonName) 135 if err != nil { 136 return nil, err 137 } 138 return checker.CheckCertificate(c, cert) 139 } 140 141 // GetCertChecker returns an instance of CertChecker for given CA. 142 // 143 // It caches CertChecker objects in local memory and reuses them between 144 // requests. 145 func GetCertChecker(c context.Context, cn string) (*CertChecker, error) { 146 return certCheckerCache.LRU(c).GetOrCreate(c, cn, func() (*CertChecker, time.Duration, error) { 147 // To avoid storing CertChecker for non-existent CAs in local memory forever, 148 // we do a datastore check when creating the checker. It happens once during 149 // the process lifetime. 150 switch exists, err := ds.Exists(c, ds.NewKey(c, "CA", cn, 0, nil)); { 151 case err != nil: 152 return nil, 0, transient.Tag.Apply(err) 153 case !exists.All(): 154 return nil, 0, Error{ 155 error: fmt.Errorf("no such CA %q", cn), 156 Reason: NoSuchCA, 157 } 158 } 159 return &CertChecker{ 160 CN: cn, 161 CRL: certconfig.NewCRLChecker(cn, certconfig.CRLShardCount, RefetchCRLPeriod), 162 }, 0, nil 163 }) 164 } 165 166 // GetCA returns CA entity with ParsedConfig and ParsedCert fields set. 167 func (ch *CertChecker) GetCA(c context.Context) (*certconfig.CA, error) { 168 value, err := ch.ca.Get(c, func(any) (ca any, exp time.Duration, err error) { 169 ca, err = ch.refetchCA(c) 170 if err == nil { 171 exp = RefetchCAPeriod 172 } 173 return 174 }) 175 if err != nil { 176 return nil, err 177 } 178 ca, _ := value.(*certconfig.CA) 179 // nil 'ca' means 'refetchCA' could not find it in the datastore. May happen 180 // if CA entity was deleted after GetCertChecker call. It could have been also 181 // "soft-deleted" by setting Removed == true. 182 if ca == nil || ca.Removed { 183 return nil, Error{ 184 error: fmt.Errorf("no such CA %q", ch.CN), 185 Reason: NoSuchCA, 186 } 187 } 188 return ca, nil 189 } 190 191 // CheckCertificate checks certificate's signature, validity period and 192 // revocation status. 193 // 194 // It returns nil error iff cert was directly signed by the CA, not expired yet, 195 // and its serial number is not in the CA's CRL (if CA's CRL is configured). 196 // 197 // On success also returns *certconfig.CA instance used to check the 198 // certificate, since 'GetCA' may return another instance (in case certconfig.CA 199 // cache happened to expire between the calls). 200 func (ch *CertChecker) CheckCertificate(c context.Context, cert *x509.Certificate) (*certconfig.CA, error) { 201 // Has the cert expired already? 202 now := clock.Now(c) 203 if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { 204 return nil, Error{ 205 error: fmt.Errorf("certificate has expired"), 206 Reason: CertificateExpired, 207 } 208 } 209 210 // Grab CA cert from the datastore. 211 ca, err := ch.GetCA(c) 212 if err != nil { 213 return nil, err 214 } 215 216 // Verify the signature. 217 if cert.Issuer.CommonName != ca.ParsedCert.Subject.CommonName { 218 return nil, Error{ 219 error: fmt.Errorf("can't check a signature made by %q", cert.Issuer.CommonName), 220 Reason: UnknownCA, 221 } 222 } 223 if err = cert.CheckSignatureFrom(ca.ParsedCert); err != nil { 224 return nil, Error{ 225 error: err, 226 Reason: SignatureCheckError, 227 } 228 } 229 230 // Check the revocation list if it is configured in the CA config. 231 if ca.ParsedConfig.CrlUrl != "" { 232 // Check we fetched the CRL already. 233 if !ca.Ready { 234 return nil, Error{ 235 error: fmt.Errorf("CRL of CA %q is not ready yet", ch.CN), 236 Reason: NotReadyCA, 237 } 238 } 239 // Check if the serial number is in the last fetched CRL. 240 switch revoked, err := ch.CRL.IsRevokedSN(c, cert.SerialNumber); { 241 case err != nil: 242 return nil, err 243 case revoked: 244 return nil, Error{ 245 error: fmt.Errorf("certificate with SN %s has been revoked", cert.SerialNumber), 246 Reason: CertificateRevoked, 247 } 248 } 249 } 250 251 return ca, nil 252 } 253 254 // refetchCA is called lazily whenever we need to fetch the CA entity. 255 // 256 // If CA entity has disappeared since CertChecker was created, it returns nil 257 // (that will be cached in ch.ca as usual). It acts as an indicator to GetCA to 258 // return NoSuchCA error, since returning a error here would just cause a retry 259 // of 'refetchCA' later. 260 func (ch *CertChecker) refetchCA(c context.Context) (*certconfig.CA, error) { 261 ca := &certconfig.CA{CN: ch.CN} 262 switch err := ds.Get(c, ca); { 263 case err == ds.ErrNoSuchEntity: 264 return nil, nil 265 case err != nil: 266 return nil, transient.Tag.Apply(err) 267 } 268 269 parsedConf, err := ca.ParseConfig() 270 if err != nil { 271 return nil, fmt.Errorf("can't parse stored config for %q - %s", ca.CN, err) 272 } 273 ca.ParsedConfig = parsedConf 274 275 parsedCert, err := x509.ParseCertificate(ca.Cert) 276 if err != nil { 277 return nil, fmt.Errorf("can't parse stored cert for %q - %s", ca.CN, err) 278 } 279 ca.ParsedCert = parsedCert 280 281 return ca, nil 282 }