dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/credentials/certprovider/pemfile/watcher.go (about)

     1  /*
     2   * Licensed to the Apache Software Foundation (ASF) under one or more
     3   * contributor license agreements.  See the NOTICE file distributed with
     4   * this work for additional information regarding copyright ownership.
     5   * The ASF licenses this file to You under the Apache License, Version 2.0
     6   * (the "License"); you may not use this file except in compliance with
     7   * the License.  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   *
    20   * Copyright 2020 gRPC authors.
    21   *
    22   */
    23  
    24  // Package pemfile provides a file watching certificate provider plugin
    25  // implementation which works for files with PEM contents.
    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  	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
    40  	"github.com/dubbogo/grpc-go/grpclog"
    41  )
    42  
    43  const defaultCertRefreshDuration = 1 * time.Hour
    44  
    45  var (
    46  	// For overriding from unit tests.
    47  	newDistributor = func() distributor { return certprovider.NewDistributor() }
    48  
    49  	logger = grpclog.Component("pemfile")
    50  )
    51  
    52  // Options configures a certificate provider plugin that watches a specified set
    53  // of files that contain certificates and keys in PEM format.
    54  type Options struct {
    55  	// CertFile is the file that holds the identity certificate.
    56  	// Optional. If this is set, KeyFile must also be set.
    57  	CertFile string
    58  	// KeyFile is the file that holds identity private key.
    59  	// Optional. If this is set, CertFile must also be set.
    60  	KeyFile string
    61  	// RootFile is the file that holds trusted root certificate(s).
    62  	// Optional.
    63  	RootFile string
    64  	// RefreshDuration is the amount of time the plugin waits before checking
    65  	// for updates in the specified files.
    66  	// Optional. If not set, a default value (1 hour) will be used.
    67  	RefreshDuration time.Duration
    68  }
    69  
    70  func (o Options) canonical() []byte {
    71  	return []byte(fmt.Sprintf("%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.RefreshDuration))
    72  }
    73  
    74  func (o Options) validate() error {
    75  	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" {
    76  		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
    77  	}
    78  	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
    79  		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
    80  	}
    81  	// C-core has a limitation that they cannot verify that a certificate file
    82  	// matches a key file. So, the only way to get around this is to make sure
    83  	// that both files are in the same directory and that they do an atomic
    84  	// read. Even though Java/Go do not have this limitation, we want the
    85  	// overall plugin behavior to be consistent across languages.
    86  	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
    87  		return errors.New("pemfile: certificate and key file must be in the same directory")
    88  	}
    89  	return nil
    90  }
    91  
    92  // NewProvider returns a new certificate provider plugin that is configured to
    93  // watch the PEM files specified in the passed in options.
    94  func NewProvider(o Options) (certprovider.Provider, error) {
    95  	if err := o.validate(); err != nil {
    96  		return nil, err
    97  	}
    98  	return newProvider(o), nil
    99  }
   100  
   101  // newProvider is used to create a new certificate provider plugin after
   102  // validating the options, and hence does not return an error.
   103  func newProvider(o Options) certprovider.Provider {
   104  	if o.RefreshDuration == 0 {
   105  		o.RefreshDuration = defaultCertRefreshDuration
   106  	}
   107  
   108  	provider := &watcher{opts: o}
   109  	if o.CertFile != "" && o.KeyFile != "" {
   110  		provider.identityDistributor = newDistributor()
   111  	}
   112  	if o.RootFile != "" {
   113  		provider.rootDistributor = newDistributor()
   114  	}
   115  
   116  	ctx, cancel := context.WithCancel(context.Background())
   117  	provider.cancel = cancel
   118  	go provider.run(ctx)
   119  	return provider
   120  }
   121  
   122  // watcher is a certificate provider plugin that implements the
   123  // certprovider.Provider interface. It watches a set of certificate and key
   124  // files and provides the most up-to-date key material for consumption by
   125  // credentials implementation.
   126  type watcher struct {
   127  	identityDistributor distributor
   128  	rootDistributor     distributor
   129  	opts                Options
   130  	certFileContents    []byte
   131  	keyFileContents     []byte
   132  	rootFileContents    []byte
   133  	cancel              context.CancelFunc
   134  }
   135  
   136  // distributor wraps the methods on certprovider.Distributor which are used by
   137  // the plugin. This is very useful in tests which need to know exactly when the
   138  // plugin updates its key material.
   139  type distributor interface {
   140  	KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error)
   141  	Set(km *certprovider.KeyMaterial, err error)
   142  	Stop()
   143  }
   144  
   145  // updateIdentityDistributor checks if the cert/key files that the plugin is
   146  // watching have changed, and if so, reads the new contents and updates the
   147  // identityDistributor with the new key material.
   148  //
   149  // Skips updates when file reading or parsing fails.
   150  // TODO(easwars): Retry with limit (on the number of retries or the amount of
   151  // time) upon failures.
   152  func (w *watcher) updateIdentityDistributor() {
   153  	if w.identityDistributor == nil {
   154  		return
   155  	}
   156  
   157  	certFileContents, err := os.ReadFile(w.opts.CertFile)
   158  	if err != nil {
   159  		logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err)
   160  		return
   161  	}
   162  	keyFileContents, err := os.ReadFile(w.opts.KeyFile)
   163  	if err != nil {
   164  		logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err)
   165  		return
   166  	}
   167  	// If the file contents have not changed, skip updating the distributor.
   168  	if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) {
   169  		return
   170  	}
   171  
   172  	cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
   173  	if err != nil {
   174  		logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err)
   175  		return
   176  	}
   177  	w.certFileContents = certFileContents
   178  	w.keyFileContents = keyFileContents
   179  	w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil)
   180  }
   181  
   182  // updateRootDistributor checks if the root cert file that the plugin is
   183  // watching hs changed, and if so, updates the rootDistributor with the new key
   184  // material.
   185  //
   186  // Skips updates when root cert reading or parsing fails.
   187  // TODO(easwars): Retry with limit (on the number of retries or the amount of
   188  // time) upon failures.
   189  func (w *watcher) updateRootDistributor() {
   190  	if w.rootDistributor == nil {
   191  		return
   192  	}
   193  
   194  	rootFileContents, err := os.ReadFile(w.opts.RootFile)
   195  	if err != nil {
   196  		logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err)
   197  		return
   198  	}
   199  	trustPool := x509.NewCertPool()
   200  	if !trustPool.AppendCertsFromPEM(rootFileContents) {
   201  		logger.Warning("failed to parse root certificate")
   202  		return
   203  	}
   204  	// If the file contents have not changed, skip updating the distributor.
   205  	if bytes.Equal(w.rootFileContents, rootFileContents) {
   206  		return
   207  	}
   208  
   209  	w.rootFileContents = rootFileContents
   210  	w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil)
   211  }
   212  
   213  // run is a long running goroutine which watches the configured files for
   214  // changes, and pushes new key material into the appropriate distributors which
   215  // is returned from calls to KeyMaterial().
   216  func (w *watcher) run(ctx context.Context) {
   217  	ticker := time.NewTicker(w.opts.RefreshDuration)
   218  	for {
   219  		w.updateIdentityDistributor()
   220  		w.updateRootDistributor()
   221  		select {
   222  		case <-ctx.Done():
   223  			ticker.Stop()
   224  			if w.identityDistributor != nil {
   225  				w.identityDistributor.Stop()
   226  			}
   227  			if w.rootDistributor != nil {
   228  				w.rootDistributor.Stop()
   229  			}
   230  			return
   231  		case <-ticker.C:
   232  		}
   233  	}
   234  }
   235  
   236  // KeyMaterial returns the key material sourced by the watcher.
   237  // Callers are expected to use the returned value as read-only.
   238  func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) {
   239  	km := &certprovider.KeyMaterial{}
   240  	if w.identityDistributor != nil {
   241  		identityKM, err := w.identityDistributor.KeyMaterial(ctx)
   242  		if err != nil {
   243  			return nil, err
   244  		}
   245  		km.Certs = identityKM.Certs
   246  	}
   247  	if w.rootDistributor != nil {
   248  		rootKM, err := w.rootDistributor.KeyMaterial(ctx)
   249  		if err != nil {
   250  			return nil, err
   251  		}
   252  		km.Roots = rootKM.Roots
   253  	}
   254  	return km, nil
   255  }
   256  
   257  // Close cleans up resources allocated by the watcher.
   258  func (w *watcher) Close() {
   259  	w.cancel()
   260  }