From 152c01ead80e3360f11ad228729409c3a00fe7db Mon Sep 17 00:00:00 2001 From: igolaizola <11333576+igolaizola@users.noreply.github.com> Date: Mon, 17 Feb 2020 11:24:19 +0100 Subject: [PATCH] Add end to end tests using OpenSSL End to end tests are now executed in the folowing modes: - pion client + pion server - openssl client + pion server - pion client + openssl server OpenSSL is launched using `exec.Command` and therefore `openssl` command must be available before running these tests. Tests are enabled via `openssl` tag. --- .github/workflows/e2e.yaml | 20 +++ README.md | 2 +- e2e/Dockerfile | 11 ++ e2e/e2e_openssl_test.go | 246 +++++++++++++++++++++++++++++++++++ e2e/e2e_openssl_v113_test.go | 17 +++ e2e/e2e_test.go | 198 +++++++++++++++++----------- e2e/e2e_v113_test.go | 9 +- 7 files changed, 423 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/e2e.yaml create mode 100644 e2e/Dockerfile create mode 100644 e2e/e2e_openssl_test.go create mode 100644 e2e/e2e_openssl_v113_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 000000000..c6b4cf42b --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,20 @@ +name: E2E +on: + pull_request: + branches: + - master + push: + branches: + - master + +jobs: + e2e-test: + name: Test + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + - name: test + run: | + docker build -t pion-dtls-e2e -f e2e/Dockerfile . + docker run -i --rm pion-dtls-e2e diff --git a/README.md b/README.md index 715b8a146..408644740 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu * [Sean DuBois](https://github.com/Sean-Der) - *Original Author* * [Michiel De Backker](https://github.com/backkem) - *Public API* * [Chris Hiszpanski](https://github.com/thinkski) - *Support Signature Algorithms Extension* -* [Iñigo Garcia Olaizola](https://github.com/igolaizola) - *Serialization & resumption, cert verification* +* [Iñigo Garcia Olaizola](https://github.com/igolaizola) - *Serialization & resumption, cert verification, E2E* * [Daniele Sluijters](https://github.com/daenney) - *AES-CCM support* * [Jin Lei](https://github.com/jinleileiking) - *Logging* * [Hugo Arregui](https://github.com/hugoArregui) diff --git a/e2e/Dockerfile b/e2e/Dockerfile new file mode 100644 index 000000000..670903463 --- /dev/null +++ b/e2e/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:1.13-alpine3.11 + +RUN apk add --no-cache \ + openssl + +ENV CGO_ENABLED=0 + +COPY . /go/src/github.com/pion/dtls +WORKDIR /go/src/github.com/pion/dtls/e2e + +CMD ["go", "test", "-tags=openssl", "-v", "."] diff --git a/e2e/e2e_openssl_test.go b/e2e/e2e_openssl_test.go new file mode 100644 index 000000000..701919002 --- /dev/null +++ b/e2e/e2e_openssl_test.go @@ -0,0 +1,246 @@ +// +build openssl,!js + +package e2e + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/pion/dtls/v2" +) + +func serverOpenSSL(c *comm) { + go func() { + c.serverMutex.Lock() + defer c.serverMutex.Unlock() + + cfg := c.serverConfig + + // create openssl arguments + args := []string{"s_server", + "-dtls1_2", + "-quiet", + "-verify_quiet", + "-verify_return_error", + fmt.Sprintf("-accept=%d", c.serverPort), + } + ciphers := ciphersOpenSSL(cfg) + if ciphers != "" { + args = append(args, fmt.Sprintf("-cipher=%s", ciphers)) + } + + // psk arguments + if cfg.PSK != nil { + psk, err := cfg.PSK(nil) + if err != nil { + c.errChan <- err + return + } + args = append(args, fmt.Sprintf("-psk=%X", psk)) + if len(cfg.PSKIdentityHint) > 0 { + args = append(args, fmt.Sprintf("-psk_hint=%s", cfg.PSKIdentityHint)) + } + } + + // certs arguments + if len(cfg.Certificates) > 0 { + // create temporary cert files + certPEM, keyPEM, err := writeTempPEM(cfg) + if err != nil { + c.errChan <- err + return + } + args = append(args, + fmt.Sprintf("-cert=%s", certPEM), + fmt.Sprintf("-key=%s", keyPEM)) + defer func() { + _ = os.Remove(certPEM) + _ = os.Remove(keyPEM) + }() + } else { + args = append(args, "-nocert") + } + + // launch command + // #nosec G204 + cmd := exec.CommandContext(c.ctx, "openssl", args...) + var inner net.Conn + inner, c.serverConn = net.Pipe() + cmd.Stdin = inner + cmd.Stdout = inner + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + c.errChan <- err + _ = inner.Close() + return + } + + // Ensure that server has started + time.Sleep(500 * time.Millisecond) + + c.serverReady <- struct{}{} + simpleReadWrite(c.errChan, c.serverChan, c.serverConn, c.messageRecvCount) + }() +} + +func clientOpenSSL(c *comm) { + select { + case <-c.serverReady: + // OK + case <-time.After(time.Second): + c.errChan <- errors.New("waiting on serverReady err: timeout") + } + + c.clientMutex.Lock() + defer c.clientMutex.Unlock() + + cfg := c.clientConfig + + // create openssl arguments + args := []string{"s_client", + "-dtls1_2", + "-quiet", + "-verify_quiet", + "-verify_return_error", + "-servername=localhost", + fmt.Sprintf("-connect=127.0.0.1:%d", c.serverPort), + } + ciphers := ciphersOpenSSL(cfg) + if ciphers != "" { + args = append(args, fmt.Sprintf("-cipher=%s", ciphers)) + } + + // psk arguments + if cfg.PSK != nil { + psk, err := cfg.PSK(nil) + if err != nil { + c.errChan <- err + return + } + args = append(args, fmt.Sprintf("-psk=%X", psk)) + } + + // certificate arguments + if len(cfg.Certificates) > 0 { + // create temporary cert files + certPEM, keyPEM, err := writeTempPEM(cfg) + if err != nil { + c.errChan <- err + return + } + args = append(args, fmt.Sprintf("-CAfile=%s", certPEM)) + defer func() { + _ = os.Remove(certPEM) + _ = os.Remove(keyPEM) + }() + } + + // launch command + // #nosec G204 + cmd := exec.CommandContext(c.ctx, "openssl", args...) + var inner net.Conn + inner, c.clientConn = net.Pipe() + cmd.Stdin = inner + cmd.Stdout = inner + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + c.errChan <- err + _ = inner.Close() + return + } + + simpleReadWrite(c.errChan, c.clientChan, c.clientConn, c.messageRecvCount) +} + +func ciphersOpenSSL(cfg *dtls.Config) string { + // See https://tls.mbed.org/supported-ssl-ciphersuites + translate := map[dtls.CipherSuiteID]string{ + dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM: "ECDHE-ECDSA-AES128-CCM", + dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8: "ECDHE-ECDSA-AES128-CCM8", + dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "ECDHE-ECDSA-AES128-GCM-SHA256", + dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "ECDHE-RSA-AES128-GCM-SHA256", + + dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "ECDHE-ECDSA-AES256-SHA", + dtls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "ECDHE-RSA-AES128-SHA", + + dtls.TLS_PSK_WITH_AES_128_CCM: "PSK-AES128-CCM", + dtls.TLS_PSK_WITH_AES_128_CCM_8: "PSK-AES128-CCM8", + dtls.TLS_PSK_WITH_AES_128_GCM_SHA256: "PSK-AES128-GCM-SHA256", + } + + var ciphers []string + for _, c := range cfg.CipherSuites { + if text, ok := translate[c]; ok { + ciphers = append(ciphers, text) + } + } + return strings.Join(ciphers, ";") +} + +func writeTempPEM(cfg *dtls.Config) (string, string, error) { + certOut, err := ioutil.TempFile("", "cert.pem") + if err != nil { + return "", "", fmt.Errorf("failed to create temporary file: %w", err) + } + keyOut, err := ioutil.TempFile("", "key.pem") + if err != nil { + return "", "", fmt.Errorf("failed to create temporary file: %w", err) + } + + cert := cfg.Certificates[0] + derBytes := cert.Certificate[0] + if err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { + return "", "", fmt.Errorf("failed to write data to cert.pem: %w", err) + } + if err = certOut.Close(); err != nil { + return "", "", fmt.Errorf("error closing cert.pem: %w", err) + } + + priv := cert.PrivateKey + var privBytes []byte + privBytes, err = x509.MarshalPKCS8PrivateKey(priv) + if err != nil { + return "", "", fmt.Errorf("unable to marshal private key: %w", err) + } + if err = pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { + return "", "", fmt.Errorf("failed to write data to key.pem: %w", err) + } + if err = keyOut.Close(); err != nil { + return "", "", fmt.Errorf("error closing key.pem: %w", err) + } + return certOut.Name(), keyOut.Name(), nil +} + +func TestPionOpenSSLE2ESimple(t *testing.T) { + t.Run("OpenSSLServer", func(t *testing.T) { + testPionE2ESimple(t, serverOpenSSL, clientPion) + }) + t.Run("OpenSSLClient", func(t *testing.T) { + testPionE2ESimple(t, serverPion, clientOpenSSL) + }) +} +func TestPionOpenSSLE2ESimplePSK(t *testing.T) { + t.Run("OpenSSLServer", func(t *testing.T) { + testPionE2ESimplePSK(t, serverOpenSSL, clientPion) + }) + t.Run("OpenSSLClient", func(t *testing.T) { + testPionE2ESimplePSK(t, serverPion, clientOpenSSL) + }) +} +func TestPionOpenSSLE2EMTUs(t *testing.T) { + t.Run("OpenSSLServer", func(t *testing.T) { + testPionE2EMTUs(t, serverOpenSSL, clientPion) + }) + t.Run("OpenSSLClient", func(t *testing.T) { + testPionE2EMTUs(t, serverPion, clientOpenSSL) + }) +} diff --git a/e2e/e2e_openssl_v113_test.go b/e2e/e2e_openssl_v113_test.go new file mode 100644 index 000000000..979b9e38d --- /dev/null +++ b/e2e/e2e_openssl_v113_test.go @@ -0,0 +1,17 @@ +// +build openssl,go1.13,!js + +package e2e + +import ( + "testing" +) + +func TestPionOpenSSLE2ESimpleED25519(t *testing.T) { + t.Skip("TODO: make ED25519 test work with openssl") + t.Run("OpenSSLServer", func(t *testing.T) { + testPionE2ESimpleED25519(t, serverOpenSSL, clientPion) + }) + t.Run("OpenSSLClient", func(t *testing.T) { + testPionE2ESimpleED25519(t, serverPion, clientOpenSSL) + }) +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 8af23e9e9..531402b44 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -66,85 +66,69 @@ func simpleReadWrite(errChan chan error, outChan chan string, conn io.ReadWriter } } -func assertE2ECommunication(ctx context.Context, clientConfig, serverConfig *dtls.Config, serverPort int, t *testing.T) { - var ( - messageRecvCount uint64 // Counter to make sure both sides got a message - clientMutex sync.Mutex - clientConn net.Conn - serverMutex sync.Mutex - serverConn net.Conn - serverListener net.Listener - serverReady = make(chan struct{}) - errChan = make(chan error) - clientChan = make(chan string) - serverChan = make(chan string) - ) - - // DTLS Client - go func() { - select { - case <-serverReady: - // OK - case <-time.After(time.Second): - errChan <- errors.New("waiting on serverReady err: timeout") - } - - clientMutex.Lock() - defer clientMutex.Unlock() +type comm struct { + ctx context.Context + clientConfig, serverConfig *dtls.Config + serverPort int + messageRecvCount *uint64 // Counter to make sure both sides got a message + clientMutex *sync.Mutex + clientConn net.Conn + serverMutex *sync.Mutex + serverConn net.Conn + serverListener net.Listener + serverReady chan struct{} + errChan chan error + clientChan chan string + serverChan chan string + client func(*comm) + server func(*comm) +} - var err error - clientConn, err = dtls.DialWithContext(ctx, "udp", - &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort}, - clientConfig, - ) - if err != nil { - errChan <- err - return - } +func newComm(ctx context.Context, clientConfig, serverConfig *dtls.Config, serverPort int, server, client func(*comm)) *comm { + messageRecvCount := uint64(0) + c := &comm{ + ctx: ctx, + clientConfig: clientConfig, + serverConfig: serverConfig, + serverPort: serverPort, + messageRecvCount: &messageRecvCount, + clientMutex: &sync.Mutex{}, + serverMutex: &sync.Mutex{}, + serverReady: make(chan struct{}), + errChan: make(chan error), + clientChan: make(chan string), + serverChan: make(chan string), + server: server, + client: client, + } + return c +} - simpleReadWrite(errChan, clientChan, clientConn, &messageRecvCount) - }() +func (c *comm) assert(t *testing.T) { + // DTLS Client + go c.client(c) // DTLS Server - go func() { - serverMutex.Lock() - defer serverMutex.Unlock() - - var err error - serverListener, err = dtls.Listen("udp", - &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort}, - serverConfig, - ) - if err != nil { - errChan <- err - return - } - serverReady <- struct{}{} - serverConn, err = serverListener.Accept() - if err != nil { - errChan <- err - return - } - - simpleReadWrite(errChan, serverChan, serverConn, &messageRecvCount) - }() + go c.server(c) defer func() { - clientMutex.Lock() - serverMutex.Lock() - defer clientMutex.Unlock() - defer serverMutex.Unlock() + c.clientMutex.Lock() + c.serverMutex.Lock() + defer c.clientMutex.Unlock() + defer c.serverMutex.Unlock() - if err := clientConn.Close(); err != nil { + if err := c.clientConn.Close(); err != nil { t.Fatal(err) } - if err := serverConn.Close(); err != nil { + if err := c.serverConn.Close(); err != nil { t.Fatal(err) } - if err := serverListener.Close(); err != nil { - t.Fatal(err) + if c.serverListener != nil { + if err := c.serverListener.Close(); err != nil { + t.Fatal(err) + } } }() @@ -152,11 +136,11 @@ func assertE2ECommunication(ctx context.Context, clientConfig, serverConfig *dtl seenClient, seenServer := false, false for { select { - case err := <-errChan: + case err := <-c.errChan: t.Fatal(err) case <-time.After(testTimeLimit): t.Fatalf("Test timeout, seenClient %t seenServer %t", seenClient, seenServer) - case clientMsg := <-clientChan: + case clientMsg := <-c.clientChan: if clientMsg != testMessage { t.Fatalf("clientMsg does not equal test message: %s %s", clientMsg, testMessage) } @@ -165,7 +149,7 @@ func assertE2ECommunication(ctx context.Context, clientConfig, serverConfig *dtl if seenClient && seenServer { return } - case serverMsg := <-serverChan: + case serverMsg := <-c.serverChan: if serverMsg != testMessage { t.Fatalf("serverMsg does not equal test message: %s %s", serverMsg, testMessage) } @@ -179,13 +163,60 @@ func assertE2ECommunication(ctx context.Context, clientConfig, serverConfig *dtl }() } +func clientPion(c *comm) { + select { + case <-c.serverReady: + // OK + case <-time.After(time.Second): + c.errChan <- errors.New("waiting on serverReady err: timeout") + } + + c.clientMutex.Lock() + defer c.clientMutex.Unlock() + + var err error + c.clientConn, err = dtls.DialWithContext(c.ctx, "udp", + &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: c.serverPort}, + c.clientConfig, + ) + if err != nil { + c.errChan <- err + return + } + + simpleReadWrite(c.errChan, c.clientChan, c.clientConn, c.messageRecvCount) +} + +func serverPion(c *comm) { + c.serverMutex.Lock() + defer c.serverMutex.Unlock() + + var err error + c.serverListener, err = dtls.Listen("udp", + &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: c.serverPort}, + c.serverConfig, + ) + if err != nil { + c.errChan <- err + return + } + c.serverReady <- struct{}{} + c.serverConn, err = c.serverListener.Accept() + if err != nil { + c.errChan <- err + return + } + + simpleReadWrite(c.errChan, c.serverChan, c.serverConn, c.messageRecvCount) +} + /* Simple DTLS Client/Server can communicate - Assert that you can send messages both ways - Assert that Close() on both ends work - Assert that no Goroutines are leaked */ -func TestPionE2ESimple(t *testing.T) { +func testPionE2ESimple(t *testing.T, server, client func(*comm)) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() @@ -203,7 +234,7 @@ func TestPionE2ESimple(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - cert, err := selfsign.GenerateSelfSigned() + cert, err := selfsign.GenerateSelfSignedWithDNS("localhost") if err != nil { t.Fatal(err) } @@ -213,12 +244,13 @@ func TestPionE2ESimple(t *testing.T) { CipherSuites: []dtls.CipherSuiteID{cipherSuite}, InsecureSkipVerify: true, } - assertE2ECommunication(ctx, cfg, cfg, serverPort, t) + comm := newComm(ctx, cfg, cfg, serverPort, server, client) + comm.assert(t) }) } } -func TestPionE2ESimplePSK(t *testing.T) { +func testPionE2ESimplePSK(t *testing.T, server, client func(*comm)) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() @@ -244,12 +276,13 @@ func TestPionE2ESimplePSK(t *testing.T) { PSKIdentityHint: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, CipherSuites: []dtls.CipherSuiteID{cipherSuite}, } - assertE2ECommunication(ctx, cfg, cfg, serverPort, t) + comm := newComm(ctx, cfg, cfg, serverPort, server, client) + comm.assert(t) }) } } -func TestPionE2EMTUs(t *testing.T) { +func testPionE2EMTUs(t *testing.T, server, client func(*comm)) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() @@ -268,7 +301,7 @@ func TestPionE2EMTUs(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - cert, err := selfsign.GenerateSelfSigned() + cert, err := selfsign.GenerateSelfSignedWithDNS("localhost") if err != nil { t.Fatal(err) } @@ -279,7 +312,18 @@ func TestPionE2EMTUs(t *testing.T) { InsecureSkipVerify: true, MTU: mtu, } - assertE2ECommunication(ctx, cfg, cfg, serverPort, t) + comm := newComm(ctx, cfg, cfg, serverPort, server, client) + comm.assert(t) }) } } + +func TestPionE2ESimple(t *testing.T) { + testPionE2ESimple(t, serverPion, clientPion) +} +func TestPionE2ESimplePSK(t *testing.T) { + testPionE2ESimplePSK(t, serverPion, clientPion) +} +func TestPionE2EMTUs(t *testing.T) { + testPionE2EMTUs(t, serverPion, clientPion) +} diff --git a/e2e/e2e_v113_test.go b/e2e/e2e_v113_test.go index 1f945667d..7b96e56c4 100644 --- a/e2e/e2e_v113_test.go +++ b/e2e/e2e_v113_test.go @@ -19,7 +19,7 @@ import ( // ED25519 is not supported in Go 1.12 crypto/x509. // Once Go 1.12 is deprecated, move this test to e2e_test.go. -func TestPionE2ESimpleED25519(t *testing.T) { +func testPionE2ESimpleED25519(t *testing.T, server, client func(*comm)) { lim := test.TimeOut(time.Second * 30) defer lim.Stop() @@ -53,7 +53,12 @@ func TestPionE2ESimpleED25519(t *testing.T) { CipherSuites: []dtls.CipherSuiteID{cipherSuite}, InsecureSkipVerify: true, } - assertE2ECommunication(ctx, cfg, cfg, serverPort, t) + comm := newComm(ctx, cfg, cfg, serverPort, server, client) + comm.assert(t) }) } } + +func TestPionE2ESimpleED25519(t *testing.T) { + testPionE2ESimpleED25519(t, serverPion, clientPion) +}