Test state machine asynchronous behavior.
Add a framework for testing the asynchronous codepath. Move some handshake
state machine coverage tests to cover a range of record-layer and
handshake-layer asynchronicity.
This adds tests for the previous two async bugs fixed.
Change-Id: I422ef33ba3eeb0ad04766871ed8bc59b677b169e
Reviewed-on: https://boringssl-review.googlesource.com/1410
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/CMakeLists.txt b/ssl/test/CMakeLists.txt
index 1b70e99..22c6619 100644
--- a/ssl/test/CMakeLists.txt
+++ b/ssl/test/CMakeLists.txt
@@ -3,6 +3,7 @@
add_executable(
bssl_shim
+ async_bio.cc
bssl_shim.cc
)
diff --git a/ssl/test/async_bio.cc b/ssl/test/async_bio.cc
new file mode 100644
index 0000000..f30f412
--- /dev/null
+++ b/ssl/test/async_bio.cc
@@ -0,0 +1,161 @@
+/* Copyright (c) 2014, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include "async_bio.h"
+
+#include <errno.h>
+#include <openssl/mem.h>
+
+namespace {
+
+extern const BIO_METHOD async_bio_method;
+
+struct async_bio {
+ size_t read_quota;
+ size_t write_quota;
+};
+
+async_bio *get_data(BIO *bio) {
+ if (bio->method != &async_bio_method) {
+ return NULL;
+ }
+ return (async_bio *)bio->ptr;
+}
+
+static int async_write(BIO *bio, const char *in, int inl) {
+ async_bio *a = get_data(bio);
+ if (a == NULL || bio->next_bio == NULL) {
+ return 0;
+ }
+
+ BIO_clear_retry_flags(bio);
+
+ if (a->write_quota == 0) {
+ BIO_set_retry_write(bio);
+ errno = EAGAIN;
+ return -1;
+ }
+
+ if ((size_t)inl > a->write_quota) {
+ inl = a->write_quota;
+ }
+ int ret = BIO_write(bio->next_bio, in, inl);
+ if (ret <= 0) {
+ BIO_copy_next_retry(bio);
+ } else {
+ a->write_quota -= ret;
+ }
+ return ret;
+}
+
+static int async_read(BIO *bio, char *out, int outl) {
+ async_bio *a = get_data(bio);
+ if (a == NULL || bio->next_bio == NULL) {
+ return 0;
+ }
+
+ BIO_clear_retry_flags(bio);
+
+ if (a->read_quota == 0) {
+ BIO_set_retry_read(bio);
+ errno = EAGAIN;
+ return -1;
+ }
+
+ if ((size_t)outl > a->read_quota) {
+ outl = a->read_quota;
+ }
+ int ret = BIO_read(bio->next_bio, out, outl);
+ if (ret <= 0) {
+ BIO_copy_next_retry(bio);
+ } else {
+ a->read_quota -= ret;
+ }
+ return ret;
+}
+
+static long async_ctrl(BIO *bio, int cmd, long num, void *ptr) {
+ if (bio->next_bio == NULL) {
+ return 0;
+ }
+ BIO_clear_retry_flags(bio);
+ int ret = BIO_ctrl(bio->next_bio, cmd, num, ptr);
+ BIO_copy_next_retry(bio);
+ return ret;
+}
+
+static int async_new(BIO *bio) {
+ async_bio *a = (async_bio *)OPENSSL_malloc(sizeof(*a));
+ if (a == NULL) {
+ return 0;
+ }
+ memset(a, 0, sizeof(*a));
+ bio->init = 1;
+ bio->ptr = (char *)a;
+ return 1;
+}
+
+static int async_free(BIO *bio) {
+ if (bio == NULL) {
+ return 0;
+ }
+
+ OPENSSL_free(bio->ptr);
+ bio->ptr = NULL;
+ bio->init = 0;
+ bio->flags = 0;
+ return 1;
+}
+
+static long async_callback_ctrl(BIO *bio, int cmd, bio_info_cb fp) {
+ if (bio->next_bio == NULL) {
+ return 0;
+ }
+ return BIO_callback_ctrl(bio->next_bio, cmd, fp);
+}
+
+const BIO_METHOD async_bio_method = {
+ BIO_TYPE_FILTER,
+ "async bio",
+ async_write,
+ async_read,
+ NULL /* puts */,
+ NULL /* gets */,
+ async_ctrl,
+ async_new,
+ async_free,
+ async_callback_ctrl,
+};
+
+} // namespace
+
+BIO *async_bio_create() {
+ return BIO_new(&async_bio_method);
+}
+
+void async_bio_allow_read(BIO *bio, size_t bytes) {
+ async_bio *a = get_data(bio);
+ if (a == NULL) {
+ return;
+ }
+ a->read_quota += bytes;
+}
+
+void async_bio_allow_write(BIO *bio, size_t bytes) {
+ async_bio *a = get_data(bio);
+ if (a == NULL) {
+ return;
+ }
+ a->write_quota += bytes;
+}
diff --git a/ssl/test/async_bio.h b/ssl/test/async_bio.h
new file mode 100644
index 0000000..7bec5fe
--- /dev/null
+++ b/ssl/test/async_bio.h
@@ -0,0 +1,35 @@
+/* Copyright (c) 2014, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#ifndef HEADER_ASYNC_BIO
+#define HEADER_ASYNC_BIO
+
+#include <openssl/bio.h>
+
+
+// async_bio_create creates a filter BIO for testing asynchronous
+// state machines. Reads and writes will fail and return EAGAIN unless
+// explicitly allowed. Each async BIO has a read quota and a write
+// quota. Initially both are zero. As each is incremented, bytes are
+// allowed to flow through the BIO.
+BIO *async_bio_create();
+
+// async_bio_allow_read increments |bio|'s read quota by |bytes|.
+void async_bio_allow_read(BIO *bio, size_t bytes);
+
+// async_bio_allow_write increments |bio|'s write quota by |bytes|.
+void async_bio_allow_write(BIO *bio, size_t bytes);
+
+
+#endif // HEADER_ASYNC_BIO
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 40bd533..5a76567 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -23,6 +23,8 @@
#include <openssl/bytestring.h>
#include <openssl/ssl.h>
+#include "async_bio.h"
+
static int usage(const char *program) {
fprintf(stderr, "Usage: %s (client|server) (normal|resume) [flags...]\n",
program);
@@ -155,33 +157,22 @@
return NULL;
}
-static SSL *setup_ssl(SSL_CTX *ssl_ctx, int fd) {
- SSL *ssl = NULL;
- BIO *bio = NULL;
-
- ssl = SSL_new(ssl_ctx);
- if (ssl == NULL) {
- goto err;
+static int retry_async(SSL *ssl, int ret, BIO *bio) {
+ // No error; don't retry.
+ if (ret >= 0) {
+ return 0;
}
-
-
- bio = BIO_new_fd(fd, 1 /* take ownership */);
- if (bio == NULL) {
- goto err;
+ // See if we needed to read or write more. If so, allow one byte through on
+ // the appropriate end to maximally stress the state machine.
+ int err = SSL_get_error(ssl, ret);
+ if (err == SSL_ERROR_WANT_READ) {
+ async_bio_allow_read(bio, 1);
+ return 1;
+ } else if (err == SSL_ERROR_WANT_WRITE) {
+ async_bio_allow_write(bio, 1);
+ return 1;
}
-
- SSL_set_bio(ssl, bio, bio);
-
- return ssl;
-
-err:
- if (bio != NULL) {
- BIO_free(bio);
- }
- if (ssl != NULL) {
- SSL_free(ssl);
- }
- return NULL;
+ return 0;
}
static int do_exchange(SSL_SESSION **out_session,
@@ -192,13 +183,14 @@
int is_resume,
int fd,
SSL_SESSION *session) {
+ int async = 0;
const char *expected_certificate_types = NULL;
const char *expected_next_proto = NULL;
expected_server_name = NULL;
early_callback_called = 0;
advertise_npn = NULL;
- SSL *ssl = setup_ssl(ssl_ctx, fd);
+ SSL *ssl = SSL_new(ssl_ctx);
if (ssl == NULL) {
BIO_print_errors_fp(stdout);
return 1;
@@ -271,12 +263,26 @@
return 1;
}
select_next_proto = argv[i];
+ } else if (strcmp(argv[i], "-async") == 0) {
+ async = 1;
} else {
fprintf(stderr, "Unknown argument: %s\n", argv[i]);
return 1;
}
}
+ BIO *bio = BIO_new_fd(fd, 1 /* take ownership */);
+ if (bio == NULL) {
+ BIO_print_errors_fp(stdout);
+ return 1;
+ }
+ if (async) {
+ BIO *async = async_bio_create();
+ BIO_push(async, bio);
+ bio = async;
+ }
+ SSL_set_bio(ssl, bio, bio);
+
if (session != NULL) {
if (SSL_set_session(ssl, session) != 1) {
fprintf(stderr, "failed to set session\n");
@@ -285,11 +291,13 @@
}
int ret;
- if (is_server) {
- ret = SSL_accept(ssl);
- } else {
- ret = SSL_connect(ssl);
- }
+ do {
+ if (is_server) {
+ ret = SSL_accept(ssl);
+ } else {
+ ret = SSL_connect(ssl);
+ }
+ } while (async && retry_async(ssl, ret, bio));
if (ret != 1) {
SSL_free(ssl);
BIO_print_errors_fp(stdout);
@@ -342,7 +350,10 @@
for (;;) {
uint8_t buf[512];
- int n = SSL_read(ssl, buf, sizeof(buf));
+ int n;
+ do {
+ n = SSL_read(ssl, buf, sizeof(buf));
+ } while (async && retry_async(ssl, n, bio));
if (n < 0) {
SSL_free(ssl);
BIO_print_errors_fp(stdout);
@@ -353,7 +364,10 @@
for (int i = 0; i < n; i++) {
buf[i] ^= 0xff;
}
- int w = SSL_write(ssl, buf, n);
+ int w;
+ do {
+ w = SSL_write(ssl, buf, n);
+ } while (async && retry_async(ssl, w, bio));
if (w != n) {
SSL_free(ssl);
BIO_print_errors_fp(stdout);
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 4f34ce4..eb1d57c 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -391,6 +391,11 @@
// SendFallbackSCSV causes the client to include
// TLS_FALLBACK_SCSV in the ClientHello.
SendFallbackSCSV bool
+
+ // MaxHandshakeRecordLength, if non-zero, is the maximum size of a
+ // handshake record. Handshake messages will be split at the record
+ // layer.
+ MaxHandshakeRecordLength int
}
func (c *Config) serverInit() {
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 02ed8f0..52582ad8 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -719,6 +719,9 @@
if m > maxPlaintext {
m = maxPlaintext
}
+ if typ == recordTypeHandshake && c.config.Bugs.MaxHandshakeRecordLength > 0 && m > c.config.Bugs.MaxHandshakeRecordLength {
+ m = c.config.Bugs.MaxHandshakeRecordLength
+ }
explicitIVLen := 0
explicitIVIsSeq := false
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index cf9e44c..bdd3566 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -218,17 +218,6 @@
expectedError: ":UNEXPECTED_MESSAGE:",
},
{
- testType: serverTest,
- name: "NPNServerTest",
- config: Config{
- NextProtos: []string{"bar"},
- },
- flags: []string{
- "-advertise-npn", "\x03foo\x03bar\x03baz",
- "-expect-next-proto", "bar",
- },
- },
- {
name: "SkipChangeCipherSpec-Client",
config: Config{
Bugs: ProtocolBugs{
@@ -323,19 +312,6 @@
expectedError: ":CCS_RECEIVED_EARLY:",
},
{
- name: "SessionTicketsDisabled-Client",
- config: Config{
- SessionTicketsDisabled: true,
- },
- },
- {
- testType: serverTest,
- name: "SessionTicketsDisabled-Server",
- config: Config{
- SessionTicketsDisabled: true,
- },
- },
- {
name: "SkipNewSessionTicket",
config: Config{
Bugs: ProtocolBugs{
@@ -346,18 +322,6 @@
expectedError: ":CCS_RECEIVED_EARLY:",
},
{
- name: "FalseStart",
- config: Config{
- CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- NextProtos: []string{"foo"},
- },
- flags: []string{
- "-false-start",
- "-select-next-proto", "foo",
- },
- resumeSession: true,
- },
- {
name: "FalseStart-SessionTicketsDisabled",
config: Config{
CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
@@ -824,6 +788,135 @@
}
}
+// Adds tests that try to cover the range of the handshake state machine, under
+// various conditions. Some of these are redundant with other tests, but they
+// only cover the synchronous case.
+func addStateMachineCoverageTests(async bool, splitHandshake bool) {
+ var suffix string
+ var flags []string
+ var maxHandshakeRecordLength int
+ if async {
+ suffix = "-Async"
+ flags = append(flags, "-async")
+ } else {
+ suffix = "-Sync"
+ }
+ if splitHandshake {
+ suffix += "-SplitHandshakeRecords"
+ maxHandshakeRecordLength = 10
+ }
+
+ // Basic handshake, with resumption. Client and server.
+ testCases = append(testCases, testCase{
+ name: "Basic-Client" + suffix,
+ config: Config{
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: flags,
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "Basic-Server" + suffix,
+ config: Config{
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: flags,
+ })
+
+ // No session ticket support; server doesn't send NewSessionTicket.
+ testCases = append(testCases, testCase{
+ name: "SessionTicketsDisabled-Client" + suffix,
+ config: Config{
+ SessionTicketsDisabled: true,
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: flags,
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "SessionTicketsDisabled-Server" + suffix,
+ config: Config{
+ SessionTicketsDisabled: true,
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: flags,
+ })
+
+ // NPN on client and server; results in post-handshake message.
+ testCases = append(testCases, testCase{
+ name: "NPN-Client" + suffix,
+ config: Config{
+ CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+ NextProtos: []string{"foo"},
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: append(flags, "-select-next-proto", "foo"),
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "NPN-Server" + suffix,
+ config: Config{
+ NextProtos: []string{"bar"},
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: append(flags,
+ "-advertise-npn", "\x03foo\x03bar\x03baz",
+ "-expect-next-proto", "bar"),
+ })
+
+ // Client does False Start and negotiates NPN.
+ testCases = append(testCases, testCase{
+ name: "FalseStart" + suffix,
+ config: Config{
+ CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+ NextProtos: []string{"foo"},
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: append(flags,
+ "-false-start",
+ "-select-next-proto", "foo"),
+ resumeSession: true,
+ })
+
+ // TLS client auth.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ClientAuth-Client" + suffix,
+ config: Config{
+ CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+ ClientAuth: RequireAnyClientCert,
+ Bugs: ProtocolBugs{
+ MaxHandshakeRecordLength: maxHandshakeRecordLength,
+ },
+ },
+ flags: append(flags,
+ "-cert-file", rsaCertificateFile,
+ "-key-file", rsaKeyFile),
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ClientAuth-Server" + suffix,
+ config: Config{
+ Certificates: []Certificate{rsaCertificate},
+ },
+ flags: append(flags, "-require-any-client-certificate"),
+ })
+}
+
func worker(statusChan chan statusMsg, c chan *testCase, buildDir string, wg *sync.WaitGroup) {
defer wg.Done()
@@ -874,6 +967,11 @@
addBadECDSASignatureTests()
addCBCPaddingTests()
addClientAuthTests()
+ for _, async := range []bool{false, true} {
+ for _, splitHandshake := range []bool{false, true} {
+ addStateMachineCoverageTests(async, splitHandshake)
+ }
+ }
var wg sync.WaitGroup