Skip to content

Commit

Permalink
go-tlsdialer: initial repository setup
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 committed Feb 1, 2024
1 parent d74c2f4 commit 9411cb3
Show file tree
Hide file tree
Showing 22 changed files with 1,883 additions and 0 deletions.
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,103 @@ 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/tnt.sock, unix:path/tnt.sock, /abs/path/tnt.sock,
// ./rel/path/tnt.sock, unix/:path/tnt.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.Println("Error", err)
} else {
fmt.Printf("Data: %v", data)
}
}
```

[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
62 changes: 62 additions & 0 deletions conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package tlsdialer

import (
"errors"
"io"
"net"

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

type tntConn 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 tntConn satisfy the Conn interface.
func (c *tntConn) Addr() net.Addr {
return c.net.RemoteAddr()
}

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

// Write makes tntConn satisfy the Conn interface.
func (c *tntConn) Write(p []byte) (int, 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")
} else {
return l, nil
}
}

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

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

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

// ProtocolInfo makes tntConn satisfy the Conn interface.
func (c *tntConn) ProtocolInfo() tarantool.ProtocolInfo {
return tarantool.ProtocolInfo{}
}
27 changes: 27 additions & 0 deletions deadline_io.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 {
d.c.SetWriteDeadline(time.Now().Add(d.to))

Check failure on line 15 in deadline_io.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.c.SetWriteDeadline` is not checked (errcheck)

Check failure on line 15 in deadline_io.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.c.SetWriteDeadline` is not checked (errcheck)
}
n, err = d.c.Write(b)
return
}

func (d *deadlineIO) Read(b []byte) (n int, err error) {
if d.to > 0 {
d.c.SetReadDeadline(time.Now().Add(d.to))

Check failure on line 23 in deadline_io.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.c.SetReadDeadline` is not checked (errcheck)

Check failure on line 23 in deadline_io.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `d.c.SetReadDeadline` is not checked (errcheck)
}
n, err = d.c.Read(b)
return
}
140 changes: 140 additions & 0 deletions dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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 interface{}
if sslCtx, err = sslCreateContext(sslOpts); err != nil {
return
}

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

// interface{} is a hack. It helps to avoid dependency of go-openssl in build
// of tests with the tag 'go_tarantool_ssl_disable'.
func sslCreateContext(sslOpts opts) (ctx interface{}, err error) {
var sslCtx *openssl.Ctx

// 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
}
ctx = sslCtx
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 {

Check failure on line 45 in dial.go

View workflow job for this annotation

GitHub Actions / golangci-lint

line is 106 characters (lll)
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 != "" {
sslCtx.SetCipherList(sslOpts.Ciphers)

Check failure on line 59 in dial.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `sslCtx.SetCipherList` is not checked (errcheck)

Check failure on line 59 in dial.go

View workflow job for this annotation

GitHub Actions / golangci-lint

Error return value of `sslCtx.SetCipherList` is not checked (errcheck)
}

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

Please sign in to comment.