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 }