diff --git a/cmd/kafka-proxy/server.go b/cmd/kafka-proxy/server.go index 13f29d35..73fde814 100644 --- a/cmd/kafka-proxy/server.go +++ b/cmd/kafka-proxy/server.go @@ -103,10 +103,12 @@ func initFlags() { Server.Flags().DurationVar(&c.Proxy.ListenerKeepAlive, "proxy-listener-keep-alive", 60*time.Second, "Keep alive period for an active network connection. If zero, keep-alives are disabled") Server.Flags().BoolVar(&c.Proxy.TLS.Enable, "proxy-listener-tls-enable", false, "Whether or not to use TLS listener") + Server.Flags().DurationVar(&c.Proxy.TLS.Refresh, "proxy-listener-tls-refresh", 0*time.Second, "Interval for refreshing server TLS certificates. If set to zero, the refresh watch is disabled") Server.Flags().StringVar(&c.Proxy.TLS.ListenerCertFile, "proxy-listener-cert-file", "", "PEM encoded file with server certificate") Server.Flags().StringVar(&c.Proxy.TLS.ListenerKeyFile, "proxy-listener-key-file", "", "PEM encoded file with private key for the server certificate") Server.Flags().StringVar(&c.Proxy.TLS.ListenerKeyPassword, "proxy-listener-key-password", os.Getenv("PROXY_LISTENER_KEY_PASSWORD"), "Password to decrypt rsa private key") - Server.Flags().StringVar(&c.Proxy.TLS.CAChainCertFile, "proxy-listener-ca-chain-cert-file", "", "PEM encoded CA's certificate file. If provided, client certificate is required and verified") + Server.Flags().StringVar(&c.Proxy.TLS.ListenerCAChainCertFile, "proxy-listener-ca-chain-cert-file", "", "PEM encoded CA's certificate file. If provided, client certificate is required and verified") + Server.Flags().StringVar(&c.Proxy.TLS.ListenerCRLFile, "proxy-listener-crl-file", "", "PEM encoded X509 CRLs file") Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCipherSuites, "proxy-listener-cipher-suites", []string{}, "List of supported cipher suites") Server.Flags().StringSliceVar(&c.Proxy.TLS.ListenerCurvePreferences, "proxy-listener-curve-preferences", []string{}, "List of curve preferences") diff --git a/config/config.go b/config/config.go index 7ce09ba1..dcc7c607 100644 --- a/config/config.go +++ b/config/config.go @@ -88,10 +88,12 @@ type Config struct { TLS struct { Enable bool + Refresh time.Duration ListenerCertFile string ListenerKeyFile string ListenerKeyPassword string - CAChainCertFile string + ListenerCAChainCertFile string + ListenerCRLFile string ListenerCipherSuites []string ListenerCurvePreferences []string ClientCert struct { diff --git a/go.mod b/go.mod index 3f568da1..38363334 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fsnotify/fsnotify v1.4.9 github.com/go-ldap/ldap/v3 v3.2.3 github.com/google/uuid v1.6.0 + github.com/grepplabs/cert-source v0.0.6 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874 github.com/hashicorp/go-plugin v1.6.3 @@ -26,7 +27,7 @@ require ( github.com/spf13/viper v1.0.2 github.com/stretchr/testify v1.10.0 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c - github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 golang.org/x/net v0.34.0 golang.org/x/oauth2 v0.24.0 google.golang.org/api v0.126.0 diff --git a/go.sum b/go.sum index f8da858b..6cdc1179 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cU github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grepplabs/cert-source v0.0.6 h1:FjrFco5wQrMqGI4wzCvkIksK0xOoyIC6FV2Cg53thHg= +github.com/grepplabs/cert-source v0.0.6/go.mod h1:gs3IoykME1cFfZ6/h6hch8yg8ktUInsR9OY2xSHA2r4= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -281,8 +283,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 h1:tBiBTKHnIjovYoLX/TPkcf+OjqqKGQrPtGT3Foz+Pgo= -github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76/go.mod h1:SQliXeA7Dhkt//vS29v3zpbEwoa+zb2Cn5xj5uO4K5U= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= diff --git a/proxy/tls.go b/proxy/tls.go index 0e8884b9..502e3745 100644 --- a/proxy/tls.go +++ b/proxy/tls.go @@ -8,12 +8,16 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "log/slog" "net" "os" "reflect" "strings" "time" + tlsconfig "github.com/grepplabs/cert-source/config" + tlsserver "github.com/grepplabs/cert-source/tls/server" + tlsserverconfig "github.com/grepplabs/cert-source/tls/server/config" "github.com/grepplabs/kafka-proxy/config" "github.com/pkg/errors" "github.com/youmark/pkcs8" @@ -59,25 +63,6 @@ var ( func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) { opts := conf.Proxy.TLS - if opts.ListenerKeyFile == "" || opts.ListenerCertFile == "" { - return nil, errors.New("Listener key and cert files must not be empty") - } - certPEMBlock, err := os.ReadFile(opts.ListenerCertFile) - if err != nil { - return nil, err - } - keyPEMBlock, err := os.ReadFile(opts.ListenerKeyFile) - if err != nil { - return nil, err - } - keyPEMBlock, err = decryptPEM(keyPEMBlock, opts.ListenerKeyPassword) - if err != nil { - return nil, err - } - cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock) - if err != nil { - return nil, err - } cipherSuites, err := getCipherSuites(opts.ListenerCipherSuites) if err != nil { return nil, err @@ -86,35 +71,27 @@ func newTLSListenerConfig(conf *config.Config) (*tls.Config, error) { if err != nil { return nil, err } - - cfg := &tls.Config{ - Certificates: []tls.Certificate{cert}, - ClientAuth: tls.NoClientCert, - PreferServerCipherSuites: true, - MinVersion: tls.VersionTLS12, - CurvePreferences: curvePreferences, - CipherSuites: cipherSuites, - } - if opts.CAChainCertFile != "" { - caCertPEMBlock, err := os.ReadFile(opts.CAChainCertFile) - if err != nil { - return nil, err - } - clientCAs := x509.NewCertPool() - if ok := clientCAs.AppendCertsFromPEM(caCertPEMBlock); !ok { - return nil, errors.New("Failed to parse listener root certificate") - } - cfg.ClientCAs = clientCAs - cfg.ClientAuth = tls.RequireAndVerifyClientCert - } - tlsValidateFunc, err := tlsClientCertVerificationFunc(conf) if err != nil { return nil, err } - cfg.VerifyPeerCertificate = tlsValidateFunc - - return cfg, nil + tlsConfig, err := tlsserverconfig.GetServerTLSConfig(slog.Default(), + &tlsconfig.TLSServerConfig{ + Enable: true, + Refresh: opts.Refresh, + KeyPassword: opts.ListenerKeyPassword, + File: tlsconfig.TLSServerFiles{ + Key: opts.ListenerKeyFile, + Cert: opts.ListenerCertFile, + ClientCAs: opts.ListenerCAChainCertFile, + ClientCRL: opts.ListenerCRLFile, + }, + }, + tlsserver.WithTLSServerVerifyPeerCertificate(tlsValidateFunc), + tlsserver.WithTLSServerCipherSuites(cipherSuites), + tlsserver.WithTLSServerCurvePreferences(curvePreferences), + ) + return tlsConfig, nil } func getCipherSuites(enabledCipherSuites []string) ([]uint16, error) { diff --git a/proxy/tls_client_cert_validate_test.go b/proxy/tls_client_cert_validate_test.go index d095d11a..1128db65 100644 --- a/proxy/tls_client_cert_validate_test.go +++ b/proxy/tls_client_cert_validate_test.go @@ -36,7 +36,7 @@ func TestValidEnabledClientCertSubjectValidate(t *testing.T) { c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -71,7 +71,7 @@ func TestInvalidEnabledClientCertSubjectValidate(t *testing.T) { } c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -106,7 +106,7 @@ func TestValidEnabledClientCertSubjectMayContainNotRequiredValues(t *testing.T) } c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -135,7 +135,7 @@ func TestValidEnabledClientCertSubjectMayContainValuesInDifferentOrder(t *testin } c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -179,7 +179,7 @@ func TestClientCertMultipleSubjects(t *testing.T) { c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -223,7 +223,7 @@ func TestClientCertMultipleSubjectsPatterns(t *testing.T) { c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -260,7 +260,7 @@ func TestClientCertMultiplePatternMatchingFields(t *testing.T) { c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -297,7 +297,7 @@ func TestClientCertMultiplePatternNotMatchingFields(t *testing.T) { c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() @@ -334,7 +334,7 @@ func TestClientCertMultiplePatternMatchingFieldsOrderDoesNotMatter(t *testing.T) c.Proxy.TLS.ListenerCertFile = bundle.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle.CACert.Name() + c.Proxy.TLS.ListenerCAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.CAChainCertFile = bundle.CACert.Name() c.Kafka.TLS.ClientCertFile = bundle.ClientCert.Name() diff --git a/proxy/tls_test.go b/proxy/tls_test.go index 82a1f9b2..4a22148e 100644 --- a/proxy/tls_test.go +++ b/proxy/tls_test.go @@ -312,7 +312,7 @@ func TestTLSVerifyClientCertDifferentCAs(t *testing.T) { c := new(config.Config) c.Proxy.TLS.ListenerCertFile = bundle1.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle1.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle2.CACert.Name() // client CA + c.Proxy.TLS.ListenerCAChainCertFile = bundle2.CACert.Name() // client CA c.Kafka.TLS.CAChainCertFile = bundle1.ServerCert.Name() c.Kafka.TLS.ClientCertFile = bundle2.ClientCert.Name() @@ -335,7 +335,7 @@ func TestTLSVerifyClientCertSameCAs(t *testing.T) { c := new(config.Config) c.Proxy.TLS.ListenerCertFile = bundle1.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle1.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle1.CACert.Name() // client CA + c.Proxy.TLS.ListenerCAChainCertFile = bundle1.CACert.Name() // client CA c.Kafka.TLS.CAChainCertFile = bundle1.ServerCert.Name() c.Kafka.TLS.ClientCertFile = bundle1.ClientCert.Name() @@ -358,7 +358,7 @@ func TestTLSMissingClientCert(t *testing.T) { c := new(config.Config) c.Proxy.TLS.ListenerCertFile = bundle1.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle1.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle1.CACert.Name() // client CA + c.Proxy.TLS.ListenerCAChainCertFile = bundle1.CACert.Name() // client CA c.Kafka.TLS.CAChainCertFile = bundle1.ServerCert.Name() @@ -379,7 +379,7 @@ func TestTLSBadClientCert(t *testing.T) { c := new(config.Config) c.Proxy.TLS.ListenerCertFile = bundle1.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle1.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle1.CACert.Name() // client CA + c.Proxy.TLS.ListenerCAChainCertFile = bundle1.CACert.Name() // client CA c.Kafka.TLS.CAChainCertFile = bundle1.ServerCert.Name() c.Kafka.TLS.ClientCertFile = bundle2.ClientCert.Name() @@ -431,7 +431,7 @@ func configWithCertToCompare(bundle1 *CertsBundle, bundle2 *CertsBundle, sameCer c.Proxy.TLS.ListenerCertFile = bundle1.ServerCert.Name() c.Proxy.TLS.ListenerKeyFile = bundle1.ServerKey.Name() - c.Proxy.TLS.CAChainCertFile = bundle2.CACert.Name() // client CA + c.Proxy.TLS.ListenerCAChainCertFile = bundle2.CACert.Name() // client CA c.Kafka.TLS.CAChainCertFile = bundle1.ServerCert.Name() c.Kafka.TLS.ClientCertFile = bundle2.ClientCert.Name() diff --git a/vendor/github.com/grepplabs/cert-source/LICENSE b/vendor/github.com/grepplabs/cert-source/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/grepplabs/cert-source/config/config.go b/vendor/github.com/grepplabs/cert-source/config/config.go new file mode 100644 index 00000000..6bf7149a --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/config/config.go @@ -0,0 +1,33 @@ +package config + +import ( + "time" +) + +type TLSServerConfig struct { + Enable bool `help:"Enable server-side TLS."` + Refresh time.Duration `default:"0s" help:"Interval for refreshing server TLS certificates."` + File TLSServerFiles `embed:"" prefix:"file."` + KeyPassword string `help:"Optional password to decrypt RSA private key."` +} + +type TLSServerFiles struct { + Key string `placeholder:"FILE" help:"Path to the server TLS key file."` + Cert string `placeholder:"FILE" help:"Path to the server TLS certificate file."` + ClientCAs string `placeholder:"FILE" name:"client-ca" help:"Optional path to server client CA file for client verification."` + ClientCRL string `placeholder:"FILE" name:"client-crl" help:"TLS X509 CRL signed be the client CA. If no revocation list is specified, only client CA is verified."` +} + +type TLSClientConfig struct { + Enable bool `help:"Enable client-side TLS."` + Refresh time.Duration `default:"0s" help:"Interval for refreshing client TLS certificates."` + InsecureSkipVerify bool `help:"Skip TLS verification on client side."` + File TLSClientFiles `embed:"" prefix:"file."` + KeyPassword string `help:"Optional password to decrypt RSA private key."` +} + +type TLSClientFiles struct { + Key string `placeholder:"FILE" help:"Optional path to client TLS key file."` + Cert string `placeholder:"FILE" help:"Optional path to client TLS certificate file."` + RootCAs string `placeholder:"FILE" name:"root-ca" help:"Optional path to client root CAs for server verification."` +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/keyutil/crypto.go b/vendor/github.com/grepplabs/cert-source/tls/keyutil/crypto.go new file mode 100644 index 00000000..7adb4ca8 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/keyutil/crypto.go @@ -0,0 +1,91 @@ +package keyutil + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "strings" + + "github.com/youmark/pkcs8" +) + +func DecryptPrivateKeyPEM(pemData []byte, password string) ([]byte, error) { + keyBlock, _ := pem.Decode(pemData) + if keyBlock == nil { + return nil, errors.New("failed to parse PEM") + } + //nolint:staticcheck // Ignore SA1019: Support legacy encrypted PEM blocks + if x509.IsEncryptedPEMBlock(keyBlock) { + if password == "" { + return nil, errors.New("PEM is encrypted, but password is empty") + } + key, err := x509.DecryptPEMBlock(keyBlock, []byte(password)) + if err != nil { + return nil, err + } + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: key, + } + return pem.EncodeToMemory(block), nil + } else if strings.Contains(string(pemData), "ENCRYPTED PRIVATE KEY") { + if password == "" { + return nil, errors.New("PEM is encrypted, but password is empty") + } + key, err := pkcs8.ParsePKCS8PrivateKey(keyBlock.Bytes, []byte(password)) + if err != nil { + return nil, err + } + return MarshalPrivateKeyToPEM(key) + } + return pemData, nil +} + +func EncryptPKCS8PrivateKeyPEM(pemData []byte, password string) ([]byte, error) { + if password == "" { + return nil, errors.New("password cannot be empty") + } + keyBlock, _ := pem.Decode(pemData) + if keyBlock == nil { + return nil, errors.New("failed to parse PEM") + } + + var ( + key any + err error + ) + switch keyBlock.Type { + case "PRIVATE KEY": + key, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, err + } + case "RSA PRIVATE KEY": + rsaKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, err + } + key, err = x509.MarshalPKCS8PrivateKey(rsaKey) + if err != nil { + return nil, err + } + // Parse back to interface{} to match the signature for pkcs8.MarshalPrivateKey + key, err = x509.ParsePKCS8PrivateKey(key.([]byte)) + if err != nil { + return nil, err + } + default: + return nil, errors.New("unsupported key type: " + keyBlock.Type) + } + + encryptedBytes, err := pkcs8.MarshalPrivateKey(key, []byte(password), pkcs8.DefaultOpts) + if err != nil { + return nil, err + } + encryptedBlock := &pem.Block{ + Type: "ENCRYPTED PRIVATE KEY", + Bytes: encryptedBytes, + } + + return pem.EncodeToMemory(encryptedBlock), nil +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/keyutil/helper.go b/vendor/github.com/grepplabs/cert-source/tls/keyutil/helper.go new file mode 100644 index 00000000..4a459ab3 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/keyutil/helper.go @@ -0,0 +1,365 @@ +package keyutil + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + cryptorand "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io" + "os" +) + +const ( + X509CRLBlockType = "X509 CRL" + CertificateBlockType = "CERTIFICATE" + ECPrivateKeyBlockType = "EC PRIVATE KEY" + RSAPrivateKeyBlockType = "RSA PRIVATE KEY" + PrivateKeyBlockType = "PRIVATE KEY" + PublicKeyBlockType = "PUBLIC KEY" + ECPublicKeyType = "EC PUBLIC KEY" +) + +func ParseCRLsPEM(pemCrls []byte) ([]*x509.RevocationList, error) { + ok := false + var lists []*x509.RevocationList + for len(pemCrls) > 0 { + var block *pem.Block + block, pemCrls = pem.Decode(pemCrls) + if block == nil { + break + } + if block.Type != X509CRLBlockType { + continue + } + list, err := x509.ParseRevocationList(block.Bytes) + if err != nil { + return lists, err + } + lists = append(lists, list) + ok = true + } + if !ok { + return lists, errors.New("data does not contain any valid CRL") + } + return lists, nil +} + +func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { + ok := false + var certs []*x509.Certificate + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + if block.Type != CertificateBlockType || len(block.Headers) != 0 { + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, err + } + + certs = append(certs, cert) + ok = true + } + + if !ok { + return certs, errors.New("data does not contain any valid RSA or ECDSA certificates") + } + return certs, nil +} + +func GetHexFormatted(buf []byte, sep string) string { + var ret bytes.Buffer + for _, cur := range buf { + if ret.Len() > 0 { + _, _ = ret.WriteString(sep) + } + _, _ = fmt.Fprintf(&ret, "%02x", cur) + } + return ret.String() +} + +func GenerateECKeys() (crypto.PrivateKey, []byte, crypto.PublicKey, []byte, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) + if err != nil { + return nil, nil, nil, nil, err + } + return marshalKeysToPEM(privateKey, privateKey.Public()) +} + +func GenerateRSAKeys() (crypto.PrivateKey, []byte, crypto.PublicKey, []byte, error) { + privateKey, err := rsa.GenerateKey(cryptorand.Reader, 4096) + if err != nil { + return nil, nil, nil, nil, err + } + return marshalKeysToPEM(privateKey, privateKey.Public()) +} + +func KeysMatch(priv crypto.PrivateKey, pub crypto.PublicKey) bool { + privKey, ok := priv.(interface { + Public() crypto.PublicKey + }) + if !ok { + return false + } + pubKey, ok := privKey.Public().(interface { + Equal(crypto.PublicKey) bool + }) + if !ok { + return false + } + return pubKey.Equal(pub) +} + +func marshalKeysToPEM(privateKey crypto.PrivateKey, publicKey crypto.PublicKey) (crypto.PrivateKey, []byte, crypto.PublicKey, []byte, error) { + privatePem, err := MarshalPrivateKeyToPEM(privateKey) + if err != nil { + return nil, nil, nil, nil, err + } + publicPem, err := MarshalPublicKeyToPEM(publicKey) + if err != nil { + return nil, nil, nil, nil, err + } + return privateKey, privatePem, publicKey, publicPem, nil +} + +func ParsePrivateKeyPEM(keyData []byte) (crypto.PrivateKey, error) { + var privateKeyPemBlock *pem.Block + for { + privateKeyPemBlock, keyData = pem.Decode(keyData) + if privateKeyPemBlock == nil { + break + } + + switch privateKeyPemBlock.Type { + case ECPrivateKeyBlockType: + if key, err := x509.ParseECPrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + case RSAPrivateKeyBlockType: + if key, err := x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + case PrivateKeyBlockType: + if key, err := x509.ParsePKCS8PrivateKey(privateKeyPemBlock.Bytes); err == nil { + return key, nil + } + } + } + return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA private key") +} + +func ParsePublicKeysPEM(keyData []byte) ([]crypto.PublicKey, error) { + var block *pem.Block + var keys []crypto.PublicKey + for { + block, keyData = pem.Decode(keyData) + if block == nil { + break + } + if privateKey, err := parseRSAPrivateKey(block.Bytes); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := parseRSAPublicKey(block.Bytes); err == nil { + keys = append(keys, publicKey) + continue + } + if privateKey, err := parseECPrivateKey(block.Bytes); err == nil { + keys = append(keys, &privateKey.PublicKey) + continue + } + if publicKey, err := parseECPublicKey(block.Bytes); err == nil { + keys = append(keys, publicKey) + continue + } + } + + if len(keys) == 0 { + return nil, fmt.Errorf("data does not contain any valid RSA or ECDSA public keys") + } + return keys, nil +} + +func MarshalPrivateKeyToPEM(privateKey crypto.PrivateKey) ([]byte, error) { + switch t := privateKey.(type) { + case *ecdsa.PrivateKey: + derBytes, err := x509.MarshalECPrivateKey(t) + if err != nil { + return nil, err + } + block := &pem.Block{ + Type: ECPrivateKeyBlockType, + Bytes: derBytes, + } + return pem.EncodeToMemory(block), nil + case *rsa.PrivateKey: + block := &pem.Block{ + Type: RSAPrivateKeyBlockType, + Bytes: x509.MarshalPKCS1PrivateKey(t), + } + return pem.EncodeToMemory(block), nil + default: + return nil, fmt.Errorf("private key is not a recognized type: %T", privateKey) + } +} + +func MarshalPublicKeyToPEM(publicKey crypto.PublicKey) ([]byte, error) { + switch t := publicKey.(type) { + case *ecdsa.PublicKey: + derBytes, err := x509.MarshalPKIXPublicKey(t) + if err != nil { + return nil, err + } + return pem.EncodeToMemory( + &pem.Block{ + Type: ECPublicKeyType, + Bytes: derBytes, + }, + ), nil + case *rsa.PublicKey: + derBytes, err := x509.MarshalPKIXPublicKey(t) + if err != nil { + return nil, err + } + return pem.EncodeToMemory( + &pem.Block{ + Type: PublicKeyBlockType, + Bytes: derBytes, + }, + ), nil + default: + return nil, fmt.Errorf("private key is not a recognized type: %T", publicKey) + } +} + +func ReadPrivateKey(r io.Reader) (crypto.PrivateKey, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + key, err := ParsePrivateKeyPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading private key: %v", err) + } + return key, nil +} + +func ReadPrivateKeyFile(filename string) (crypto.PrivateKey, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + key, err := ParsePrivateKeyPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading private key: %v", err) + } + return key, nil +} + +func ReadPublicKeys(r io.Reader) ([]crypto.PublicKey, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + keys, err := ParsePublicKeysPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading public key: %v", err) + } + return keys, nil +} + +func ReadPublicKeyFile(filename string) (crypto.PublicKey, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + keys, err := ParsePublicKeysPEM(data) + if err != nil { + return nil, fmt.Errorf("error reading public key: %v", err) + } + return keys[0], nil +} + +func parseRSAPublicKey(data []byte) (*rsa.PublicKey, error) { + var err error + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { + if cert, err := x509.ParseCertificate(data); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + var pubKey *rsa.PublicKey + var ok bool + if pubKey, ok = parsedKey.(*rsa.PublicKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid RSA Public Key") + } + + return pubKey, nil +} + +func parseRSAPrivateKey(data []byte) (*rsa.PrivateKey, error) { + var err error + var parsedKey any + if parsedKey, err = x509.ParsePKCS1PrivateKey(data); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(data); err != nil { + return nil, err + } + } + var privKey *rsa.PrivateKey + var ok bool + if privKey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid RSA Private Key") + } + + return privKey, nil +} + +func parseECPublicKey(data []byte) (*ecdsa.PublicKey, error) { + var err error + + var parsedKey any + if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { + if cert, err := x509.ParseCertificate(data); err == nil { + parsedKey = cert.PublicKey + } else { + return nil, err + } + } + + var pubKey *ecdsa.PublicKey + var ok bool + if pubKey, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid ECDSA Public Key") + } + + return pubKey, nil +} + +func parseECPrivateKey(data []byte) (*ecdsa.PrivateKey, error) { + var err error + + var parsedKey any + if parsedKey, err = x509.ParseECPrivateKey(data); err != nil { + return nil, err + } + + var privKey *ecdsa.PrivateKey + var ok bool + if privKey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, fmt.Errorf("data doesn't contain valid ECDSA Private Key") + } + + return privKey, nil +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/config/config.go b/vendor/github.com/grepplabs/cert-source/tls/server/config/config.go new file mode 100644 index 00000000..c037b090 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/config/config.go @@ -0,0 +1,30 @@ +package config + +import ( + "crypto/tls" + "fmt" + "log/slog" + + "github.com/grepplabs/cert-source/config" + tlsserver "github.com/grepplabs/cert-source/tls/server" + "github.com/grepplabs/cert-source/tls/server/filesource" +) + +func GetServerTLSConfig(logger *slog.Logger, conf *config.TLSServerConfig, opts ...tlsserver.TLSServerConfigOption) (*tls.Config, error) { + fs, err := filesource.New( + filesource.WithLogger(logger), + filesource.WithX509KeyPair(conf.File.Cert, conf.File.Key), + filesource.WithClientAuthFile(conf.File.ClientCAs), + filesource.WithClientCRLFile(conf.File.ClientCRL), + filesource.WithRefresh(conf.Refresh), + filesource.WithKeyPassword(conf.KeyPassword), + ) + if err != nil { + return nil, fmt.Errorf("setup server cert file source: %w", err) + } + tlsConfig, err := tlsserver.NewServerConfig(logger, fs, opts...) + if err != nil { + return nil, fmt.Errorf("setup server TLS config: %w", err) + } + return tlsConfig, nil +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/filesource/filesource.go b/vendor/github.com/grepplabs/cert-source/tls/server/filesource/filesource.go new file mode 100644 index 00000000..8d9aa287 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/filesource/filesource.go @@ -0,0 +1,151 @@ +package filesource + +import ( + "errors" + "log/slog" + "os" + "path/filepath" + "sync/atomic" + "time" + + "github.com/grepplabs/cert-source/tls/keyutil" + tlscert "github.com/grepplabs/cert-source/tls/server/source" + "github.com/grepplabs/cert-source/tls/watcher" +) + +const ( + defaultCertFile = "server-crt.pem" + defaultKeyFile = "server-key.pem" +) + +type fileSource struct { + certFile string + keyFile string + keyPassword string + clientAuthFile string + clientCRLFile string + refresh time.Duration + logger *slog.Logger + notifyFunc func() + lastServerCerts atomic.Pointer[tlscert.ServerCerts] +} + +func New(opts ...Option) (tlscert.ServerCertsSource, error) { + s := &fileSource{ + logger: slog.Default(), + } + if dir, err := os.Getwd(); err == nil { + s.certFile = filepath.Join(dir, defaultCertFile) + s.keyFile = filepath.Join(dir, defaultKeyFile) + } else { + return nil, err + } + for _, opt := range opts { + opt(s) + } + lastServerCerts, err := s.getServerCerts() + if err != nil { + return nil, err + } + s.lastServerCerts.Store(lastServerCerts) + return s, nil +} + +func MustNew(opts ...Option) tlscert.ServerCertsSource { + serverSource, err := New(opts...) + if err != nil { + panic(`filesource: New(): ` + err.Error()) + } + return serverSource +} + +func (s *fileSource) getServerCerts() (*tlscert.ServerCerts, error) { + pemBlocks, err := s.Load() + if err != nil { + return nil, err + } + certificates, err := pemBlocks.Certificates() + if err != nil { + return nil, err + } + clientCAs, err := pemBlocks.ClientCAs() + if err != nil { + return nil, err + } + clientCRLs, err := pemBlocks.ClientCRLs() + if err != nil { + return nil, err + } + if err = pemBlocks.ValidateCRLs(); err != nil { + return nil, err + } + return &tlscert.ServerCerts{ + Certificates: certificates, + ClientCAs: clientCAs, + ClientCRLs: clientCRLs, + RevokedSerialNumbers: tlscert.NewRevokedSerialNumbers(clientCRLs), + Checksum: pemBlocks.Checksum(), + }, nil +} + +func (s *fileSource) refreshServerCerts() (*tlscert.ServerCerts, error) { + serverCerts, err := s.getServerCerts() + if err != nil { + return nil, err + } + s.lastServerCerts.Store(serverCerts) + return serverCerts, nil +} + +func (s *fileSource) ServerCerts() chan tlscert.ServerCerts { + initialServerCert := s.lastServerCerts.Load() + ch := make(chan tlscert.ServerCerts, 1) + if initialServerCert != nil { + ch <- *initialServerCert + } + if s.refresh <= 0 { + close(ch) + } else { + go func() { + watcher.Watch(s.logger, ch, s.refresh, initialServerCert, s.refreshServerCerts, s.notifyFunc) + close(ch) + }() + } + return ch +} + +func (s *fileSource) Load() (pemBlocks *tlscert.ServerPEMs, err error) { + if s.certFile == "" { + return nil, errors.New("cert file source: certFile is required") + } + if s.keyFile == "" { + return nil, errors.New("cert file source: keyFile is required") + } + if s.clientAuthFile == "" && s.clientCRLFile != "" { + return nil, errors.New("cert file source: clientAuthFile is required when clientCRLFile is provided") + } + pemBlocks = &tlscert.ServerPEMs{} + if pemBlocks.CertPEMBlock, err = s.readFile(s.certFile); err != nil { + return nil, err + } + if pemBlocks.KeyPEMBlock, err = s.readFile(s.keyFile); err != nil { + return nil, err + } + if pemBlocks.KeyPEMBlock, err = keyutil.DecryptPrivateKeyPEM(pemBlocks.KeyPEMBlock, s.keyPassword); err != nil { + return nil, err + } + if pemBlocks.ClientAuthPEMBlock, err = s.readFile(s.clientAuthFile); err != nil { + return nil, err + } + if pemBlocks.CRLPEMBlock, err = s.readFile(s.clientCRLFile); err != nil { + return nil, err + } + return pemBlocks, nil +} + +func (s *fileSource) readFile(name string) ([]byte, error) { + if name == "" { + return nil, nil + } + return os.ReadFile(name) +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/filesource/option.go b/vendor/github.com/grepplabs/cert-source/tls/server/filesource/option.go new file mode 100644 index 00000000..0bc89439 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/filesource/option.go @@ -0,0 +1,51 @@ +package filesource + +import ( + "log/slog" + "time" +) + +type Option func(*fileSource) + +func WithLogger(logger *slog.Logger) Option { + return func(c *fileSource) { + c.logger = logger + } +} + +func WithX509KeyPair(certFile, keyFile string) Option { + return func(c *fileSource) { + c.certFile = certFile + c.keyFile = keyFile + } +} + +func WithKeyPassword(keyPassword string) Option { + return func(c *fileSource) { + c.keyPassword = keyPassword + } +} + +func WithClientAuthFile(clientAuthFile string) Option { + return func(c *fileSource) { + c.clientAuthFile = clientAuthFile + } +} + +func WithClientCRLFile(clientCRLFile string) Option { + return func(c *fileSource) { + c.clientCRLFile = clientCRLFile + } +} + +func WithRefresh(refresh time.Duration) Option { + return func(c *fileSource) { + c.refresh = refresh + } +} + +func WithNotifyFunc(notifyFunc func()) Option { + return func(c *fileSource) { + c.notifyFunc = notifyFunc + } +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/option.go b/vendor/github.com/grepplabs/cert-source/tls/server/option.go new file mode 100644 index 00000000..d7c1d2ba --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/option.go @@ -0,0 +1,59 @@ +package tlsserver + +import ( + "crypto/tls" + "crypto/x509" +) + +type TLSServerConfigOption func(*tls.Config) + +func WithTLSServerNextProtos(nextProto []string) TLSServerConfigOption { + return func(c *tls.Config) { + c.NextProtos = nextProto + } +} + +func WithTLSServerCurvePreferences(curvePreferences []tls.CurveID) TLSServerConfigOption { + return func(c *tls.Config) { + if len(curvePreferences) != 0 { + c.CurvePreferences = curvePreferences + } else { + c.CurvePreferences = nil + } + } +} + +func WithTLSServerCipherSuites(cipherSuites []uint16) TLSServerConfigOption { + return func(c *tls.Config) { + if len(cipherSuites) != 0 { + c.CipherSuites = cipherSuites + } else { + c.CipherSuites = nil + } + } +} + +type VerifyPeerCertificateFunc func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error + +// WithTLSServerVerifyPeerCertificate sets or chains a custom VerifyPeerCertificate function on a *tls.Config. +// If a nil function is provided, it unsets the certificate verification function (including the standard verification). +// If an existing verification function is present, the new function is chained so that it is invoked only if the existing one succeeds. +func WithTLSServerVerifyPeerCertificate(verifyFunc VerifyPeerCertificateFunc) TLSServerConfigOption { + return func(c *tls.Config) { + if verifyFunc == nil { + c.VerifyPeerCertificate = nil + return + } + prevFunc := c.VerifyPeerCertificate + if prevFunc == nil { + c.VerifyPeerCertificate = verifyFunc + } else { + c.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + if err := prevFunc(rawCerts, verifiedChains); err != nil { + return err + } + return verifyFunc(rawCerts, verifiedChains) + } + } + } +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/server.go b/vendor/github.com/grepplabs/cert-source/tls/server/server.go new file mode 100644 index 00000000..28a94a08 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/server.go @@ -0,0 +1,106 @@ +package tlsserver + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/grepplabs/cert-source/tls/keyutil" + "github.com/grepplabs/cert-source/tls/server/source" +) + +const ( + initLoadTimeout = 5 * time.Second +) + +// MustNewServerConfig is like NewServerConfig but panics if the config cannot be created. +func MustNewServerConfig(logger *slog.Logger, src source.ServerCertsSource, opts ...TLSServerConfigOption) *tls.Config { + c, err := NewServerConfig(logger, src, opts...) + if err != nil { + panic(`tls: NewServerConfig(): ` + err.Error()) + } + return c +} + +// NewServerConfig provides new server TLS configuration. +func NewServerConfig(logger *slog.Logger, src source.ServerCertsSource, opts ...TLSServerConfigOption) (*tls.Config, error) { + store, err := NewServerCertsStore(logger, src) + if err != nil { + return nil, err + } + tlsConfig := tls.Config{ + GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) { + cs := store.LoadServerCerts() + x := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: cs.Certificates, + } + if cs.ClientCAs != nil { + x.ClientCAs = cs.ClientCAs + x.ClientAuth = tls.RequireAndVerifyClientCert + x.VerifyPeerCertificate = verifyClientCertificate(logger, store) + } + for _, opt := range opts { + opt(x) + } + return x, nil + }, + } + // ignored as GetConfigForClient is used. it is only required to invoke http.ListenAndServeTLS("", "") + cs := store.LoadServerCerts() + tlsConfig.Certificates = cs.Certificates + if cs.ClientCAs != nil { + tlsConfig.ClientCAs = cs.ClientCAs + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.VerifyPeerCertificate = verifyClientCertificate(logger, store) + } + for _, opt := range opts { + opt(&tlsConfig) + } + return &tlsConfig, nil +} + +func NewServerCertsStore(logger *slog.Logger, src source.ServerCertsSource) (*source.ServerCertsStore, error) { + store := source.NewServerCertsStore(logger) + logger.Info("initial server certs loading") + + certsChan := src.ServerCerts() + + select { + case certs := <-certsChan: + store.SetServerCerts(certs) + case <-time.After(initLoadTimeout): + return nil, errors.New("get server certs timeout") + } + + go func() { + for certs := range certsChan { + store.SetServerCerts(certs) + } + }() + return store, nil +} + +func verifyClientCertificate(logger *slog.Logger, store *source.ServerCertsStore) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + cs := store.LoadServerCerts() + if len(cs.ClientCRLs) == 0 { + return nil + } + for _, chain := range verifiedChains { + for _, cert := range chain { + if !cert.IsCA { + if cs.IsClientCertRevoked(cert.SerialNumber) { + err := fmt.Errorf("client certificte %s was revoked", keyutil.GetHexFormatted(cert.SerialNumber.Bytes(), ":")) + logger.Debug(err.Error()) + return err + } + } + } + } + return nil + } +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/source/pems.go b/vendor/github.com/grepplabs/cert-source/tls/server/source/pems.go new file mode 100644 index 00000000..26e11bbf --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/source/pems.go @@ -0,0 +1,89 @@ +package source + +import ( + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "errors" + + "github.com/grepplabs/cert-source/tls/keyutil" +) + +type ServerPEMsLoader interface { + Load() (*ServerPEMs, error) +} + +type ServerPEMs struct { + CertPEMBlock []byte + KeyPEMBlock []byte + ClientAuthPEMBlock []byte + CRLPEMBlock []byte +} + +func (s ServerPEMs) Checksum() []byte { + hash := sha256.New() + hash.Write(s.CertPEMBlock) + hash.Write(s.KeyPEMBlock) + hash.Write(s.ClientAuthPEMBlock) + return hash.Sum(s.CRLPEMBlock) +} + +func (s ServerPEMs) Certificates() ([]tls.Certificate, error) { + cert, err := tls.X509KeyPair(s.CertPEMBlock, s.KeyPEMBlock) + if err != nil { + return nil, err + } + return []tls.Certificate{cert}, nil +} + +func (s ServerPEMs) ClientCAs() (*x509.CertPool, error) { + if len(s.ClientAuthPEMBlock) == 0 { + return nil, nil + } + certPool, err := x509.SystemCertPool() + if err != nil { + certPool = x509.NewCertPool() + } + if !certPool.AppendCertsFromPEM(s.ClientAuthPEMBlock) { + return nil, errors.New("server PEMs: building client CAs failed") + } + return certPool, nil +} + +func (s ServerPEMs) ClientCRLs() ([]*x509.RevocationList, error) { + if len(s.CRLPEMBlock) == 0 { + return nil, nil + } + return keyutil.ParseCRLsPEM(s.CRLPEMBlock) +} + +func (s ServerPEMs) ValidateCRLs() error { + if len(s.ClientAuthPEMBlock) == 0 { + return nil + } + clientCRLs, err := s.ClientCRLs() + if err != nil { + return err + } + if len(clientCRLs) == 0 { + return nil + } + certs, err := keyutil.ParseCertsPEM(s.ClientAuthPEMBlock) + if err != nil { + return err + } + for _, clientCRL := range clientCRLs { + ok := false + for _, cert := range certs { + err := clientCRL.CheckSignatureFrom(cert) + if err == nil { + ok = true + continue + } + } + if !ok { + return errors.New("server PEMs: CRL validation failure") + } + } + return nil +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/server/source/store.go b/vendor/github.com/grepplabs/cert-source/tls/server/source/store.go new file mode 100644 index 00000000..f769d74e --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/server/source/store.go @@ -0,0 +1,86 @@ +package source + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "log/slog" + "math/big" + "strings" + "sync/atomic" + + "github.com/grepplabs/cert-source/tls/keyutil" +) + +type ServerCertsSource interface { + ServerCerts() chan ServerCerts +} + +type ServerCerts struct { + Certificates []tls.Certificate + ClientCAs *x509.CertPool + ClientCRLs []*x509.RevocationList + Checksum []byte + RevokedSerialNumbers map[string]struct{} +} + +func (s *ServerCerts) GetChecksum() []byte { + return s.Checksum +} + +func NewRevokedSerialNumbers(clientCRLs []*x509.RevocationList) map[string]struct{} { + revokedSerialNumbers := make(map[string]struct{}) + for _, clientCRL := range clientCRLs { + for _, revoked := range clientCRL.RevokedCertificateEntries { + revokedSerialNumbers[string(revoked.SerialNumber.Bytes())] = struct{}{} + } + } + return revokedSerialNumbers +} + +func (s *ServerCerts) IsClientCertRevoked(serialNumber *big.Int) bool { + _, ok := s.RevokedSerialNumbers[string(serialNumber.Bytes())] + return ok +} + +type ServerCertsStore struct { + cs atomic.Pointer[ServerCerts] + logger *slog.Logger +} + +func NewServerCertsStore(logger *slog.Logger) *ServerCertsStore { + s := &ServerCertsStore{ + logger: logger, + } + s.cs.Store(&ServerCerts{}) + return s +} + +func (s *ServerCertsStore) LoadServerCerts() ServerCerts { + return *s.cs.Load() +} + +func (s *ServerCertsStore) SetServerCerts(certs ServerCerts) { + s.cs.Store(&certs) + s.logger.Info(fmt.Sprintf("stored x509 server certs for names [%s]", names(certs.Certificates))) +} + +func names(certs []tls.Certificate) []string { + var result []string + for _, c := range certs { + x509Cert, err := x509.ParseCertificate(c.Certificate[0]) + if err != nil { + continue + } + var names []string + if len(x509Cert.Subject.CommonName) > 0 { + names = append(names, x509Cert.Subject.CommonName) + } + names = append(names, x509Cert.DNSNames...) + for _, ip := range x509Cert.IPAddresses { + names = append(names, ip.String()) + } + result = append(result, fmt.Sprintf("%s=%s", keyutil.GetHexFormatted(x509Cert.SerialNumber.Bytes(), ":"), strings.Join(names, ","))) + } + return result +} diff --git a/vendor/github.com/grepplabs/cert-source/tls/watcher/watch.go b/vendor/github.com/grepplabs/cert-source/tls/watcher/watch.go new file mode 100644 index 00000000..b1e52501 --- /dev/null +++ b/vendor/github.com/grepplabs/cert-source/tls/watcher/watch.go @@ -0,0 +1,53 @@ +package watcher + +import ( + "fmt" + "log/slog" + "reflect" + "time" +) + +func Watch[T any, PT interface { + GetChecksum() []byte + *T +}](logger *slog.Logger, ch chan T, refresh time.Duration, init PT, loadFn func() (PT, error), changedFn func()) { + once := refresh <= 0 + + if refresh < time.Second { + refresh = time.Second + } + logger.Info(fmt.Sprintf("cert watch is started, refresh interval %s", refresh)) + + var last = init + for { + next, err := loadFn() + if err != nil { + logger.Error("cannot load certificates", slog.String("error", err.Error())) + time.Sleep(refresh) + continue + } + if last != nil { + if reflect.DeepEqual(next.GetChecksum(), last.GetChecksum()) { + if once && init != nil { + // init value is set, so assume it was already sent to channel + logger.Info("cert watch is disabled") + return + } + time.Sleep(refresh) + continue + } + } + + ch <- *next + last = next + + if changedFn != nil { + changedFn() + } + if once { + logger.Info("cert watch is disabled") + return + } + time.Sleep(refresh) + } +} diff --git a/vendor/github.com/youmark/pkcs8/.travis.yml b/vendor/github.com/youmark/pkcs8/.travis.yml deleted file mode 100644 index 3608f7dc..00000000 --- a/vendor/github.com/youmark/pkcs8/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -arch: - - amd64 - - ppc64le -language: go - -go: - - "1.10.x" - - "1.11.x" - - "1.12.x" - - "1.13.x" - - master - -script: - - go test -v ./... diff --git a/vendor/modules.txt b/vendor/modules.txt index 5be47f3e..03179eb4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -165,6 +165,15 @@ github.com/googleapis/gax-go/v2 github.com/googleapis/gax-go/v2/apierror github.com/googleapis/gax-go/v2/apierror/internal/proto github.com/googleapis/gax-go/v2/internal +# github.com/grepplabs/cert-source v0.0.6 +## explicit; go 1.21 +github.com/grepplabs/cert-source/config +github.com/grepplabs/cert-source/tls/keyutil +github.com/grepplabs/cert-source/tls/server +github.com/grepplabs/cert-source/tls/server/config +github.com/grepplabs/cert-source/tls/server/filesource +github.com/grepplabs/cert-source/tls/server/source +github.com/grepplabs/cert-source/tls/watcher # github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce ## explicit github.com/hashicorp/errwrap @@ -337,8 +346,8 @@ github.com/xdg/scram # github.com/xdg/stringprep v1.0.0 ## explicit github.com/xdg/stringprep -# github.com/youmark/pkcs8 v0.0.0-20240424034433-3c2c7870ae76 -## explicit; go 1.22 +# github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 +## explicit; go 1.17 github.com/youmark/pkcs8 # go.opencensus.io v0.24.0 ## explicit; go 1.13