From 0f936e2939a083fa0e755f019697f3ad1eeee2bf Mon Sep 17 00:00:00 2001 From: Mihai Doarna Date: Mon, 10 Jun 2024 18:13:15 +0300 Subject: [PATCH] Auth: Add root and client certificate value fields in LDAP config (#88746) * add root and client certificate value fields for LDAP * update error messages for connection error --- pkg/services/ldap/api/support_bundle.go | 1 + pkg/services/ldap/ldap.go | 89 ++++++++++--- pkg/services/ldap/ldap_test.go | 159 +++++++++++++++++++++++ pkg/services/ldap/settings.go | 17 ++- pkg/services/ldap/testdata/invalid.cert | 1 + pkg/services/ldap/testdata/invalid.pem | 1 + pkg/services/ldap/testdata/parsable.cert | 21 +++ pkg/services/ldap/testdata/parsable.pem | 28 ++++ 8 files changed, 289 insertions(+), 28 deletions(-) create mode 100644 pkg/services/ldap/testdata/invalid.cert create mode 100644 pkg/services/ldap/testdata/invalid.pem create mode 100644 pkg/services/ldap/testdata/parsable.cert create mode 100644 pkg/services/ldap/testdata/parsable.pem diff --git a/pkg/services/ldap/api/support_bundle.go b/pkg/services/ldap/api/support_bundle.go index 38ea6439024..f85784d6f43 100644 --- a/pkg/services/ldap/api/support_bundle.go +++ b/pkg/services/ldap/api/support_bundle.go @@ -42,6 +42,7 @@ func (s *Service) supportBundleCollector(context.Context) (*supportbundles.Suppo for _, server := range ldapConfig.Servers { server.BindPassword = "********" // censor password on config dump server.ClientKey = "********" // censor client key on config dump + server.ClientKeyValue = "********" if !strings.Contains(server.SearchFilter, server.Attr.Username) { bWriter.WriteString(fmt.Sprintf( diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index cdef230d271..e4d75119ebf 100644 --- a/pkg/services/ldap/ldap.go +++ b/pkg/services/ldap/ldap.go @@ -3,6 +3,7 @@ package ldap import ( "crypto/tls" "crypto/x509" + "encoding/base64" "errors" "fmt" "math" @@ -96,28 +97,14 @@ func New(config *ServerConfig, cfg *setting.Cfg) IServer { // Dial dials in the LDAP // TODO: decrease cyclomatic complexity func (server *Server) Dial() error { - var err error - var certPool *x509.CertPool - if server.Config.RootCACert != "" { - certPool = x509.NewCertPool() - for _, caCertFile := range util.SplitString(server.Config.RootCACert) { - // nolint:gosec - // We can ignore the gosec G304 warning on this one because `caCertFile` comes from ldap config. - pem, err := os.ReadFile(caCertFile) - if err != nil { - return err - } - if !certPool.AppendCertsFromPEM(pem) { - return errors.New("Failed to append CA certificate " + caCertFile) - } - } + certPool, err := getRootCACertPool(*server.Config) + if err != nil { + return err } - var clientCert tls.Certificate - if server.Config.ClientCert != "" && server.Config.ClientKey != "" { - clientCert, err = tls.LoadX509KeyPair(server.Config.ClientCert, server.Config.ClientKey) - if err != nil { - return err - } + + clientCert, err := getClientCert(*server.Config) + if err != nil { + return err } timeout := time.Duration(server.Config.Timeout) * time.Second @@ -313,6 +300,66 @@ func (server *Server) Users(logins []string) ( return serializedUsers, nil } +func getRootCACertPool(config ServerConfig) (*x509.CertPool, error) { + var pemCerts [][]byte + + if config.RootCACert != "" { + for _, caCertFile := range util.SplitString(config.RootCACert) { + // nolint:gosec + // We can ignore the gosec G304 warning on this one because `caCertFile` comes from ldap config. + pem, err := os.ReadFile(caCertFile) + if err != nil { + return nil, err + } + + pemCerts = append(pemCerts, pem) + } + } else if len(config.RootCACertValue) > 0 { + for _, cert := range config.RootCACertValue { + pem, err := base64.StdEncoding.DecodeString(cert) + if err != nil { + return nil, err + } + + pemCerts = append(pemCerts, pem) + } + } + + if len(pemCerts) > 0 { + certPool := x509.NewCertPool() + + for _, pem := range pemCerts { + if !certPool.AppendCertsFromPEM(pem) { + return nil, errors.New("failed to append CA certificate") + } + } + + return certPool, nil + } + + return nil, nil +} + +func getClientCert(config ServerConfig) (tls.Certificate, error) { + if config.ClientCert != "" && config.ClientKey != "" { + return tls.LoadX509KeyPair(config.ClientCert, config.ClientKey) + } else if config.ClientCertValue != "" && config.ClientKeyValue != "" { + certPem, err := base64.StdEncoding.DecodeString(config.ClientCertValue) + if err != nil { + return tls.Certificate{}, err + } + + keyPem, err := base64.StdEncoding.DecodeString(config.ClientKeyValue) + if err != nil { + return tls.Certificate{}, err + } + + return tls.X509KeyPair(certPem, keyPem) + } + + return tls.Certificate{}, nil +} + // getUsersIteration is a helper function for Users() method. // It divides the users by equal parts for the anticipated requests func getUsersIteration(logins []string, fn func(int, int) error) error { diff --git a/pkg/services/ldap/ldap_test.go b/pkg/services/ldap/ldap_test.go index 27f0563f065..516f7869c40 100644 --- a/pkg/services/ldap/ldap_test.go +++ b/pkg/services/ldap/ldap_test.go @@ -14,6 +14,43 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +const ( + validCert = `LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYVENDQWtXZ0F3SUJBZ0lKQUxtVlZ1RFd1NE5ZTUEwR0NTcUdTSWIzRFFFQkN +3VUFNRVV4Q3pBSkJnTlYKQkFZVEFrRlZNUk13RVFZRFZRUUlEQXBUYjIxbExWTjBZWFJsTVNFd0h3WURWUVFLREJoSmJuUmxjbTVsZENCWAphV1JuYVhSekl +GQjBlU0JNZEdRd0hoY05NVFl4TWpNeE1UUXpORFEzV2hjTk5EZ3dOakkxTVRRek5EUTNXakJGCk1Rc3dDUVlEVlFRR0V3SkJWVEVUTUJFR0ExVUVDQXdLVTI +5dFpTMVRkR0YwWlRFaE1COEdBMVVFQ2d3WVNXNTAKWlhKdVpYUWdWMmxrWjJsMGN5QlFkSGtnVEhSa01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUTh +BTUlJQgpDZ0tDQVFFQXpVQ0ZvemdOYjFoMU0wanpOUlNDamhPQm5SK3VWYlZwYVdmWFlJUitBaFdEZEVlNXJ5WStDZ2F2Ck9nOGJmTHlieXpGZGVobFlkRFJ +na2VkRUIvR2pHOGFKdzA2bDBxRjRqRE9BdzBrRXlnV0N1Mm1jSDdYT3hSdCsKWUFIM1RWSGEvSHUxVzNXanprb2JxcXFMUThna0tXV00yN2ZPZ0FaNkdpZWF +KQk42VkJTTU1jUGV5M0hXTEJtYworVFlKbXYxZGJhTzJqSGhLaDhwZkt3MFcxMlZNOFAxUElPOGd2NFBodS91dUpZaWVCV0tpeEJFeXkwbEhqeWl4CllGQ1I +xMnhkaDRDQTQ3cTk1OFpSR25uRFVHRlZFMVFoZ1JhY0pDT1o5YmQ1dDltcjhLTGFWQllUQ0pvNUVSRTgKanltYWI1ZFBxZTVxS2ZKc0NaaXFXZ2xialVvOXR +3SURBUUFCbzFBd1RqQWRCZ05WSFE0RUZnUVV4cHV3Y3MvQwpZUU95dWkrcjFHKzNLeEJOaHhrd0h3WURWUjBqQkJnd0ZvQVV4cHV3Y3MvQ1lRT3l1aStyMUc +rM0t4Qk5oeGt3CkRBWURWUjBUQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQWlXVUtzLzJ4L3ZpTkNLaTNZNmIKbEV1Q3RBR2h6T09aOUV +qcnZKOCtDT0gzUmFnM3RWQldyY0JaMy91aGhQcTVneTlscXc0T2t2RXdzOTkvNWpGcwpYMUZKNk1LQmdxZnV5N3loNXMxWWZNMEFOSFljek1tWXBaZUFjUWY +yQ0dBYVZmd1RUZlNsek5Mc0YybFcvbHk3CnlhcEZ6bFlTSkxHb1ZFK09IRXU4ZzVTbE5BQ1VFZmtYdys1RWdoaCtLemxJTjdSNlE3cjJpeFdORkJDL2pXZjc +KTktVZkp5WDhxSUc1bWQxWVVlVDZHQlc5Qm0yLzEvUmlPMjRKVGFZbGZMZEtLOVRZYjhzRzVCK09MYWIyREltRwo5OUNKMjVSa0FjU29iV05GNXpEME82bGd +PbzNjRWRCL2tzQ3EzaG10bEMvRGxMWi9EOENKKzdWdVpuUzFyUjJuCmFRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==` + validKey = `LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFdlFJQkFEQU5CZ2txaGtpRzl3MEJBUUVGQUFTQ0JLY3dnZ1NqQWdFQUFv +SUJBUUROUUlXak9BMXZXSFV6ClNQTTFGSUtPRTRHZEg2NVZ0V2xwWjlkZ2hINENGWU4wUjdtdkpqNEtCcTg2RHh0OHZKdkxNVjE2R1ZoME5HQ1IKNTBRSDhh +TWJ4b25EVHFYU29YaU1NNEREU1FUS0JZSzdhWndmdGM3RkczNWdBZmROVWRyOGU3VmJkYVBPU2h1cQpxb3REeUNRcFpZemJ0ODZBQm5vYUo1b2tFM3BVRkl3 +eHc5N0xjZFlzR1p6NU5nbWEvVjF0bzdhTWVFcUh5bDhyCkRSYlhaVXp3L1U4Zzd5Qy9nK0c3KzY0bGlKNEZZcUxFRVRMTFNVZVBLTEZnVUpIWGJGMkhnSURq +dXIzbnhsRWEKZWNOUVlWVVRWQ0dCRnB3a0k1bjF0M20zMmF2d290cFVGaE1JbWprUkVUeVBLWnB2bDArcDdtb3A4bXdKbUtwYQpDVnVOU2oyM0FnTUJBQUVD +Z2dFQUJuNEkvQjIweHhYY056QVNpVlpKdnVhOURkUkh0bXhUbGtMem5CajB4Mm9ZCnkxL05iczNkM29GUm41dUV1aEJaT1RjcGhzZ3dkUlNIRFhac1AzZ1VP +YmV3K2QyTi96aWVVSWo4aExEVmx2SlAKclUvczRVL2w1M1EwTGlOQnlFOVRodkwrekpMUENLSnRkNXVIWmpCNWZGbTY5K1E3Z3U4eGc0eEhJdWIrMHBQNQpQ +SGFubUhDRHJiZ05OL29xbGFyNEZaMk1YVGdla1c2QW15Yy9rb0U5aEluNEJhYTJLZS9CL0FVR1k0cE1STHFwClRBcnQrR1RWZVdlb0ZZOVFBQ1VwYUhwSmhH +Yi9QaW91NnRsVTU3ZTQyY0xva2kxZjArU0FSc0JCS3lYQTdCQjEKMWZNSDEwS1FZRkE2OGRUWVdsS3pRYXUvSzR4YXFnNEZLbXR3RjY2R1FRS0JnUUQ5T3BO +VVM3b1J4TUhWSmFCUgpUTldXK1YxRlh5Y3FvamVrRnBEaWpQYjJYNUNXVjE2b2VXZ2FYcDBuT0hGZHk5RVdzM0d0R3BmWmFzYVJWSHNYClNIdFBoNE5iOEpx +SGRHRTAvQ0Q2dDArNERuczhCbjljU3F0ZFFCN1IzSm43SU1YaTlYL1U4TERLbytBMTgvSnEKVjhWZ1VuZ01ueTlZak1rUUliSzhUUldrWVFLQmdRRFBmNG54 +TzZqdSt0T0hIT1JRdHkzYllERDArT1YzSTArTAoweXowdVByZXJ5QlZpOW5ZNDNLYWtINTJEN1VaRXd3c0JqakdYRCtXSDh4RXNtQldzR05YSnUwMjVQdnpJ +Sm96CmxBRWlYdk1wL05tWXArdFk0ckRtTzhSaHlWb2NCcVdIemgzOG0wSUZPZDRCeUZENW5MRURyQTNwRFZvMGFOZ1kKbjBHd1J5c1pGd0tCZ1FEa0NqM202 +Wk1Vc1VXRXR5K2FSMEVKaG1LeU9EQkRPblkwOUlWaEgyUy9GZXhWRnpVTgpMdGZLOTIwNmhwL0F3ZXozTG4ydVQ0WnpxcTVLN2ZNelVuaUpkQldkVkIwMDRs +OHZvZVhwSWU5T1p1d2ZjQko5CmdGaTF6eXB4L3VGRHY0MjFCelFwQk4rUWZPZEtidmJkUVZGam5xQ3hiU0RyODB5VmxHTXJJNWZid1FLQmdHMDkKb1JyZXBP +N0VJTzhHTi9HQ3J1TEsvcHRLR2t5aHkzUTZ4blZFbWRiNDdoWDduY0pBNUlvWlBtcmJsQ1ZTVU5zdwpuMTFYSGFia3NMOE9CZ2c5cnQ4b1FFVGhRdi9hRHpU +T1c5YURsSk5yYWdlamlCVHdxOTlhWWVaMWdqbzFDWnE0CjJqS3VicENmeVpDNHJHRHRySWZaWWkxcStTMlVjUWh0ZDhEZGh3UWJBb0dBQU00RXBEQTR5SEI1 +eWllazFwL28KQ2JxUkN0YS9EeDZFeW8wS2xOQXlQdUZQQXNodXBHNE5CeDdtVDJBU2ZMKzJWQkhvaTZtSFNyaStCRFg1cnlZRgpmTVl2cDdVUllvcTd3N3Fp +dlJsdnZFZzV5b1lySzEzRjIrR2o2eEo0akVOOW0wS2RNL2czbUpHcTBIQlRJUXJwClNtNzVXWHNmbE94dVRuMDhMYmdHYzRzPQotLS0tLUVORCBSU0EgUFJJ +VkFURSBLRVktLS0tLQ==` +) + func TestNew(t *testing.T) { result := New(&ServerConfig{ Attr: AttributeMap{}, @@ -23,6 +60,128 @@ func TestNew(t *testing.T) { assert.Implements(t, (*IServer)(nil), result) } +func TestServer_Dial(t *testing.T) { + t.Run("fails having no host but with valid root and client certificate files", func(t *testing.T) { + serverConfig := &ServerConfig{ + Host: "", + RootCACert: "./testdata/parsable.cert", + ClientCert: "./testdata/parsable.cert", + ClientKey: "./testdata/parsable.pem", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "connect") + }) + + t.Run("fails with invalid root certificate file", func(t *testing.T) { + serverConfig := &ServerConfig{ + RootCACert: "./testdata/invalid.cert", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "failed to append CA certificate") + }) + + t.Run("fails with non existing root certificate file", func(t *testing.T) { + serverConfig := &ServerConfig{ + RootCACert: "./testdata/nofile.cert", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "no such file or directory") + }) + + t.Run("fails with invalid client certificate file", func(t *testing.T) { + serverConfig := &ServerConfig{ + ClientCert: "./testdata/invalid.cert", + ClientKey: "./testdata/invalid.pem", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "failed to find any PEM data") + }) + + t.Run("fails with non existing client certificate file", func(t *testing.T) { + serverConfig := &ServerConfig{ + ClientCert: "./testdata/nofile.cert", + ClientKey: "./testdata/parsable.pem", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "no such file or directory") + }) + + t.Run("fails having no host but with valid root and client certificate values", func(t *testing.T) { + serverConfig := &ServerConfig{ + Host: "", + RootCACertValue: []string{validCert}, + ClientCertValue: validCert, + ClientKeyValue: validKey, + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "connect") + }) + + t.Run("fails with invalid base64 root certificate value", func(t *testing.T) { + serverConfig := &ServerConfig{ + RootCACertValue: []string{"invalid-certificate"}, + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "illegal base64 data") + }) + + t.Run("fails with invalid root certificate value", func(t *testing.T) { + serverConfig := &ServerConfig{ + RootCACertValue: []string{"aW52YWxpZC1jZXJ0aWZpY2F0ZQ=="}, + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "failed to append CA certificate") + }) + + t.Run("fails with invalid base64 client certificate value", func(t *testing.T) { + serverConfig := &ServerConfig{ + ClientCertValue: "invalid-certificate", + ClientKeyValue: validKey, + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "illegal base64 data") + }) + + t.Run("fails with invalid client certificate value", func(t *testing.T) { + serverConfig := &ServerConfig{ + ClientCertValue: validCert, + ClientKeyValue: "aW52YWxpZC1rZXk=", + } + server := New(serverConfig, &setting.Cfg{}) + + err := server.Dial() + require.Error(t, err) + require.ErrorContains(t, err, "failed to find any PEM data") + }) +} + func TestServer_Close(t *testing.T) { t.Run("close the connection", func(t *testing.T) { connection := &MockConnection{} diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index 3cd6c19ebd3..db905557af6 100644 --- a/pkg/services/ldap/settings.go +++ b/pkg/services/ldap/settings.go @@ -33,13 +33,16 @@ type ServerConfig struct { TLSCiphers []string `toml:"tls_ciphers"` tlsCiphers []uint16 `toml:"-"` - RootCACert string `toml:"root_ca_cert"` - ClientCert string `toml:"client_cert"` - ClientKey string `toml:"client_key"` - BindDN string `toml:"bind_dn"` - BindPassword string `toml:"bind_password"` - Timeout int `toml:"timeout"` - Attr AttributeMap `toml:"attributes"` + RootCACert string `toml:"root_ca_cert"` + RootCACertValue []string + ClientCert string `toml:"client_cert"` + ClientCertValue string + ClientKey string `toml:"client_key"` + ClientKeyValue string + BindDN string `toml:"bind_dn"` + BindPassword string `toml:"bind_password"` + Timeout int `toml:"timeout"` + Attr AttributeMap `toml:"attributes"` SearchFilter string `toml:"search_filter"` SearchBaseDNs []string `toml:"search_base_dns"` diff --git a/pkg/services/ldap/testdata/invalid.cert b/pkg/services/ldap/testdata/invalid.cert new file mode 100644 index 00000000000..d566306ff53 --- /dev/null +++ b/pkg/services/ldap/testdata/invalid.cert @@ -0,0 +1 @@ +invalid certificate file diff --git a/pkg/services/ldap/testdata/invalid.pem b/pkg/services/ldap/testdata/invalid.pem new file mode 100644 index 00000000000..6508d3e078e --- /dev/null +++ b/pkg/services/ldap/testdata/invalid.pem @@ -0,0 +1 @@ +invalid private key file diff --git a/pkg/services/ldap/testdata/parsable.cert b/pkg/services/ldap/testdata/parsable.cert new file mode 100644 index 00000000000..f0623b9305e --- /dev/null +++ b/pkg/services/ldap/testdata/parsable.cert @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+Cgav +Og8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+ +YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc ++TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyix +YFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8 +jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/C +YQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6b +lEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFs +X1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7 +yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7 +NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG +99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2n +aQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pkg/services/ldap/testdata/parsable.pem b/pkg/services/ldap/testdata/parsable.pem new file mode 100644 index 00000000000..604caa58322 --- /dev/null +++ b/pkg/services/ldap/testdata/parsable.pem @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz +SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR +50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq +qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r +DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa +ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa +CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY +y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP +rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5 +PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp +TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1 +1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR +TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX +SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq +V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L +0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz +lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY +n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN +LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9 +gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09 +oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw +n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4 +2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o +CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF +fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp +Sm75WXsflOxuTn08LbgGc4s= +-----END RSA PRIVATE KEY----- \ No newline at end of file