Test various empty string cases with NPN callbacks
NPN is a little odd, owing to being a three-step process. The client
offers NPN, then the server accepts NPN and offers a list of protocols,
then the client picks a protocol.
The server is permitted to accept NPN but then offer zero supported
protocols. This worked, but was not tested or clearly documented.
In the last step, the client *must* pick a protocol, but it is permitted
to pick the empty string. The semantics of this are not explicitly
stated in the draft, but one can infer it means we aren't picking a
protocol. This also worked but was not tested or clearly documented.
Change-Id: I26d7089f4902834ff68a97467fc826e957d5fdf8
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/69027
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
Auto-Submit: David Benjamin <davidben@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 04c191f..6a92a28 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3265,30 +3265,49 @@
// and deprecated in favor of it.
// SSL_CTX_set_next_protos_advertised_cb sets a callback that is called when a
-// TLS server needs a list of supported protocols for Next Protocol
-// Negotiation. The returned list must be in wire format. The list is returned
-// by setting |*out| to point to it and |*out_len| to its length. This memory
-// will not be modified, but one should assume that |ssl| keeps a reference to
-// it.
+// TLS server needs a list of supported protocols for Next Protocol Negotiation.
//
-// The callback should return |SSL_TLSEXT_ERR_OK| if it wishes to advertise.
-// Otherwise, no such extension will be included in the ServerHello.
+// If the callback wishes to advertise NPN to the client, it should return
+// |SSL_TLSEXT_ERR_OK| and then set |*out| and |*out_len| to describe to a
+// buffer containing a (possibly empty) list of supported protocols in wire
+// format. That is, each protocol is prefixed with a 1-byte length, then
+// concatenated. From there, the client will select a protocol, possibly one not
+// on the server's list. The caller can use |SSL_get0_next_proto_negotiated|
+// after the handshake completes to query the final protocol.
+//
+// The returned buffer must remain valid and unmodified for at least the
+// duration of the |SSL| operation (e.g. |SSL_do_handshake|) that triggered the
+// callback.
+//
+// If the caller wishes not to advertise NPN, it should return
+// |SSL_TLSEXT_ERR_NOACK|. No NPN extension will be included in the ServerHello,
+// and the TLS server will behave as if it does not implement NPN.
OPENSSL_EXPORT void SSL_CTX_set_next_protos_advertised_cb(
SSL_CTX *ctx,
int (*cb)(SSL *ssl, const uint8_t **out, unsigned *out_len, void *arg),
void *arg);
// SSL_CTX_set_next_proto_select_cb sets a callback that is called when a client
-// needs to select a protocol from the server's provided list. |*out| must be
-// set to point to the selected protocol (which may be within |in|). The length
-// of the protocol name must be written into |*out_len|. The server's advertised
-// protocols are provided in |in| and |in_len|. The callback can assume that
-// |in| is syntactically valid.
+// needs to select a protocol from the server's provided list, passed in wire
+// format in |in_len| bytes from |in|. The callback can assume that |in| is
+// syntactically valid.
//
-// The client must select a protocol. It is fatal to the connection if this
-// callback returns a value other than |SSL_TLSEXT_ERR_OK|.
+// On success, the callback should return |SSL_TLSEXT_ERR_OK| and set |*out| and
+// |*out_len| to describe a buffer containing the selected protocol, or an
+// empty buffer to select no protocol. The returned buffer may point within
+// |in|, or it may point to some other buffer that remains valid and unmodified
+// for at least the duration of the |SSL| operation (e.g. |SSL_do_handshake|)
+// that triggered the callback.
//
-// Configuring this callback enables NPN on a client.
+// Returning any other value indicates a fatal error and will terminate the TLS
+// connection. To proceed without selecting a protocol, the callback must return
+// |SSL_TLSEXT_ERR_OK| and set |*out| and |*out_len| to an empty buffer. (E.g.
+// NULL and zero, respectively.)
+//
+// Configuring this callback enables NPN on a client. Although the callback can
+// then decline to negotiate a protocol, merely configuring the callback causes
+// the client to offer NPN in the ClientHello. Callers thus should not configure
+// this callback in TLS client contexts that are not intended to use NPN.
OPENSSL_EXPORT void SSL_CTX_set_next_proto_select_cb(
SSL_CTX *ctx, int (*cb)(SSL *ssl, uint8_t **out, uint8_t *out_len,
const uint8_t *in, unsigned in_len, void *arg),
@@ -3296,7 +3315,7 @@
// SSL_get0_next_proto_negotiated sets |*out_data| and |*out_len| to point to
// the client's requested protocol for this connection. If the client didn't
-// request any protocol, then |*out_data| is set to NULL.
+// request any protocol, then |*out_len| is set to zero.
//
// Note that the client can request any protocol it chooses. The value returned
// from this function need not be a member of the list of supported protocols
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 38ab01c..5bf9af4 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -524,7 +524,7 @@
}
}
- if (!config->expect_next_proto.empty()) {
+ if (!config->expect_next_proto.empty() || config->expect_no_next_proto) {
const uint8_t *next_proto;
unsigned next_proto_len;
SSL_get0_next_proto_negotiated(ssl, &next_proto, &next_proto_len);
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 082efce..1681c2b 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -455,6 +455,14 @@
// NextProtos is a list of supported, application level protocols.
NextProtos []string
+ // NoFallbackNextProto, if true, causes the client to decline to pick an NPN
+ // protocol, instead of picking an opportunistic, fallback protocol.
+ NoFallbackNextProto bool
+
+ // NegotiateNPNWithNoProtos, if true, causes the server to negotiate NPN
+ // despite having no protocols configured.
+ NegotiateNPNWithNoProtos bool
+
// ApplicationSettings is a set of application settings to use which each
// application protocol.
ApplicationSettings map[string][]byte
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 0c338d7..7172b3d 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -2265,6 +2265,10 @@
if hs.serverHello.extensions.nextProtoNeg {
nextProto := new(nextProtoMsg)
proto, fallback := mutualProtocol(c.config.NextProtos, hs.serverHello.extensions.nextProtos)
+ if fallback && c.config.NoFallbackNextProto {
+ proto = ""
+ fallback = false
+ }
nextProto.proto = proto
c.clientProtocol = proto
c.clientProtocolFallback = fallback
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index ceaf029..7459b5b 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -1664,11 +1664,7 @@
if c.vers < VersionTLS13 || config.Bugs.NegotiateNPNAtAllVersions {
if len(hs.clientHello.alpnProtocols) == 0 || c.config.Bugs.NegotiateALPNAndNPN {
- // Although sending an empty NPN extension is reasonable, Firefox has
- // had a bug around this. Best to send nothing at all if
- // config.NextProtos is empty. See
- // https://code.google.com/p/go/issues/detail?id=5445.
- if hs.clientHello.nextProtoNeg && len(config.NextProtos) > 0 {
+ if hs.clientHello.nextProtoNeg && (len(config.NextProtos) > 0 || config.NegotiateNPNWithNoProtos) {
serverExtensions.nextProtoNeg = true
serverExtensions.nextProtos = config.NextProtos
serverExtensions.npnAfterAlpn = config.Bugs.SwapNPNAndALPN
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 8a31afa..39b7611 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -5683,7 +5683,7 @@
expectedError: halfHelloRequestError,
})
- // NPN on client and server; results in post-handshake message.
+ // NPN on client and server; results in post-ChangeCipherSpec message.
tests = append(tests, testCase{
name: "NPN-Client",
config: Config{
@@ -5715,6 +5715,73 @@
},
})
+ // The client may select no protocol after seeing the server list.
+ tests = append(tests, testCase{
+ name: "NPN-Client-ClientSelectEmpty",
+ config: Config{
+ MaxVersion: VersionTLS12,
+ NextProtos: []string{"foo"},
+ },
+ flags: []string{"-select-empty-next-proto"},
+ resumeSession: true,
+ expectations: connectionExpectations{
+ noNextProto: true,
+ nextProtoType: npn,
+ },
+ })
+ tests = append(tests, testCase{
+ testType: serverTest,
+ name: "NPN-Server-ClientSelectEmpty",
+ config: Config{
+ MaxVersion: VersionTLS12,
+ NextProtos: []string{"no-match"},
+ NoFallbackNextProto: true,
+ },
+ flags: []string{
+ "-advertise-npn", "\x03foo\x03bar\x03baz",
+ "-expect-no-next-proto",
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ noNextProto: true,
+ nextProtoType: npn,
+ },
+ })
+
+ // The server may negotiate NPN, despite offering no protocols. In this
+ // case, the server must still be prepared for the client to select a
+ // fallback protocol.
+ tests = append(tests, testCase{
+ name: "NPN-Client-ServerAdvertiseEmpty",
+ config: Config{
+ MaxVersion: VersionTLS12,
+ NegotiateNPNWithNoProtos: true,
+ },
+ flags: []string{"-select-next-proto", "foo"},
+ resumeSession: true,
+ expectations: connectionExpectations{
+ nextProto: "foo",
+ nextProtoType: npn,
+ },
+ })
+ tests = append(tests, testCase{
+ testType: serverTest,
+ name: "NPN-Server-ServerAdvertiseEmpty",
+ config: Config{
+ MaxVersion: VersionTLS12,
+ NextProtos: []string{"foo"},
+ },
+ flags: []string{
+ "-advertise-empty-npn",
+ "-expect-next-proto", "foo",
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ nextProto: "foo",
+ nextProtoType: npn,
+ },
+ })
+
// Client does False Start and negotiates NPN.
tests = append(tests, testCase{
name: "FalseStart",
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index b84fd08..24b1bdd 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -293,9 +293,13 @@
BoolFlag("-require-any-client-certificate",
&TestConfig::require_any_client_certificate),
StringFlag("-advertise-npn", &TestConfig::advertise_npn),
+ BoolFlag("-advertise-empty-npn", &TestConfig::advertise_empty_npn),
StringFlag("-expect-next-proto", &TestConfig::expect_next_proto),
+ BoolFlag("-expect-no-next-proto", &TestConfig::expect_no_next_proto),
BoolFlag("-false-start", &TestConfig::false_start),
StringFlag("-select-next-proto", &TestConfig::select_next_proto),
+ BoolFlag("-select-empty-next-proto",
+ &TestConfig::select_empty_next_proto),
BoolFlag("-async", &TestConfig::async),
BoolFlag("-write-different-record-sizes",
&TestConfig::write_different_record_sizes),
@@ -713,10 +717,6 @@
const uint8_t *in, unsigned inlen,
void *arg) {
const TestConfig *config = GetTestConfig(ssl);
- if (config->select_next_proto.empty()) {
- return SSL_TLSEXT_ERR_NOACK;
- }
-
*out = (uint8_t *)config->select_next_proto.data();
*outlen = config->select_next_proto.size();
return SSL_TLSEXT_ERR_OK;
@@ -725,7 +725,7 @@
static int NextProtosAdvertisedCallback(SSL *ssl, const uint8_t **out,
unsigned int *out_len, void *arg) {
const TestConfig *config = GetTestConfig(ssl);
- if (config->advertise_npn.empty()) {
+ if (config->advertise_npn.empty() && !config->advertise_empty_npn) {
return SSL_TLSEXT_ERR_NOACK;
}
@@ -1721,7 +1721,7 @@
SSL_CTX_set_next_protos_advertised_cb(ssl_ctx.get(),
NextProtosAdvertisedCallback, NULL);
- if (!select_next_proto.empty()) {
+ if (!select_next_proto.empty() || select_empty_next_proto) {
SSL_CTX_set_next_proto_select_cb(ssl_ctx.get(), NextProtoSelectCallback,
NULL);
}
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 89cb2be..89656d1 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -67,9 +67,12 @@
std::string expect_certificate_types;
bool require_any_client_certificate = false;
std::string advertise_npn;
+ bool advertise_empty_npn = false;
std::string expect_next_proto;
+ bool expect_no_next_proto = false;
bool false_start = false;
std::string select_next_proto;
+ bool select_empty_next_proto = false;
bool async = false;
bool write_different_record_sizes = false;
bool cbc_record_splitting = false;