From e141d1de9a75b26085ce24417c6e2bca17f479ee Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 1 Feb 2024 03:01:32 +0300 Subject: [PATCH 1/3] ci: setup ci workflow In this commit the ci workflow is created for the repository. --- .codespellrc | 5 + .github/workflows/check.yml | 72 ++++++++++++++ .github/workflows/testing.yml | 174 ++++++++++++++++++++++++++++++++++ .golangci-lint.yaml | 28 ++++++ .luacheckrc | 20 ++++ 5 files changed, 299 insertions(+) create mode 100644 .codespellrc create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/testing.yml create mode 100644 .golangci-lint.yaml create mode 100644 .luacheckrc diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..3bde87e --- /dev/null +++ b/.codespellrc @@ -0,0 +1,5 @@ +[codespell] +skip = */testdata,./LICENSE +ignore-words-list = gost +count = +quiet-level = 3 diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..2ec1fe5 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,72 @@ +name: Run checks + +on: + push: + pull_request: + +jobs: + luacheck: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@master + + - name: Setup Tarantool + uses: tarantool/setup-tarantool@v2 + with: + tarantool-version: '2.8' + + - name: Setup tt + run: | + curl -L https://tarantool.io/release/2/installer.sh | sudo bash + sudo apt install -y tt + tt version + + - name: Setup luacheck + run: tt rocks install luacheck 0.25.0 + + - name: Run luacheck + run: ./.rocks/bin/luacheck . + + golangci-lint: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/setup-go@v2 + + - uses: actions/checkout@v2 + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + continue-on-error: true + with: + # The first run is for GitHub Actions error format. + args: --config=.golangci-lint.yaml + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # The second run is for human-readable error format with a file name + # and a line number. + args: --out-${NO_FUTURE}format colored-line-number --config=.golangci-lint.yaml + + codespell: + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.repository + steps: + - uses: actions/checkout@master + + - name: Install codespell + run: pip3 install codespell + + - name: Run codespell + run: codespell diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..98fad77 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,174 @@ +name: testing + +on: + push: + pull_request: + pull_request_target: + types: [labeled] + workflow_dispatch: + +jobs: + run-tests-ee: + # Does not run on pull requests from forks and on forks by default. + # Tests from forks will run only when the pull request is labeled with + # `full-ci`. To avoid security problems, the label must be reset manually + # for every run. + # + # We need to use `pull_request_target` because it has access to base + # repository secrets unlike `pull_request`. + if: | + github.repository == 'tarantool/go-tlsdialer' && + (github.event_name == 'push' || + (github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name != github.repository && + github.event.label.name == 'full-ci')) || + github.event_name == 'workflow_dispatch' + + runs-on: ubuntu-latest + + env: + COVERAGE_FILE: 'coverage.out' + + strategy: + fail-fast: false + matrix: + golang: + - '1.20' + - 'stable' + sdk-path: + - 'release/linux/x86_64/2.10/' + sdk-version: + - 'sdk-gc64-2.10.8-0-r598.linux.x86_64' + coveralls: [ false ] + include: + - golang: '1.20' + sdk-path: 'release/linux/x86_64/2.11/' + sdk-version: 'sdk-gc64-2.11.1-0-r598.linux.x86_64' + coveralls: false + - golang: 'stable' + sdk-path: 'release/linux/x86_64/2.11/' + sdk-version: 'sdk-gc64-2.11.1-0-r598.linux.x86_64' + coveralls: false + - golang: '1.20' + sdk-path: 'release/linux/x86_64/3.0/' + sdk-version: 'sdk-gc64-3.0.0-0-gf58f7d82a-r23.linux.x86_64' + coveralls: false + - golang: 'stable' + sdk-path: 'release/linux/x86_64/3.0/' + sdk-version: 'sdk-gc64-3.0.0-0-gf58f7d82a-r23.linux.x86_64' + coveralls: true + + steps: + - uses: actions/checkout@v3 + + - name: Setup Tarantool ${{ matrix.sdk-version }} + run: | + ARCHIVE_NAME=tarantool-enterprise-${{ matrix.sdk-version }}.tar.gz + curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${{ matrix.sdk-path }}${ARCHIVE_NAME} + tar -xzf ${ARCHIVE_NAME} + rm -f ${ARCHIVE_NAME} + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v3 + with: + go-version: ${{matrix.golang}} + + - name: Run regression tests + run: | + source tarantool-enterprise/env.sh + go test -v -count=1 -shuffle=on ./... + go test -race -v -count=1 -shuffle=on ./... + + - name: Run tests, collect code coverage data and send to Coveralls + if: ${{ matrix.coveralls }} + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + source tarantool-enterprise/env.sh + go clean -testcache + go get golang.org/x/tools/cmd/cover + go test ./... -v -count=1 -shuffle=on -covermode=atomic -coverprofile=${COVERAGE_FILE} + go tool cover -func=${COVERAGE_FILE} + go get github.com/mattn/goveralls + go install github.com/mattn/goveralls + goveralls -coverprofile=${COVERAGE_FILE} -service=github + + testing_mac_os: + # We want to run on external PRs, but not on our own internal + # PRs as they'll be run by the push to the branch. + # + # The main trick is described here: + # https://github.com/Dart-Code/Dart-Code/pull/2375 + if: | + github.repository == 'tarantool/go-tlsdialer' && + (github.event_name == 'push' || + (github.event_name == 'pull_request_target' && + github.event.pull_request.head.repo.full_name != github.repository && + github.event.label.name == 'full-ci')) || + github.event_name == 'workflow_dispatch' + + strategy: + fail-fast: false + matrix: + golang: + - '1.20' + - 'stable' + runs-on: + - macos-11 + - macos-12 + tarantool-ee: + - 'gc64-2.11.2-0-r613' + + env: + # Set as absolute paths to avoid any possible confusion + # after changing a current directory. + SRCDIR: ${{ format('{0}/{1}', github.workspace, github.repository) }} + + runs-on: ${{ matrix.runs-on }} + steps: + - name: Clone the connector + uses: actions/checkout@v3 + with: + path: ${{ env.SRCDIR }} + + - name: Install latest tt from brew + run: brew install tt + + - name: Install tarantool + env: + TT_CLI_EE_USERNAME: ${{ secrets.TT_EE_USERNAME }} + TT_CLI_EE_PASSWORD: ${{ secrets.TT_EE_PASSWORD }} + run: | + tt init + tt -V install tarantool-ee ${{ matrix.tarantool-ee }} + # Delete the tt config so that it does not affect the test environment. + rm -f tt.yaml + + - name: Add Tarantool to Path + run: | + echo "${GITHUB_WORKSPACE}/bin" >> $GITHUB_PATH + + - name: Set Tarantool include directory to the environment + run: | + echo "TT_CLI_TARANTOOL_PREFIX=${GITHUB_WORKSPACE}/include/" >> $GITHUB_ENV + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.golang }} + + # Workaround for Mac OS 12 testrace failure + # https://github.com/golang/go/issues/49138 + - name: disable MallocNanoZone for macos-12 + run: echo "MallocNanoZone=0" >> $GITHUB_ENV + if: matrix.runs-on == 'macos-12' + + - name: Install test dependencies + run: | + brew install tt + + - name: Run regression tests + run: | + cd "${SRCDIR}" + go test -v -count=1 -shuffle=on ./... + go test -race -v -count=1 -shuffle=on ./... diff --git a/.golangci-lint.yaml b/.golangci-lint.yaml new file mode 100644 index 0000000..4d9fbbf --- /dev/null +++ b/.golangci-lint.yaml @@ -0,0 +1,28 @@ +run: + timeout: 3m + +linters: + enable: + - errorlint + - forbidigo + - gocritic + - godot + - goimports + - lll + - reassign + - revive + - stylecheck + - testpackage + - unconvert + - unused + +linters-settings: + lll: + line-length: 100 + tab-width: 4 + +issues: + exclude-rules: + - linters: + - lll + source: "\t?// *(see )?https://" diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..b8fc708 --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,20 @@ +redefined = false + +globals = { + 'box', + 'utf8', + 'checkers', + '_TARANTOOL' +} + +include_files = { + '**/*.lua', + '*.luacheckrc', + '*.rockspec' +} + +exclude_files = { + '**/*.rocks/' +} + +max_line_length = 120 From 24f76a68c0d2fad0e0d33398318de4a5fdbe381e Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 1 Feb 2024 03:03:03 +0300 Subject: [PATCH 2/3] docs: create a README file In thes commit `README.md` and `CHANGELOG.md` files are created. `README` shows the purpose of this repository. --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 README.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..759daee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic +Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. + +## [Unreleased] + +### Added + +### Changed + +### Removed + +### Fixed diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fc6440 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +[![Go Reference][godoc-badge]][godoc-url] +[![Code Coverage][coverage-badge]][coverage-url] + +# tlsdialer + +This package allows creating a TLS dialer for +[`go-tarantool`](https://github.com/tarantool/go-tarantool). +It serves as an interlayer between go-tarantool and go-openssl. + +go-tlsdialer uses tarantool connection, but also types and methods from +go-openssl. + +## Run tests + +To run a default set of tests: + +```go +go test -v ./... +``` + +[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 +[coverage-url]: https://coveralls.io/github/tarantool/go-tlsdialer?branch=master From 4c1964286ef8f361ac9eec664d76759f23905d6e Mon Sep 17 00:00:00 2001 From: DerekBum Date: Thu, 1 Feb 2024 03:05:32 +0300 Subject: [PATCH 3/3] 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 https://github.com/tarantool/go-tarantool/issues/301 --- CHANGELOG.md | 3 + README.md | 145 ++++++++ conn.go | 66 ++++ deadlineio.go | 31 ++ dial.go | 138 ++++++++ dialer.go | 141 ++++++++ dialer_test.go | 469 ++++++++++++++++++++++++++ example_test.go | 64 ++++ export_test.go | 16 + go.mod | 21 ++ go.sum | 39 +++ integration_test.go | 670 +++++++++++++++++++++++++++++++++++++ opts.go | 30 ++ testdata/ca.crt | 20 ++ testdata/config.lua | 39 +++ testdata/empty | 0 testdata/generate.sh | 38 +++ testdata/invalidpasswords | 1 + testdata/localhost.crt | 22 ++ testdata/localhost.enc.key | 30 ++ testdata/localhost.key | 28 ++ testdata/passwords | 2 + 22 files changed, 2013 insertions(+) create mode 100644 conn.go create mode 100644 deadlineio.go create mode 100644 dial.go create mode 100644 dialer.go create mode 100644 dialer_test.go create mode 100644 example_test.go create mode 100644 export_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 integration_test.go create mode 100644 opts.go create mode 100644 testdata/ca.crt create mode 100644 testdata/config.lua create mode 100644 testdata/empty create mode 100755 testdata/generate.sh create mode 100644 testdata/invalidpasswords create mode 100644 testdata/localhost.crt create mode 100644 testdata/localhost.enc.key create mode 100644 testdata/localhost.key create mode 100644 testdata/passwords diff --git a/CHANGELOG.md b/CHANGELOG.md index 759daee..4918140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 9fc6440..67cd955 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/conn.go b/conn.go new file mode 100644 index 0000000..448dd32 --- /dev/null +++ b/conn.go @@ -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{} +} diff --git a/deadlineio.go b/deadlineio.go new file mode 100644 index 0000000..679b28b --- /dev/null +++ b/deadlineio.go @@ -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 +} diff --git a/dial.go b/dial.go new file mode 100644 index 0000000..b1a80ea --- /dev/null +++ b/dial.go @@ -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 +} diff --git a/dialer.go b/dialer.go new file mode 100644 index 0000000..e03fce1 --- /dev/null +++ b/dialer.go @@ -0,0 +1,141 @@ +package tlsdialer + +import ( + "bufio" + "context" + "fmt" + + "github.com/tarantool/go-tarantool/v2" +) + +const bufSize = 128 * 1024 + +type openSSLDialer struct { + address string + sslKeyFile string + sslCertFile string + sslCaFile string + sslCiphers string + sslPassword string + sslPasswordFile string +} + +func (d openSSLDialer) Dial(ctx context.Context, + dialOpts tarantool.DialOpts) (tarantool.Conn, error) { + var err error + conn := new(ttConn) + + network, address := parseAddress(d.address) + conn.net, err = sslDialContext(ctx, network, address, opts{ + KeyFile: d.sslKeyFile, + CertFile: d.sslCertFile, + CaFile: d.sslCaFile, + Ciphers: d.sslCiphers, + Password: d.sslPassword, + PasswordFile: d.sslPasswordFile, + }) + if err != nil { + return nil, fmt.Errorf("failed to dial: %w", err) + } + + dc := &deadlineIO{to: dialOpts.IoTimeout, c: conn.net} + conn.reader = bufio.NewReaderSize(dc, bufSize) + conn.writer = bufio.NewWriterSize(dc, bufSize) + + return conn, nil +} + +// 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 +} + +// Dial makes OpenSSLDialer satisfy the Dialer interface. +func (d OpenSSLDialer) Dial(ctx context.Context, + opts tarantool.DialOpts) (tarantool.Conn, error) { + if d.Auth != tarantool.AutoAuth { + d.RequiredProtocolInfo.Auth = d.Auth + } + + dialer := tarantool.AuthDialer{ + Dialer: tarantool.ProtocolDialer{ + Dialer: tarantool.GreetingDialer{ + Dialer: openSSLDialer{ + address: d.Address, + sslKeyFile: d.SslKeyFile, + sslCertFile: d.SslCertFile, + sslCaFile: d.SslCaFile, + sslCiphers: d.SslCiphers, + sslPassword: d.SslPassword, + sslPasswordFile: d.SslPasswordFile, + }, + }, + RequiredProtocolInfo: d.RequiredProtocolInfo, + }, + Auth: d.Auth, + Username: d.User, + Password: d.Password, + } + + return dialer.Dial(ctx, opts) +} + +// parseAddress split address into network and address parts. +func parseAddress(address string) (string, string) { + network := "tcp" + addrLen := len(address) + + switch { + case addrLen >= 6 && address[0:6] == "tcp://": + address = address[6:] + case addrLen >= 4 && address[0:4] == "tcp:": + address = address[4:] + } + + return network, address +} diff --git a/dialer_test.go b/dialer_test.go new file mode 100644 index 0000000..0eebe4e --- /dev/null +++ b/dialer_test.go @@ -0,0 +1,469 @@ +package tlsdialer_test + +import ( + "context" + "fmt" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-iproto" + "github.com/tarantool/go-openssl" + "github.com/tarantool/go-tlsdialer" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" +) + +const ttHost = "127.0.0.1:3014" + +func genSalt() [64]byte { + salt := [64]byte{} + for i := 0; i < 44; i++ { + salt[i] = 'a' + } + return salt +} + +var ( + opts = tarantool.Opts{ + Timeout: 5 * time.Second, + } + + testDialUser = "test" + testDialPass = "test" + testDialVersion = [64]byte{'t', 'e', 's', 't'} + + // Salt with end zeros. + testDialSalt = genSalt() + + idRequestExpected = []byte{ + 0xce, 0x00, 0x00, 0x00, 29, // Length. + 0x82, // Header map. + 0x00, 0x49, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0x54, + 0xcf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, // Version. + 0x55, + 0x97, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // Features. + } + + idResponseTyped = tarantool.ProtocolInfo{ + Version: 6, + Features: []iproto.Feature{iproto.Feature(1), iproto.Feature(21)}, + Auth: tarantool.ChapSha1Auth, + } + + idResponse = []byte{ + 0xce, 0x00, 0x00, 0x00, 37, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + + 0x83, // Data map. + 0x54, + 0x06, // Version. + 0x55, + 0x92, 0x01, 0x15, // Features. + 0x5b, + 0xa9, 'c', 'h', 'a', 'p', '-', 's', 'h', 'a', '1', + } + + idResponseNotSupported = []byte{ + 0xce, 0x00, 0x00, 0x00, 25, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x80, 0x30, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + 0x81, + 0x31, + 0xa3, 'e', 'r', 'r', + } + + authRequestExpectedChapSha1 = []byte{ + 0xce, 0x00, 0x00, 0x00, 57, // Length. + 0x82, // Header map. + 0x00, 0x07, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0xce, 0x00, 0x00, 0x00, 0x23, + 0xa4, 't', 'e', 's', 't', // Login. + 0xce, 0x00, 0x00, 0x00, 0x21, + 0x92, // Tuple. + 0xa9, 'c', 'h', 'a', 'p', '-', 's', 'h', 'a', '1', + + // Scramble. + 0xb4, 0x1b, 0xd4, 0x20, 0x45, 0x73, 0x22, + 0xcf, 0xab, 0x05, 0x03, 0xf3, 0x89, 0x4b, + 0xfe, 0xc7, 0x24, 0x5a, 0xe6, 0xe8, 0x31, + } + + authRequestExpectedPapSha256 = []byte{ + 0xce, 0x00, 0x00, 0x00, 0x2a, // Length. + 0x82, // Header map. + 0x00, 0x07, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + + 0x82, // Data map. + 0xce, 0x00, 0x00, 0x00, 0x23, + 0xa4, 't', 'e', 's', 't', // Login. + 0xce, 0x00, 0x00, 0x00, 0x21, + 0x92, // Tuple. + 0xaa, 'p', 'a', 'p', '-', 's', 'h', 'a', '2', '5', '6', + 0xa4, 't', 'e', 's', 't', + } + + okResponse = []byte{ + 0xce, 0x00, 0x00, 0x00, 19, // Length. + 0x83, // Header map. + 0x00, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xce, 0x00, 0x00, 0x00, 0x00, + 0x05, 0xce, 0x00, 0x00, 0x00, 0x61, + } + + errResponse = []byte{0xce} +) + +type testDialOpts struct { + name string + address string + wantErr bool + expectedErr string + expectedProtocolInfo tarantool.ProtocolInfo + + // These options configure the behavior of the server. + isErrGreeting bool + isErrID bool + isIDUnsupported bool + isPapSha256Auth bool + isErrAuth bool +} + +type dialServerActual struct { + IDRequest []byte + AuthRequest []byte +} + +func testDialAccept(opts testDialOpts, l net.Listener) chan dialServerActual { + ch := make(chan dialServerActual, 1) + + go func() { + client, err := l.Accept() + if err != nil { + return + } + defer client.Close() + if opts.isErrGreeting { + _, _ = client.Write(errResponse) + return + } + // Write greeting. + if _, err = client.Write(testDialVersion[:]); err != nil { + return + } + if _, err = client.Write(testDialSalt[:]); err != nil { + return + } + + // Read Id request. + idRequestActual := make([]byte, len(idRequestExpected)) + if _, err = client.Read(idRequestActual); err != nil { + return + } + + // Make Id response. + switch { + case opts.isErrID: + _, err = client.Write(errResponse) + case opts.isIDUnsupported: + _, err = client.Write(idResponseNotSupported) + default: + _, err = client.Write(idResponse) + } + if err != nil { + return + } + + // Read Auth request. + authRequestExpected := authRequestExpectedChapSha1 + if opts.isPapSha256Auth { + authRequestExpected = authRequestExpectedPapSha256 + } + authRequestActual := make([]byte, len(authRequestExpected)) + if _, err = client.Read(authRequestActual); err != nil { + return + } + + // Make Auth response. + if opts.isErrAuth { + _, err = client.Write(errResponse) + } else { + _, err = client.Write(okResponse) + } + if err != nil { + return + } + + ch <- dialServerActual{ + IDRequest: idRequestActual, + AuthRequest: authRequestActual, + } + }() + + return ch +} + +func testDialer(t *testing.T, l net.Listener, dialer tarantool.Dialer, + opts testDialOpts) { + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + ch := testDialAccept(opts, l) + conn, err := dialer.Dial(ctx, tarantool.DialOpts{ + IoTimeout: time.Second * 2, + }) + if opts.wantErr { + require.Error(t, err) + require.Contains(t, err.Error(), opts.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, opts.expectedProtocolInfo, conn.ProtocolInfo()) + require.Equal(t, testDialVersion[:], []byte(conn.Greeting().Version)) + require.Equal(t, testDialSalt[:44], []byte(conn.Greeting().Salt)) + + actual := <-ch + require.Equal(t, idRequestExpected, actual.IDRequest) + + authRequestExpected := authRequestExpectedChapSha1 + if opts.isPapSha256Auth { + authRequestExpected = authRequestExpectedPapSha256 + } + require.Equal(t, authRequestExpected, actual.AuthRequest) + conn.Close() +} + +func createSslListener(t *testing.T, opts tlsdialer.SslTestOpts) net.Listener { + ctx, err := tlsdialer.SslCreateContext(opts) + require.NoError(t, err) + l, err := openssl.Listen("tcp", "127.0.0.1:0", ctx) + require.NoError(t, err) + return l +} + +func TestOpenSslDialer_Dial_opts(t *testing.T) { + for _, test := range sslTests { + t.Run(test.name, func(t *testing.T) { + l := createSslListener(t, test.serverOpts) + defer l.Close() + addr := l.Addr().String() + + dialer := tlsdialer.OpenSSLDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: test.clientOpts.KeyFile, + SslCertFile: test.clientOpts.CertFile, + SslCaFile: test.clientOpts.CaFile, + SslCiphers: test.clientOpts.Ciphers, + SslPassword: test.clientOpts.Password, + SslPasswordFile: test.clientOpts.PasswordFile, + } + testDialer(t, l, dialer, testDialOpts{ + wantErr: !test.ok, + expectedProtocolInfo: idResponseTyped.Clone(), + }) + }) + } +} + +func TestOpenSslDialer_Dial_basic(t *testing.T) { + l := createSslListener(t, tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tlsdialer.OpenSSLDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + } + + cases := []testDialOpts{ + { + name: "all is ok", + expectedProtocolInfo: idResponseTyped.Clone(), + }, + { + name: "id request unsupported", + expectedProtocolInfo: tarantool.ProtocolInfo{}, + isIDUnsupported: true, + }, + { + name: "greeting response error", + wantErr: true, + expectedErr: "failed to read greeting", + isErrGreeting: true, + }, + { + name: "id response error", + wantErr: true, + expectedErr: "failed to identify", + isErrID: true, + }, + { + name: "auth response error", + wantErr: true, + expectedErr: "failed to authenticate", + isErrAuth: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + testDialer(t, l, dialer, tc) + }) + } +} + +func TestOpenSslDialer_Dial_requirements(t *testing.T) { + l := createSslListener(t, tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tlsdialer.OpenSSLDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + RequiredProtocolInfo: tarantool.ProtocolInfo{ + Features: []iproto.Feature{42}, + }, + } + + testDialAccept(testDialOpts{}, l) + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "invalid server protocol") +} + +func TestOpenSslDialer_Dial_papSha256Auth(t *testing.T) { + l := createSslListener(t, tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + dialer := tlsdialer.OpenSSLDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + Auth: tarantool.PapSha256Auth, + } + + // Response from the server. + protocol := idResponseTyped.Clone() + protocol.Auth = tarantool.ChapSha1Auth + + testDialer(t, l, dialer, testDialOpts{ + expectedProtocolInfo: protocol, + isPapSha256Auth: true, + }) +} + +func TestOpenSslDialer_Dial_ctx_cancel(t *testing.T) { + serverOpts := tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + clientOpts := tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + } + + l := createSslListener(t, serverOpts) + defer l.Close() + addr := l.Addr().String() + testDialAccept(testDialOpts{}, l) + + dialer := tlsdialer.OpenSSLDialer{ + Address: addr, + User: testDialUser, + Password: testDialPass, + SslKeyFile: clientOpts.KeyFile, + SslCertFile: clientOpts.CertFile, + SslCaFile: clientOpts.CaFile, + SslCiphers: clientOpts.Ciphers, + SslPassword: clientOpts.Password, + SslPasswordFile: clientOpts.PasswordFile, + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{}) + if err == nil { + conn.Close() + } + require.Error(t, err) +} + +func TestAddressFormat_tcp(t *testing.T) { + l := createSslListener(t, tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }) + + defer l.Close() + addr := l.Addr().String() + + protocolInfo := idResponseTyped.Clone() + + cases := []testDialOpts{ + { + name: "base", + address: addr, + expectedProtocolInfo: protocolInfo, + }, + { + name: "tcp://", + address: fmt.Sprintf("tcp://%s", addr), + expectedProtocolInfo: protocolInfo, + }, + { + name: "tcp:", + address: fmt.Sprintf("tcp:%s", addr), + expectedProtocolInfo: protocolInfo, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dialer := tlsdialer.OpenSSLDialer{ + Address: tc.address, + User: testDialUser, + Password: testDialPass, + } + + testDialer(t, l, dialer, tc) + }) + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..1c3d089 --- /dev/null +++ b/example_test.go @@ -0,0 +1,64 @@ +package tlsdialer_test + +import ( + "context" + "fmt" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tlsdialer" +) + +func ExampleOpenSSLDialer() { + dialer := tlsdialer.OpenSSLDialer{ + Address: "127.0.0.1:3014", + User: "test", + Password: "test", + } + opts := tarantool.Opts{ + Timeout: 5 * time.Second, + } + + // Start Tarantool instance with enabled ssl and set keys and credentials. + listen := "127.0.0.1:3014" + "?transport=ssl&" + listen += "ssl_key_file=testdata/localhost.key&" + listen += "ssl_cert_file=testdata/localhost.crt" + + inst, err := test_helpers.StartTarantool( + test_helpers.StartOpts{ + Dialer: dialer, + InitScript: "testdata/config.lua", + Listen: listen, + SslCertsDir: "testdata", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }, + ) + if err != nil { + fmt.Printf("Failed to create a Tarantool instance: %s", err) + return + } + defer test_helpers.StopTarantool(inst) + + 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.NewPingRequest()).Get() + if err != nil { + fmt.Printf("Error: %s", err) + } else { + fmt.Printf("Data: %v", data) + } + + // Output: + // Data: [] +} diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..1ab7f6f --- /dev/null +++ b/export_test.go @@ -0,0 +1,16 @@ +package tlsdialer + +import "github.com/tarantool/go-openssl" + +type SslTestOpts struct { + KeyFile string + CertFile string + CaFile string + Ciphers string + Password string + PasswordFile string +} + +func SslCreateContext(sslOpts SslTestOpts) (ctx *openssl.Ctx, err error) { + return sslCreateContext(opts(sslOpts)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1355d11 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/tarantool/go-tlsdialer + +require ( + github.com/stretchr/testify v1.7.1 + github.com/tarantool/go-iproto v1.0.0 + github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca + github.com/tarantool/go-tarantool/v2 v2.0.0-20240202153142-e765b0ab1424 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mattn/go-pointer v0.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +go 1.20 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..de58509 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= +github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarantool/go-iproto v1.0.0 h1:quC4hdFhCuFYaCqOFgUxH2foRkhAy+TlEy7gQLhdVjw= +github.com/tarantool/go-iproto v1.0.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= +github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca h1:oOrBh73tDDyooIXajfr+0pfnM+89404ClAhJpTTHI7E= +github.com/tarantool/go-openssl v0.0.8-0.20231004103608-336ca939d2ca/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= +github.com/tarantool/go-tarantool/v2 v2.0.0-20240202153142-e765b0ab1424 h1:rPuYmT55Dq15VhW8Ao1s8tT9vRlARB1N6aGqwIe8gsA= +github.com/tarantool/go-tarantool/v2 v2.0.0-20240202153142-e765b0ab1424/go.mod h1:fGJBRxbkZmNQap9VxQ7xQHlDhRiHFnqP3gK4ghtlro0= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration_test.go b/integration_test.go new file mode 100644 index 0000000..d270b15 --- /dev/null +++ b/integration_test.go @@ -0,0 +1,670 @@ +package tlsdialer_test + +import ( + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + "github.com/tarantool/go-tlsdialer" +) + +var server = "127.0.0.1:3013" + +var startOpts = test_helpers.StartOpts{ + Dialer: dialer, + InitScript: "testdata/config.lua", + Listen: server, + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, +} + +var dialer = tarantool.NetDialer{ + Address: server, + User: "test", + Password: "test", +} + +func serverTt(serverOpts tlsdialer.SslTestOpts, + auth tarantool.Auth) (test_helpers.TarantoolInstance, error) { + listen := ttHost + "?transport=ssl&" + + key := serverOpts.KeyFile + if key != "" { + listen += fmt.Sprintf("ssl_key_file=%s&", key) + } + + cert := serverOpts.CertFile + if cert != "" { + listen += fmt.Sprintf("ssl_cert_file=%s&", cert) + } + + ca := serverOpts.CaFile + if ca != "" { + listen += fmt.Sprintf("ssl_ca_file=%s&", ca) + } + + ciphers := serverOpts.Ciphers + if ciphers != "" { + listen += fmt.Sprintf("ssl_ciphers=%s&", ciphers) + } + + password := serverOpts.Password + if password != "" { + listen += fmt.Sprintf("ssl_password=%s&", password) + } + + passwordFile := serverOpts.PasswordFile + if passwordFile != "" { + listen += fmt.Sprintf("ssl_password_file=%s&", passwordFile) + } + + listen = listen[:len(listen)-1] + + return test_helpers.StartTarantool( + test_helpers.StartOpts{ + Dialer: tlsdialer.OpenSSLDialer{ + Address: ttHost, + Auth: auth, + User: "test", + Password: "test", + SslKeyFile: serverOpts.KeyFile, + SslCertFile: serverOpts.CertFile, + SslCaFile: serverOpts.CaFile, + SslCiphers: serverOpts.Ciphers, + SslPassword: serverOpts.Password, + SslPasswordFile: serverOpts.PasswordFile, + }, + Auth: auth, + InitScript: "testdata/config.lua", + Listen: listen, + SslCertsDir: "testdata", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 10, + RetryTimeout: 500 * time.Millisecond, + }, + ) +} + +func serverTtStop(inst test_helpers.TarantoolInstance) { + test_helpers.StopTarantoolWithCleanup(inst) +} + +func checkTtConn(dialer tarantool.Dialer) error { + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := tarantool.Connect(ctx, dialer, tarantool.Opts{ + Timeout: 500 * time.Millisecond, + SkipSchema: true, + }) + if err != nil { + return err + } + conn.Close() + return nil +} + +func assertConnectionTtFail(t testing.TB, serverOpts tlsdialer.SslTestOpts, + dialer tlsdialer.OpenSSLDialer) { + t.Helper() + + inst, err := serverTt(serverOpts, tarantool.AutoAuth) + defer serverTtStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + + err = checkTtConn(dialer) + if err == nil { + t.Errorf("An unexpected connection to the server") + } +} + +func assertConnectionTtOk(t testing.TB, serverOpts tlsdialer.SslTestOpts, + dialer tlsdialer.OpenSSLDialer) { + t.Helper() + + inst, err := serverTt(serverOpts, tarantool.AutoAuth) + defer serverTtStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + + err = checkTtConn(dialer) + if err != nil { + t.Errorf("An unexpected connection error %q", err.Error()) + } +} + +type sslTest struct { + name string + ok bool + serverOpts tlsdialer.SslTestOpts + clientOpts tlsdialer.SslTestOpts +} + +/* +Requirements from Tarantool Enterprise Edition manual: +https://www.tarantool.io/ru/enterprise_doc/security/#configuration + +For a server: +KeyFile - mandatory +CertFile - mandatory +CaFile - optional +Ciphers - optional + +For a client: +KeyFile - optional, mandatory if server.CaFile set +CertFile - optional, mandatory if server.CaFile set +CaFile - optional, +Ciphers - optional. +*/ +var sslTests = []sslTest{ + { + "key_crt_server", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + tlsdialer.SslTestOpts{}, + }, + { + "key_crt_server_and_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{}, + }, + { + "key_crt_ca_server_key_crt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server_and_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_key", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "any_invalid_path", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_crt", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "any_invalid_path", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_ca", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "any_invalid_path", + }, + }, + { + "key_crt_ca_server_and_client_empty_key", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/empty", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_crt", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/empty", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_ca", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/empty", + }, + }, + { + "key_crt_server_and_key_crt_ca_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_key_crt_ca_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_and_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + }, + { + "non_equal_ciphers_client", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "TLS_AES_128_GCM_SHA256", + }, + }, + { + "pass_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + }, + }, + { + "passfile_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_passfile_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "inv_pass_and_passfile_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/passwords", + }, + }, + { + "pass_and_inv_passfile_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "pass_and_not_existing_passfile_key_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "mysslpassword", + PasswordFile: "testdata/notafile", + }, + }, + { + "inv_pass_and_inv_passfile_key_encrypt_client", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + PasswordFile: "testdata/invalidpasswords", + }, + }, + { + "not_existing_passfile_key_encrypt_client", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/notafile", + }, + }, + { + "no_pass_key_encrypt_client", + false, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.enc.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "pass_key_non_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + Password: "invalidpassword", + }, + }, + { + "passfile_key_non_encrypt_client", + true, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + PasswordFile: "testdata/invalidpasswords", + }, + }, +} + +func makeOpenSslDialer(opts tlsdialer.SslTestOpts) tlsdialer.OpenSSLDialer { + return tlsdialer.OpenSSLDialer{ + Address: ttHost, + User: "test", + Password: "test", + SslKeyFile: opts.KeyFile, + SslCertFile: opts.CertFile, + SslCaFile: opts.CaFile, + SslCiphers: opts.Ciphers, + SslPassword: opts.Password, + SslPasswordFile: opts.PasswordFile, + } +} + +func TestSslOpts(t *testing.T) { + for _, test := range sslTests { + dialer := makeOpenSslDialer(test.clientOpts) + if test.ok { + t.Run("ok_tt_"+test.name, func(t *testing.T) { + assertConnectionTtOk(t, test.serverOpts, dialer) + }) + } else { + t.Run("fail_tt_"+test.name, func(t *testing.T) { + assertConnectionTtFail(t, test.serverOpts, dialer) + }) + } + } +} + +func TestOpts_PapSha256Auth(t *testing.T) { + isLess, err := test_helpers.IsTarantoolVersionLess(2, 11, 0) + if err != nil { + t.Fatalf("Could not check Tarantool version: %s", err) + } + if isLess { + t.Skip("Skipping test for Tarantool without pap-sha256 support") + } + + sslOpts := tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + } + + inst, err := serverTt(sslOpts, tarantool.PapSha256Auth) + defer serverTtStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + + client := tlsdialer.OpenSSLDialer{ + Address: ttHost, + Auth: tarantool.PapSha256Auth, + User: "test", + Password: "test", + RequiredProtocolInfo: tarantool.ProtocolInfo{}, + SslKeyFile: sslOpts.KeyFile, + SslCertFile: sslOpts.CertFile, + } + + conn := test_helpers.ConnectWithValidation(t, client, opts) + conn.Close() + + client.Auth = tarantool.AutoAuth + conn = test_helpers.ConnectWithValidation(t, client, opts) + conn.Close() +} + +func TestReadDeadline(t *testing.T) { + sslOpts := tlsdialer.SslTestOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + } + + inst, err := serverTt(sslOpts, tarantool.AutoAuth) + defer serverTtStop(inst) + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } + + dialer := tlsdialer.OpenSSLDialer{ + Address: ttHost, + User: testDialUser, + Password: testDialPass, + } + + ioTimeout := 5 * time.Second + + ctx, cancel := test_helpers.GetConnectContext() + defer cancel() + conn, err := dialer.Dial(ctx, tarantool.DialOpts{ + IoTimeout: ioTimeout, + }) + if conn == nil { + t.Fatalf("Unable to connect: %s", err) + } + defer conn.Close() + + largeMessage := [1024 * 1024]byte{} + + timeStart := time.Now() + n, err := conn.Read(largeMessage[:]) + timeEnd := time.Now() + + if n != 0 || err == nil { + t.Errorf("Successful read, expected i/o timeout") + } + eps := 500 * time.Millisecond + if timeStart.Add(ioTimeout).After(timeEnd.Add(eps)) || + timeStart.Add(ioTimeout).Before(timeEnd.Add(-eps)) { + t.Errorf("Incorrect timeout while reading") + } +} + +// runTestMain is a body of TestMain function +// (see https://pkg.go.dev/testing#hdr-Main). +// Using defer + os.Exit is not works so TestMain body +// is a separate function, see +// https://stackoverflow.com/questions/27629380/how-to-exit-a-go-program-honoring-deferred-calls +func runTestMain(m *testing.M) int { + // Tarantool supports streams and interactive transactions since version 2.10.0 + isStreamUnsupported, err := test_helpers.IsTarantoolVersionLess(2, 10, 0) + if err != nil { + log.Fatalf("Could not check the Tarantool version: %s", err) + } + + startOpts.MemtxUseMvccEngine = !isStreamUnsupported + + inst, err := test_helpers.StartTarantool(startOpts) + defer test_helpers.StopTarantoolWithCleanup(inst) + + if err != nil { + log.Printf("Failed to prepare test tarantool: %s", err) + return 1 + } + + return m.Run() +} + +func TestMain(m *testing.M) { + code := runTestMain(m) + os.Exit(code) +} diff --git a/opts.go b/opts.go new file mode 100644 index 0000000..1d24571 --- /dev/null +++ b/opts.go @@ -0,0 +1,30 @@ +package tlsdialer + +type opts struct { + // KeyFile is a path to a private SSL key file. + KeyFile string + // CertFile is a path to an SSL certificate file. + CertFile string + // CaFile is a path to a trusted certificate authorities (CA) file. + CaFile string + // Ciphers 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 + Ciphers string + // Password is a password for decrypting the private SSL key file. + // The priority is as follows: try to decrypt with Password, then + // try PasswordFile. + Password string + // PasswordFile 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. + PasswordFile string +} diff --git a/testdata/ca.crt b/testdata/ca.crt new file mode 100644 index 0000000..2fa1a12 --- /dev/null +++ b/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUMMZTmNkhr4qOfSwInVk2dAJvoBEwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCRq/eaA3I6CB8t770H2XDdzcp1yuC/+TZOxV5o0LuRkogTvL2kYULBrfx1 +rVZu8zQJTx1fmSRj1cN8j+IrmXN5goZ3mYFTnnIOgkyi+hJysVlo5s0Kp0qtLLGM +OuaVbxw2oAy75if5X3pFpiDaMvFBtJKsh8+SkncBIC5bbKC5AoLdFANLmPiH0CGr +Mv3rL3ycnbciI6J4uKHcWnYGGiMjBomaZ7jd/cOjcjmGfpI5d0nq13G11omkyEyR +wNX0eJRL02W+93Xu7tD+FEFMxFvak+70GvX+XWomwYw/Pjlio8KbTAlJxhfK2Lh6 +H798k17VfxIrOk0KjzZS7+a20hZ/AgMBAAGjUzBRMB0GA1UdDgQWBBT2f5o8r75C +PWST36akpkKRRTbhvjAfBgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA9pb75p6mnqp2MQHSr +5SKRf2UV4wQIUtXgF6V9vNfvVzJii+Lzrqir1YMk5QgavCzD96KlJcqJCcH559RY +5743AxI3tdWfA3wajBctoy35oYnT4M30qbkryYLTUlv7PmeNWvrksTURchyyDt5/ +3T73yj5ZalmzKN6+xLfUDdnudspfWlUMutKU50MU1iuQESf4Fwd53vOg9jMcWJ2E +vAgfVI0XAvYdU3ybJrUvBq5zokYR2RzGv14uHxwVPnLBjrBEHRnbrXvLZJhuIS2b +xZ3CqwWi+9bvNqHz09HvhkU2b6fCGweKaAUGSo8OfQ5FRkjTUomMI/ZLs/qtJ6JR +zzVt +-----END CERTIFICATE----- diff --git a/testdata/config.lua b/testdata/config.lua new file mode 100644 index 0000000..21795e0 --- /dev/null +++ b/testdata/config.lua @@ -0,0 +1,39 @@ +-- Do not set listen for now so connector won't be +-- able to send requests until everything is configured. +local auth_type = os.getenv("TEST_TNT_AUTH_TYPE") +if auth_type == "auto" then + auth_type = nil +end + +box.cfg{ + auth_type = auth_type, + work_dir = os.getenv("TEST_TNT_WORK_DIR"), +} + +box.once("init", function() + local s = box.schema.space.create('test', { + id = 617, + if_not_exists = true, + }) + s:create_index('primary', { + type = 'tree', + parts = {1, 'uint'}, + if_not_exists = true + }) + + box.schema.func.create('box.info') + + -- auth testing: access control + box.schema.user.create('test', {password = 'test'}) + box.schema.user.grant('test', 'read,write', 'space', 'test') + + box.schema.user.create('no_grants') +end) + +box.space.test:truncate() + +-- Set listen only when every other thing is configured. +box.cfg{ + auth_type = auth_type, + listen = os.getenv("TEST_TNT_LISTEN"), +} diff --git a/testdata/empty b/testdata/empty new file mode 100644 index 0000000..e69de29 diff --git a/testdata/generate.sh b/testdata/generate.sh new file mode 100755 index 0000000..4b8cf36 --- /dev/null +++ b/testdata/generate.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -xeuo pipefail +# An example how-to re-generate testing certificates (because usually +# TLS certificates have expiration dates and some day they will expire). +# +# The instruction is valid for: +# +# $ openssl version +# OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022) + +cat < domains.ext +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 +EOF + +openssl req -x509 -nodes -new -sha256 -days 8192 -newkey rsa:2048 -keyout ca.key -out ca.pem -subj "/C=US/CN=Example-Root-CA" +openssl x509 -outform pem -in ca.pem -out ca.crt + +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" +openssl x509 -req -sha256 -days 8192 -in localhost.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile domains.ext -out localhost.crt +password=mysslpassword + +# Tarantool tries every line from the password file. +cat < passwords +unusedpassword +$password +EOF + +cat < invalidpasswords +unusedpassword1 +EOF + +openssl rsa -aes256 -passout "pass:${password}" -in localhost.key -out localhost.enc.key diff --git a/testdata/invalidpasswords b/testdata/invalidpasswords new file mode 100644 index 0000000..b09d795 --- /dev/null +++ b/testdata/invalidpasswords @@ -0,0 +1 @@ +unusedpassword1 diff --git a/testdata/localhost.crt b/testdata/localhost.crt new file mode 100644 index 0000000..fd04b99 --- /dev/null +++ b/testdata/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIUAvSBJ3nSv7kdKw1IQ7AjchzI7T8wDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA1MjYwNjE3NDBaFw00NDEwMjkwNjE3NDBaMGcxCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnbFY+BMqlddktbitgaZICws4Zyj8LFy9QzO+ +AYSQyqFuTCI+cGqbP5r6Qf4f3xHNGykHJGn18brpiFWhNMaVkkgU3dycU8fFayVN +hLEJAXd4acWP1h5/aH4cOZgl+xJlmU2iLHtP/TLYEDDiVkfqL/MgUIMxbndIaiU0 +/e81v+2gi8ydyI6aElN8KbAaFPzXCZ28/RmO/0m36YzF+FSMVD1Hx8xO5V+Q9N1q +dsyrMdh0nCxDDXGdBgDrKt5+U1uJkDpTHfjMAkf7oBoRd8DJ8O74bpue03W5WxKQ +NjNfvHSgkBaQSdnxR93FSCr/Gs6WcUd50Y8z+ZCTNkup0KROTwIDAQABo3YwdDAf +BgNVHSMEGDAWgBT2f5o8r75CPWST36akpkKRRTbhvjAJBgNVHRMEAjAAMAsGA1Ud +DwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFOwH +aHK6QrEfltP7wwldUWrQJ9viMA0GCSqGSIb3DQEBCwUAA4IBAQAGHGuloGJqLoPZ +2iRnb/NaiArowLnUz4Z3ENKMB2KbZFGijMJSXO9i9ZLCFL+O93vCTspKGtHqVX2o +dxcrF7EZ9EaHIijWjKGEp1PszunBIca+Te+zyRg9Z+F9gwRsJYB8ctBGjIhe4qEv +ZSlRY489UVNKLTcHcl6nlUled9hciBJHpXuitiiNhzUareP38hROyiUhrAy8L21L +t7Ww5YGRuSTxM5LQfPZcCy40++TlyvXs0DCQ8ZuUbqZv64bNHbaLOyxIqKfPypXa +nS3AYZzUJjHj7vZwHoL1SyvBjx/DQAsWaEv137d8FlMqCsWLXfCsuNpKeQYZOyDS +7ploP9Gl +-----END CERTIFICATE----- diff --git a/testdata/localhost.enc.key b/testdata/localhost.enc.key new file mode 100644 index 0000000..b881820 --- /dev/null +++ b/testdata/localhost.enc.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIm+0WC9xe38cCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBNOE4KD+yauMfsnOiNAaaZBIIE +0DtXaHGpacJ8MjjL6zciYhgJOD9SJHE4vwPxpNDWuS9mf6wk/cdBNFMqnYwJmlYw +J/eQ+Z8MsZUqjnhDQz9YXXd8JftexAAa1bHnmfv2N/czJCx57dAHVdmJzgibfp18 +GCpqR23tklEO2Nj2HCbR59rh7IsnW9mD6jh+mVtkOix5HMCUSxwc3bEUutIQE80P +JHG2BsEfAeeHZa+QgG3Y15c6uSXD6wY73ldPPOgZ3NFOqcw/RDqYf1zsohx7auxi +Y6zHA7LdYtQjbNJ5slIfxPhAh75Fws0g4QvWbAwqqdEOVmlamYYjAOdVBBxTvcRs +/63ZN55VTQ8rYhShNA3BVFOLHaRD4mnlKE5Xh7gJXltCED7EHdpHdT9K3uM9U7nW +b2JSylt2RzY+LDsio2U0xsQp9jHzRRw81p8P1jmo5alP8jPACMsE8nnNNSDF4p43 +fG7hNNBq/dhq80iOnaArY05TIBMsD079tB0VKrYyyfaL0RbsAdgtCEmF9bCpnsTM +y9ExcJGQQJx9WNAHkSyjdzJd0jR6Zc0MrgRuj26nJ3Ahq58zaQKdfFO9RfGWd38n +MH3jshEtAuF+jXFbMcM4rVdIBPSuhYgHzYIC6yteziy7+6hittpWeNGLKpC5oZ8R +oEwH3MVsjCbd6Pp3vdcR412vLMgy1ZUOraDoY08FXC82RBJViVX6LLltIJu96kiX +WWUcRZAwzlJsTvh1EGmDcNNKCgmvWQaojqTNgTjxjJ3SzD2/TV6uQrSLgZ6ulyNl +7vKWt/YMTvIgoJA9JeH8Aik/XNd4bRXL+VXfUHpLTgn+WKiq2irVYd9R/yITDunP +a/kzqxitjU4OGdf/LOtYxfxfoGvFw5ym4KikoHKVg4ILcIQ+W4roOQQlu4/yezAK +fwYCrMVJWq4ESuQh3rn7eFR+eyBV6YcNBLm4iUcQTMhnXMMYxQ3TnDNga5eYhmV1 +ByYx+nFQDrbDolXo5JfXs3x6kXhoT/7wMHgsXtmRSd5PSBbaeJTrbMGA0Op6YgWr +EpvX3Yt863s4h+JgDpg9ouH+OJGgn7LGGye+TjjuDds8CStFdcFDDOayBS3EH4Cr +jgJwzvTdTZl+1YLYJXB67M4zmVPRRs5H88+fZYYA9bhZACL/rQBj2wDq/sIxvrIM +SCjOhSJ4z5Sm3XaBKnRG2GBBt67MeHB0+T3HR3VHKR+zStbCnsbOLythsE/CIA8L +fBNXMvnWa5bLgaCaEcK6Q3LOamJiKaigbmhI+3U3NUdb9cT1GhE0rtx6/IO9eapz +IUDOrtX9U+1o6iW2dahezxwLo9ftRwQ7qwG4qOk/Co/1c2WuuQ+d4YPpj/JOO5mf +LanA35mQjQrr2MZII91psznx05ffb5xMp2pqNbC6DVuZq8ZlhvVHGk+wM9RK3kYP +/ITwpbUvLmmN892kvZgLAXadSupBV8R/L5ZjDUO9U2all9p4eGfWZBk/yiivOLmh +VQxKCqAmThTO1hRa56+AjgzRJO6cY85ra+4Mm3FhhdR4gYvap2QTq0o2Vn0WlCHh +1SIeaDKfw9v4aGBbhqyQU2mPlXO5JiLktO+lZ5styVq9Qm+b0ROZxHzL1lRUNbRA +VfQO4fRnINKPgyzgH3tNxJTzw4pLkrkBD/g+zxDZVqkx +-----END ENCRYPTED PRIVATE KEY----- diff --git a/testdata/localhost.key b/testdata/localhost.key new file mode 100644 index 0000000..ed0f558 --- /dev/null +++ b/testdata/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCdsVj4EyqV12S1 +uK2BpkgLCzhnKPwsXL1DM74BhJDKoW5MIj5waps/mvpB/h/fEc0bKQckafXxuumI +VaE0xpWSSBTd3JxTx8VrJU2EsQkBd3hpxY/WHn9ofhw5mCX7EmWZTaIse0/9MtgQ +MOJWR+ov8yBQgzFud0hqJTT97zW/7aCLzJ3IjpoSU3wpsBoU/NcJnbz9GY7/Sbfp +jMX4VIxUPUfHzE7lX5D03Wp2zKsx2HScLEMNcZ0GAOsq3n5TW4mQOlMd+MwCR/ug +GhF3wMnw7vhum57TdblbEpA2M1+8dKCQFpBJ2fFH3cVIKv8azpZxR3nRjzP5kJM2 +S6nQpE5PAgMBAAECggEAFv81l9wHsll6pOu9VfJ/gCjPPXAjMn8F1OaXV5ZTHVHk +iXLXA0LwyBpcU8JxOHFapZLaqUtQpEObahf+zfkF+BLOBDr3i1pPZpxGjUraIt4e +7+HxY4sIDp+Rky6mn1JkAbLqKy2CkUzYaKgQYf/T3dFJjaRMUa1QoLYzX7MCdi5B +GnBICzi2UVsn3HU934l/gJKV+SlprdbrGJ+fRklP2AxLey3EOrwooUViy+k3+w5E +dzBH2HpLL0XuIHaBXQ01J6Mu3ud9ApFLC+Rh+2UFTW/WPnNe+B6BO5CGNN52Pfdr +Q5l+VzmRkXXo2fio+w4z/az8axT/DdhKGT2oBlp35QKBgQDZVGdKjkalH3QH2pdy +CWJIiybzY1R0CpimfgDLIdqEsps9qqgLXsUFB5yTcCRmg8RSWWHvhMVMyJtBcsdY +xGhmHxsFBxuray60UljxBcRQTwqvAX7mP8WEv8t80kbhyaxvOfkg8JD1n2hS7NjL +dOIG9Mh8L0YSOCRkbfv90OnYXQKBgQC5wGs35Ksnqi2swX5MLYfcBaImzoNde86n +cXJ0yyF82O1pk8DkmU2EDcUoQfkKxr3ANvVDG4vYaguIhYsJqPg/t8XQt/epDz/O +WZhqogn0ysaTv2FHrWcgPAkq82hpNII5NfPP8aRaYh8OUSfh4WHkW84m6+usqwjI +wbOq36qmmwKBgGMFFdreYEmzvwYlDoOiyukKncCfLUeB3HNfTbU/w3RafGjobJBh +qZrVEP4MRkl/F9/9YaXj9JE7haGYTkOfmYGOAp2T04OS3kDClEucuQluOgvqvorh +23jUej5xAGK3pJ046M2dTi7bZokB6PUqWCGbPg127JI4ijxH8FyA50rxAoGAQO2d +jMAFg6vco1JPT1lq7+GYOHBfQsIQDj99fo2yeu1or0rSVhWwHsShcdz9rGKj2Rhc +ysRKMa9/sIzdeNbzT3JxVu+3RgTqjLqMqFlTmZl3qBVxb5iRP5c8rSLAEGYmTtEp +FDqm9GDv8hU0F6SsjyH4AWrdylFOlL4Ai237PJkCgYBDC1wAwBD8WXJqRrYVGj7X +l4TQQ0hO7La/zgbasSgLNaJcYu32nut6D0O8IlmcQ2nO0BGPjQmJFGp6xawjViRu +np7fEkJQEf1pK0yeA8A3urjXccuUXEA9kKeqaSZYDzICPFaOlezPPPpW0hbkhnPe +dQn3DcoY6e6o0K5ltt1RvQ== +-----END PRIVATE KEY----- diff --git a/testdata/passwords b/testdata/passwords new file mode 100644 index 0000000..5853004 --- /dev/null +++ b/testdata/passwords @@ -0,0 +1,2 @@ +unusedpassword +mysslpassword