watcher.go 8.2 KB

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