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.
26package pemfile
27
28import (
29	"bytes"
30	"context"
31	"crypto/tls"
32	"crypto/x509"
33	"errors"
34	"fmt"
35	"io/ioutil"
36	"path/filepath"
37	"time"
38
39	"google.golang.org/grpc/credentials/tls/certprovider"
40	"google.golang.org/grpc/grpclog"
41)
42
43const defaultCertRefreshDuration = 1 * time.Hour
44
45var (
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.
54type 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
70func (o Options) canonical() []byte {
71	return []byte(fmt.Sprintf("%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.RefreshDuration))
72}
73
74func (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.
94func 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.
103func 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.
126type 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.
139type 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.
152func (w *watcher) updateIdentityDistributor() {
153	if w.identityDistributor == nil {
154		return
155	}
156
157	certFileContents, err := ioutil.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 := ioutil.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.
189func (w *watcher) updateRootDistributor() {
190	if w.rootDistributor == nil {
191		return
192	}
193
194	rootFileContents, err := ioutil.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().
216func (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.
238func (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.
258func (w *watcher) Close() {
259	w.cancel()
260}
261