Skip to content

Commit b17735b

Browse files
DifferentialOrangeoleg-jukovec
authored andcommitted
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 1bd61d1 commit b17735b

10 files changed

+267
-10
lines changed

CHANGELOG.md

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

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

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

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

+41-7
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
package tarantool
55

66
import (
7+
"bufio"
78
"errors"
89
"io/ioutil"
910
"net"
11+
"os"
12+
"strings"
1013
"time"
1114

1215
"github.com/tarantool/go-openssl"
@@ -43,7 +46,7 @@ func sslCreateContext(opts SslOpts) (ctx interface{}, err error) {
4346
}
4447

4548
if opts.KeyFile != "" {
46-
if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil {
49+
if err = sslLoadKey(sslCtx, opts.KeyFile, opts.Password, opts.PasswordFile); err != nil {
4750
return
4851
}
4952
}
@@ -95,16 +98,47 @@ func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) {
9598
return
9699
}
97100

98-
func sslLoadKey(ctx *openssl.Ctx, keyFile string) (err error) {
101+
func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string,
102+
passwordFile string) error {
99103
var keyBytes []byte
104+
var err, firstDecryptErr error
105+
100106
if keyBytes, err = ioutil.ReadFile(keyFile); err != nil {
101-
return
107+
return err
102108
}
103109

104-
var key openssl.PrivateKey
105-
if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil {
106-
return
110+
// If the key is encrypted and password is not provided,
111+
// openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase
112+
// interactively. On the other hand,
113+
// openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine
114+
// for non-encrypted key with any password, including empty string. If
115+
// the key is encrypted, we fast fail with password error instead of
116+
// requesting the pass phrase interactively.
117+
passwords := []string{password}
118+
if passwordFile != "" {
119+
file, err := os.Open(passwordFile)
120+
if err == nil {
121+
defer file.Close()
122+
123+
scanner := bufio.NewScanner(file)
124+
// Tarantool itself tries each password file line.
125+
for scanner.Scan() {
126+
password = strings.TrimSpace(scanner.Text())
127+
passwords = append(passwords, password)
128+
}
129+
} else {
130+
firstDecryptErr = err
131+
}
132+
}
133+
134+
for _, password := range passwords {
135+
key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
136+
if err == nil {
137+
return ctx.UsePrivateKey(key)
138+
} else if firstDecryptErr == nil {
139+
firstDecryptErr = err
140+
}
107141
}
108142

109-
return ctx.UsePrivateKey(key)
143+
return firstDecryptErr
110144
}

ssl_test.go

+168
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,164 @@ 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+
"not_existing_passfile_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+
PasswordFile: "testdata/notafile",
569+
},
570+
},
571+
{
572+
"no_pass_key_encrypt_client",
573+
false,
574+
SslOpts{
575+
KeyFile: "testdata/localhost.key",
576+
CertFile: "testdata/localhost.crt",
577+
CaFile: "testdata/ca.crt",
578+
},
579+
SslOpts{
580+
KeyFile: "testdata/localhost.enc.key",
581+
CertFile: "testdata/localhost.crt",
582+
},
583+
},
584+
{
585+
"pass_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+
Password: "invalidpassword",
596+
},
597+
},
598+
{
599+
"passfile_key_non_encrypt_client",
600+
true,
601+
SslOpts{
602+
KeyFile: "testdata/localhost.key",
603+
CertFile: "testdata/localhost.crt",
604+
CaFile: "testdata/ca.crt",
605+
},
606+
SslOpts{
607+
KeyFile: "testdata/localhost.key",
608+
CertFile: "testdata/localhost.crt",
609+
PasswordFile: "testdata/invalidpasswords",
610+
},
611+
},
444612
}
445613

446614
func isTestTntSsl() bool {

testdata/generate.sh

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

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
unusedpassword1

testdata/localhost.enc.key

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

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
unusedpassword
2+
mysslpassword

0 commit comments

Comments
 (0)