Skip to content

Commit

Permalink
api: support SSL private key file decryption
Browse files Browse the repository at this point in the history
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. tarantool/tarantool-ee#22
2. tarantool/go-openssl#9
3. tarantool/tarantool-python#274
  • Loading branch information
DifferentialOrange committed Aug 1, 2023
1 parent bd2cb2b commit 6e7d9a7
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
72 changes: 66 additions & 6 deletions ssl.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
package tarantool

import (
"bufio"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"strings"
"time"

"github.com/tarantool/go-openssl"
Expand Down Expand Up @@ -43,7 +47,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
}
}
Expand Down Expand Up @@ -95,16 +99,72 @@ 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.
key, err = openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
if err == nil {
return ctx.UsePrivateKey(key)
} else {
errs = append(errs, err)
}

if passwordFile != "" {
var file *os.File
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())

key, err = openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
if err == nil {
return ctx.UsePrivateKey(key)
} else {
errs = append(errs, err)
}
}
} else {
errs = append(errs, err)
}
}

if len(errs) > 1 {
// Convenient multiple error wrapping was introduced only in Go 1.20
// https://pkg.go.dev/errors#example-Join
// https://github.com/golang/go/issues/53435
rerr := errors.New("got multiple errors on SSL decryption")
var i int
for i, err = range errs {
if i == 0 {
// gofmt forbids error strings to end with punctuation or newlines
rerr = fmt.Errorf("%s: %w", rerr, err)
} else {
rerr = fmt.Errorf("%s, %w", rerr, err)
}
}

return rerr
}

return ctx.UsePrivateKey(key)
return errs[0]
}
139 changes: 139 additions & 0 deletions ssl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -441,6 +451,135 @@ 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",
},
},
{
"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",
},
},
{
"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",
},
},
{
"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",
PasswordFile: "testdata/invalidpasswords",
},
},
}

func isTestTntSsl() bool {
Expand Down
13 changes: 13 additions & 0 deletions testdata/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF > passwords
unusedpassword
$password
EOF

cat <<EOF > invalidpasswords
unusedpassword1
EOF

openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key
1 change: 1 addition & 0 deletions testdata/invalidpasswords
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unusedpassword1
30 changes: 30 additions & 0 deletions testdata/localhost.enc.key
Original file line number Diff line number Diff line change
@@ -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-----
2 changes: 2 additions & 0 deletions testdata/passwords
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
unusedpassword
mysslpassword

0 comments on commit 6e7d9a7

Please sign in to comment.