github.com/hxx258456/ccgo@v0.0.5-0.20230213014102-48b35f46f66f/grpc/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 "errors" 32 "fmt" 33 "io/ioutil" 34 "path/filepath" 35 "time" 36 37 "github.com/hxx258456/ccgo/x509" 38 39 tls "github.com/hxx258456/ccgo/gmtls" 40 41 "github.com/hxx258456/ccgo/grpc/credentials/tls/certprovider" 42 "github.com/hxx258456/ccgo/grpc/grpclog" 43 ) 44 45 const defaultCertRefreshDuration = 1 * time.Hour 46 47 var ( 48 // For overriding from unit tests. 49 newDistributor = func() distributor { return certprovider.NewDistributor() } 50 51 logger = grpclog.Component("pemfile") 52 ) 53 54 // Options configures a certificate provider plugin that watches a specified set 55 // of files that contain certificates and keys in PEM format. 56 type Options struct { 57 // CertFile is the file that holds the identity certificate. 58 // Optional. If this is set, KeyFile must also be set. 59 CertFile string 60 // KeyFile is the file that holds identity private key. 61 // Optional. If this is set, CertFile must also be set. 62 KeyFile string 63 // RootFile is the file that holds trusted root certificate(s). 64 // Optional. 65 RootFile string 66 // RefreshDuration is the amount of time the plugin waits before checking 67 // for updates in the specified files. 68 // Optional. If not set, a default value (1 hour) will be used. 69 RefreshDuration time.Duration 70 } 71 72 func (o Options) canonical() []byte { 73 return []byte(fmt.Sprintf("%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.RefreshDuration)) 74 } 75 76 func (o Options) validate() error { 77 if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" { 78 return fmt.Errorf("pemfile: at least one credential file needs to be specified") 79 } 80 if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified { 81 return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified") 82 } 83 // C-core has a limitation that they cannot verify that a certificate file 84 // matches a key file. So, the only way to get around this is to make sure 85 // that both files are in the same directory and that they do an atomic 86 // read. Even though Java/Go do not have this limitation, we want the 87 // overall plugin behavior to be consistent across languages. 88 if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir { 89 return errors.New("pemfile: certificate and key file must be in the same directory") 90 } 91 return nil 92 } 93 94 // NewProvider returns a new certificate provider plugin that is configured to 95 // watch the PEM files specified in the passed in options. 96 func NewProvider(o Options) (certprovider.Provider, error) { 97 if err := o.validate(); err != nil { 98 return nil, err 99 } 100 return newProvider(o), nil 101 } 102 103 // newProvider is used to create a new certificate provider plugin after 104 // validating the options, and hence does not return an error. 105 func newProvider(o Options) certprovider.Provider { 106 if o.RefreshDuration == 0 { 107 o.RefreshDuration = defaultCertRefreshDuration 108 } 109 110 provider := &watcher{opts: o} 111 if o.CertFile != "" && o.KeyFile != "" { 112 provider.identityDistributor = newDistributor() 113 } 114 if o.RootFile != "" { 115 provider.rootDistributor = newDistributor() 116 } 117 118 ctx, cancel := context.WithCancel(context.Background()) 119 provider.cancel = cancel 120 go provider.run(ctx) 121 return provider 122 } 123 124 // watcher is a certificate provider plugin that implements the 125 // certprovider.Provider interface. It watches a set of certificate and key 126 // files and provides the most up-to-date key material for consumption by 127 // credentials implementation. 128 type watcher struct { 129 identityDistributor distributor 130 rootDistributor distributor 131 opts Options 132 certFileContents []byte 133 keyFileContents []byte 134 rootFileContents []byte 135 cancel context.CancelFunc 136 } 137 138 // distributor wraps the methods on certprovider.Distributor which are used by 139 // the plugin. This is very useful in tests which need to know exactly when the 140 // plugin updates its key material. 141 type distributor interface { 142 KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) 143 Set(km *certprovider.KeyMaterial, err error) 144 Stop() 145 } 146 147 // updateIdentityDistributor checks if the cert/key files that the plugin is 148 // watching have changed, and if so, reads the new contents and updates the 149 // identityDistributor with the new key material. 150 // 151 // Skips updates when file reading or parsing fails. 152 // TODO(easwars): Retry with limit (on the number of retries or the amount of 153 // time) upon failures. 154 func (w *watcher) updateIdentityDistributor() { 155 if w.identityDistributor == nil { 156 return 157 } 158 159 certFileContents, err := ioutil.ReadFile(w.opts.CertFile) 160 if err != nil { 161 logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err) 162 return 163 } 164 keyFileContents, err := ioutil.ReadFile(w.opts.KeyFile) 165 if err != nil { 166 logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err) 167 return 168 } 169 // If the file contents have not changed, skip updating the distributor. 170 if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) { 171 return 172 } 173 174 cert, err := tls.X509KeyPair(certFileContents, keyFileContents) 175 if err != nil { 176 logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err) 177 return 178 } 179 w.certFileContents = certFileContents 180 w.keyFileContents = keyFileContents 181 w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil) 182 } 183 184 // updateRootDistributor checks if the root cert file that the plugin is 185 // watching hs changed, and if so, updates the rootDistributor with the new key 186 // material. 187 // 188 // Skips updates when root cert reading or parsing fails. 189 // TODO(easwars): Retry with limit (on the number of retries or the amount of 190 // time) upon failures. 191 func (w *watcher) updateRootDistributor() { 192 if w.rootDistributor == nil { 193 return 194 } 195 196 rootFileContents, err := ioutil.ReadFile(w.opts.RootFile) 197 if err != nil { 198 logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err) 199 return 200 } 201 trustPool := x509.NewCertPool() 202 if !trustPool.AppendCertsFromPEM(rootFileContents) { 203 logger.Warning("failed to parse root certificate") 204 return 205 } 206 // If the file contents have not changed, skip updating the distributor. 207 if bytes.Equal(w.rootFileContents, rootFileContents) { 208 return 209 } 210 211 w.rootFileContents = rootFileContents 212 w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil) 213 } 214 215 // run is a long running goroutine which watches the configured files for 216 // changes, and pushes new key material into the appropriate distributors which 217 // is returned from calls to KeyMaterial(). 218 func (w *watcher) run(ctx context.Context) { 219 ticker := time.NewTicker(w.opts.RefreshDuration) 220 for { 221 w.updateIdentityDistributor() 222 w.updateRootDistributor() 223 select { 224 case <-ctx.Done(): 225 ticker.Stop() 226 if w.identityDistributor != nil { 227 w.identityDistributor.Stop() 228 } 229 if w.rootDistributor != nil { 230 w.rootDistributor.Stop() 231 } 232 return 233 case <-ticker.C: 234 } 235 } 236 } 237 238 // KeyMaterial returns the key material sourced by the watcher. 239 // Callers are expected to use the returned value as read-only. 240 func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) { 241 km := &certprovider.KeyMaterial{} 242 if w.identityDistributor != nil { 243 identityKM, err := w.identityDistributor.KeyMaterial(ctx) 244 if err != nil { 245 return nil, err 246 } 247 km.Certs = identityKM.Certs 248 } 249 if w.rootDistributor != nil { 250 rootKM, err := w.rootDistributor.KeyMaterial(ctx) 251 if err != nil { 252 return nil, err 253 } 254 km.Roots = rootKM.Roots 255 } 256 return km, nil 257 } 258 259 // Close cleans up resources allocated by the watcher. 260 func (w *watcher) Close() { 261 w.cancel() 262 }