/* * * Copyright 2020 gRPC 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 pemfile provides a file watching certificate provider plugin // implementation which works for files with PEM contents. // // Experimental // // Notice: All APIs in this package are experimental and may be removed in a // later release. package pemfile import ( "bytes" "context" "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "time" "google.golang.org/grpc/credentials/tls/certprovider" "google.golang.org/grpc/grpclog" ) const ( defaultCertRefreshDuration = 1 * time.Hour defaultRootRefreshDuration = 2 * time.Hour ) var ( // For overriding from unit tests. newDistributor = func() distributor { return certprovider.NewDistributor() } logger = grpclog.Component("pemfile") ) // Options configures a certificate provider plugin that watches a specified set // of files that contain certificates and keys in PEM format. type Options struct { // CertFile is the file that holds the identity certificate. // Optional. If this is set, KeyFile must also be set. CertFile string // KeyFile is the file that holds identity private key. // Optional. If this is set, CertFile must also be set. KeyFile string // RootFile is the file that holds trusted root certificate(s). // Optional. RootFile string // CertRefreshDuration is the amount of time the plugin waits before // checking for updates in the specified identity certificate and key file. // Optional. If not set, a default value (1 hour) will be used. CertRefreshDuration time.Duration // RootRefreshDuration is the amount of time the plugin waits before // checking for updates in the specified root file. // Optional. If not set, a default value (2 hour) will be used. RootRefreshDuration time.Duration } // NewProvider returns a new certificate provider plugin that is configured to // watch the PEM files specified in the passed in options. func NewProvider(o Options) (certprovider.Provider, error) { if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" { return nil, fmt.Errorf("pemfile: at least one credential file needs to be specified") } if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified { return nil, fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified") } if o.CertRefreshDuration == 0 { o.CertRefreshDuration = defaultCertRefreshDuration } if o.RootRefreshDuration == 0 { o.RootRefreshDuration = defaultRootRefreshDuration } provider := &watcher{opts: o} if o.CertFile != "" && o.KeyFile != "" { provider.identityDistributor = newDistributor() } if o.RootFile != "" { provider.rootDistributor = newDistributor() } ctx, cancel := context.WithCancel(context.Background()) provider.cancel = cancel go provider.run(ctx) return provider, nil } // watcher is a certificate provider plugin that implements the // certprovider.Provider interface. It watches a set of certificate and key // files and provides the most up-to-date key material for consumption by // credentials implementation. type watcher struct { identityDistributor distributor rootDistributor distributor opts Options certFileContents []byte keyFileContents []byte rootFileContents []byte cancel context.CancelFunc } // distributor wraps the methods on certprovider.Distributor which are used by // the plugin. This is very useful in tests which need to know exactly when the // plugin updates its key material. type distributor interface { KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) Set(km *certprovider.KeyMaterial, err error) Stop() } // updateIdentityDistributor checks if the cert/key files that the plugin is // watching have changed, and if so, reads the new contents and updates the // identityDistributor with the new key material. // // Skips updates when file reading or parsing fails. // TODO(easwars): Retry with limit (on the number of retries or the amount of // time) upon failures. func (w *watcher) updateIdentityDistributor() { if w.identityDistributor == nil { return } certFileContents, err := ioutil.ReadFile(w.opts.CertFile) if err != nil { logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err) return } keyFileContents, err := ioutil.ReadFile(w.opts.KeyFile) if err != nil { logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err) return } // If the file contents have not changed, skip updating the distributor. if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) { return } cert, err := tls.X509KeyPair(certFileContents, keyFileContents) if err != nil { logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err) return } w.certFileContents = certFileContents w.keyFileContents = keyFileContents w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil) } // updateRootDistributor checks if the root cert file that the plugin is // watching hs changed, and if so, updates the rootDistributor with the new key // material. // // Skips updates when root cert reading or parsing fails. // TODO(easwars): Retry with limit (on the number of retries or the amount of // time) upon failures. func (w *watcher) updateRootDistributor() { if w.rootDistributor == nil { return } rootFileContents, err := ioutil.ReadFile(w.opts.RootFile) if err != nil { logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err) return } trustPool := x509.NewCertPool() if !trustPool.AppendCertsFromPEM(rootFileContents) { logger.Warning("failed to parse root certificate") return } // If the file contents have not changed, skip updating the distributor. if bytes.Equal(w.rootFileContents, rootFileContents) { return } w.rootFileContents = rootFileContents w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil) } // run is a long running goroutine which watches the configured files for // changes, and pushes new key material into the appropriate distributors which // is returned from calls to KeyMaterial(). func (w *watcher) run(ctx context.Context) { // Update both root and identity certs at the beginning. Subsequently, // update only the appropriate file whose ticker has fired. w.updateIdentityDistributor() w.updateRootDistributor() identityTicker := time.NewTicker(w.opts.CertRefreshDuration) rootTicker := time.NewTicker(w.opts.RootRefreshDuration) for { select { case <-ctx.Done(): identityTicker.Stop() rootTicker.Stop() if w.identityDistributor != nil { w.identityDistributor.Stop() } if w.rootDistributor != nil { w.rootDistributor.Stop() } return case <-identityTicker.C: w.updateIdentityDistributor() case <-rootTicker.C: w.updateRootDistributor() } } } // KeyMaterial returns the key material sourced by the watcher. // Callers are expected to use the returned value as read-only. func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) { km := &certprovider.KeyMaterial{} if w.identityDistributor != nil { identityKM, err := w.identityDistributor.KeyMaterial(ctx) if err != nil { return nil, err } km.Certs = identityKM.Certs } if w.rootDistributor != nil { rootKM, err := w.rootDistributor.KeyMaterial(ctx) if err != nil { return nil, err } km.Roots = rootKM.Roots } return km, nil } // Close cleans up resources allocated by the watcher. func (w *watcher) Close() { w.cancel() }