diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c3793a741..e1cbf221c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - linux: + run-tests-ce: # 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. # @@ -26,9 +26,6 @@ jobs: - '2.9' - '2.x-latest' coveralls: [false] - include: - - tarantool: '2.x-latest' - coveralls: true steps: - name: Clone the connector @@ -66,3 +63,60 @@ jobs: - name: Check workability of benchmark tests run: make bench-deps bench DURATION=1x COUNT=1 + run-tests-ee: + if: github.event_name == 'push' || + github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + sdk-version: + - '1.10.11-0-gf0b0e7ecf-r470' + - '2.8.3-21-g7d35cd2be-r470' + coveralls: [false] + ssl: [false] + include: + - sdk-version: '2.10.0-beta2-59-gc51e2ba67-r469' + coveralls: true + ssl: true + + steps: + - name: Clone the connector + uses: actions/checkout@v2 + + - name: Setup Tarantool ${{ matrix.sdk-version }} + run: | + ARCHIVE_NAME=tarantool-enterprise-bundle-${{ matrix.sdk-version }}.tar.gz + curl -O -L https://${{ secrets.SDK_DOWNLOAD_TOKEN }}@download.tarantool.io/enterprise/${ARCHIVE_NAME} + tar -xzf ${ARCHIVE_NAME} + rm -f ${ARCHIVE_NAME} + + - name: Setup golang for the connector and tests + uses: actions/setup-go@v2 + with: + go-version: 1.13 + + - name: Install test dependencies + run: | + source tarantool-enterprise/env.sh + make deps + + - name: Run tests + run: | + source tarantool-enterprise/env.sh + make test + env: + TEST_TNT_SSL: ${{matrix.ssl}} + + - 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 + make coveralls + + - name: Check workability of benchmark tests + run: make bench-deps bench DURATION=1x COUNT=1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 854ce6153..cf5dcd2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - queue-utube handling (#85) - Master discovery (#113) - SQL support (#62) +- SSL support (#155) ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a276cd6e2..1fe4d9f58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,13 @@ make test The tests set up all required `tarantool` processes before run and clean up afterwards. +If you have Tarantool Enterprise Edition 2.10 or newer, you can run additional +SSL tests. To do this, you need to set an environment variable 'TEST_TNT_SSL': + +```bash +TEST_TNT_SSL=true make test +``` + If you want to run the tests for a specific package: ```bash make test- diff --git a/Makefile b/Makefile index 12d369606..fcb06b9ab 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,10 @@ clean: deps: clean ( cd ./queue; tarantoolctl rocks install queue 1.1.0 ) +.PHONY: testdata +testdata: + (cd ./testdata; ./generate.sh) + .PHONY: test test: go test ./... -v -p 1 diff --git a/connection.go b/connection.go index 3be878011..71c22890e 100644 --- a/connection.go +++ b/connection.go @@ -25,6 +25,11 @@ const ( connClosed = 2 ) +const ( + connTransportNone = "" + connTransportSsl = "ssl" +) + type ConnEventKind int type ConnLogKind int @@ -207,6 +212,32 @@ type Opts struct { Handle interface{} // Logger is user specified logger used for error messages. Logger Logger + // Transport is the connection type, by default the connection is unencrypted. + Transport string + // SslOpts is used only if the Transport == 'ssl' is set. + Ssl SslOpts +} + +// SslOpts is a way to configure ssl transport. +type SslOpts struct { + // KeyFile is a path to a private SSL key file. + KeyFile string + // CertFile is a path to an SSL sertificate 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 } // Connect creates and configures a new Connection. @@ -358,8 +389,10 @@ func (conn *Connection) Handle() interface{} { func (conn *Connection) dial() (err error) { var connection net.Conn network := "tcp" + opts := conn.opts address := conn.addr - timeout := conn.opts.Reconnect / 2 + timeout := opts.Reconnect / 2 + transport := opts.Transport if timeout == 0 { timeout = 500 * time.Millisecond } else if timeout > 5*time.Second { @@ -383,11 +416,17 @@ func (conn *Connection) dial() (err error) { } else if addrLen >= 4 && address[0:4] == "tcp:" { address = address[4:] } - connection, err = net.DialTimeout(network, address, timeout) + if transport == connTransportNone { + connection, err = net.DialTimeout(network, address, timeout) + } else if transport == connTransportSsl { + connection, err = sslDialTimeout(network, address, timeout, opts.Ssl) + } else { + err = errors.New("An unsupported transport type: " + transport) + } if err != nil { return } - dc := &DeadlineIO{to: conn.opts.Timeout, c: connection} + dc := &DeadlineIO{to: opts.Timeout, c: connection} r := bufio.NewReaderSize(dc, 128*1024) w := bufio.NewWriterSize(dc, 128*1024) greeting := make([]byte, 128) @@ -400,8 +439,8 @@ func (conn *Connection) dial() (err error) { conn.Greeting.auth = bytes.NewBuffer(greeting[64:108]).String() // Auth - if conn.opts.User != "" { - scr, err := scramble(conn.Greeting.auth, conn.opts.Pass) + if opts.User != "" { + scr, err := scramble(conn.Greeting.auth, opts.Pass) if err != nil { err = errors.New("auth: scrambling failure " + err.Error()) connection.Close() diff --git a/example_test.go b/example_test.go index 386ad11f2..0ed0f79c2 100644 --- a/example_test.go +++ b/example_test.go @@ -20,11 +20,29 @@ type Tuple struct { func example_connect() *tarantool.Connection { conn, err := tarantool.Connect(server, opts) if err != nil { - panic("Connection is not established") + panic("Connection is not established: " + err.Error()) } return conn } +// Example demonstrates how to use SSL transport. +func ExampleSslOpts() { + var opts = tarantool.Opts{ + User: "test", + Pass: "test", + Transport: "ssl", + Ssl: tarantool.SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + } + _, err := tarantool.Connect("127.0.0.1:3013", opts) + if err != nil { + panic("Connection is not established: " + err.Error()) + } +} + func ExampleConnection_Select() { conn := example_connect() defer conn.Close() diff --git a/export_test.go b/export_test.go index 931e78c9b..8cb2b713a 100644 --- a/export_test.go +++ b/export_test.go @@ -1,5 +1,19 @@ package tarantool +import ( + "net" + "time" +) + func (schema *Schema) ResolveSpaceIndex(s interface{}, i interface{}) (spaceNo, indexNo uint32, err error) { return schema.resolveSpaceIndex(s, i) } + +func SslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + return sslDialTimeout(network, address, timeout, opts) +} + +func SslCreateContext(opts SslOpts) (ctx interface{}, err error) { + return sslCreateContext(opts) +} diff --git a/go.mod b/go.mod index 152329b1f..6dcaee974 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/uuid v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/stretchr/testify v1.7.1 // indirect + github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 google.golang.org/appengine v1.6.7 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/vmihailenco/msgpack.v2 v2.9.2 diff --git a/go.sum b/go.sum index 1f9e791b4..1af7f9933 100644 --- a/go.sum +++ b/go.sum @@ -7,17 +7,25 @@ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ 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/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.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87 h1:JGzuBxNBq5saVtPUcuu5Y4+kbJON6H02//OT+RNqGts= +github.com/tarantool/go-openssl v0.0.8-0.20220419150948-be4921aa2f87/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/ssl.go b/ssl.go new file mode 100644 index 000000000..d9373ace2 --- /dev/null +++ b/ssl.go @@ -0,0 +1,110 @@ +//go:build !go_tarantool_ssl_disable +// +build !go_tarantool_ssl_disable + +package tarantool + +import ( + "errors" + "io/ioutil" + "net" + "time" + + "github.com/tarantool/go-openssl" +) + +func sslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + var ctx interface{} + if ctx, err = sslCreateContext(opts); err != nil { + return + } + + return openssl.DialTimeout(network, address, timeout, ctx.(*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(opts SslOpts) (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 opts.CertFile != "" { + if err = sslLoadCert(sslCtx, opts.CertFile); err != nil { + return + } + } + + if opts.KeyFile != "" { + if err = sslLoadKey(sslCtx, opts.KeyFile); err != nil { + return + } + } + + if opts.CaFile != "" { + if err = sslCtx.LoadVerifyLocations(opts.CaFile, ""); err != nil { + return + } + verifyFlags := openssl.VerifyPeer | openssl.VerifyFailIfNoPeerCert + sslCtx.SetVerify(verifyFlags, nil) + } + + if opts.Ciphers != "" { + sslCtx.SetCipherList(opts.Ciphers) + } + + return +} + +func sslLoadCert(ctx *openssl.Ctx, certFile string) (err error) { + var certBytes []byte + if certBytes, err = ioutil.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) (err error) { + var keyBytes []byte + if keyBytes, err = ioutil.ReadFile(keyFile); err != nil { + return + } + + var key openssl.PrivateKey + if key, err = openssl.LoadPrivateKeyFromPEM(keyBytes); err != nil { + return + } + + return ctx.UsePrivateKey(key) +} diff --git a/ssl_disable.go b/ssl_disable.go new file mode 100644 index 000000000..8d0ab406b --- /dev/null +++ b/ssl_disable.go @@ -0,0 +1,19 @@ +//go:build go_tarantool_ssl_disable +// +build go_tarantool_ssl_disable + +package tarantool + +import ( + "errors" + "net" + "time" +) + +func sslDialTimeout(network, address string, timeout time.Duration, + opts SslOpts) (connection net.Conn, err error) { + return nil, errors.New("SSL support is disabled.") +} + +func sslCreateContext(opts SslOpts) (ctx interface{}, err error) { + return nil, errors.New("SSL support is disabled.") +} diff --git a/ssl_test.go b/ssl_test.go new file mode 100644 index 000000000..2a433bf5b --- /dev/null +++ b/ssl_test.go @@ -0,0 +1,466 @@ +//go:build !go_tarantool_ssl_disable +// +build !go_tarantool_ssl_disable + +package tarantool_test + +import ( + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/tarantool/go-openssl" + . "github.com/tarantool/go-tarantool" + "github.com/tarantool/go-tarantool/test_helpers" +) + +const sslHost = "127.0.0.1" +const tntHost = "127.0.0.1:3014" + +func serverSsl(network, address string, opts SslOpts) (net.Listener, error) { + ctx, err := SslCreateContext(opts) + if err != nil { + return nil, errors.New("Unable to create SSL context: " + err.Error()) + } + + return openssl.Listen(network, address, ctx.(*openssl.Ctx)) +} + +func serverSslAccept(l net.Listener) (<-chan string, <-chan error) { + message := make(chan string, 1) + errors := make(chan error, 1) + + go func() { + conn, err := l.Accept() + if err != nil { + errors <- err + } else { + bytes, err := ioutil.ReadAll(conn) + if err != nil { + errors <- err + } else { + message <- string(bytes) + } + conn.Close() + } + + close(message) + close(errors) + }() + + return message, errors +} + +func serverSslRecv(msgs <-chan string, errs <-chan error) (string, error) { + return <-msgs, <-errs +} + +func clientSsl(network, address string, opts SslOpts) (net.Conn, error) { + timeout := 5 * time.Second + return SslDialTimeout(network, address, timeout, opts) +} + +func createClientServerSsl(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, error, <-chan string, <-chan error) { + t.Helper() + + l, err := serverSsl("tcp", sslHost+":0", serverOpts) + if err != nil { + t.Fatalf("Unable to create server, error %q", err.Error()) + } + + msgs, errs := serverSslAccept(l) + + port := l.Addr().(*net.TCPAddr).Port + c, err := clientSsl("tcp", sslHost+":"+strconv.Itoa(port), clientOpts) + + return l, c, err, msgs, errs +} + +func createClientServerSslOk(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { + t.Helper() + + l, c, err, msgs, errs := createClientServerSsl(t, serverOpts, clientOpts) + if err != nil { + t.Fatalf("Unable to create client, error %q", err.Error()) + } + + return l, c, msgs, errs +} + +func serverTnt(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, error) { + listen := tntHost + "?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) + } + + listen = listen[:len(listen)-1] + + return test_helpers.StartTarantool(test_helpers.StartOpts{ + InitScript: "config.lua", + Listen: listen, + SslCertsDir: "testdata", + PingServer: tntHost, + PingTransport: "ssl", + PingSsl: clientOpts, + WorkDir: "work_dir_ssl", + User: "test", + Pass: "test", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) +} + +func serverTntStop(inst test_helpers.TarantoolInstance) { + test_helpers.StopTarantoolWithCleanup(inst) +} + +func assertConnectionSslFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, err, _, _ := createClientServerSsl(t, serverOpts, clientOpts) + if err == nil { + t.Fatalf("An unexpected connection to the server.") + c.Close() + } + l.Close() +} + +func assertConnectionSslOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, msgs, errs := createClientServerSslOk(t, serverOpts, clientOpts) + const message = "any test string" + c.Write([]byte(message)) + c.Close() + + recv, err := serverSslRecv(msgs, errs) + l.Close() + + if err != nil { + t.Errorf("An unexpected server error: %q", err.Error()) + } else if recv != message { + t.Errorf("An unexpected server message: %q, expected %q", recv, message) + } +} + +func assertConnectionTntFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverTnt(serverOpts, clientOpts) + defer serverTntStop(inst) + + if err == nil { + t.Fatalf("An unexpected connection to the server") + } +} + +func assertConnectionTntOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverTnt(serverOpts, clientOpts) + defer serverTntStop(inst) + + if err != nil { + t.Fatalf("An unexpected server error %q", err.Error()) + } +} + +type test struct { + name string + ok bool + serverOpts SslOpts + clientOpts SslOpts +} + +/* + 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 tests = []test{ + { + "empty", + false, + SslOpts{}, + SslOpts{}, + }, + { + "key_crt_client", + false, + SslOpts{}, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_server", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{}, + }, + { + "key_crt_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{}, + }, + { + "key_crt_ca_server_key_crt_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + }, + { + "key_crt_ca_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_key", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "any_invalid_path", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_crt", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "any_invalid_path", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_invalid_path_ca", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "any_invalid_path", + }, + }, + { + "key_crt_ca_server_and_client_empty_key", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/empty", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_crt", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/empty", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_server_and_client_empty_ca", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/empty", + }, + }, + { + "key_crt_server_and_key_crt_ca_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_key_crt_ca_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + }, + }, + { + "key_crt_ca_ciphers_server_and_client", + true, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + }, + { + "non_equal_ciphers_client", + false, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "ECDHE-RSA-AES256-GCM-SHA384", + }, + SslOpts{ + KeyFile: "testdata/localhost.key", + CertFile: "testdata/localhost.crt", + CaFile: "testdata/ca.crt", + Ciphers: "TLS_AES_128_GCM_SHA256", + }, + }, +} + +func TestSslOpts(t *testing.T) { + testTntSsl, exists := os.LookupEnv("TEST_TNT_SSL") + isTntSsl := false + if exists && (testTntSsl == "1" || strings.ToUpper(testTntSsl) == "TRUE") { + isTntSsl = true + } + + for _, test := range tests { + if test.ok { + t.Run("ok_ssl_"+test.name, func(t *testing.T) { + assertConnectionSslOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_ssl_"+test.name, func(t *testing.T) { + assertConnectionSslFail(t, test.serverOpts, test.clientOpts) + }) + } + if !isTntSsl { + continue + } + if test.ok { + t.Run("ok_tnt_"+test.name, func(t *testing.T) { + assertConnectionTntOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_tnt_"+test.name, func(t *testing.T) { + assertConnectionTntFail(t, test.serverOpts, test.clientOpts) + }) + } + } +} diff --git a/test_helpers/main.go b/test_helpers/main.go index cc3416b91..c79ec1819 100644 --- a/test_helpers/main.go +++ b/test_helpers/main.go @@ -13,9 +13,12 @@ package test_helpers import ( "errors" "fmt" + "io" + "io/ioutil" "log" "os" "os/exec" + "path/filepath" "regexp" "strconv" "time" @@ -32,12 +35,28 @@ type StartOpts struct { // https://www.tarantool.io/en/doc/latest/reference/configuration/#cfg-basic-listen Listen string + // PingServer changes a host to connect to test startup of a Tarantool + // instance. By default, it uses Listen value as the host for the connection. + PingServer string + + // PingTransport changes Opts.Transport for a connection that checks startup + // of a Tarantool instance. + PingTransport string + + // PingSsl changes Opts.Ssl for a connection that checks startup of + // a Tarantool instance. + PingSsl tarantool.SslOpts + // WorkDir is box.cfg work_dir parameter for tarantool. // Specify folder to store tarantool data files. // Folder must be unique for each tarantool process used simultaneously. // https://www.tarantool.io/en/doc/latest/reference/configuration/#confval-work_dir WorkDir string + // SslCertsDir is a path to a directory with SSL certificates. It will be + // copied to the working directory. + SslCertsDir string + // User is a username used to connect to tarantool. // All required grants must be given in InitScript. User string @@ -185,6 +204,14 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { return inst, err } + // Copy SSL certificates. + if startOpts.SslCertsDir != "" { + err = copySslCerts(startOpts.WorkDir, startOpts.SslCertsDir) + if err != nil { + return inst, err + } + } + // Options for restarting tarantool instance. inst.Opts = startOpts @@ -204,11 +231,19 @@ func StartTarantool(startOpts StartOpts) (TarantoolInstance, error) { User: startOpts.User, Pass: startOpts.Pass, SkipSchema: true, + Transport: startOpts.PingTransport, + Ssl: startOpts.PingSsl, } var i uint for i = 0; i <= startOpts.ConnectRetry; i++ { - err = isReady(startOpts.Listen, &opts) + var server string + if startOpts.PingServer != "" { + server = startOpts.PingServer + } else { + server = startOpts.Listen + } + err = isReady(server, &opts) // Both connect and ping is ok. if err == nil { @@ -254,3 +289,60 @@ func StopTarantoolWithCleanup(inst TarantoolInstance) { } } } + +func copySslCerts(dst string, sslCertsDir string) (err error) { + dstCertPath := filepath.Join(dst, sslCertsDir) + if err = os.Mkdir(dstCertPath, 0755); err != nil { + return + } + if err = copyDirectoryFiles(sslCertsDir, dstCertPath); err != nil { + return + } + return +} + +func copyDirectoryFiles(scrDir, dest string) error { + entries, err := ioutil.ReadDir(scrDir) + if err != nil { + return err + } + for _, entry := range entries { + sourcePath := filepath.Join(scrDir, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + _, err := os.Stat(sourcePath) + if err != nil { + return err + } + + if err := copyFile(sourcePath, destPath); err != nil { + return err + } + + if err := os.Chmod(destPath, entry.Mode()); err != nil { + return err + } + } + return nil +} + +func copyFile(srcFile, dstFile string) error { + out, err := os.Create(dstFile) + if err != nil { + return err + } + + defer out.Close() + + in, err := os.Open(srcFile) + if err != nil { + return err + } + defer in.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + + return nil +} diff --git a/testdata/ca.crt b/testdata/ca.crt new file mode 100644 index 000000000..2fa1a12ff --- /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/empty b/testdata/empty new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/generate.sh b/testdata/generate.sh new file mode 100755 index 000000000..f29f41c90 --- /dev/null +++ b/testdata/generate.sh @@ -0,0 +1,25 @@ +#!/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 diff --git a/testdata/localhost.crt b/testdata/localhost.crt new file mode 100644 index 000000000..fd04b9900 --- /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.key b/testdata/localhost.key new file mode 100644 index 000000000..ed0f55876 --- /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-----