google.golang.org/grpc@v1.72.2/credentials/tls/certprovider/pemfile/watcher.go (about)

     1  /*
     2   *
     3   * Copyright 2020 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  // Package pemfile provides a file watching certificate provider plugin
    20  // implementation which works for files with PEM contents.
    21  //
    22  // # Experimental
    23  //
    24  // Notice: All APIs in this package are experimental and may be removed in a
    25  // later release.
    26  package pemfile
    27  
    28  import (
    29  	"bytes"
    30  	"context"
    31  	"crypto/tls"
    32  	"crypto/x509"
    33  	"errors"
    34  	"fmt"
    35  	"os"
    36  	"path/filepath"
    37  	"time"
    38  
    39  	"google.golang.org/grpc/credentials/tls/certprovider"
    40  	"google.golang.org/grpc/grpclog"
    41  	"google.golang.org/grpc/internal/credentials/spiffe"
    42  )
    43  
    44  const defaultCertRefreshDuration = 1 * time.Hour
    45  
    46  var (
    47  	// For overriding from unit tests.
    48  	newDistributor = func() distributor { return certprovider.NewDistributor() }
    49  
    50  	logger = grpclog.Component("pemfile")
    51  )
    52  
    53  // Options configures a certificate provider plugin that watches a specified set
    54  // of files that contain certificates and keys in PEM format.
    55  type Options struct {
    56  	// CertFile is the file that holds the identity certificate.
    57  	// Optional. If this is set, KeyFile must also be set.
    58  	CertFile string
    59  	// KeyFile is the file that holds identity private key.
    60  	// Optional. If this is set, CertFile must also be set.
    61  	KeyFile string
    62  	// RootFile is the file that holds trusted root certificate(s).
    63  	// Optional.
    64  	RootFile string
    65  	// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
    66  	// If a given provider configures both the RootFile and the
    67  	// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
    68  	// Optional.
    69  	SPIFFEBundleMapFile string
    70  	// RefreshDuration is the amount of time the plugin waits before checking
    71  	// for updates in the specified files.
    72  	// Optional. If not set, a default value (1 hour) will be used.
    73  	RefreshDuration time.Duration
    74  }
    75  
    76  func (o Options) canonical() []byte {
    77  	return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
    78  }
    79  
    80  func (o Options) validate() error {
    81  	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
    82  		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
    83  	}
    84  	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
    85  		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
    86  	}
    87  	// C-core has a limitation that they cannot verify that a certificate file
    88  	// matches a key file. So, the only way to get around this is to make sure
    89  	// that both files are in the same directory and that they do an atomic
    90  	// read. Even though Java/Go do not have this limitation, we want the
    91  	// overall plugin behavior to be consistent across languages.
    92  	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
    93  		return errors.New("pemfile: certificate and key file must be in the same directory")
    94  	}
    95  	return nil
    96  }
    97  
    98  // NewProvider returns a new certificate provider plugin that is configured to
    99  // watch the PEM files specified in the passed in options.
   100  func NewProvider(o Options) (certprovider.Provider, error) {
   101  	if err := o.validate(); err != nil {
   102  		return nil, err
   103  	}
   104  	return newProvider(o), nil
   105  }
   106  
   107  // newProvider is used to create a new certificate provider plugin after
   108  // validating the options, and hence does not return an error.
   109  func newProvider(o Options) certprovider.Provider {
   110  	if o.RefreshDuration == 0 {
   111  		o.RefreshDuration = defaultCertRefreshDuration
   112  	}
   113  
   114  	provider := &watcher{opts: o}
   115  	if o.CertFile != "" && o.KeyFile != "" {
   116  		provider.identityDistributor = newDistributor()
   117  	}
   118  	if o.RootFile != "" {
   119  		provider.rootDistributor = newDistributor()
   120  	}
   121  
   122  	ctx, cancel := context.WithCancel(context.Background())
   123  	provider.cancel = cancel
   124  	go provider.run(ctx)
   125  	return provider
   126  }
   127  
   128  // watcher is a certificate provider plugin that implements the
   129  // certprovider.Provider interface. It watches a set of certificate and key
   130  // files and provides the most up-to-date key material for consumption by
   131  // credentials implementation.
   132  type watcher struct {
   133  	identityDistributor         distributor
   134  	rootDistributor             distributor
   135  	opts                        Options
   136  	certFileContents            []byte
   137  	keyFileContents             []byte
   138  	rootFileContents            []byte
   139  	spiffeBundleMapFileContents []byte
   140  	cancel                      context.CancelFunc
   141  }
   142  
   143  // distributor wraps the methods on certprovider.Distributor which are used by
   144  // the plugin. This is very useful in tests which need to know exactly when the
   145  // plugin updates its key material.
   146  type distributor interface {
   147  	KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error)
   148  	Set(km *certprovider.KeyMaterial, err error)
   149  	Stop()
   150  }
   151  
   152  // updateIdentityDistributor checks if the cert/key files that the plugin is
   153  // watching have changed, and if so, reads the new contents and updates the
   154  // identityDistributor with the new key material.
   155  //
   156  // Skips updates when file reading or parsing fails.
   157  // TODO(easwars): Retry with limit (on the number of retries or the amount of
   158  // time) upon failures.
   159  func (w *watcher) updateIdentityDistributor() {
   160  	if w.identityDistributor == nil {
   161  		return
   162  	}
   163  
   164  	certFileContents, err := os.ReadFile(w.opts.CertFile)
   165  	if err != nil {
   166  		logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err)
   167  		return
   168  	}
   169  	keyFileContents, err := os.ReadFile(w.opts.KeyFile)
   170  	if err != nil {
   171  		logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err)
   172  		return
   173  	}
   174  	// If the file contents have not changed, skip updating the distributor.
   175  	if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) {
   176  		return
   177  	}
   178  
   179  	cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
   180  	if err != nil {
   181  		logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err)
   182  		return
   183  	}
   184  	w.certFileContents = certFileContents
   185  	w.keyFileContents = keyFileContents
   186  	w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil)
   187  }
   188  
   189  // updateRootDistributor checks if the root cert file that the plugin is
   190  // watching hs changed, and if so, updates the rootDistributor with the new key
   191  // material.
   192  //
   193  // Skips updates when root cert reading or parsing fails.
   194  // TODO(easwars): Retry with limit (on the number of retries or the amount of
   195  // time) upon failures.
   196  func (w *watcher) updateRootDistributor() {
   197  	if w.rootDistributor == nil {
   198  		return
   199  	}
   200  
   201  	// If SPIFFEBundleMap is set, use it and DON'T use the RootFile, even if it
   202  	// fails
   203  	if w.opts.SPIFFEBundleMapFile != "" {
   204  		w.maybeUpdateSPIFFEBundleMap()
   205  	} else {
   206  		w.maybeUpdateRootFile()
   207  	}
   208  }
   209  
   210  func (w *watcher) maybeUpdateSPIFFEBundleMap() {
   211  	spiffeBundleMapContents, err := os.ReadFile(w.opts.SPIFFEBundleMapFile)
   212  	if err != nil {
   213  		logger.Warningf("spiffeBundleMapFile (%s) read failed: %v", w.opts.SPIFFEBundleMapFile, err)
   214  		return
   215  	}
   216  	// If the file contents have not changed, skip updating the distributor.
   217  	if bytes.Equal(w.spiffeBundleMapFileContents, spiffeBundleMapContents) {
   218  		return
   219  	}
   220  	bundleMap, err := spiffe.BundleMapFromBytes(spiffeBundleMapContents)
   221  	if err != nil {
   222  		logger.Warning("Failed to parse spiffe bundle map")
   223  		return
   224  	}
   225  	w.spiffeBundleMapFileContents = spiffeBundleMapContents
   226  	w.rootDistributor.Set(&certprovider.KeyMaterial{SPIFFEBundleMap: bundleMap}, nil)
   227  }
   228  
   229  func (w *watcher) maybeUpdateRootFile() {
   230  	rootFileContents, err := os.ReadFile(w.opts.RootFile)
   231  	if err != nil {
   232  		logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err)
   233  		return
   234  	}
   235  	trustPool := x509.NewCertPool()
   236  	if !trustPool.AppendCertsFromPEM(rootFileContents) {
   237  		logger.Warning("Failed to parse root certificate")
   238  		return
   239  	}
   240  	// If the file contents have not changed, skip updating the distributor.
   241  	if bytes.Equal(w.rootFileContents, rootFileContents) {
   242  		return
   243  	}
   244  
   245  	w.rootFileContents = rootFileContents
   246  	w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil)
   247  }
   248  
   249  // run is a long running goroutine which watches the configured files for
   250  // changes, and pushes new key material into the appropriate distributors which
   251  // is returned from calls to KeyMaterial().
   252  func (w *watcher) run(ctx context.Context) {
   253  	ticker := time.NewTicker(w.opts.RefreshDuration)
   254  	for {
   255  		w.updateIdentityDistributor()
   256  		w.updateRootDistributor()
   257  		select {
   258  		case <-ctx.Done():
   259  			ticker.Stop()
   260  			if w.identityDistributor != nil {
   261  				w.identityDistributor.Stop()
   262  			}
   263  			if w.rootDistributor != nil {
   264  				w.rootDistributor.Stop()
   265  			}
   266  			return
   267  		case <-ticker.C:
   268  		}
   269  	}
   270  }
   271  
   272  // KeyMaterial returns the key material sourced by the watcher.
   273  // Callers are expected to use the returned value as read-only.
   274  func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) {
   275  	km := &certprovider.KeyMaterial{}
   276  	if w.identityDistributor != nil {
   277  		identityKM, err := w.identityDistributor.KeyMaterial(ctx)
   278  		if err != nil {
   279  			return nil, err
   280  		}
   281  		km.Certs = identityKM.Certs
   282  	}
   283  	if w.rootDistributor != nil {
   284  		rootKM, err := w.rootDistributor.KeyMaterial(ctx)
   285  		if err != nil {
   286  			return nil, err
   287  		}
   288  		km.SPIFFEBundleMap = rootKM.SPIFFEBundleMap
   289  		km.Roots = rootKM.Roots
   290  	}
   291  	return km, nil
   292  }
   293  
   294  // Close cleans up resources allocated by the watcher.
   295  func (w *watcher) Close() {
   296  	w.cancel()
   297  }