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..8f5956275 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,49 @@ 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 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 + var errs []error + + // 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 { + errs = append(errs, err) + } + } + + for _, password := range passwords { + key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) + if err == nil { + return ctx.UsePrivateKey(key) + } else { + errs = append(errs, err) + } } - return ctx.UsePrivateKey(key) + return errs[0] } 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