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  }