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-----