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 2, 2024
1 parent 0574cb3 commit 405f439
Show file tree
Hide file tree
Showing 23 changed files with 1,934 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.

### Added

* `OpenSslDialer` type (#1).

### Changed

### Removed
Expand Down
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
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 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) {
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 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{}
}
31 changes: 31 additions & 0 deletions deadline_io.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
}
143 changes: 143 additions & 0 deletions dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
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 {
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 405f439

Please sign in to comment.