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 }