diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 26d1a24d6..7bccdd59b 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 @@ -63,3 +60,57 @@ jobs: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | make coveralls + 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6168ede00..087a3907a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release. - Go modules support (#91) - queue-utube handling (#85) - Master discovery (#113) +- SSL support (#155) ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c42babc35..70b4e49c3 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/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 0a6b6cb37..4c0412571 100644 --- a/example_test.go +++ b/example_test.go @@ -19,11 +19,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..3a1ac8710 --- /dev/null +++ b/ssl_test.go @@ -0,0 +1,465 @@ +//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 localHost = "127.0.0.1" +const serverSsl = "127.0.0.1:3014" + +func serverLocal(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 serverLocalAccept(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 serverLocalRecv(msgs <-chan string, errs <-chan error) (string, error) { + return <-msgs, <-errs +} + +func clientLocal(network, address string, opts SslOpts) (net.Conn, error) { + timeout := 5 * time.Second + return SslDialTimeout(network, address, timeout, opts) +} + +func createClientServerLocal(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, error, <-chan string, <-chan error) { + t.Helper() + + l, err := serverLocal("tcp", localHost+":0", serverOpts) + if err != nil { + t.Fatalf("Unable to create server, error %q", err.Error()) + } + + msgs, errs := serverLocalAccept(l) + + port := l.Addr().(*net.TCPAddr).Port + c, err := clientLocal("tcp", localHost+":"+strconv.Itoa(port), clientOpts) + + return l, c, err, msgs, errs +} + +func createClientServerLocalOk(t testing.TB, serverOpts, + clientOpts SslOpts) (net.Listener, net.Conn, <-chan string, <-chan error) { + t.Helper() + + l, c, err, msgs, errs := createClientServerLocal(t, serverOpts, clientOpts) + if err != nil { + t.Fatalf("Unable to create client, error %q", err.Error()) + } + + return l, c, msgs, errs +} + +func serverEe(serverOpts, clientOpts SslOpts) (test_helpers.TarantoolInstance, error) { + listen := serverSsl + "?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: serverSsl, + PingTransport: "ssl", + PingSsl: clientOpts, + WorkDir: "work_dir_ssl", + User: "test", + Pass: "test", + WaitStart: 100 * time.Millisecond, + ConnectRetry: 3, + RetryTimeout: 500 * time.Millisecond, + }) +} + +func serverEeStop(inst test_helpers.TarantoolInstance) { + test_helpers.StopTarantoolWithCleanup(inst) +} + +func assertConnectionLocalFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, err, _, _ := createClientServerLocal(t, serverOpts, clientOpts) + if err == nil { + t.Fatalf("An unexpected connection to the server.") + c.Close() + } + l.Close() +} + +func assertConnectionLocalOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + l, c, msgs, errs := createClientServerLocalOk(t, serverOpts, clientOpts) + const message = "any test string" + c.Write([]byte(message)) + c.Close() + + recv, err := serverLocalRecv(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 assertConnectionEeFail(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverEe(serverOpts, clientOpts) + defer serverEeStop(inst) + + if err == nil { + t.Fatalf("An unexpected connection to the server") + } +} + +func assertConnectionEeOk(t testing.TB, serverOpts, clientOpts SslOpts) { + t.Helper() + + inst, err := serverEe(serverOpts, clientOpts) + defer serverEeStop(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-ee manual. + + 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 TestSslOptsLocal(t *testing.T) { + testSsl, exists := os.LookupEnv("TEST_TNT_SSL") + isTarantoolSsl := false + if exists && (testSsl == "1" || strings.ToUpper(testSsl) == "TRUE") { + isTarantoolSsl = true + } + + for _, test := range tests { + if test.ok { + t.Run("ok_local_"+test.name, func(t *testing.T) { + assertConnectionLocalOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_local_"+test.name, func(t *testing.T) { + assertConnectionLocalFail(t, test.serverOpts, test.clientOpts) + }) + } + if !isTarantoolSsl { + continue + } + if test.ok { + t.Run("ok_ee_"+test.name, func(t *testing.T) { + assertConnectionEeOk(t, test.serverOpts, test.clientOpts) + }) + } else { + t.Run("fail_ee_"+test.name, func(t *testing.T) { + assertConnectionEeFail(t, test.serverOpts, test.clientOpts) + }) + } + } +} diff --git a/test_helpers/main.go b/test_helpers/main.go index cc3416b91..d79f1538b 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 + + // PingTransport 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..4afc1db72 --- /dev/null +++ b/testdata/ca.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUI5F+JIaUclYVrvVAN43ZjsWJBz8wDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA0MTgxNDEzNDJaFw0yNTAyMDUxNDEzNDJaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDSBetBGfBNqUR5IEdvdPesgviq+KI12+lbY4rYdpSMlleGKhoa8j/fKf3k +xTRRR+3JFmcuJiIJ1u5drtadeAYQN5uiFlpeVYAQDmU/dAtaf/9uMvtifnOQnr9/ +EjYVq9NWyhHYrVfneTlyq2lWoCkdfUKa3XW0SY3/q8mAK5A9Cz6hZrCb1CY6JA7L +PbLWTQdA7Ygz182b1AfHEykmhdUzDTSSn83PsCU4b79lTOixCeDro0451XcXUivy +M9WPUsNOK8Vm5n7mXzbC6Y82lxPgh59Vi4whoj5Dn3xNUV5VEBPSeksbyRlqV8f4 +us/9aY/gcIhXYvKge4hkDop5Yw9rAgMBAAGjUzBRMB0GA1UdDgQWBBQKlMvtZpXR +lYJSQ8Z5vq2ng6ogTjAfBgNVHSMEGDAWgBQKlMvtZpXRlYJSQ8Z5vq2ng6ogTjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQApllvLFt/5rlCXxwwP +It8tah7IvwlwclI/aFYAGST5ZzRSso+BBow+vhRAJFL55U4FKg/OT5iuOr0EAyKc +fgyCRV5wmc25dCwQHFdtlEWrasl1r+dMEQDSCjhPjDSWyKpxMXZ5zxEkvXmpCHf5 +lOonIP3WFiwFMZdGBC6BRO8xeHL1YWFLTnzd8hHXqLAVkTI+SmdqeGuXA1YW5YU+ +Fuc7KUiXjBGFKlpN8k+UWl2G4j2lHvKLj7XFc3XcZf7fhF+8EuF3MiULtVRiR6JG +wdE9NQJ1V2kz2lZWf5CGRVw9QH5NWcy6gLX/rCaz8SJ8qeOIM6dy4fpFgLnyrUfe +C0Ii +-----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..b1535ebb1 --- /dev/null +++ b/testdata/generate.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# 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 1024 -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 1024 -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..6b778ab41 --- /dev/null +++ b/testdata/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIUIQ9teOpEraORXDFmj4B1nD60QDwwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MjA0MTgxNDE1MDdaFw0yNTAyMDUxNDE1MDdaMGcxCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1gy1r3bDeMbTqlVooHAkIS2Favj/xMcTh/tN +JCYFL+nIe/lgOAE+DETVO+n7sRe5KtgNfYfzgenWNs72crPQZV2+Sfyp7flZVEyj +AJdAmByFvluh8Q2qPXcSf/sFBgVaykErwQqGCVQwNguwVbsS1WNyyPBeU5R03Io0 +uHRpGGrJkMh6ZM7e0Nq0/Li1wX9U9qkymfKgKYoj7wJ/F20jc9aOnt4dRiCW14FO +PtwzGM68TyMZYCUkazIQZdcbP/RVktOsxtw90BoUgpeL6m7aQTqFDfN35o+VfSiB +BPys703x2wR4kgjmK/mPAE+7UI0QP6YA4gTr/K6XLAY/t+12xwIDAQABo3YwdDAf +BgNVHSMEGDAWgBQKlMvtZpXRlYJSQ8Z5vq2ng6ogTjAJBgNVHRMEAjAAMAsGA1Ud +DwQEAwIE8DAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFCWd +0LO1teKyhgUgbEGzPSN3c0v1MA0GCSqGSIb3DQEBCwUAA4IBAQA35CUSiDoygBos +k/30WBaZya+4pvbdqlkCNjkC/5z25GA/1FRRYUOACK4T0udVZfsEgrQ2m9RAyhwf +jFheypwtVPvITIsMbKec94WIQ7NlTHUBbv2CADnpPVLxl9cGT+dv/6WfCAD4vVt8 +51sazHp+DPV763ITlohHBuRAlpq5yhu3X/wlnx/TKbjDRo3ZnrjiEkl42J689Pzg +V6Ad+FQ5pnKjhzA9XDMfWUg6eq263NRAYlQ/i5IYO3LDLzRhYKWZgnFWLLWIrh4B +KdIHwQmNCpbqVse2fvLxTWCj3gpWOxDvhBFnhCEcHZK9crOyvDbSS4gzXjBSPa8a +tIOOp/aj +-----END CERTIFICATE----- diff --git a/testdata/localhost.key b/testdata/localhost.key new file mode 100644 index 000000000..5cf0300a2 --- /dev/null +++ b/testdata/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDWDLWvdsN4xtOq +VWigcCQhLYVq+P/ExxOH+00kJgUv6ch7+WA4AT4MRNU76fuxF7kq2A19h/OB6dY2 +zvZys9BlXb5J/Knt+VlUTKMAl0CYHIW+W6HxDao9dxJ/+wUGBVrKQSvBCoYJVDA2 +C7BVuxLVY3LI8F5TlHTcijS4dGkYasmQyHpkzt7Q2rT8uLXBf1T2qTKZ8qApiiPv +An8XbSNz1o6e3h1GIJbXgU4+3DMYzrxPIxlgJSRrMhBl1xs/9FWS06zG3D3QGhSC +l4vqbtpBOoUN83fmj5V9KIEE/KzvTfHbBHiSCOYr+Y8AT7tQjRA/pgDiBOv8rpcs +Bj+37XbHAgMBAAECggEAJ5Eo4pr3DjPeu51XHlUscI+cGoaVrPfJxfivrU9z5QP3 +oecaoK+mGxl8OzuI4ZcLjP5sG/jODAVAKlh+mPxhOOOnwcB5XvqZRyp1dS4AbD3V +gTcqC8elYQBKRKsPpinGOx3p6yC5Xy8XTF5Dxc0zcYuVE8zO+u813PCVR0WJidAY +8HU5CZPFR+VjLQAS0NX7TgKw4s3/wGX/Xe8+SFMj4Kv318b5Mo6Rom9wAWZ0cV/J ++xYGxalZXgMAWGSbNXn4ZxqwHEDdZU5V70hFwRdXkDDuuc8+SEz2L9gRYwLGE00l +sk8MUGSJWolgAADY4a2oxnV3MGdeLnHWBaf7yNVPaQKBgQD2/zl4V5LMx9hF97Is +f2Jm10JK6IFRGuc7NHFthrxxnDLAVGXboE5wcXsCQgy6MFMQaNp6Z9m+KU5IVEqo +0+5TLE39xhOOrZGYV9AOguY6Djy/HBwTC/fkj98GNXNRoPGUnI2WJNqVf3YcwymQ +tlXiUYNNgW3zMF+2NsGERsknfwKBgQDd2gwpHiz++Ita4iyRCWDwiCbjhQweVGMI +bYHxvUpRYU47kXyZeyPJzKDICLgPyscP2DW2Cq/mtZlj8C7EmfXC+4/q2WqEoqu/ +ri6vjYbGA3910ZhVw9Fm7YObsKDHgmdKFBcU0PfM+sUXUoGCuJdsuyveZMXJj+cc +urD4U4YUuQKBgQDgvfqc9ZItoum3QFpvArmWuecoIccI9WHRDTboYeC1v1quhhzo +akT1K0yyEUdjYvUxyTaCilwmG4+PYKNOWwhPxdBxSPoiqOwBomU9zv2NdcbwXbNt +4UX+Qqq8C4aSj89BWfG63G3H+eKO6UW41y/ubhz7OPnCfhGYytnqcj42IQKBgGCe +W/l0WOhaWmakZgBsczmOMlGYWiTDX7YF6zfa1okGtcmfnQJC7N0wLz2u/mpyR6uQ +4CN+GAmEQV+S9OtOmgfnA1Cmi4tkRSRfuZlfK7kFQPeQ5qVDeOk4u76MeBkro8xR +X5QDRkiRVlDH7/t8ZQZHpd1fSfx0nZSXggdEaPqRAoGBAPSd0NmSeVHgvX5vPtPW +mo1Pz/86gUUEAEKFOz3J1OI2/wORofZdKnzWn1JowDA8orPHXRwaMeeth6Xj+oPn +WwNcEWkmL6s/8rDV0DUtI3fOUzPhOOtfFuOU1JStT6o7lIHMhap9VbWIeUj3ZyiK +oSWEHRvGlFB2tu06zD1A8C7K +-----END PRIVATE KEY-----