Skip to content

Commit ff47fd4

Browse files
committed
api: add OpenSslDialer implementation
To disable SSL by default we want to transfer OpenSslDialer and any other ssl logic to the new go-tlsdialer repository. go-tlsdialer serves as an interlayer between go-tarantool and go-openssl. All ssl logic from go-tarantool is moved to the go-tlsdialer. go-tlsdialer still uses tarantool connection, but also types and methods from go-openssl. This way we are removing the direct go-openssl dependency from go-tarantool, without creating a tarantool dependency in go-openssl. Moved all ssl code from go-tarantool, some test helpers. Part of tarantool/go-tarantool#301
1 parent 24f76a6 commit ff47fd4

22 files changed

+1993
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1010

1111
### Added
1212

13+
* `OpenSslDialer` type to use SSL transport for `tarantool/go-tarantool/v2`
14+
connection (#1).
15+
1316
### Changed
1417

1518
### Removed

README.md

+125
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,131 @@ To run a default set of tests:
1818
go test -v ./...
1919
```
2020

21+
## OpenSslDialer
22+
23+
User can create a dialer by filling the struct:
24+
```go
25+
// OpenSslDialer allows to use SSL transport for connection.
26+
type OpenSslDialer struct {
27+
// Address is an address to connect.
28+
// It could be specified in following ways:
29+
//
30+
// - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013,
31+
// tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013)
32+
//
33+
// - Unix socket, first '/' or '.' indicates Unix socket
34+
// (unix:///abs/path/tt.sock, unix:path/tt.sock, /abs/path/tt.sock,
35+
// ./rel/path/tt.sock, unix/:path/tt.sock)
36+
Address string
37+
// Auth is an authentication method.
38+
Auth tarantool.Auth
39+
// Username for logging in to Tarantool.
40+
User string
41+
// User password for logging in to Tarantool.
42+
Password string
43+
// RequiredProtocol contains minimal protocol version and
44+
// list of protocol features that should be supported by
45+
// Tarantool server. By default, there are no restrictions.
46+
RequiredProtocolInfo tarantool.ProtocolInfo
47+
// SslKeyFile is a path to a private SSL key file.
48+
SslKeyFile string
49+
// SslCertFile is a path to an SSL certificate file.
50+
SslCertFile string
51+
// SslCaFile is a path to a trusted certificate authorities (CA) file.
52+
SslCaFile string
53+
// SslCiphers is a colon-separated (:) list of SSL cipher suites the connection
54+
// can use.
55+
//
56+
// We don't provide a list of supported ciphers. This is what OpenSSL
57+
// does. The only limitation is usage of TLSv1.2 (because other protocol
58+
// versions don't seem to support the GOST cipher). To add additional
59+
// ciphers (GOST cipher), you must configure OpenSSL.
60+
//
61+
// See also
62+
//
63+
// * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html
64+
SslCiphers string
65+
// SslPassword is a password for decrypting the private SSL key file.
66+
// The priority is as follows: try to decrypt with SslPassword, then
67+
// try SslPasswordFile.
68+
SslPassword string
69+
// SslPasswordFile is a path to the list of passwords for decrypting
70+
// the private SSL key file. The connection tries every line from the
71+
// file as a password.
72+
SslPasswordFile string
73+
}
74+
```
75+
To create a connection from the created dialer a `Dial` function could be used:
76+
```go
77+
package tarantool
78+
79+
import (
80+
"context"
81+
"fmt"
82+
"time"
83+
84+
"github.com/tarantool/go-tarantool/v2"
85+
"github.com/tarantool/go-tlsdialer"
86+
)
87+
88+
func main() {
89+
dialer := tlsdialer.OpenSslDialer{
90+
Address: "127.0.0.1:3301",
91+
User: "guest",
92+
}
93+
opts := tarantool.Opts{
94+
Timeout: 5 * time.Second,
95+
}
96+
97+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
98+
defer cancel()
99+
100+
conn, err := tarantool.Connect(ctx, dialer, opts)
101+
if err != nil {
102+
fmt.Printf("Failed to create an example connection: %s", err)
103+
return
104+
}
105+
106+
// Use the connection.
107+
data, err := conn.Do(tarantool.NewInsertRequest(999).
108+
Tuple([]interface{}{99999, "BB"}),
109+
).Get()
110+
if err != nil {
111+
fmt.Printf("Error: %s", err)
112+
} else {
113+
fmt.Printf("Data: %v", data)
114+
}
115+
}
116+
```
117+
118+
### Application build
119+
120+
Since tlsdialer uses SSL for connection to the Tarantool-EE, CGo should be
121+
enabled while building and OpenSSL binary should be built with the application.
122+
123+
OpenSSL could be build in two ways. Both of them require downloading the source
124+
code of OpenSSL. It could be done from the [official website](https://www.openssl.org/source/)
125+
or from the [GitHub repository](https://github.com/openssl/openssl).
126+
1. **Static build**. Run this command from the installation directory to configure
127+
the OpenSSL:
128+
```shell
129+
./config no-shared
130+
```
131+
2. **Dynamic build**. Run this command from the installation directory to configure
132+
the OpenSSL:
133+
```shell
134+
./config
135+
```
136+
After configuring, run make to build OpenSSL:
137+
```shell
138+
make
139+
```
140+
And then build your application using:
141+
```shell
142+
CGO_CFLAGS="-I/path/to/openssl/include" CGO_LDFLAGS="-L/path/to/openssl/lib -lssl -lcrypto" go build -o myapp main.go
143+
```
144+
After compiling your Go application, you can run it as usual.
145+
21146
[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tlsdialer.svg
22147
[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tlsdialer
23148
[coverage-badge]: https://coveralls.io/repos/github/tarantool/go-tlsdialer/badge.svg?branch=master

conn.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package tlsdialer
2+
3+
import (
4+
"errors"
5+
"io"
6+
"net"
7+
8+
"github.com/tarantool/go-tarantool/v2"
9+
)
10+
11+
type ttConn struct {
12+
net net.Conn
13+
reader io.Reader
14+
writer writeFlusher
15+
}
16+
17+
// writeFlusher is the interface that groups the basic Write and Flush methods.
18+
type writeFlusher interface {
19+
io.Writer
20+
Flush() error
21+
}
22+
23+
// Addr makes ttConn satisfy the Conn interface.
24+
func (c *ttConn) Addr() net.Addr {
25+
return c.net.RemoteAddr()
26+
}
27+
28+
// Read makes ttConn satisfy the Conn interface.
29+
func (c *ttConn) Read(p []byte) (int, error) {
30+
return c.reader.Read(p)
31+
}
32+
33+
// Write makes ttConn satisfy the Conn interface.
34+
func (c *ttConn) Write(p []byte) (int, error) {
35+
var (
36+
l int
37+
err error
38+
)
39+
40+
if l, err = c.writer.Write(p); err != nil {
41+
return l, err
42+
} else if l != len(p) {
43+
return l, errors.New("wrong length written")
44+
}
45+
return l, nil
46+
}
47+
48+
// Flush makes ttConn satisfy the Conn interface.
49+
func (c *ttConn) Flush() error {
50+
return c.writer.Flush()
51+
}
52+
53+
// Close makes ttConn satisfy the Conn interface.
54+
func (c *ttConn) Close() error {
55+
return c.net.Close()
56+
}
57+
58+
// Greeting makes ttConn satisfy the Conn interface.
59+
func (c *ttConn) Greeting() tarantool.Greeting {
60+
return tarantool.Greeting{}
61+
}
62+
63+
// ProtocolInfo makes ttConn satisfy the Conn interface.
64+
func (c *ttConn) ProtocolInfo() tarantool.ProtocolInfo {
65+
return tarantool.ProtocolInfo{}
66+
}

deadlineio.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package tlsdialer
2+
3+
import (
4+
"net"
5+
"time"
6+
)
7+
8+
type deadlineIO struct {
9+
to time.Duration
10+
c net.Conn
11+
}
12+
13+
func (d *deadlineIO) Write(b []byte) (n int, err error) {
14+
if d.to > 0 {
15+
if err := d.c.SetWriteDeadline(time.Now().Add(d.to)); err != nil {
16+
return 0, err
17+
}
18+
}
19+
n, err = d.c.Write(b)
20+
return
21+
}
22+
23+
func (d *deadlineIO) Read(b []byte) (n int, err error) {
24+
if d.to > 0 {
25+
if err := d.c.SetReadDeadline(time.Now().Add(d.to)); err != nil {
26+
return 0, err
27+
}
28+
}
29+
n, err = d.c.Read(b)
30+
return
31+
}

dial.go

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package tlsdialer
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"errors"
7+
"net"
8+
"os"
9+
"strings"
10+
11+
"github.com/tarantool/go-openssl"
12+
)
13+
14+
func sslDialContext(ctx context.Context, network, address string,
15+
sslOpts opts) (connection net.Conn, err error) {
16+
var sslCtx *openssl.Ctx
17+
if sslCtx, err = sslCreateContext(sslOpts); err != nil {
18+
return
19+
}
20+
21+
return openssl.DialContext(ctx, network, address, sslCtx, 0)
22+
}
23+
24+
func sslCreateContext(sslOpts opts) (sslCtx *openssl.Ctx, err error) {
25+
// Require TLSv1.2, because other protocol versions don't seem to
26+
// support the GOST cipher.
27+
if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil {
28+
return
29+
}
30+
sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION)
31+
sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION)
32+
33+
if sslOpts.CertFile != "" {
34+
if err = sslLoadCert(sslCtx, sslOpts.CertFile); err != nil {
35+
return
36+
}
37+
}
38+
39+
if sslOpts.KeyFile != "" {
40+
if err = sslLoadKey(sslCtx, sslOpts.KeyFile, sslOpts.Password,
41+
sslOpts.PasswordFile); err != nil {
42+
return
43+
}
44+
}
45+
46+
if sslOpts.CaFile != "" {
47+
if err = sslCtx.LoadVerifyLocations(sslOpts.CaFile, ""); err != nil {
48+
return
49+
}
50+
verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert
51+
sslCtx.SetVerify(verifyFlags, nil)
52+
}
53+
54+
if sslOpts.Ciphers != "" {
55+
if err = sslCtx.SetCipherList(sslOpts.Ciphers); err != nil {
56+
return
57+
}
58+
}
59+
60+
return
61+
}
62+
63+
func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) {
64+
var certBytes []byte
65+
if certBytes, err = os.ReadFile(certFile); err != nil {
66+
return
67+
}
68+
69+
certs := openssl.SplitPEM(certBytes)
70+
if len(certs) == 0 {
71+
err = errors.New("No PEM certificate found in " + certFile)
72+
return
73+
}
74+
first, certs := certs[0], certs[1:]
75+
76+
var cert *openssl.Certificate
77+
if cert, err = openssl.LoadCertificateFromPEM(first); err != nil {
78+
return
79+
}
80+
if err = ctx.UseCertificate(cert); err != nil {
81+
return
82+
}
83+
84+
for _, pem := range certs {
85+
if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil {
86+
break
87+
}
88+
if err = ctx.AddChainCertificate(cert); err != nil {
89+
break
90+
}
91+
}
92+
return
93+
}
94+
95+
func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string,
96+
passwordFile string) error {
97+
var keyBytes []byte
98+
var err, firstDecryptErr error
99+
100+
if keyBytes, err = os.ReadFile(keyFile); err != nil {
101+
return err
102+
}
103+
104+
// If the key is encrypted and password is not provided,
105+
// openssl.LoadPrivateKeyFromPEM(keyBytes) asks to enter PEM pass phrase
106+
// interactively. On the other hand,
107+
// openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password) works fine
108+
// for non-encrypted key with any password, including empty string. If
109+
// the key is encrypted, we fast fail with password error instead of
110+
// requesting the pass phrase interactively.
111+
passwords := []string{password}
112+
if passwordFile != "" {
113+
file, err := os.Open(passwordFile)
114+
if err == nil {
115+
defer file.Close()
116+
117+
scanner := bufio.NewScanner(file)
118+
// Tarantool itself tries each password file line.
119+
for scanner.Scan() {
120+
password = strings.TrimSpace(scanner.Text())
121+
passwords = append(passwords, password)
122+
}
123+
} else {
124+
firstDecryptErr = err
125+
}
126+
}
127+
128+
for _, password := range passwords {
129+
key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
130+
if err == nil {
131+
return ctx.UsePrivateKey(key)
132+
} else if firstDecryptErr == nil {
133+
firstDecryptErr = err
134+
}
135+
}
136+
137+
return firstDecryptErr
138+
}

0 commit comments

Comments
 (0)