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