From e8d3d2597d655e36beb7aec48aca3a25dd717b44 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Wed, 26 Jul 2023 15:58:20 +0300 Subject: [PATCH 1/4] ci: bump latest EE SDK Run with release 2.11.0 SDK instead of a dev one. 1. https://github.com/tarantool/tt/commit/12bf40453614a89a4c898c2ad8e2da739e6de0af --- .github/workflows/testing.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 61e185789..268a6a3ea 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -143,8 +143,8 @@ jobs: - sdk-version: 'bundle-2.10.0-1-gfa775b383-r486-linux-x86_64' coveralls: false ssl: true - - sdk-path: 'dev/linux/x86_64/master/' - sdk-version: 'sdk-gc64-2.11.0-entrypoint-113-g803baaffe-r529.linux.x86_64' + - sdk-path: 'release/linux/x86_64/2.11/' + sdk-version: 'sdk-gc64-2.11.0-0-r577.linux.x86_64' coveralls: true ssl: true From cccc4612d38ce7753867b992bd7cec9bafbebbfc Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 15:36:47 +0300 Subject: [PATCH 2/4] test: remove core tarantool server SSL cases "empty" and "key_crt_client" test cases do not provide SSL files to a server started with SSL transport. In these cases server fails to start, and tests ensures that server fails. It doesn't related to go-tarantool connector testing in any way -- it's the test of a tarantool binary. Since testing core tarantool is not the part of go-tarantool project, this patch removes these cases. The main motivation of this patch is the next commit in the patchset, which separates check for server start and client success of fail. --- ssl_test.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ssl_test.go b/ssl_test.go index 12cab7304..0b751a1cb 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -214,21 +214,6 @@ CaFile - optional, Ciphers - optional */ var tests = []test{ - { - "empty", - false, - SslOpts{}, - SslOpts{}, - }, - { - "key_crt_client", - false, - SslOpts{}, - SslOpts{ - KeyFile: "testdata/localhost.key", - CertFile: "testdata/localhost.crt", - }, - }, { "key_crt_server", true, From 09b35c757ca3a40f0e013ef0e88ab07e8ad8615b Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 15:34:38 +0300 Subject: [PATCH 3/4] test: separate SSL server and client check Current SSL tests are as follows. We start a Tarantool server with default helpers. "ok" tests are successful if everything had started, "fail" tests are successful if ping check had failed (aka we failed to connect). This is a dangerous approach, since "server had failed to start" here is indistinguishable from "client cannot connect". Moreover, because of it each tnt_fail test runs for 5 seconds (10 retry attempts * 500 ms retry wait), which is frustrating. After this patch, there is a separate check for a server start and for a client success or fail. --- ssl_test.go | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/ssl_test.go b/ssl_test.go index 0b751a1cb..3243788c8 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -94,7 +94,7 @@ func createClientServerSslOk(t testing.TB, serverOpts, return l, c, msgs, errs } -func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { +func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, error) { listen := tntHost + "?transport=ssl&" key := serverOpts.KeyFile @@ -126,7 +126,7 @@ func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.Tarantoo SslCertsDir: "testdata", ClientServer: tntHost, ClientTransport: "ssl", - ClientSsl: clientOpts, + ClientSsl: serverOpts, User: "test", Pass: "test", WaitStart: 100 * time.Millisecond, @@ -139,6 +139,23 @@ func serverTntStop(inst test_helpers.TarantoolInstance) { test_helpers.StopTarantoolWithCleanup(inst) } +func checkTntConn(clientOpts SslOpts) error { + conn, err := Connect(tntHost, Opts{ + Auth: AutoAuth, + Timeout: 500 * time.Millisecond, + User: "test", + Pass: "test", + SkipSchema: true, + Transport: "ssl", + Ssl: clientOpts, + }) + if err != nil { + return err + } + conn.Close() + return nil +} + func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() @@ -171,9 +188,13 @@ func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) - serverTntStop(inst) + inst, err := serverTnt(serverOpts, AutoAuth) + defer serverTntStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + err = checkTntConn(clientOpts) if err == nil { t.Errorf("An unexpected connection to the server") } @@ -182,11 +203,15 @@ func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { t.Helper() - inst, err := serverTnt(serverOpts, clientOpts, AutoAuth) - serverTntStop(inst) + inst, err := serverTnt(serverOpts, AutoAuth) + defer serverTntStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + err = checkTntConn(clientOpts) if err != nil { - t.Errorf("An unexpected server error %q", err.Error()) + t.Errorf("An unexpected connection error %q", err.Error()) } } @@ -470,10 +495,11 @@ func TestOpts_PapSha256Auth(t *testing.T) { KeyFile: "testdata/localhost.key", CertFile: "testdata/localhost.crt", } - inst, err := serverTnt(sslOpts, sslOpts, PapSha256Auth) + + inst, err := serverTnt(sslOpts, PapSha256Auth) defer serverTntStop(inst) if err != nil { - t.Errorf("An unexpected server error: %s", err) + t.Fatalf("An unexpected server error %q", err.Error()) } clientOpts := opts From afa063be29c8e1d3c05d96edaf3ac1569795d8bb Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 16:00:15 +0300 Subject: [PATCH 4/4] api: support SSL private key file decryption Support `ssl_password` and `ssl_password_file` options in SslOpts. Tarantool EE supports SSL passwords and password files since 2.11.0 [1]. Since it is possible to use corresponding non-encrypted key, cert and CA on server, tests works fine even for Tarantool EE 2.10.0. Same as in Tarantool, we try `SslOpts.Password`, then each line in `SslOpts.PasswordFile`. If all of the above fail, we re-raise errors. If the key is encrypted and password is not provided, `openssl.LoadPrivateKeyFromPEM(keyBytes)` asks to enter PEM pass phrase interactively. On the other hand, `openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)` works fine for non-encrypted key with any password, including empty string. If the key is encrypted, we fast fail with password error instead of requesting the pass phrase interactively. The patch also bumps go-openssl since latest patch fixes flaky tests [2]. The patch is based on a similar patch for tarantool-python [3]. 1. https://github.com/tarantool/tarantool-ee/issues/22 2. https://github.com/tarantool/go-openssl/pull/9 3. https://github.com/tarantool/tarantool-python/pull/274 --- CHANGELOG.md | 1 + connection.go | 8 ++ go.mod | 2 +- go.sum | 4 +- ssl.go | 48 +++++++++-- ssl_test.go | 168 +++++++++++++++++++++++++++++++++++++ testdata/generate.sh | 13 +++ testdata/invalidpasswords | 1 + testdata/localhost.enc.key | 30 +++++++ testdata/passwords | 2 + 10 files changed, 267 insertions(+), 10 deletions(-) create mode 100644 testdata/invalidpasswords create mode 100644 testdata/localhost.enc.key create mode 100644 testdata/passwords diff --git a/CHANGELOG.md b/CHANGELOG.md index bf229b143..79486609d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - IsNullable flag for Field (#302) - More linters on CI (#310) - Meaningful description for read/write socket errors (#129) +- Support password and password file to decrypt private SSL key file (#319) ### Changed diff --git a/connection.go b/connection.go index 37de22e82..9bb42626a 100644 --- a/connection.go +++ b/connection.go @@ -345,6 +345,14 @@ type SslOpts struct { // // * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html Ciphers string + // Password is a password for decrypting the private SSL key file. + // The priority is as follows: try to decrypt with Password, then + // try PasswordFile. + Password string + // PasswordFile is a path to the list of passwords for decrypting + // the private SSL key file. The connection tries every line from the + // file as a password. + PasswordFile string } // Clone returns a copy of the Opts object. diff --git a/go.mod b/go.mod index 44bc63483..bd848308c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.7.1 github.com/tarantool/go-iproto v0.1.0 - github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 + github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect diff --git a/go.sum b/go.sum index d38645c63..1810c2b3a 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ= github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= -github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU= -github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a h1:eeElglRXJ3xWKkHmDbeXrQWlZyQ4t3Ca1YlZsrfdXFU= +github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= diff --git a/ssl.go b/ssl.go index d9373ace2..a23238849 100644 --- a/ssl.go +++ b/ssl.go @@ -4,9 +4,12 @@ package tarantool import ( + "bufio" "errors" "io/ioutil" "net" + "os" + "strings" "time" "github.com/tarantool/go-openssl" @@ -43,7 +46,7 @@ func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { } if opts.KeyFile != "" { - if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil { + if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil { return } } @@ -95,16 +98,47 @@ func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { return } -func sslLoadKey(ctx *openssl.Ctx, keyFile string) (err error) { +func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string, + passwordFile string) error { var keyBytes []byte + var err, firstDecryptErr error + if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { - return + return err } - var key openssl.PrivateKey - if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil { - return + // If the key is encrypted and password is not provided, + // openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase + // interactively. On the other hand, + // openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine + // for non-encrypted key with any password, including empty string. If + // the key is encrypted, we fast fail with password error instead of + // requesting the pass phrase interactively. + passwords := []string{password} + if passwordFile != "" { + file, err := os.Open(passwordFile) + if err == nil { + defer file.Close() + + scanner := bufio.NewScanner(file) + // Tarantool itself tries each password file line. + for scanner.Scan() { + password = strings.TrimSpace(scanner.Text()) + passwords = append(passwords, password) + } + } else { + firstDecryptErr = err + } + } + + for _, password := range passwords { + key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) + if err == nil { + return ctx.UsePrivateKey(key) + } else if firstDecryptErr == nil { + firstDecryptErr = err + } } - return ctx.UsePrivateKey(key) + return firstDecryptErr } diff --git a/ssl_test.go b/ssl_test.go index 3243788c8..30078703c 100644 --- a/ssl_test.go +++ b/ssl_test.go @@ -117,6 +117,16 @@ func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, e listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers) } + password := serverOpts.Password + if password != "" { + listen += fmt.Sprintf("ssl_password=%s&", password) + } + + passwordFile := serverOpts.PasswordFile + if passwordFile != "" { + listen += fmt.Sprintf("ssl_password_file=%s&", passwordFile) + } + listen = listen[:len(listen)-1] return test_helpers.StartTarantool(test_helpers.StartOpts{ @@ -441,6 +451,164 @@ var tests = []test{ Ciphers: "TLS_AES_128_GCM_SHA256", }, }, + { + "pass_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + }, + }, + { + "passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "inv_pass_and_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_inv_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "pass_and_not_existing_passfile_key_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/notafile", + }, + }, + { + "inv_pass_and_inv_passfile_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "not_existing_passfile_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/notafile", + }, + }, + { + "no_pass_key_encrypt_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "pass_key_non_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + }, + }, + { + "passfile_key_non_encrypt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/invalidpasswords", + }, + }, } func isTestTntSsl() bool { diff --git a/testdata/generate.sh b/testdata/generate.sh index f29f41c90..4b8cf3630 100755 --- a/testdata/generate.sh +++ b/testdata/generate.sh @@ -23,3 +23,16 @@ openssl x509 -outform pem -in ca.pem -out ca.crt openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost" openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt +password=mysslpassword + +# Tarantool tries every line from the password file. +cat < passwords +unusedpassword +$password +EOF + +cat < invalidpasswords +unusedpassword1 +EOF + +openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key diff --git a/testdata/invalidpasswords b/testdata/invalidpasswords new file mode 100644 index 000000000..b09d795aa --- /dev/null +++ b/testdata/invalidpasswords @@ -0,0 +1 @@ +unusedpassword1 diff --git a/testdata/localhost.enc.key b/testdata/localhost.enc.key new file mode 100644 index 000000000..b881820a3 --- /dev/null +++ b/testdata/localhost.enc.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIm+0WC9xe38cCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBNOE4KD+yauMfsnOiNAaaZBIIE +0DtXaHGpacJ8MjjL6zciYhgJOD9SJHE4vwPxpNDWuS9mf6wk/cdBNFMqnYwJmlYw +J/eQ+Z8MsZUqjnhDQz9YXXd8JftexAAa1bHnmfv2N/czJCx57dAHVdmJzgibfp18 +GCpqR23tklEO2Nj2HCbR59rh7IsnW9mD6jh+mVtkOix5HMCUSxwc3bEUutIQE80P +JHG2BsEfAeeHZa+QgG3Y15c6uSXD6wY73ldPPOgZ3NFOqcw/RDqYf1zsohx7auxi +Y6zHA7LdYtQjbNJ5slIfxPhAh75Fws0g4QvWbAwqqdEOVmlamYYjAOdVBBxTvcRs +/63ZN55VTQ8rYhShNA3BVFOLHaRD4mnlKE5Xh7gJXltCED7EHdpHdT9K3uM9U7nW +b2JSylt2RzY+LDsio2U0xsQp9jHzRRw81p8P1jmo5alP8jPACMsE8nnNNSDF4p43 +fG7hNNBq/dhq80iOnaArY05TIBMsD079tB0VKrYyyfaL0RbsAdgtCEmF9bCpnsTM +y9ExcJGQQJx9WNAHkSyjdzJd0jR6Zc0MrgRuj26nJ3Ahq58zaQKdfFO9RfGWd38n +MH3jshEtAuF+jXFbMcM4rVdIBPSuhYgHzYIC6yteziy7+6hittpWeNGLKpC5oZ8R +oEwH3MVsjCbd6Pp3vdcR412vLMgy1ZUOraDoY08FXC82RBJViVX6LLltIJu96kiX +WWUcRZAwzlJsTvh1EGmDcNNKCgmvWQaojqTNgTjxjJ3SzD2/TV6uQrSLgZ6ulyNl +7vKWt/YMTvIgoJA9JeH8Aik/XNd4bRXL+VXfUHpLTgn+WKiq2irVYd9R/yITDunP +a/kzqxitjU4OGdf/LOtYxfxfoGvFw5ym4KikoHKVg4ILcIQ+W4roOQQlu4/yezAK +fwYCrMVJWq4ESuQh3rn7eFR+eyBV6YcNBLm4iUcQTMhnXMMYxQ3TnDNga5eYhmV1 +ByYx+nFQDrbDolXo5JfXs3x6kXhoT/7wMHgsXtmRSd5PSBbaeJTrbMGA0Op6YgWr +EpvX3Yt863s4h+JgDpg9ouH+OJGgn7LGGye+TjjuDds8CStFdcFDDOayBS3EH4Cr +jgJwzvTdTZl+1YLYJXB67M4zmVPRRs5H88+fZYYA9bhZACL/rQBj2wDq/sIxvrIM +SCjOhSJ4z5Sm3XaBKnRG2GBBt67MeHB0+T3HR3VHKR+zStbCnsbOLythsE/CIA8L +fBNXMvnWa5bLgaCaEcK6Q3LOamJiKaigbmhI+3U3NUdb9cT1GhE0rtx6/IO9eapz +IUDOrtX9U+1o6iW2dahezxwLo9ftRwQ7qwG4qOk/Co/1c2WuuQ+d4YPpj/JOO5mf +LanA35mQjQrr2MZII91psznx05ffb5xMp2pqNbC6DVuZq8ZlhvVHGk+wM9RK3kYP +/ITwpbUvLmmN892kvZgLAXadSupBV8R/L5ZjDUO9U2all9p4eGfWZBk/yiivOLmh +VQxKCqAmThTO1hRa56+AjgzRJO6cY85ra+4Mm3FhhdR4gYvap2QTq0o2Vn0WlCHh +1SIeaDKfw9v4aGBbhqyQU2mPlXO5JiLktO+lZ5styVq9Qm+b0ROZxHzL1lRUNbRA +VfQO4fRnINKPgyzgH3tNxJTzw4pLkrkBD/g+zxDZVqkx +-----END ENCRYPTED PRIVATE KEY----- diff --git a/testdata/passwords b/testdata/passwords new file mode 100644 index 000000000..58530047f --- /dev/null +++ b/testdata/passwords @@ -0,0 +1,2 @@ +unusedpassword +mysslpassword