From 74f5eaf47ec2597850516913c614d17e10a8e093 Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Wed, 12 Apr 2017 14:13:34 -0700 Subject: [PATCH] connector/ldap: support the StartTLS flow for secure connections When connecting to an LDAP server, there are three ways to connect: 1. Insecurely through port 389 (LDAP). 2. Securely through port 696 (LDAPS). 3. Insecurely through port 389 then negotiate TLS (StartTLS). This PR adds support for the 3rd flow, letting dex connect to the standard LDAP port then negotiating TLS through the LDAP protocol itself. See a writeup here: http://www.openldap.org/faq/data/cache/185.html --- Documentation/ldap-connector.md | 12 +- connector/ldap/gen-certs.sh | 49 ++++++++ connector/ldap/ldap.go | 18 ++- connector/ldap/ldap_test.go | 191 +++++++++++++++++++++++++---- connector/ldap/testdata/ca.crt | 19 +++ connector/ldap/testdata/ca.key | 27 ++++ connector/ldap/testdata/server.crt | 18 +++ connector/ldap/testdata/server.key | 27 ++++ 8 files changed, 334 insertions(+), 27 deletions(-) create mode 100755 connector/ldap/gen-certs.sh create mode 100644 connector/ldap/testdata/ca.crt create mode 100644 connector/ldap/testdata/ca.key create mode 100644 connector/ldap/testdata/server.crt create mode 100644 connector/ldap/testdata/server.key diff --git a/Documentation/ldap-connector.md b/Documentation/ldap-connector.md index 9dd3e447..174e8486 100644 --- a/Documentation/ldap-connector.md +++ b/Documentation/ldap-connector.md @@ -30,20 +30,28 @@ connectors: name: LDAP config: # Host and optional port of the LDAP server in the form "host:port". - # If the port is not supplied, it will be guessed based on "insecureNoSSL". - # 389 for insecure connections, 636 otherwise. + # If the port is not supplied, it will be guessed based on "insecureNoSSL", + # and "startTLS" flags. 389 for insecure or StartTLS connections, 636 + # otherwise. host: ldap.example.com:636 # Following field is required if the LDAP host is not using TLS (port 389). # Because this option inherently leaks passwords to anyone on the same network # as dex, THIS OPTION MAY BE REMOVED WITHOUT WARNING IN A FUTURE RELEASE. + # # insecureNoSSL: true # If a custom certificate isn't provide, this option can be used to turn on # TLS certificate checks. As noted, it is insecure and shouldn't be used outside # of explorative phases. + # # insecureSkipVerify: true + # When connecting to the server, connect using the ldap:// protocol then issue + # a StartTLS command. If unspecified, connections will use the ldaps:// protocol + # + # startTLS: true + # Path to a trusted root certificate file. Default: use the host's root CA. rootCA: /etc/dex/ldap.ca diff --git a/connector/ldap/gen-certs.sh b/connector/ldap/gen-certs.sh new file mode 100755 index 00000000..8b0ea49b --- /dev/null +++ b/connector/ldap/gen-certs.sh @@ -0,0 +1,49 @@ +#!/bin/bash -e + +# Stolen from the coreos/matchbox repo. + +echo " +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name + +[req_distinguished_name] + +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.101 = localhost +" > openssl.config + +openssl genrsa -out testdata/ca.key 2048 +openssl genrsa -out testdata/server.key 2048 + +openssl req \ + -x509 -new -nodes \ + -key testdata/ca.key \ + -days 10000 -out testdata/ca.crt \ + -subj "/CN=ldap-tests" + +openssl req \ + -new \ + -key testdata/server.key \ + -out testdata/server.csr \ + -subj "/CN=localhost" \ + -config openssl.config + +openssl x509 -req \ + -in testdata/server.csr \ + -CA testdata/ca.crt \ + -CAkey testdata/ca.key \ + -CAcreateserial \ + -out testdata/server.crt \ + -days 10000 \ + -extensions v3_req \ + -extfile openssl.config + +rm testdata/server.csr +rm testdata/ca.srl +rm openssl.config diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 01c8f922..8f541c5e 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -61,6 +61,11 @@ type Config struct { // Don't verify the CA. InsecureSkipVerify bool `json:"insecureSkipVerify"` + // Connect to the insecure port then issue a StartTLS command to negotiate a + // secure connection. If unsupplied secure connections will use the LDAPS + // protocol. + StartTLS bool `json:"startTLS"` + // Path to a trusted root certificate file. RootCA string `json:"rootCA"` @@ -238,9 +243,18 @@ func (c *ldapConnector) do(ctx context.Context, f func(c *ldap.Conn) error) erro conn *ldap.Conn err error ) - if c.InsecureNoSSL { + switch { + case c.InsecureNoSSL: conn, err = ldap.Dial("tcp", c.Host) - } else { + case c.StartTLS: + conn, err = ldap.Dial("tcp", c.Host) + if err != nil { + return fmt.Errorf("failed to connect: %v", err) + } + if err := conn.StartTLS(c.tlsConfig); err != nil { + return fmt.Errorf("start TLS failed: %v", err) + } + default: conn, err = ldap.DialTLS("tcp", c.Host, c.tlsConfig) } if err != nil { diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go index 9e78c346..cd4d69ee 100644 --- a/connector/ldap/ldap_test.go +++ b/connector/ldap/ldap_test.go @@ -21,6 +21,15 @@ import ( const envVar = "DEX_LDAP_TESTS" +// connectionMethod indicates how the test should connect to the LDAP server. +type connectionMethod int32 + +const ( + connectStartTLS connectionMethod = iota + connectLDAPS + connectLDAP +) + // subtest is a login test against a given schema. type subtest struct { // Name of the sub-test. @@ -110,7 +119,7 @@ userpassword: bar }, } - runTests(t, schema, c, tests) + runTests(t, schema, connectLDAP, c, tests) } func TestGroupQuery(t *testing.T) { @@ -198,7 +207,7 @@ member: cn=jane,ou=People,dc=example,dc=org }, } - runTests(t, schema, c, tests) + runTests(t, schema, connectLDAP, c, tests) } func TestGroupsOnUserEntity(t *testing.T) { @@ -295,7 +304,93 @@ gidNumber: 1002 }, }, } - runTests(t, schema, c, tests) + runTests(t, schema, connectLDAP, c, tests) +} + +func TestStartTLS(t *testing.T) { + schema := ` +dn: dc=example,dc=org +objectClass: dcObject +objectClass: organization +o: Example Company +dc: example + +dn: ou=People,dc=example,dc=org +objectClass: organizationalUnit +ou: People + +dn: cn=jane,ou=People,dc=example,dc=org +objectClass: person +objectClass: inetOrgPerson +sn: doe +cn: jane +mail: janedoe@example.com +userpassword: foo +` + c := &Config{} + c.UserSearch.BaseDN = "ou=People,dc=example,dc=org" + c.UserSearch.NameAttr = "cn" + c.UserSearch.EmailAttr = "mail" + c.UserSearch.IDAttr = "DN" + c.UserSearch.Username = "cn" + + tests := []subtest{ + { + name: "validpassword", + username: "jane", + password: "foo", + want: connector.Identity{ + UserID: "cn=jane,ou=People,dc=example,dc=org", + Username: "jane", + Email: "janedoe@example.com", + EmailVerified: true, + }, + }, + } + runTests(t, schema, connectStartTLS, c, tests) +} + +func TestLDAPS(t *testing.T) { + schema := ` +dn: dc=example,dc=org +objectClass: dcObject +objectClass: organization +o: Example Company +dc: example + +dn: ou=People,dc=example,dc=org +objectClass: organizationalUnit +ou: People + +dn: cn=jane,ou=People,dc=example,dc=org +objectClass: person +objectClass: inetOrgPerson +sn: doe +cn: jane +mail: janedoe@example.com +userpassword: foo +` + c := &Config{} + c.UserSearch.BaseDN = "ou=People,dc=example,dc=org" + c.UserSearch.NameAttr = "cn" + c.UserSearch.EmailAttr = "mail" + c.UserSearch.IDAttr = "DN" + c.UserSearch.Username = "cn" + + tests := []subtest{ + { + name: "validpassword", + username: "jane", + password: "foo", + want: connector.Identity{ + UserID: "cn=jane,ou=People,dc=example,dc=org", + Username: "jane", + Email: "janedoe@example.com", + EmailVerified: true, + }, + }, + } + runTests(t, schema, connectLDAPS, c, tests) } // runTests runs a set of tests against an LDAP schema. It does this by @@ -305,7 +400,7 @@ gidNumber: 1002 // machine's PATH. // // The DEX_LDAP_TESTS must be set to "1" -func runTests(t *testing.T, schema string, config *Config, tests []subtest) { +func runTests(t *testing.T, schema string, connMethod connectionMethod, config *Config, tests []subtest) { if os.Getenv(envVar) != "1" { t.Skipf("%s not set. Skipping test (run 'export %s=1' to run tests)", envVar, envVar) } @@ -316,6 +411,11 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) { } } + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + tempDir, err := ioutil.TempDir("", "") if err != nil { t.Fatal(err) @@ -324,7 +424,13 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) { configBytes := new(bytes.Buffer) - if err := slapdConfigTmpl.Execute(configBytes, tmplData{tempDir, includes(t)}); err != nil { + data := tmplData{ + TempDir: tempDir, + Includes: includes(t, wd), + } + data.TLSCertPath, data.TLSKeyPath = tlsAssets(t, wd) + + if err := slapdConfigTmpl.Execute(configBytes, data); err != nil { t.Fatal(err) } @@ -344,7 +450,7 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) { cmd := exec.Command( "slapd", "-d", "any", - "-h", "ldap://localhost:10363/ ldaps://localhost:10636/ ldapi://"+socketPath, + "-h", "ldap://localhost:10389/ ldaps://localhost:10636/ ldapi://"+socketPath, "-f", configPath, ) cmd.Stdout = slapdOut @@ -385,18 +491,30 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) { wg.Wait() }() - // Wait for slapd to come up. - time.Sleep(100 * time.Millisecond) + // Try a few times to connect to the LDAP server. On slower machines + // it can take a while for it to come up. + connected := false + wait := 100 * time.Millisecond + for i := 0; i < 5; i++ { + time.Sleep(wait) - ldapadd := exec.Command( - "ldapadd", "-x", - "-D", "cn=admin,dc=example,dc=org", - "-w", "admin", - "-f", schemaPath, - "-H", "ldap://localhost:10363/", - ) - if out, err := ldapadd.CombinedOutput(); err != nil { - t.Errorf("ldapadd: %s", out) + ldapadd := exec.Command( + "ldapadd", "-x", + "-D", "cn=admin,dc=example,dc=org", + "-w", "admin", + "-f", schemaPath, + "-H", "ldap://localhost:10389/", + ) + if out, err := ldapadd.CombinedOutput(); err != nil { + t.Logf("ldapadd: %s", out) + wait = wait * 2 // backoff + continue + } + connected = true + break + } + if !connected { + t.Errorf("ldapadd command failed") return } @@ -405,8 +523,19 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) { // We need to configure host parameters but don't want to overwrite user or // group search configuration. - c.Host = "localhost:10363" - c.InsecureNoSSL = true + switch connMethod { + case connectStartTLS: + c.Host = "localhost:10389" + c.RootCA = "testdata/ca.crt" + c.StartTLS = true + case connectLDAPS: + c.Host = "localhost:10636" + c.RootCA = "testdata/ca.crt" + case connectLDAP: + c.Host = "localhost:10389" + c.InsecureNoSSL = true + } + c.BindDN = "cn=admin,dc=example,dc=org" c.BindPW = "admin" @@ -488,10 +617,16 @@ type tmplData struct { TempDir string // List of schema files to include. Includes []string + // TLS assets for LDAPS. + TLSKeyPath string + TLSCertPath string } // Config template copied from: // http://www.zytrax.com/books/ldap/ch5/index.html#step1-slapd +// +// TLS instructions found here: +// http://www.openldap.org/doc/admin24/tls.html var slapdConfigTmpl = template.Must(template.New("").Parse(` {{ range $i, $include := .Includes }} include {{ $include }} @@ -511,6 +646,9 @@ rootpw admin # change path as necessary directory {{ .TempDir }} +TLSCertificateFile {{ .TLSCertPath }} +TLSCertificateKeyFile {{ .TLSKeyPath }} + # Indices to maintain for this directory # unique id so equality match only index uid eq @@ -534,11 +672,18 @@ cachesize 10000 checkpoint 128 15 `)) -func includes(t *testing.T) (paths []string) { - wd, err := os.Getwd() - if err != nil { - t.Fatalf("getting working directory: %v", err) +func tlsAssets(t *testing.T, wd string) (certPath, keyPath string) { + certPath = filepath.Join(wd, "testdata", "server.crt") + keyPath = filepath.Join(wd, "testdata", "server.key") + for _, p := range []string{certPath, keyPath} { + if _, err := os.Stat(p); err != nil { + t.Fatalf("failed to find TLS asset file: %s %v", p, err) + } } + return +} + +func includes(t *testing.T, wd string) (paths []string) { for _, f := range includeFiles { p := filepath.Join(wd, "testdata", f) if _, err := os.Stat(p); err != nil { diff --git a/connector/ldap/testdata/ca.crt b/connector/ldap/testdata/ca.crt new file mode 100644 index 00000000..1bfd9697 --- /dev/null +++ b/connector/ldap/testdata/ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/TCCAeWgAwIBAgIJAIrt+AlVUsXKMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV +BAMMCmxkYXAtdGVzdHMwHhcNMTcwNDEyMjAxNzI5WhcNNDQwODI4MjAxNzI5WjAV +MRMwEQYDVQQDDApsZGFwLXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAzKJkt2WsALUDA3tQsedx7UJKIxis05+dU5FbBxf/BMSch8gCNh/cWErH +IDljWGwLKbc9UefIz3BzbcNBPLgLGMp7t9Pf9HCBNf7lShLZB2BEGpgpCpd0urox +xTqMEfchssJj75HOZRweHfBDDHk8LMHQYUBn5qTiuMYvBUbPVq69argE/kt5yAEW +COZzzx38a11iY0gtPjY4Tc9vICsLHhTssNn/1wf+GFNzSTHqijC7NKW0txUneFQJ +h6LAmKV/uZC84W1tqMDZKKpABiTpB+JbDvwsb9eXJ6YG6TgbKcrXjLy4ogbIrIRA +s2DqMih792mxusIl6lRf3hTtCdyodwIDAQABo1AwTjAdBgNVHQ4EFgQUnfj9sAq4 +2xBbV4rf5FNvYaE2Bg0wHwYDVR0jBBgwFoAUnfj9sAq42xBbV4rf5FNvYaE2Bg0w +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAFGnBH1qpLJLvrLWKNI5w +u8pFYO3RGqmfJ3BGf60MQxdUaTIUNQxPfPATbth7t8GRJwpWESRDlaXWq9fM9rkt +fbmuqjAMGTFloNd9ra6e2F0CKjwZWcn/3eG/mVw/5d1Ku9Ow8luKrZuzNzVJd13r +hoNc1wYXN0pHWkNiRUuR/E4fE/sn+tYOpJ4XYQvKAcSrNrq8m5O9VG5gLvlTeNno +6q9hBy+5XKYUdHlzbAGm9QL0e1R45Mu4qxcFluKEmzS1rXlLsLs4/pqHgreXlYgL +f7K0cFvaJGnFRKaxa6Bpf1EPNtqSc/pQZh01Ww8CUu1xh2+5KufgJQjAHVG3a1ow +dQ== +-----END CERTIFICATE----- diff --git a/connector/ldap/testdata/ca.key b/connector/ldap/testdata/ca.key new file mode 100644 index 00000000..551bd7f2 --- /dev/null +++ b/connector/ldap/testdata/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAzKJkt2WsALUDA3tQsedx7UJKIxis05+dU5FbBxf/BMSch8gC +Nh/cWErHIDljWGwLKbc9UefIz3BzbcNBPLgLGMp7t9Pf9HCBNf7lShLZB2BEGpgp +Cpd0uroxxTqMEfchssJj75HOZRweHfBDDHk8LMHQYUBn5qTiuMYvBUbPVq69argE +/kt5yAEWCOZzzx38a11iY0gtPjY4Tc9vICsLHhTssNn/1wf+GFNzSTHqijC7NKW0 +txUneFQJh6LAmKV/uZC84W1tqMDZKKpABiTpB+JbDvwsb9eXJ6YG6TgbKcrXjLy4 +ogbIrIRAs2DqMih792mxusIl6lRf3hTtCdyodwIDAQABAoIBAHQpEucQbe0Q058c +VxhF+2PlJ1R441JV3ubbMkL6mibIvNpO7QJwX5I3EIX4Ta6Z1lRd0g82dcVbXgrG +tbeT+aie+E/Hk++cFZzjDqFXxZ7sRHycN1/tzbNZknsU2wIvuQ9STYxmxjSbG3V/ +N3BTOZdmhbVO7Cv/GTwuM+7Y3UWkc74HaXfAgo1UIO9MtqgqP3H1Tv6ZIeKzl+mP +wrvei0eQe6jI4W6+vUOX3SlrlrMxMTLK/Ce2MP1pJx++m8Ga23+vtna+lkOWnwcD +NmhYl4dL31sDcE6Hz/T6Wwfdlfyugw8vi3a3GEYGMIwy27CFf/ccYnWPOI3oIHDe +RwlXLCECgYEA595xJmfUpwqgYY80pT3JG3+64NWJ7f/gH0Ey9fivZfnTegjkI2Kc +Uf7+odCq9I1TFtx10M72N4pXT1uLzJtINYty4ZIfOLG7jSraVbOuf9AvMNCYw+cT +Fcf/HGUJEE95TKYDrGfklOYFNs3ZCcKOCYJOWCuwki8Vm2vtJpV6gnkCgYEA4e5b +DI+YworLjokY8eP4aOF5BMuiFdGkYDjVQZG45RwjJdLwBjaf+HA4pAuJAr2LWiLX +cdKpk+3AlJ8UMLIM+hBP4hBqnrPaRTkEhTXpbUA1lvL9o0mVDFgNh90guu5TeJza +sW7JLaStmAyCxYGxbW4LTjR8GX9DPOPmLs5ZRm8CgYAyFW5DaXIZksYJzLEGcE4c +Tn7DSdy9N+PlXGPxlYHteQUg+wKsUgSKAZZmxXfn0w77hSs9qzar0IoDbjbIP1Jd +nn12E+YCjQGCAJugn2s12HYZCTW2Oxd4QPbt3zUR/NiqocFxYA+TygueRuB2pzue ++jKKAQXmzZzRMYLMLsWDoQKBgAnrCcoyX5VivG7ka9jqlhQcmdBxFAt7KYkj1ZDM +Ud6U7qIRcYIEUd95JbNl4jzhj0WEtAqGIfWhgUvE9ADzQAiWQLt+1v9ii9lwGFe0 +tyuZnwCiaCoL5+Qj1Ww6c95g6f8oe51AbMp5KTm8it0axWw1YX+sZCpGYPBCXO9/ +FYI3AoGBAMacjjbPjjfOXxBRhRz1rEDTrIStDj5KM4fgslKVGysqpH/mw7gSC8SK +qn0anL2s3SAe9PQpOzM3pFFRZx4XMOk4ojYRZtp3FjPFDRnYuYzkfkbU7eV04awO +6nrua8KNLNK+ir9iCi46tP6Zr3F81zWGUoVArVUgCRDbA9e0swB0 +-----END RSA PRIVATE KEY----- diff --git a/connector/ldap/testdata/server.crt b/connector/ldap/testdata/server.crt new file mode 100644 index 00000000..a4b8358f --- /dev/null +++ b/connector/ldap/testdata/server.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC3DCCAcSgAwIBAgIJANsmsx7hUWnHMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV +BAMMCmxkYXAtdGVzdHMwHhcNMTcwNDEyMjAxNzI5WhcNNDQwODI4MjAxNzI5WjAU +MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDlWGC5X/TWgysEimM7n0hSkXRCITwAFxKG0C4EeppmL42DBcjQa0xrElRF +h57EBZltbSfvTMDBZAyhx5oZKoETDfwy5jFzf4L4PazSkvfn4qWmCnrq4HNO5Vl7 +GBsW93bljsh2nfvoKDX2vBpEUe0qrZzJtRHq0ytfd6zXZ9+WFMsmhD9poADrH4hB +/UOV3uCJPybOoy/WsANQpSgJPD886zakmF+54XQ3tExKzFA1rR4HJbU26h99U5kH +346sV7/xKJLENQVIH1qsqyA1UPDZRWusABjdIPc9Racy0/MxTVE0k5lQbBvz9QSe +HZvW+ct/aZX5tjxr9JlSY7tK2I9FAgMBAAGjMDAuMAkGA1UdEwQCMAAwCwYDVR0P +BAQDAgXgMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEA +RZp/fNjoQNaO6KW0Ay0aaPW6jPrcqGjzFgeIXaw/0UaWm5jhptWtjOAILV+afIrd +4cKDg65o4xRdQYYbqmutFMAO/DeyDyMi3IL60qk0osipPDIORx5Ai2ZBQvUsGtwV +np9UwQGNO5AGeR9N5kndyldbpxaIJFhsKOV8uRSi+4PRbMH3G0kJIX6wwZU4Ri/k +3lWJQfqULH0vtMQCWSJuaYHxWYFq4AM+H/zpLwg1WG2eKVgSMWotxMRi5LOFSBbG +XuOxAb0SNBcXl6kjRYbQyHBxIJMsB1lk64g7dTJqXuYFUwmIGL/vTr6PL6EKYk65 +/aWO8cvwXOrYaf9umgcqvg== +-----END CERTIFICATE----- diff --git a/connector/ldap/testdata/server.key b/connector/ldap/testdata/server.key new file mode 100644 index 00000000..20fd6520 --- /dev/null +++ b/connector/ldap/testdata/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA5VhguV/01oMrBIpjO59IUpF0QiE8ABcShtAuBHqaZi+NgwXI +0GtMaxJURYeexAWZbW0n70zAwWQMoceaGSqBEw38MuYxc3+C+D2s0pL35+Klpgp6 +6uBzTuVZexgbFvd25Y7Idp376Cg19rwaRFHtKq2cybUR6tMrX3es12fflhTLJoQ/ +aaAA6x+IQf1Dld7giT8mzqMv1rADUKUoCTw/POs2pJhfueF0N7RMSsxQNa0eByW1 +NuoffVOZB9+OrFe/8SiSxDUFSB9arKsgNVDw2UVrrAAY3SD3PUWnMtPzMU1RNJOZ +UGwb8/UEnh2b1vnLf2mV+bY8a/SZUmO7StiPRQIDAQABAoIBAQDHBbKqK4MkxB8I +ia8jhk4UmPTyjjSrP1pscyv75wkltA5xrQtfEj32jKlkzRQRt2o1c4w8NbbwHAp6 +OeSYAjKQfoplAS3YtMbK9XqMIc3QBPcK5/1S5gQqaw0DrR+VBpq/CvEbPm3kQUDT +JNkGgLH3X0G4KNGrniT9a7UqGJIGgdBAr7bPESiDi9wuOwfhm/9TB8LOG8wB9cn4 +NcUipvjOcRxMFkyYtq056ZfGeoK2ooFe0lHi4j8sWXfII789OqN0plecAg8NGZsl +klSncpTObE6eTXo9Jncio3pftvszEctKssK7vuL6opajtppT6C5FnKLb6NIAOo7j +CPk1BRPhAoGBAPf8TMTr+l8MHRuVXEx52E1dBH46ZB8bMfvwb7cZ31Fn0EEmygCj +wP9eKZ8MKmHVBbU6CbxYQMICTTwRrw9H0tNoaZBwzWMz/JDHcACfsPKtfrX8T4UQ +wmVwbLctdC1Cbaxn1jYeSLoLfSe8IGPDnLpsMCzpRcQIgPS+gO69zr8vAoGBAOzB +254TKd2OQPnvUvmAVYGRYyTu/+ShH9fZyDJYtjhQbuxt6eqh3poneWJOW+KPlqDd +J0a8yv1pDXmCy5k1Oo8Nubt7cPI0y2z0nm5LvAaqPaFdUJs9nq9umH3svJh6du6Z ++TZ6MDU/eyJRq7Mc5SQrssziJidS3cU21b560xvLAoGBAPYpZY9Ia7Uz0iUSY5eq +j7Nj9VTT45UZKsnbRxnrvckSEyDJP1XZN3iG4Sv3KI8KpWrbHNTwif/Lxx0stKin +dDjU+Y0e3FJwRXL19lE4M68B17kQp2MAWufU7KX8oclXmoS8YmBAOZMsWmU6ErDV +eVt4j23VdaJ9inzoKhZTJcqTAoGAH9znJZsGo16lt/1ReWqgF1Ptt+bCYY6drnsM +ylnODD4m74LLXFx0jOKLH4PUMeWJLBUXWBnIZ9pfid7kb7YOL3p1aJnwVWhtiDhT +qhxfLbZznOfmFT5xwMJtm2Tk7NBueSYXuBExs7jbZX8AUJau7/NBmPlGkTxBxGzg +z0XQa4kCgYBxYBXwFpLLjBO+bMMkoVOlMDj7feCOWP9CsnKQSHYqPbmmb+8mA7pN +mIWfjSVynVe+Ncn0I5Uijbs9QDYqcfApJQ+iXeb+VGrg4QkLHHGd/5kIY28Evc6A +KVyRIuiYNmgOXGpaFpMXSw718N4U7jWW7lqUxK2rvEupFhaL52oJFQ== +-----END RSA PRIVATE KEY-----