Skip to content

Commit

Permalink
api: add OpenSSLDialer implementation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
DerekBum authored and oleg-jukovec committed Feb 8, 2024
1 parent 01f24e2 commit e2caece
Show file tree
Hide file tree
Showing 22 changed files with 2,013 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.

### Added

* `OpenSSLDialer` type to use SSL transport for `tarantool/go-tarantool/v2`
connection (#1).

### Changed

### Removed
Expand Down
145 changes: 145 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,151 @@ To run a default set of tests:
go test -v ./...
```

## OpenSSLDialer

User can create a dialer by filling the struct:
```go
// OpenSSLDialer allows to use SSL transport for connection.
type OpenSSLDialer struct {
// Address is an address to connect.
// It could be specified in following ways:
//
// - TCP connections (tcp://192.168.1.1:3013, tcp://my.host:3013,
// tcp:192.168.1.1:3013, tcp:my.host:3013, 192.168.1.1:3013, my.host:3013)
//
// - Unix socket, first '/' or '.' indicates Unix socket
// (unix:///abs/path/tt.sock, unix:path/tt.sock, /abs/path/tt.sock,
// ./rel/path/tt.sock, unix/:path/tt.sock)
Address string
// Auth is an authentication method.
Auth tarantool.Auth
// Username for logging in to Tarantool.
User string
// User password for logging in to Tarantool.
Password string
// RequiredProtocol contains minimal protocol version and
// list of protocol features that should be supported by
// Tarantool server. By default, there are no restrictions.
RequiredProtocolInfo tarantool.ProtocolInfo
// SslKeyFile is a path to a private SSL key file.
SslKeyFile string
// SslCertFile is a path to an SSL certificate file.
SslCertFile string
// SslCaFile is a path to a trusted certificate authorities (CA) file.
SslCaFile string
// SslCiphers is a colon-separated (:) list of SSL cipher suites the connection
// can use.
//
// We don't provide a list of supported ciphers. This is what OpenSSL
// does. The only limitation is usage of TLSv1.2 (because other protocol
// versions don't seem to support the GOST cipher). To add additional
// ciphers (GOST cipher), you must configure OpenSSL.
//
// See also
//
// * https://www.openssl.org/docs/man1.1.1/man1/ciphers.html
SslCiphers string
// SslPassword is a password for decrypting the private SSL key file.
// The priority is as follows: try to decrypt with SslPassword, then
// try SslPasswordFile.
SslPassword string
// SslPasswordFile is a path to the list of passwords for decrypting
// the private SSL key file. The connection tries every line from the
// file as a password.
SslPasswordFile string
}
```
To create a connection from the created dialer a `Dial` function could be used:
```go
package tarantool

import (
"context"
"fmt"
"time"

"github.com/tarantool/go-tarantool/v2"
"github.com/tarantool/go-tlsdialer"
)

func main() {
dialer := tlsdialer.OpenSSLDialer{
Address: "127.0.0.1:3301",
User: "guest",
}
opts := tarantool.Opts{
Timeout: 5 * time.Second,
}

ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

conn, err := tarantool.Connect(ctx, dialer, opts)
if err != nil {
fmt.Printf("Failed to create an example connection: %s", err)
return
}

// Use the connection.
data, err := conn.Do(tarantool.NewInsertRequest(999).
Tuple([]interface{}{99999, "BB"}),
).Get()
if err != nil {
fmt.Printf("Error: %s", err)
} else {
fmt.Printf("Data: %v", data)
}
}
```

## Application build

Since tlsdialer uses OpenSSL for connection to the Tarantool-EE, Cgo should be
enabled while building and OpenSSL libraries and includes should be available
in build time.

### Building with system OpenSSL

Build your application using the command:
1. **Static build**.
```shell
CGO_ENABLED=1 go build -ldflags "-linkmode external -extldflags '-static -lssl -lcrypto'" -o myapp main.go
```
2. **Dynamic build**.
```shell
CGO_ENABLED=1 go build -o myapp main.go
```

### Building with a custom OpenSSL version

OpenSSL could be build in two ways. Both of them require downloading the source
code of OpenSSL. It could be done from the [official website](https://www.openssl.org/source/)
or from the [GitHub repository](https://github.com/openssl/openssl).
1. **Static build**. Run this command from the installation directory to configure
the OpenSSL:
```shell
./config no-shared --prefix=/tmp/openssl/
```
2. **Dynamic build**. Run this command from the installation directory to configure
the OpenSSL:
```shell
./config --prefix=/tmp/openssl/
```
After configuring, run this command to install and build OpenSSL:
```shell
make install
```
And then build your application using the command:
1. **Static build**.
```shell
CGO_ENABLED=1 CGO_CFLAGS="-I/tmp/openssl/include" CGO_LDFLAGS="-L/tmp/openssl/lib" PKG_CONFIG_PATH="/tmp/openssl/lib/pkgconfig" go build -ldflags "-linkmode=external -extldflags '-static -lssl -lcrypto'" -o myapp main.go
```
2. **Dynamic build**.
```shell
CGO_ENABLED=1 CGO_CFLAGS="-I/tmp/openssl/include" CGO_LDFLAGS="-L/tmp/openssl/lib" PKG_CONFIG_PATH="/tmp/openssl/lib/pkgconfig" go build -o myapp main.go
```
After compiling your Go application, you can run it as usual.

[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-tlsdialer.svg
[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-tlsdialer
[coverage-badge]: https://coveralls.io/repos/github/tarantool/go-tlsdialer/badge.svg?branch=master
Expand Down
66 changes: 66 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tlsdialer

import (
"errors"
"io"
"net"

"github.com/tarantool/go-tarantool/v2"
)

type ttConn struct {
net net.Conn
reader io.Reader
writer writeFlusher
}

// writeFlusher is the interface that groups the basic Write and Flush methods.
type writeFlusher interface {
io.Writer
Flush() error
}

// Addr makes ttConn satisfy the Conn interface.
func (c *ttConn) Addr() net.Addr {
return c.net.RemoteAddr()
}

// Read makes ttConn satisfy the Conn interface.
func (c *ttConn) Read(p []byte) (int, error) {
return c.reader.Read(p)
}

// Write makes ttConn satisfy the Conn interface.
func (c *ttConn) Write(p []byte) (int, error) {
var (
l int
err error
)

if l, err = c.writer.Write(p); err != nil {
return l, err
} else if l != len(p) {
return l, errors.New("wrong length written")
}
return l, nil
}

// Flush makes ttConn satisfy the Conn interface.
func (c *ttConn) Flush() error {
return c.writer.Flush()
}

// Close makes ttConn satisfy the Conn interface.
func (c *ttConn) Close() error {
return c.net.Close()
}

// Greeting makes ttConn satisfy the Conn interface.
func (c *ttConn) Greeting() tarantool.Greeting {
return tarantool.Greeting{}
}

// ProtocolInfo makes ttConn satisfy the Conn interface.
func (c *ttConn) ProtocolInfo() tarantool.ProtocolInfo {
return tarantool.ProtocolInfo{}
}
31 changes: 31 additions & 0 deletions deadlineio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tlsdialer

import (
"net"
"time"
)

type deadlineIO struct {
to time.Duration
c net.Conn
}

func (d *deadlineIO) Write(b []byte) (n int, err error) {
if d.to > 0 {
if err := d.c.SetWriteDeadline(time.Now().Add(d.to)); err != nil {
return 0, err
}
}
n, err = d.c.Write(b)
return
}

func (d *deadlineIO) Read(b []byte) (n int, err error) {
if d.to > 0 {
if err := d.c.SetReadDeadline(time.Now().Add(d.to)); err != nil {
return 0, err
}
}
n, err = d.c.Read(b)
return
}
138 changes: 138 additions & 0 deletions dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package tlsdialer

import (
"bufio"
"context"
"errors"
"net"
"os"
"strings"

"github.com/tarantool/go-openssl"
)

func sslDialContext(ctx context.Context, network, address string,
sslOpts opts) (connection net.Conn, err error) {
var sslCtx *openssl.Ctx
if sslCtx, err = sslCreateContext(sslOpts); err != nil {
return
}

return openssl.DialContext(ctx, network, address, sslCtx, 0)
}

func sslCreateContext(sslOpts opts) (sslCtx *openssl.Ctx, err error) {
// Require TLSv1.2, because other protocol versions don't seem to
// support the GOST cipher.
if sslCtx, err = openssl.NewCtxWithVersion(openssl.TLSv1_2); err != nil {
return
}
sslCtx.SetMaxProtoVersion(openssl.TLS1_2_VERSION)
sslCtx.SetMinProtoVersion(openssl.TLS1_2_VERSION)

if sslOpts.CertFile != "" {
if err = sslLoadCert(sslCtx, sslOpts.CertFile); err != nil {
return
}
}

if sslOpts.KeyFile != "" {
if err = sslLoadKey(sslCtx, sslOpts.KeyFile, sslOpts.Password,
sslOpts.PasswordFile); err != nil {
return
}
}

if sslOpts.CaFile != "" {
if err = sslCtx.LoadVerifyLocations(sslOpts.CaFile, ""); err != nil {
return
}
verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert
sslCtx.SetVerify(verifyFlags, nil)
}

if sslOpts.Ciphers != "" {
if err = sslCtx.SetCipherList(sslOpts.Ciphers); err != nil {
return
}
}

return
}

func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) {
var certBytes []byte
if certBytes, err = os.ReadFile(certFile); err != nil {
return
}

certs := openssl.SplitPEM(certBytes)
if len(certs) == 0 {
err = errors.New("No PEM certificate found in " + certFile)
return
}
first, certs := certs[0], certs[1:]

var cert *openssl.Certificate
if cert, err = openssl.LoadCertificateFromPEM(first); err != nil {
return
}
if err = ctx.UseCertificate(cert); err != nil {
return
}

for _, pem := range certs {
if cert, err = openssl.LoadCertificateFromPEM(pem); err != nil {
break
}
if err = ctx.AddChainCertificate(cert); err != nil {
break
}
}
return
}

func sslLoadKey(ctx *openssl.Ctx, keyFile string, password string,
passwordFile string) error {
var keyBytes []byte
var err, firstDecryptErr error

if keyBytes, err = os.ReadFile(keyFile); err != nil {
return err
}

// 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.
passwords := []string{password}
if passwordFile != "" {
file, err := os.Open(passwordFile)
if err == nil {
defer file.Close()

scanner := bufio.NewScanner(file)
// Tarantool itself tries each password file line.
for scanner.Scan() {
password = strings.TrimSpace(scanner.Text())
passwords = append(passwords, password)
}
} else {
firstDecryptErr = err
}
}

for _, password := range passwords {
key, err := openssl.LoadPrivateKeyFromPEMWithPassword(keyBytes, password)
if err == nil {
return ctx.UsePrivateKey(key)
} else if firstDecryptErr == nil {
firstDecryptErr = err
}
}

return firstDecryptErr
}
Loading

0 comments on commit e2caece

Please sign in to comment.