Skip to content

Commit 2f20c34

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 2f20c34

13 files changed

+384
-66
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: 64 additions & 5 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"
@@ -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: 161 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{
@@ -139,6 +149,21 @@ func serverTntStop(inst test_helpers.TarantoolInstance) {
139149
test_helpers.StopTarantoolWithCleanup(inst)
140150
}
141151

152+
func skipEncrypted(t testing.TB, serverOpts SslOpts) {
153+
if serverOpts.Password == "" && serverOpts.PasswordFile == "" {
154+
return
155+
}
156+
157+
isLess, err := test_helpers.IsTarantoolVersionLess(2, 11, 0)
158+
if err != nil {
159+
t.Fatalf("Could not check the Tarantool version")
160+
}
161+
162+
if isLess {
163+
t.Skipf("Skipping test for Tarantool without SSL encryption support")
164+
}
165+
}
166+
142167
func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) {
143168
t.Helper()
144169

@@ -431,6 +456,142 @@ var tests = []test{
431456
Ciphers: "TLS_AES_128_GCM_SHA256",
432457
},
433458
},
459+
{
460+
"pass_no_key_encrypt",
461+
true,
462+
SslOpts{
463+
KeyFile: "testdata/localhost.key",
464+
CertFile: "testdata/localhost.crt",
465+
CaFile: "testdata/ca.crt",
466+
},
467+
SslOpts{
468+
KeyFile: "testdata/localhost.enc.key",
469+
CertFile: "testdata/localhost.crt",
470+
Password: "mysslpassword",
471+
},
472+
},
473+
{
474+
"pass_file_no_key_encrypt",
475+
true,
476+
SslOpts{
477+
KeyFile: "testdata/localhost.key",
478+
CertFile: "testdata/localhost.crt",
479+
CaFile: "testdata/ca.crt",
480+
},
481+
SslOpts{
482+
KeyFile: "testdata/localhost.enc.key",
483+
CertFile: "testdata/localhost.crt",
484+
PasswordFile: "testdata/passwords",
485+
},
486+
},
487+
{
488+
"pass_key_encrypt",
489+
true,
490+
SslOpts{
491+
KeyFile: "testdata/localhost.enc.key",
492+
CertFile: "testdata/localhost.crt",
493+
CaFile: "testdata/ca.crt",
494+
Password: "mysslpassword",
495+
},
496+
SslOpts{
497+
KeyFile: "testdata/localhost.enc.key",
498+
CertFile: "testdata/localhost.crt",
499+
Password: "mysslpassword",
500+
},
501+
},
502+
{
503+
"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+
PasswordFile: "testdata/passwords",
515+
},
516+
},
517+
{
518+
"pass_and_pass_file_key_encrypt",
519+
true,
520+
SslOpts{
521+
KeyFile: "testdata/localhost.enc.key",
522+
CertFile: "testdata/localhost.crt",
523+
CaFile: "testdata/ca.crt",
524+
PasswordFile: "testdata/passwords",
525+
},
526+
SslOpts{
527+
KeyFile: "testdata/localhost.enc.key",
528+
CertFile: "testdata/localhost.crt",
529+
Password: "mysslpassword",
530+
PasswordFile: "testdata/passwords",
531+
},
532+
},
533+
{
534+
"inv_pass_and_pass_file_key_encrypt",
535+
true,
536+
SslOpts{
537+
KeyFile: "testdata/localhost.enc.key",
538+
CertFile: "testdata/localhost.crt",
539+
CaFile: "testdata/ca.crt",
540+
PasswordFile: "testdata/passwords",
541+
},
542+
SslOpts{
543+
KeyFile: "testdata/localhost.enc.key",
544+
CertFile: "testdata/localhost.crt",
545+
Password: "invalidpassword",
546+
PasswordFile: "testdata/passwords",
547+
},
548+
},
549+
{
550+
"pass_and_inv_pass_file_key_encrypt",
551+
true,
552+
SslOpts{
553+
KeyFile: "testdata/localhost.enc.key",
554+
CertFile: "testdata/localhost.crt",
555+
CaFile: "testdata/ca.crt",
556+
PasswordFile: "testdata/passwords",
557+
},
558+
SslOpts{
559+
KeyFile: "testdata/localhost.enc.key",
560+
CertFile: "testdata/localhost.crt",
561+
Password: "mysslpassword",
562+
PasswordFile: "testdata/invalidpasswords",
563+
},
564+
},
565+
{
566+
"pass_and_inv_pass_file_key_encrypt",
567+
false,
568+
SslOpts{
569+
KeyFile: "testdata/localhost.enc.key",
570+
CertFile: "testdata/localhost.crt",
571+
CaFile: "testdata/ca.crt",
572+
PasswordFile: "testdata/passwords",
573+
},
574+
SslOpts{
575+
KeyFile: "testdata/localhost.enc.key",
576+
CertFile: "testdata/localhost.crt",
577+
Password: "invalidpassword",
578+
PasswordFile: "testdata/invalidpasswords",
579+
},
580+
},
581+
{
582+
"no_pass_key_encrypt",
583+
false,
584+
SslOpts{
585+
KeyFile: "testdata/localhost.enc.key",
586+
CertFile: "testdata/localhost.crt",
587+
CaFile: "testdata/ca.crt",
588+
PasswordFile: "testdata/passwords",
589+
},
590+
SslOpts{
591+
KeyFile: "testdata/localhost.enc.key",
592+
CertFile: "testdata/localhost.crt",
593+
},
594+
},
434595
}
435596

436597
func isTestTntSsl() bool {

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-----

testdata/generate.sh

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ set -xeuo pipefail
88
# $ openssl version
99
# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)
1010

11-
cat <<EOF > domains.ext
11+
cat <<EOF > domains_localhost.ext
1212
authorityKeyIdentifier=keyid,issuer
1313
basicConstraints=CA:FALSE
1414
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
@@ -18,8 +18,32 @@ DNS.1 = localhost
1818
IP.1 = 127.0.0.1
1919
EOF
2020

21+
cat <<EOF > domains_invalidhost.ext
22+
authorityKeyIdentifier=keyid,issuer
23+
basicConstraints=CA:FALSE
24+
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
25+
subjectAltName = @alt_names
26+
[alt_names]
27+
DNS.1 = invalidhostname
28+
EOF
29+
2130
openssl req -x509 -nodes -new -sha256 -days 8192 -newkey rsa:2048 -keyout ca.key -out ca.pem -subj "/C=US/CN=Example-Root-CA"
2231
openssl x509 -outform pem -in ca.pem -out ca.crt
2332

2433
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"
25-
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt
34+
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_localhost.ext -out localhost.crt
35+
openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains_invalidhost.ext -out invalidhost.crt
36+
37+
password=mysslpassword
38+
39+
# Tarantool tries every line from the password file.
40+
cat <<EOF > passwords
41+
unusedpassword
42+
$password
43+
EOF
44+
45+
cat <<EOF > invalidpasswords
46+
unusedpassword1
47+
EOF
48+
49+
openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key

0 commit comments

Comments
 (0)