Skip to content

Commit 037bd6f

Browse files
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. tarantool/tarantool-ee#22 2. tarantool/go-openssl#9 3. tarantool/tarantool-python#274
1 parent 09b35c7 commit 037bd6f

10 files changed

+274
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1515
- IsNullable flag for Field (#302)
1616
- More linters on CI (#310)
1717
- Meaningful description for read/write socket errors (#129)
18+
- Support password and password file to decrypt private SSL key file (#319)
1819

1920
### Changed
2021

connection.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@ type SslOpts struct {
345345
//
346346
// * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html
347347
Ciphers string
348+
// Password is a password for decrypting the private SSL key file.
349+
// The priority is as follows: try to decrypt with Password, then
350+
// try PasswordFile.
351+
Password string
352+
// PasswordFile is a path to the list of passwords for decrypting
353+
// the private SSL key file. The connection tries every line from the
354+
// file as a password.
355+
PasswordFile string
348356
}
349357

350358
// Clone returns a copy of the Opts object.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/shopspring/decimal v1.3.1
99
github.com/stretchr/testify v1.7.1
1010
github.com/tarantool/go-iproto v0.1.0
11-
github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195
11+
github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a
1212
github.com/vmihailenco/msgpack/v5 v5.3.5
1313
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect
1414
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT
2121
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2222
github.com/tarantool/go-iproto v0.1.0 h1:zHN9AA8LDawT+JBD0/Nxgr/bIsWkkpDzpcMuaNPSIAQ=
2323
github.com/tarantool/go-iproto v0.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo=
24-
github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195 h1:/AN3eUPsTlvF6W+Ng/8ZjnSU6o7L0H4Wb9GMks6RkzU=
25-
github.com/tarantool/go-openssl v0.0.8-0.20230307065445-720eeb389195/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A=
24+
github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a h1:eeElglRXJ3xWKkHmDbeXrQWlZyQ4t3Ca1YlZsrfdXFU=
25+
github.com/tarantool/go-openssl v0.0.8-0.20230801114713-b452431f934a/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A=
2626
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
2727
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
2828
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=

ssl.go

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
package tarantool
55

66
import (
7+
"bufio"
78
"errors"
9+
"fmt"
810
"io/ioutil"
911
"net"
12+
"os"
13+
"strings"
1014
"time"
1115

1216
"github.com/tarantool/go-openssl"
@@ -43,7 +47,7 @@ func sslCreateContext(opts SslOpts) (ctx interface{}, err error) {
4347
}
4448

4549
if opts.KeyFile != "" {
46-
if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil {
50+
if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil {
4751
return
4852
}
4953
}
@@ -95,16 +99,67 @@ func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) {
9599
return
96100
}
97101

98-
func sslLoadKey(ctx *openssl.Ctx, keyFile string) (err error) {
102+
func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string,
103+
passwordFile string) error {
99104
var keyBytes []byte
105+
var err error
106+
100107
if keyBytes, err = ioutil.ReadFile(keyFile); err != nil {
101-
return
108+
return err
102109
}
103110

104-
var key openssl.PrivateKey
105-
if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil {
106-
return
111+
var errs []error
112+
113+
// If the key is encrypted and password is not provided,
114+
// openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase
115+
// interactively. On the other hand,
116+
// openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine
117+
// for non-encrypted key with any password, including empty string. If
118+
// the key is encrypted, we fast fail with password error instead of
119+
// requesting the pass phrase interactively.
120+
passwords := []string{password}
121+
if passwordFile != "" {
122+
file, err := os.Open(passwordFile)
123+
if err == nil {
124+
defer file.Close()
125+
126+
scanner := bufio.NewScanner(file)
127+
// Tarantool itself tries each password file line.
128+
for scanner.Scan() {
129+
password = strings.TrimSpace(scanner.Text())
130+
passwords = append(passwords, password)
131+
}
132+
} else {
133+
errs = append(errs, err)
134+
}
135+
}
136+
137+
for _, password := range passwords {
138+
key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
139+
if err == nil {
140+
return ctx.UsePrivateKey(key)
141+
} else {
142+
errs = append(errs, err)
143+
}
144+
}
145+
146+
if len(errs) > 1 {
147+
// Convenient multiple error wrapping was introduced only in Go 1.20
148+
// https://pkg.go.dev/errors#example-Join
149+
// https://github.com/golang/go/issues/53435
150+
rerr := errors.New("got multiple errors on SSL decryption")
151+
var i int
152+
for i, err = range errs {
153+
if i == 0 {
154+
// gofmt forbids error strings to end with punctuation or newlines.
155+
rerr = fmt.Errorf("%s: %w", rerr, err)
156+
} else {
157+
rerr = fmt.Errorf("%s, %w", rerr, err)
158+
}
159+
}
160+
161+
return rerr
107162
}
108163

109-
return ctx.UsePrivateKey(key)
164+
return errs[0]
110165
}

ssl_test.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ func serverTnt(serverOpts SslOpts, auth Auth) (test_helpers.TarantoolInstance, e
117117
listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers)
118118
}
119119

120+
password := serverOpts.Password
121+
if password != "" {
122+
listen += fmt.Sprintf("ssl_password=%s&", password)
123+
}
124+
125+
passwordFile := serverOpts.PasswordFile
126+
if passwordFile != "" {
127+
listen += fmt.Sprintf("ssl_password_file=%s&", passwordFile)
128+
}
129+
120130
listen = listen[:len(listen)-1]
121131

122132
return test_helpers.StartTarantool(test_helpers.StartOpts{
@@ -441,6 +451,150 @@ var tests = []test{
441451
Ciphers: "TLS_AES_128_GCM_SHA256",
442452
},
443453
},
454+
{
455+
"pass_key_encrypt_client",
456+
true,
457+
SslOpts{
458+
KeyFile: "testdata/localhost.key",
459+
CertFile: "testdata/localhost.crt",
460+
CaFile: "testdata/ca.crt",
461+
},
462+
SslOpts{
463+
KeyFile: "testdata/localhost.enc.key",
464+
CertFile: "testdata/localhost.crt",
465+
Password: "mysslpassword",
466+
},
467+
},
468+
{
469+
"passfile_key_encrypt_client",
470+
true,
471+
SslOpts{
472+
KeyFile: "testdata/localhost.key",
473+
CertFile: "testdata/localhost.crt",
474+
CaFile: "testdata/ca.crt",
475+
},
476+
SslOpts{
477+
KeyFile: "testdata/localhost.enc.key",
478+
CertFile: "testdata/localhost.crt",
479+
PasswordFile: "testdata/passwords",
480+
},
481+
},
482+
{
483+
"pass_and_passfile_key_encrypt_client",
484+
true,
485+
SslOpts{
486+
KeyFile: "testdata/localhost.key",
487+
CertFile: "testdata/localhost.crt",
488+
CaFile: "testdata/ca.crt",
489+
},
490+
SslOpts{
491+
KeyFile: "testdata/localhost.enc.key",
492+
CertFile: "testdata/localhost.crt",
493+
Password: "mysslpassword",
494+
PasswordFile: "testdata/passwords",
495+
},
496+
},
497+
{
498+
"inv_pass_and_passfile_key_encrypt_client",
499+
true,
500+
SslOpts{
501+
KeyFile: "testdata/localhost.key",
502+
CertFile: "testdata/localhost.crt",
503+
CaFile: "testdata/ca.crt",
504+
},
505+
SslOpts{
506+
KeyFile: "testdata/localhost.enc.key",
507+
CertFile: "testdata/localhost.crt",
508+
Password: "invalidpassword",
509+
PasswordFile: "testdata/passwords",
510+
},
511+
},
512+
{
513+
"pass_and_inv_passfile_key_encrypt_client",
514+
true,
515+
SslOpts{
516+
KeyFile: "testdata/localhost.key",
517+
CertFile: "testdata/localhost.crt",
518+
CaFile: "testdata/ca.crt",
519+
},
520+
SslOpts{
521+
KeyFile: "testdata/localhost.enc.key",
522+
CertFile: "testdata/localhost.crt",
523+
Password: "mysslpassword",
524+
PasswordFile: "testdata/invalidpasswords",
525+
},
526+
},
527+
{
528+
"pass_and_not_existing_passfile_key_encrypt_client",
529+
true,
530+
SslOpts{
531+
KeyFile: "testdata/localhost.key",
532+
CertFile: "testdata/localhost.crt",
533+
CaFile: "testdata/ca.crt",
534+
},
535+
SslOpts{
536+
KeyFile: "testdata/localhost.enc.key",
537+
CertFile: "testdata/localhost.crt",
538+
Password: "mysslpassword",
539+
PasswordFile: "testdata/notafile",
540+
},
541+
},
542+
{
543+
"inv_pass_and_inv_passfile_key_encrypt_client",
544+
false,
545+
SslOpts{
546+
KeyFile: "testdata/localhost.key",
547+
CertFile: "testdata/localhost.crt",
548+
CaFile: "testdata/ca.crt",
549+
},
550+
SslOpts{
551+
KeyFile: "testdata/localhost.enc.key",
552+
CertFile: "testdata/localhost.crt",
553+
Password: "invalidpassword",
554+
PasswordFile: "testdata/invalidpasswords",
555+
},
556+
},
557+
{
558+
"no_pass_key_encrypt_client",
559+
false,
560+
SslOpts{
561+
KeyFile: "testdata/localhost.key",
562+
CertFile: "testdata/localhost.crt",
563+
CaFile: "testdata/ca.crt",
564+
},
565+
SslOpts{
566+
KeyFile: "testdata/localhost.enc.key",
567+
CertFile: "testdata/localhost.crt",
568+
},
569+
},
570+
{
571+
"pass_key_non_encrypt_client",
572+
true,
573+
SslOpts{
574+
KeyFile: "testdata/localhost.key",
575+
CertFile: "testdata/localhost.crt",
576+
CaFile: "testdata/ca.crt",
577+
},
578+
SslOpts{
579+
KeyFile: "testdata/localhost.key",
580+
CertFile: "testdata/localhost.crt",
581+
Password: "invalidpassword",
582+
},
583+
},
584+
{
585+
"passfile_key_non_encrypt_client",
586+
true,
587+
SslOpts{
588+
KeyFile: "testdata/localhost.key",
589+
CertFile: "testdata/localhost.crt",
590+
CaFile: "testdata/ca.crt",
591+
},
592+
SslOpts{
593+
KeyFile: "testdata/localhost.key",
594+
CertFile: "testdata/localhost.crt",
595+
PasswordFile: "testdata/invalidpasswords",
596+
},
597+
},
444598
}
445599

446600
func isTestTntSsl() bool {

testdata/generate.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,16 @@ openssl x509 -outform pem -in ca.pem -out ca.crt
2323

2424
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"
2525
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt
26+
password=mysslpassword
27+
28+
# Tarantool tries every line from the password file.
29+
cat <<EOF > passwords
30+
unusedpassword
31+
$password
32+
EOF
33+
34+
cat <<EOF > invalidpasswords
35+
unusedpassword1
36+
EOF
37+
38+
openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key

testdata/invalidpasswords

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
unusedpassword1

testdata/localhost.enc.key

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIm+0WC9xe38cCAggA
3+
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBNOE4KD+yauMfsnOiNAaaZBIIE
4+
0DtXaHGpacJ8MjjL6zciYhgJOD9SJHE4vwPxpNDWuS9mf6wk/cdBNFMqnYwJmlYw
5+
J/eQ+Z8MsZUqjnhDQz9YXXd8JftexAAa1bHnmfv2N/czJCx57dAHVdmJzgibfp18
6+
GCpqR23tklEO2Nj2HCbR59rh7IsnW9mD6jh+mVtkOix5HMCUSxwc3bEUutIQE80P
7+
JHG2BsEfAeeHZa+QgG3Y15c6uSXD6wY73ldPPOgZ3NFOqcw/RDqYf1zsohx7auxi
8+
Y6zHA7LdYtQjbNJ5slIfxPhAh75Fws0g4QvWbAwqqdEOVmlamYYjAOdVBBxTvcRs
9+
/63ZN55VTQ8rYhShNA3BVFOLHaRD4mnlKE5Xh7gJXltCED7EHdpHdT9K3uM9U7nW
10+
b2JSylt2RzY+LDsio2U0xsQp9jHzRRw81p8P1jmo5alP8jPACMsE8nnNNSDF4p43
11+
fG7hNNBq/dhq80iOnaArY05TIBMsD079tB0VKrYyyfaL0RbsAdgtCEmF9bCpnsTM
12+
y9ExcJGQQJx9WNAHkSyjdzJd0jR6Zc0MrgRuj26nJ3Ahq58zaQKdfFO9RfGWd38n
13+
MH3jshEtAuF+jXFbMcM4rVdIBPSuhYgHzYIC6yteziy7+6hittpWeNGLKpC5oZ8R
14+
oEwH3MVsjCbd6Pp3vdcR412vLMgy1ZUOraDoY08FXC82RBJViVX6LLltIJu96kiX
15+
WWUcRZAwzlJsTvh1EGmDcNNKCgmvWQaojqTNgTjxjJ3SzD2/TV6uQrSLgZ6ulyNl
16+
7vKWt/YMTvIgoJA9JeH8Aik/XNd4bRXL+VXfUHpLTgn+WKiq2irVYd9R/yITDunP
17+
a/kzqxitjU4OGdf/LOtYxfxfoGvFw5ym4KikoHKVg4ILcIQ+W4roOQQlu4/yezAK
18+
fwYCrMVJWq4ESuQh3rn7eFR+eyBV6YcNBLm4iUcQTMhnXMMYxQ3TnDNga5eYhmV1
19+
ByYx+nFQDrbDolXo5JfXs3x6kXhoT/7wMHgsXtmRSd5PSBbaeJTrbMGA0Op6YgWr
20+
EpvX3Yt863s4h+JgDpg9ouH+OJGgn7LGGye+TjjuDds8CStFdcFDDOayBS3EH4Cr
21+
jgJwzvTdTZl+1YLYJXB67M4zmVPRRs5H88+fZYYA9bhZACL/rQBj2wDq/sIxvrIM
22+
SCjOhSJ4z5Sm3XaBKnRG2GBBt67MeHB0+T3HR3VHKR+zStbCnsbOLythsE/CIA8L
23+
fBNXMvnWa5bLgaCaEcK6Q3LOamJiKaigbmhI+3U3NUdb9cT1GhE0rtx6/IO9eapz
24+
IUDOrtX9U+1o6iW2dahezxwLo9ftRwQ7qwG4qOk/Co/1c2WuuQ+d4YPpj/JOO5mf
25+
LanA35mQjQrr2MZII91psznx05ffb5xMp2pqNbC6DVuZq8ZlhvVHGk+wM9RK3kYP
26+
/ITwpbUvLmmN892kvZgLAXadSupBV8R/L5ZjDUO9U2all9p4eGfWZBk/yiivOLmh
27+
VQxKCqAmThTO1hRa56+AjgzRJO6cY85ra+4Mm3FhhdR4gYvap2QTq0o2Vn0WlCHh
28+
1SIeaDKfw9v4aGBbhqyQU2mPlXO5JiLktO+lZ5styVq9Qm+b0ROZxHzL1lRUNbRA
29+
VfQO4fRnINKPgyzgH3tNxJTzw4pLkrkBD/g+zxDZVqkx
30+
-----END ENCRYPTED PRIVATE KEY-----

testdata/passwords

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
unusedpassword
2+
mysslpassword

0 commit comments

Comments
 (0)