Skip to content

Commit 7023bbc

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]. Same as in Tarantool, we try `SslOpts.Password`, then each line in `SslOpts.PasswordFile`. If all of the above fail, we re-raise errors. The patch is based on a similar patch from tarantool-python [2]. 1. tarantool/tarantool-ee#22 2. tarantool/tarantool-python#274
1 parent 904ab21 commit 7023bbc

14 files changed

+384
-67
lines changed

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,15 @@
44
work_dir*
55
.rocks
66
bench*
7+
8+
testdata/*.crt
9+
!testdata/ca.crt
10+
!testdata/invalidhost.crt
11+
!testdata/localhost.crt
12+
testdata/*.csr
13+
testdata/*.ext
14+
testdata/*.key
15+
!testdata/localhost.key
16+
!testdata/localhost.enc.key
17+
testdata/*.pem
18+
testdata/*.srl

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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ 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+
Password string
350+
// PasswordFile is a path to the list of passwords for decrypting
351+
// the private SSL key file. The connection tries every line from the
352+
// file as a password.
353+
PasswordFile string
348354
}
349355

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

ssl.go

Lines changed: 65 additions & 6 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,71 @@ 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

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

109-
return ctx.UsePrivateKey(key)
168+
return errs[0]
110169
}

ssl_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,16 @@ func serverTnt(serverOpts, clientOpts SslOpts, auth Auth) (test_helpers.Tarantoo
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{
@@ -431,6 +441,142 @@ var tests = []test{
431441
Ciphers: "TLS_AES_128_GCM_SHA256",
432442
},
433443
},
444+
{
445+
"pass_no_key_encrypt",
446+
true,
447+
SslOpts{
448+
KeyFile: "testdata/localhost.key",
449+
CertFile: "testdata/localhost.crt",
450+
CaFile: "testdata/ca.crt",
451+
},
452+
SslOpts{
453+
KeyFile: "testdata/localhost.enc.key",
454+
CertFile: "testdata/localhost.crt",
455+
Password: "mysslpassword",
456+
},
457+
},
458+
{
459+
"pass_file_no_key_encrypt",
460+
true,
461+
SslOpts{
462+
KeyFile: "testdata/localhost.key",
463+
CertFile: "testdata/localhost.crt",
464+
CaFile: "testdata/ca.crt",
465+
},
466+
SslOpts{
467+
KeyFile: "testdata/localhost.enc.key",
468+
CertFile: "testdata/localhost.crt",
469+
PasswordFile: "testdata/passwords",
470+
},
471+
},
472+
{
473+
"pass_key_encrypt",
474+
true,
475+
SslOpts{
476+
KeyFile: "testdata/localhost.enc.key",
477+
CertFile: "testdata/localhost.crt",
478+
CaFile: "testdata/ca.crt",
479+
Password: "mysslpassword",
480+
},
481+
SslOpts{
482+
KeyFile: "testdata/localhost.enc.key",
483+
CertFile: "testdata/localhost.crt",
484+
Password: "mysslpassword",
485+
},
486+
},
487+
{
488+
"pass_file_key_encrypt",
489+
true,
490+
SslOpts{
491+
KeyFile: "testdata/localhost.enc.key",
492+
CertFile: "testdata/localhost.crt",
493+
CaFile: "testdata/ca.crt",
494+
PasswordFile: "testdata/passwords",
495+
},
496+
SslOpts{
497+
KeyFile: "testdata/localhost.enc.key",
498+
CertFile: "testdata/localhost.crt",
499+
PasswordFile: "testdata/passwords",
500+
},
501+
},
502+
{
503+
"pass_and_pass_file_key_encrypt",
504+
true,
505+
SslOpts{
506+
KeyFile: "testdata/localhost.enc.key",
507+
CertFile: "testdata/localhost.crt",
508+
CaFile: "testdata/ca.crt",
509+
PasswordFile: "testdata/passwords",
510+
},
511+
SslOpts{
512+
KeyFile: "testdata/localhost.enc.key",
513+
CertFile: "testdata/localhost.crt",
514+
Password: "mysslpassword",
515+
PasswordFile: "testdata/passwords",
516+
},
517+
},
518+
{
519+
"inv_pass_and_pass_file_key_encrypt",
520+
true,
521+
SslOpts{
522+
KeyFile: "testdata/localhost.enc.key",
523+
CertFile: "testdata/localhost.crt",
524+
CaFile: "testdata/ca.crt",
525+
PasswordFile: "testdata/passwords",
526+
},
527+
SslOpts{
528+
KeyFile: "testdata/localhost.enc.key",
529+
CertFile: "testdata/localhost.crt",
530+
Password: "invalidpassword",
531+
PasswordFile: "testdata/passwords",
532+
},
533+
},
534+
{
535+
"pass_and_inv_pass_file_key_encrypt",
536+
true,
537+
SslOpts{
538+
KeyFile: "testdata/localhost.enc.key",
539+
CertFile: "testdata/localhost.crt",
540+
CaFile: "testdata/ca.crt",
541+
PasswordFile: "testdata/passwords",
542+
},
543+
SslOpts{
544+
KeyFile: "testdata/localhost.enc.key",
545+
CertFile: "testdata/localhost.crt",
546+
Password: "mysslpassword",
547+
PasswordFile: "testdata/invalidpasswords",
548+
},
549+
},
550+
{
551+
"pass_and_inv_pass_file_key_encrypt",
552+
false,
553+
SslOpts{
554+
KeyFile: "testdata/localhost.enc.key",
555+
CertFile: "testdata/localhost.crt",
556+
CaFile: "testdata/ca.crt",
557+
PasswordFile: "testdata/passwords",
558+
},
559+
SslOpts{
560+
KeyFile: "testdata/localhost.enc.key",
561+
CertFile: "testdata/localhost.crt",
562+
Password: "invalidpassword",
563+
PasswordFile: "testdata/invalidpasswords",
564+
},
565+
},
566+
{
567+
"no_pass_key_encrypt",
568+
false,
569+
SslOpts{
570+
KeyFile: "testdata/localhost.enc.key",
571+
CertFile: "testdata/localhost.crt",
572+
CaFile: "testdata/ca.crt",
573+
PasswordFile: "testdata/passwords",
574+
},
575+
SslOpts{
576+
KeyFile: "testdata/localhost.enc.key",
577+
CertFile: "testdata/localhost.crt",
578+
},
579+
},
434580
}
435581

436582
func isTestTntSsl() bool {
@@ -457,10 +603,16 @@ func TestSslOpts(t *testing.T) {
457603
}
458604
if test.ok {
459605
t.Run("ok_tnt_"+test.name, func(t *testing.T) {
606+
if test.serverOpts.Password != "" || test.serverOpts.PasswordFile != "" {
607+
test_helpers.SkipIfSSLEncryptedUnsupported(t)
608+
}
460609
assertConnectionTntOk(t, test.serverOpts, test.clientOpts)
461610
})
462611
} else {
463612
t.Run("fail_tnt_"+test.name, func(t *testing.T) {
613+
if test.serverOpts.Password != "" || test.serverOpts.PasswordFile != "" {
614+
test_helpers.SkipIfSSLEncryptedUnsupported(t)
615+
}
464616
assertConnectionTntFail(t, test.serverOpts, test.clientOpts)
465617
})
466618
}

test_helpers/utils.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,14 @@ func SkipIfPaginationUnsupported(t *testing.T) {
190190
SkipIfFeatureUnsupported(t, "pagination", 2, 11, 0)
191191
}
192192

193+
// SkipIfSSLEncryptedUnsupported skips test run if Tarantool without
194+
// SSL encryption support is used.
195+
func SkipIfSSLEncryptedUnsupported(t *testing.T) {
196+
t.Helper()
197+
198+
SkipIfFeatureUnsupported(t, "SSL encryption", 2, 11, 0)
199+
}
200+
193201
// CheckEqualBoxErrors checks equivalence of tarantool.BoxError objects.
194202
//
195203
// Tarantool errors are not comparable by nature:

testdata/ca.crt

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
-----BEGIN CERTIFICATE-----
2-
MIIDLzCCAhegAwIBAgIUMMZTmNkhr4qOfSwInVk2dAJvoBEwDQYJKoZIhvcNAQEL
2+
MIIDLzCCAhegAwIBAgIUaw6WTgYBXFRqlGbKRYczFApoNU4wDQYJKoZIhvcNAQEL
33
BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y
4-
MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
4+
MzA3MjYwODM3MTZaFw00NTEyMjkwODM3MTZaMCcxCzAJBgNVBAYTAlVTMRgwFgYD
55
VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
6-
AoIBAQCRq/eaA3I6CB8t770H2XDdzcp1yuC/+TZOxV5o0LuRkogTvL2kYULBrfx1
7-
rVZu8zQJTx1fmSRj1cN8j+IrmXN5goZ3mYFTnnIOgkyi+hJysVlo5s0Kp0qtLLGM
8-
OuaVbxw2oAy75if5X3pFpiDaMvFBtJKsh8+SkncBIC5bbKC5AoLdFANLmPiH0CGr
9-
Mv3rL3ycnbciI6J4uKHcWnYGGiMjBomaZ7jd/cOjcjmGfpI5d0nq13G11omkyEyR
10-
wNX0eJRL02W+93Xu7tD+FEFMxFvak+70GvX+XWomwYw/Pjlio8KbTAlJxhfK2Lh6
11-
H798k17VfxIrOk0KjzZS7+a20hZ/AgMBAAGjUzBRMB0GA1UdDgQWBBT2f5o8r75C
12-
PWST36akpkKRRTbhvjAfBgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAP
13-
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA9pb75p6mnqp2MQHSr
14-
5SKRf2UV4wQIUtXgF6V9vNfvVzJii+Lzrqir1YMk5QgavCzD96KlJcqJCcH559RY
15-
5743AxI3tdWfA3wajBctoy35oYnT4M30qbkryYLTUlv7PmeNWvrksTURchyyDt5/
16-
3T73yj5ZalmzKN6+xLfUDdnudspfWlUMutKU50MU1iuQESf4Fwd53vOg9jMcWJ2E
17-
vAgfVI0XAvYdU3ybJrUvBq5zokYR2RzGv14uHxwVPnLBjrBEHRnbrXvLZJhuIS2b
18-
xZ3CqwWi+9bvNqHz09HvhkU2b6fCGweKaAUGSo8OfQ5FRkjTUomMI/ZLs/qtJ6JR
19-
zzVt
6+
AoIBAQCunm5E+dyoYw+ECp0vOabsA4L7C+dUQLhfqdOEwFSpSanjBTuUEAPB+fEr
7+
wqaZXI2EnUSxYEYO03TkZmWoJgRJq+00laWPA4AuKHpg4SS/LUoveQiQdsie+kUj
8+
YMFu3rtP2CvTMpC4HMRK2CviOnU9iA4hPvRx4o5tESxLW31jNnBDeC/tsEVVR/6i
9+
lwB9Oh1RbZI/c429N67qq5C2rpU5+o+YszDou36WTxw6XeXdkw7QF4W2BNLysPLJ
10+
AY+aPrUVxKDOgDNk77h41HDqu+SuDg6mg528yfRqyAd4ooEE8MLcT0xztn1U8HvZ
11+
SKwWTnS8TSzCmQptRGPlb5oES/NlAgMBAAGjUzBRMB0GA1UdDgQWBBQ/S0H0dFUy
12+
OuEQ/kgDzGarWm2vlDAfBgNVHSMEGDAWgBQ/S0H0dFUyOuEQ/kgDzGarWm2vlDAP
13+
BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCoP+kH96+b6GWByPTo
14+
LRK/QJmpPRWiZ5naV6CXJQNYg+nVG9wdiGXxEx4BBZs6yGdeHdiWCWbuRPMyH0Wp
15+
w2ajMmK7pC8+MGKzMSC/EISPFwKYumwe/6zNnde7eZ19n7EFwrOihgEf+hNfjOCj
16+
CqDIfMb2ztEHY7mEABMXDviKI80om2P1oIkHj5MD7z8ZetJRf1qCH7ke2cdTJ+Zr
17+
XNGiJ7sz3xRQO/QRCkbBbr/d4zeX3A5/+MXHLtzbPiWs+/XaDbGJTQIO5hFfwwAC
18+
v1/VrNmsy+3YYLLTZzmfpa60Sk3NsZbIQdvwLJJj8pOmH/zae7UZlrxlGWjQKvH5
19+
evsP
2020
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)