Merge pull request #908 from ericchiang/start-tls

connector/ldap: support the StartTLS flow for secure connections
This commit is contained in:
Eric Chiang 2017-04-12 17:03:55 -07:00 committed by GitHub
commit e609de5018
8 changed files with 334 additions and 27 deletions

View file

@ -30,20 +30,28 @@ connectors:
name: LDAP name: LDAP
config: config:
# Host and optional port of the LDAP server in the form "host:port". # 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". # If the port is not supplied, it will be guessed based on "insecureNoSSL",
# 389 for insecure connections, 636 otherwise. # and "startTLS" flags. 389 for insecure or StartTLS connections, 636
# otherwise.
host: ldap.example.com:636 host: ldap.example.com:636
# Following field is required if the LDAP host is not using TLS (port 389). # 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 # 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. # as dex, THIS OPTION MAY BE REMOVED WITHOUT WARNING IN A FUTURE RELEASE.
#
# insecureNoSSL: true # insecureNoSSL: true
# If a custom certificate isn't provide, this option can be used to turn on # 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 # TLS certificate checks. As noted, it is insecure and shouldn't be used outside
# of explorative phases. # of explorative phases.
#
# insecureSkipVerify: true # 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. # Path to a trusted root certificate file. Default: use the host's root CA.
rootCA: /etc/dex/ldap.ca rootCA: /etc/dex/ldap.ca

49
connector/ldap/gen-certs.sh Executable file
View file

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

View file

@ -61,6 +61,11 @@ type Config struct {
// Don't verify the CA. // Don't verify the CA.
InsecureSkipVerify bool `json:"insecureSkipVerify"` 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. // Path to a trusted root certificate file.
RootCA string `json:"rootCA"` 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 conn *ldap.Conn
err error err error
) )
if c.InsecureNoSSL { switch {
case c.InsecureNoSSL:
conn, err = ldap.Dial("tcp", c.Host) 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) conn, err = ldap.DialTLS("tcp", c.Host, c.tlsConfig)
} }
if err != nil { if err != nil {

View file

@ -21,6 +21,15 @@ import (
const envVar = "DEX_LDAP_TESTS" 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. // subtest is a login test against a given schema.
type subtest struct { type subtest struct {
// Name of the sub-test. // 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) { 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) { 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 // runTests runs a set of tests against an LDAP schema. It does this by
@ -305,7 +400,7 @@ gidNumber: 1002
// machine's PATH. // machine's PATH.
// //
// The DEX_LDAP_TESTS must be set to "1" // 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" { if os.Getenv(envVar) != "1" {
t.Skipf("%s not set. Skipping test (run 'export %s=1' to run tests)", envVar, envVar) 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("", "") tempDir, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -324,7 +424,13 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
configBytes := new(bytes.Buffer) 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) t.Fatal(err)
} }
@ -344,7 +450,7 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
cmd := exec.Command( cmd := exec.Command(
"slapd", "slapd",
"-d", "any", "-d", "any",
"-h", "ldap://localhost:10363/ ldaps://localhost:10636/ ldapi://"+socketPath, "-h", "ldap://localhost:10389/ ldaps://localhost:10636/ ldapi://"+socketPath,
"-f", configPath, "-f", configPath,
) )
cmd.Stdout = slapdOut cmd.Stdout = slapdOut
@ -385,18 +491,30 @@ func runTests(t *testing.T, schema string, config *Config, tests []subtest) {
wg.Wait() wg.Wait()
}() }()
// Wait for slapd to come up. // Try a few times to connect to the LDAP server. On slower machines
time.Sleep(100 * time.Millisecond) // 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 := exec.Command(
"ldapadd", "-x", "ldapadd", "-x",
"-D", "cn=admin,dc=example,dc=org", "-D", "cn=admin,dc=example,dc=org",
"-w", "admin", "-w", "admin",
"-f", schemaPath, "-f", schemaPath,
"-H", "ldap://localhost:10363/", "-H", "ldap://localhost:10389/",
) )
if out, err := ldapadd.CombinedOutput(); err != nil { if out, err := ldapadd.CombinedOutput(); err != nil {
t.Errorf("ldapadd: %s", out) t.Logf("ldapadd: %s", out)
wait = wait * 2 // backoff
continue
}
connected = true
break
}
if !connected {
t.Errorf("ldapadd command failed")
return 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 // We need to configure host parameters but don't want to overwrite user or
// group search configuration. // group search configuration.
c.Host = "localhost:10363" 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.InsecureNoSSL = true
}
c.BindDN = "cn=admin,dc=example,dc=org" c.BindDN = "cn=admin,dc=example,dc=org"
c.BindPW = "admin" c.BindPW = "admin"
@ -488,10 +617,16 @@ type tmplData struct {
TempDir string TempDir string
// List of schema files to include. // List of schema files to include.
Includes []string Includes []string
// TLS assets for LDAPS.
TLSKeyPath string
TLSCertPath string
} }
// Config template copied from: // Config template copied from:
// http://www.zytrax.com/books/ldap/ch5/index.html#step1-slapd // 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(` var slapdConfigTmpl = template.Must(template.New("").Parse(`
{{ range $i, $include := .Includes }} {{ range $i, $include := .Includes }}
include {{ $include }} include {{ $include }}
@ -511,6 +646,9 @@ rootpw admin
# change path as necessary # change path as necessary
directory {{ .TempDir }} directory {{ .TempDir }}
TLSCertificateFile {{ .TLSCertPath }}
TLSCertificateKeyFile {{ .TLSKeyPath }}
# Indices to maintain for this directory # Indices to maintain for this directory
# unique id so equality match only # unique id so equality match only
index uid eq index uid eq
@ -534,11 +672,18 @@ cachesize 10000
checkpoint 128 15 checkpoint 128 15
`)) `))
func includes(t *testing.T) (paths []string) { func tlsAssets(t *testing.T, wd string) (certPath, keyPath string) {
wd, err := os.Getwd() certPath = filepath.Join(wd, "testdata", "server.crt")
if err != nil { keyPath = filepath.Join(wd, "testdata", "server.key")
t.Fatalf("getting working directory: %v", err) 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 { for _, f := range includeFiles {
p := filepath.Join(wd, "testdata", f) p := filepath.Join(wd, "testdata", f)
if _, err := os.Stat(p); err != nil { if _, err := os.Stat(p); err != nil {

19
connector/ldap/testdata/ca.crt vendored Normal file
View file

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

27
connector/ldap/testdata/ca.key vendored Normal file
View file

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

18
connector/ldap/testdata/server.crt vendored Normal file
View file

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

27
connector/ldap/testdata/server.key vendored Normal file
View file

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