From caa2b422d7038be206d64fff77646a59607873e8 Mon Sep 17 00:00:00 2001 From: "Jeevanandam M." Date: Sat, 4 Jan 2025 12:36:53 -0800 Subject: [PATCH] feat: add SetCertificateFromFile and SetCertificateFromString (#941) - refactor root cert and client root methods to make consistent signature --- .testdata/cert.pem | 18 ++++ .testdata/key.pem | 28 ++++++ cert_watcher_test.go | 12 ++- client.go | 227 +++++++++++++++++++++++++++++-------------- client_test.go | 48 ++++++--- 5 files changed, 243 insertions(+), 90 deletions(-) create mode 100644 .testdata/cert.pem create mode 100644 .testdata/key.pem diff --git a/.testdata/cert.pem b/.testdata/cert.pem new file mode 100644 index 00000000..8587fb37 --- /dev/null +++ b/.testdata/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+jCCAeKgAwIBAgIRAJce5ewsoW44j0qvSABmq7owDQYJKoZIhvcNAQELBQAw +EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yNTAxMDQwNzA3MTNaFw0yNjAxMDQwNzA3 +MTNaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQCYkTN1g/0Z3KkS3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5 +h2MaYx8OCA7oGU7/dWOPhtE3rgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ +5fbOq4s73WTNDfYSTYNzeujDCjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnji +nqG9HNOQx6r/S6FqC5j/7PrVl1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5df +C39zEEEx75qQt7Jc0asm1AqAKSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKW +JDvTSdeoM9TPX0y0cjo6AwSrdLl7flrRAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIF +oDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuC +CWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAdHvPQe3EJ4/X6K/bklJUhIfM +KBauH8VMBfri7xLawleKssm7GdiFivSA0g1pArkl8SALBlPqhrx7rwlyyivLTZaR +VFvXaQ9eU0zGnSnDnKVz6CX/zn3TKfcgZPEBclayh0ldm7A8xSJWaWbRZ+s9e9x1 +XcQTn2KkMZfBDMnGEWQ3KZrClvO5ZfkqSiyzEm9+eF0m0E7ujTyfSVMsPdyldA6U +pHG8omQTyOzJl2I4z7DlS0AEsL0TJHV4iKr9rDei2xQz/wtful5qU/taYp2Y6zMH +8ytnDldJhmcCwmvtqvK5p6CbkatE7TFyw2CxQJHnQef+Y4W94sSZWg9CGRKDIQ== +-----END CERTIFICATE----- diff --git a/.testdata/key.pem b/.testdata/key.pem new file mode 100644 index 00000000..8e014442 --- /dev/null +++ b/.testdata/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYkTN1g/0Z3KkS +3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5h2MaYx8OCA7oGU7/dWOPhtE3 +rgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ5fbOq4s73WTNDfYSTYNzeujD +Cjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnjinqG9HNOQx6r/S6FqC5j/7PrV +l1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5dfC39zEEEx75qQt7Jc0asm1AqA +KSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKWJDvTSdeoM9TPX0y0cjo6AwSr +dLl7flrRAgMBAAECggEAJPTPNUEilxgncGXNZmdBJ2uDN536XoRFIpL1MbK/bFyo +yp00QFaVK7ZK4EJwbFKxYbF3vFOwKT0sAsPIlOWGsTtG59fzbOVTdYzJzPBLEef3 +kbd9n8hUB3RdA5T0Ji0r1Kv0FlzmYZu9NDmOYXm5lTfq2tQiKj5+i4zf3EhQZLng +4wVxBT7yQUQcstJv5K1L6HVzunSYtbHx8ZVxmw+tJ4lMCK23KPlvncZZTT8chWdT +3GOp5nYIHk9E5jQnBnj7p73sxZUCZlb8uhLtdcgAXc4scptEVO+7n5zOaXIv40Oz +yfkESgHcZWAMDvnkxdySHlD38Z2LIKDGbqR6O9wcwQKBgQDBO6fFPXO41nsxdVCB +nhCgL2hsGjaxJzGBLaOJNVMMFRASN3Yqvs4N1Hn7lawRI/FRRffxjLkZfNGEBSF2 +OipdvX19Oe2hCZxvwHPoe5sb/Dh6KE7If1hRLOCXg/8E7ADBtAp94dam1WF4Kh6N +Va6+n2YKif2rqye1YtRoUU46iQKBgQDKH/eMcMRUe9IySxHLogidOUwa0X7WrxF/ +PkXGpPbHQtMOJF5cVzh+L+foUKXNM60lgmCH0438GKU7kirC/dVtD/bwE598/XFZ +vnjPV7Adf9vBz9NN8cS/4uEfQYbvTRmrnrQK+ZhOe8hmwjapxqdWrVHNUtvx18vL +qBwR4YjsCQKBgCycMx1MFJ1FludSKCXkcf4pM7hRTPMVE065VJnmn6eYbT9nYnZ3 +2mZC+W5lnXXPkHSs7JLtZAZIVK5f6Nu8je9aQdBZQUz+RQlfquKvNp39WqSJDbcn +/yGudKNGK+fc/Ee74vgw3Tdi57+wKaGDeHY1on8oYFHzj5VGnbb/nknRAoGBAK2Z +hyQ4NmfZcU+A6mfbY0qmS5c9F5OMCZsgAQ374XiDDIK4+dKVlw/KVYRSwBTerXfp +4r7GFMzQ3hmsEM4o9YYWkCDiubjAdPp/fYOX7MtpZXWw6euoGzQzyObvgNVHgyTD +yh8jAI1oA1c+t3RaCp+HfRq8b+vnTEI+wN0auF8BAoGBAJmw+GgHCZGpw2XPNu+X +8kuVGbQYAjTOXhBM4WzZyhfH1TWKLGn7C9YixhE2AW0UWKDvy+6OqPhe8q3KVms3 +8YZ1W+vbUNEZNGE0XrB5ZMXfePiqisCz0jgP9OAuT+ii4aI3MAm3zgCEC6UTMvLq +gNBu3Tcy6udxnUf7czzJDRtE +-----END PRIVATE KEY----- diff --git a/cert_watcher_test.go b/cert_watcher_test.go index 7f2d4b98..b75fa954 100644 --- a/cert_watcher_test.go +++ b/cert_watcher_test.go @@ -57,11 +57,13 @@ func TestClient_SetRootCertificateWatcher(t *testing.T) { // Make sure that TLS handshake happens for all request // (otherwise, test may succeed because 1st TLS session is re-used) DisableKeepAlives: true, - }).SetRootCertificateWatcher(paths.RootCACert, &CertWatcherOptions{ - PoolInterval: poolingInterval, - }).SetClientRootCertificateWatcher(paths.RootCACert, &CertWatcherOptions{ - PoolInterval: poolingInterval, - }).SetDebug(false) + }).SetRootCertificatesWatcher( + &CertWatcherOptions{PoolInterval: poolingInterval}, + paths.RootCACert, + ).SetClientRootCertificatesWatcher( + &CertWatcherOptions{PoolInterval: poolingInterval}, + paths.RootCACert, + ).SetDebug(false) url := strings.Replace(ts.URL, "127.0.0.1", "localhost", 1) t.Log("Test URL:", url) diff --git a/client.go b/client.go index 3b42a6a3..013c947e 100644 --- a/client.go +++ b/client.go @@ -1488,7 +1488,52 @@ func (c *Client) RemoveProxy() *Client { return c } -// SetCertificates method helps to conveniently set client certificates into Resty. +// SetCertificateFromString method helps to set client certificates into Resty +// from cert and key files to perform SSL client authentication +// +// client.SetCertificateFromFile("certs/client.pem", "certs/client.key") +func (c *Client) SetCertificateFromFile(certFilePath, certKeyFilePath string) *Client { + cert, err := tls.LoadX509KeyPair(certFilePath, certKeyFilePath) + if err != nil { + c.Logger().Errorf("client certificate/key parsing error: %v", err) + return c + } + c.SetCertificates(cert) + return c +} + +// SetCertificateFromString method helps to set client certificates into Resty +// from string to perform SSL client authentication +// +// myClientCertStr := `-----BEGIN CERTIFICATE----- +// ... cert content ... +// -----END CERTIFICATE-----` +// +// myClientCertKeyStr := `-----BEGIN PRIVATE KEY----- +// ... cert key content ... +// -----END PRIVATE KEY-----` +// +// client.SetCertificateFromString(myClientCertStr, myClientCertKeyStr) +func (c *Client) SetCertificateFromString(certStr, certKeyStr string) *Client { + cert, err := tls.X509KeyPair([]byte(certStr), []byte(certKeyStr)) + if err != nil { + c.Logger().Errorf("client certificate/key parsing error: %v", err) + return c + } + c.SetCertificates(cert) + return c +} + +// SetCertificates method helps to conveniently set client certificates into Resty +// to perform SSL client authentication +// +// cert, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key") +// if err != nil { +// log.Printf("ERROR client certificate/key parsing error: %v", err) +// return +// } +// +// client.SetCertificates(cert) func (c *Client) SetCertificates(certs ...tls.Certificate) *Client { config, err := c.tlsConfig() if err != nil { @@ -1502,72 +1547,142 @@ func (c *Client) SetCertificates(certs ...tls.Certificate) *Client { return c } -// SetRootCertificate method helps to add one or more root certificates into the Resty client +// SetRootCertificates method helps to add one or more root certificates into the Resty client // -// client.SetRootCertificate("/path/to/root/pemFile.pem") -func (c *Client) SetRootCertificate(pemFilePath string) *Client { - rootPemData, err := os.ReadFile(pemFilePath) - if err != nil { - c.Logger().Errorf("%v", err) - return c +// // one pem file path +// client.SetRootCertificates("/path/to/root/pemFile.pem") +// +// // one or more pem file path(s) +// client.SetRootCertificates( +// "/path/to/root/pemFile1.pem", +// "/path/to/root/pemFile2.pem" +// "/path/to/root/pemFile3.pem" +// ) +// +// // if you happen to have string slices +// client.SetRootCertificates(certs...) +func (c *Client) SetRootCertificates(pemFilePaths ...string) *Client { + for _, fp := range pemFilePaths { + rootPemData, err := os.ReadFile(fp) + if err != nil { + c.Logger().Errorf("%v", err) + return c + } + c.handleCAs("root", rootPemData) } - c.handleCAs("root", rootPemData) return c } -// SetRootCertificateWatcher enables dynamic reloading of one or more root certificates. +// SetRootCertificatesWatcher method enables dynamic reloading of one or more root certificates. // It is designed for scenarios involving long-running Resty clients where certificates may be renewed. -// The caller is responsible for calling Close to stop the watcher. -// -// client.SetRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{ -// PoolInterval: time.Hour * 24, -// }) // -// defer client.Close() -func (c *Client) SetRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { - c.SetRootCertificate(pemFilePath) - c.initCertWatcher(pemFilePath, "root", options) +// client.SetRootCertificatesWatcher( +// &resty.CertWatcherOptions{ +// PoolInterval: 24 * time.Hour, +// }, +// "root-ca.crt", +// ) +func (c *Client) SetRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client { + c.SetRootCertificates(pemFilePaths...) + for _, fp := range pemFilePaths { + c.initCertWatcher(fp, "root", options) + } return c } // SetRootCertificateFromString method helps to add one or more root certificates // into the Resty client // -// client.SetRootCertificateFromString("pem certs content") +// myRootCertStr := `-----BEGIN CERTIFICATE----- +// ... cert content ... +// -----END CERTIFICATE-----` +// +// client.SetRootCertificateFromString(myRootCertStr) func (c *Client) SetRootCertificateFromString(pemCerts string) *Client { c.handleCAs("root", []byte(pemCerts)) return c } -// SetClientRootCertificate method helps to add one or more client's root +// SetClientRootCertificates method helps to add one or more client's root // certificates into the Resty client // -// client.SetClientRootCertificate("/path/to/root/pemFile.pem") -func (c *Client) SetClientRootCertificate(pemFilePath string) *Client { - rootPemData, err := os.ReadFile(pemFilePath) - if err != nil { - c.Logger().Errorf("%v", err) - return c +// // one pem file path +// client.SetClientCertificates("/path/to/client/pemFile.pem") +// +// // one or more pem file path(s) +// client.SetClientCertificates( +// "/path/to/client/pemFile1.pem", +// "/path/to/client/pemFile2.pem" +// "/path/to/client/pemFile3.pem" +// ) +// +// // if you happen to have string slices +// client.SetClientCertificates(certs...) +func (c *Client) SetClientRootCertificates(pemFilePaths ...string) *Client { + for _, fp := range pemFilePaths { + pemData, err := os.ReadFile(fp) + if err != nil { + c.Logger().Errorf("%v", err) + return c + } + c.handleCAs("client-root", pemData) } - c.handleCAs("client", rootPemData) return c } -// SetClientRootCertificateWatcher enables dynamic reloading of one or more client root certificates. +// SetClientRootCertificatesWatcher method enables dynamic reloading of one or more client root certificates. // It is designed for scenarios involving long-running Resty clients where certificates may be renewed. -// The caller is responsible for calling Close to stop the watcher. // -// client.SetClientRootCertificateWatcher("root-ca.crt", &CertWatcherOptions{ -// PoolInterval: time.Hour * 24, -// }) -// defer client.Close() -func (c *Client) SetClientRootCertificateWatcher(pemFilePath string, options *CertWatcherOptions) *Client { - c.SetClientRootCertificate(pemFilePath) - c.initCertWatcher(pemFilePath, "client", options) +// client.SetClientRootCertificatesWatcher( +// &resty.CertWatcherOptions{ +// PoolInterval: 24 * time.Hour, +// }, +// "root-ca.crt", +// ) +func (c *Client) SetClientRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client { + c.SetClientRootCertificates(pemFilePaths...) + for _, fp := range pemFilePaths { + c.initCertWatcher(fp, "client-root", options) + } + return c +} +// SetClientRootCertificateFromString method helps to add one or more clients +// root certificates into the Resty client +// +// myClientRootCertStr := `-----BEGIN CERTIFICATE----- +// ... cert content ... +// -----END CERTIFICATE-----` +// +// client.SetClientRootCertificateFromString(myClientRootCertStr) +func (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client { + c.handleCAs("client-root", []byte(pemCerts)) return c } +func (c *Client) handleCAs(scope string, permCerts []byte) { + config, err := c.tlsConfig() + if err != nil { + c.Logger().Errorf("%v", err) + return + } + + c.lock.Lock() + defer c.lock.Unlock() + switch scope { + case "root": + if config.RootCAs == nil { + config.RootCAs = x509.NewCertPool() + } + config.RootCAs.AppendCertsFromPEM(permCerts) + case "client-root": + if config.ClientCAs == nil { + config.ClientCAs = x509.NewCertPool() + } + config.ClientCAs.AppendCertsFromPEM(permCerts) + } +} + func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcherOptions) { tickerDuration := defaultWatcherPoolingInterval if options != nil && options.PoolInterval > 0 { @@ -1611,9 +1726,9 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher switch scope { case "root": - c.SetRootCertificate(pemFilePath) - case "client": - c.SetClientRootCertificate(pemFilePath) + c.SetRootCertificates(pemFilePath) + case "client-root": + c.SetClientRootCertificates(pemFilePath) } c.debugf("Cert %s reloaded.", pemFilePath) @@ -1622,38 +1737,6 @@ func (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcher }() } -// SetClientRootCertificateFromString method helps to add one or more clients -// root certificates into the Resty client -// -// client.SetClientRootCertificateFromString("pem certs content") -func (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client { - c.handleCAs("client", []byte(pemCerts)) - return c -} - -func (c *Client) handleCAs(scope string, permCerts []byte) { - config, err := c.tlsConfig() - if err != nil { - c.Logger().Errorf("%v", err) - return - } - - c.lock.Lock() - defer c.lock.Unlock() - switch scope { - case "root": - if config.RootCAs == nil { - config.RootCAs = x509.NewCertPool() - } - config.RootCAs.AppendCertsFromPEM(permCerts) - case "client": - if config.ClientCAs == nil { - config.ClientCAs = x509.NewCertPool() - } - config.ClientCAs.AppendCertsFromPEM(permCerts) - } -} - // OutputDirectory method returns the output directory value from the client. func (c *Client) OutputDirectory() string { c.lock.RLock() diff --git a/client_test.go b/client_test.go index 3721106f..82aa4b85 100644 --- a/client_test.go +++ b/client_test.go @@ -193,19 +193,40 @@ func TestClientProxy(t *testing.T) { } func TestClientSetCertificates(t *testing.T) { - client := dcnl() - client.SetCertificates(tls.Certificate{}) + certFile := filepath.Join(getTestDataPath(), "cert.pem") + keyFile := filepath.Join(getTestDataPath(), "key.pem") - transport, err := client.HTTPTransport() + t.Run("client cert from file", func(t *testing.T) { + c := dcnl() + c.SetCertificateFromFile(certFile, keyFile) + assertEqual(t, 1, len(c.TLSClientConfig().Certificates)) + }) - assertNil(t, err) - assertEqual(t, 1, len(transport.TLSClientConfig.Certificates)) + t.Run("error-client cert from file", func(t *testing.T) { + c := dcnl() + c.SetCertificateFromFile(certFile+"no", keyFile+"no") + assertEqual(t, 0, len(c.TLSClientConfig().Certificates)) + }) + + t.Run("client cert from string", func(t *testing.T) { + certPemData, _ := os.ReadFile(certFile) + keyPemData, _ := os.ReadFile(keyFile) + c := dcnl() + c.SetCertificateFromString(string(certPemData), string(keyPemData)) + assertEqual(t, 1, len(c.TLSClientConfig().Certificates)) + }) + + t.Run("error-client cert from string", func(t *testing.T) { + c := dcnl() + c.SetCertificateFromString(string("empty"), string("empty")) + assertEqual(t, 0, len(c.TLSClientConfig().Certificates)) + }) } func TestClientSetRootCertificate(t *testing.T) { t.Run("root cert", func(t *testing.T) { client := dcnl() - client.SetRootCertificate(filepath.Join(getTestDataPath(), "sample-root.pem")) + client.SetRootCertificates(filepath.Join(getTestDataPath(), "sample-root.pem")) transport, err := client.HTTPTransport() @@ -215,7 +236,7 @@ func TestClientSetRootCertificate(t *testing.T) { t.Run("root cert not exists", func(t *testing.T) { client := dcnl() - client.SetRootCertificate(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) + client.SetRootCertificates(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) transport, err := client.HTTPTransport() @@ -354,7 +375,7 @@ func TestClientTLSConfigerInterface(t *testing.T) { func TestClientSetClientRootCertificate(t *testing.T) { client := dcnl() - client.SetClientRootCertificate(filepath.Join(getTestDataPath(), "sample-root.pem")) + client.SetClientRootCertificates(filepath.Join(getTestDataPath(), "sample-root.pem")) transport, err := client.HTTPTransport() @@ -364,7 +385,7 @@ func TestClientSetClientRootCertificate(t *testing.T) { func TestClientSetClientRootCertificateNotExists(t *testing.T) { client := dcnl() - client.SetClientRootCertificate(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) + client.SetClientRootCertificates(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) transport, err := client.HTTPTransport() @@ -375,9 +396,10 @@ func TestClientSetClientRootCertificateNotExists(t *testing.T) { func TestClientSetClientRootCertificateWatcher(t *testing.T) { t.Run("Cert exists", func(t *testing.T) { client := dcnl() - client.SetClientRootCertificateWatcher(filepath.Join(getTestDataPath(), "sample-root.pem"), &CertWatcherOptions{ - PoolInterval: time.Second * 1, - }) + client.SetClientRootCertificatesWatcher( + &CertWatcherOptions{PoolInterval: time.Second * 1}, + filepath.Join(getTestDataPath(), "sample-root.pem"), + ) transport, err := client.HTTPTransport() @@ -387,7 +409,7 @@ func TestClientSetClientRootCertificateWatcher(t *testing.T) { t.Run("Cert does not exist", func(t *testing.T) { client := dcnl() - client.SetClientRootCertificateWatcher(filepath.Join(getTestDataPath(), "not-exists-sample-root.pem"), nil) + client.SetClientRootCertificatesWatcher(nil, filepath.Join(getTestDataPath(), "not-exists-sample-root.pem")) transport, err := client.HTTPTransport()