From b17735b99c7ea8374019bd1ba60170be9b0290f1 Mon Sep 17 00:00:00 2001 From: Georgy Moiseev Date: Tue, 1 Aug 2023 16:00:15 +0300 Subject: [PATCH] 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 62d3d3545..8ad9faa38 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