aboutsummaryrefslogtreecommitdiff
path: root/ssl
diff options
context:
space:
mode:
authorDavid Benjamin <davidben@google.com>2024-05-22 13:56:56 -0400
committerBoringssl LUCI CQ <boringssl-scoped@luci-project-accounts.iam.gserviceaccount.com>2024-06-05 16:02:50 +0000
commitc1d9ac02514a138129872a036e3f8a1074dcb8bd (patch)
tree0a3dab51852bfa5e04be4debc2bb7e4dc4652144 /ssl
parente1a860c3745c77cb83228dde1b73fa62eaf43930 (diff)
downloadboringssl-c1d9ac02514a138129872a036e3f8a1074dcb8bd.zip
boringssl-c1d9ac02514a138129872a036e3f8a1074dcb8bd.tar.gz
boringssl-c1d9ac02514a138129872a036e3f8a1074dcb8bd.tar.bz2
Make SSL_select_next_proto more robust to invalid calls.
SSL_select_next_proto has some fairly complex preconditions: - The peer and supported list must be valid protocol lists - The supported list must not be empty. The peer list may be empty due to one of NPN's edge cases. In the context of how this function is meant to be used, these are reasonable preconditions. The caller should not serialize its own list wrong, and it makes no sense to try to negotiate a protocol when you don't support any protocols. In particular, it complicates NPN's weird "opportunistic" protocol. However, the preconditions are unchecked and a bit subtle. Violating them will result in memory errors. Bad syntax on the protocol lists is mostly not a concern (you should encode your own list correctly and the library checks the peer's list), but the second rule is somewhat of a mess in practice: Despite having the same precondition in reality, OpenSSL did not document this. Their documentation implies things which are impossible without this precondition, but they forgot to actually write down the precondition. There's an added complexity that OpenSSL never updated the parameter names to match the role reversal between ALPN and NPN. There are thus a few cases where a buggy caller may pass an empty "supported" list. - An NPN client called SSL_select_next_proto despite not actually supporting any NPN protocols. - An NPN client called SSL_select_next_proto, flipped the parameters, and the server advertised no protocols. - An ALPN server called SSL_select_next_proto, passed its own list in as the second parameter, despite not actually supporting any ALPN protocols. In these scenarios, the "opportunistic" protocol returned in the OPENSSL_NPN_NO_OVERLAP case will be out of bounds. If the caller discards it, this does not matter. If the caller returns it through the NPN or ALPN selection callback, they have a problem. ALPN servers are expected to discard it, though some may be buggy. NPN clients may implement either behavior. Older versions of some callers have exhibited variations on the above mistakes, so empirically folks don't always get it right. OpenSSL's wrong documentation also does not help matters. Instead, have SSL_select_next_proto just check these preconditions. That is not a performance-sensitive function and these preconditions are easy to check. While I'm here, rewrite it with CBS so it is much more straightforwardly correct. What to return when the preconditions fail is tricky, but we need to output *some* protocol, so we output the empty protocol. This, per the previous test and doc fixes, is actually fine in NPN, so one of the above buggy callers is not retroactively made OK. But it is not fine in ALPN, so we still need to document that callers need to avoid this state. To that end, revamp the documentation a bit. Thanks to Joe Birr-Pixton for reporting this! Fixed: 735 Change-Id: I4378a082385e7334e6abaa6705e6b15d6843f6c5 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/69028 Reviewed-by: Bob Beck <bbe@google.com> Commit-Queue: David Benjamin <davidben@google.com>
Diffstat (limited to 'ssl')
-rw-r--r--ssl/extensions.cc17
-rw-r--r--ssl/internal.h5
-rw-r--r--ssl/ssl_lib.cc59
-rw-r--r--ssl/ssl_test.cc37
4 files changed, 88 insertions, 30 deletions
diff --git a/ssl/extensions.cc b/ssl/extensions.cc
index 20a5d30..8b2de59 100644
--- a/ssl/extensions.cc
+++ b/ssl/extensions.cc
@@ -1474,16 +1474,19 @@ bool ssl_is_alpn_protocol_allowed(const SSL_HANDSHAKE *hs,
}
// Check that the protocol name is one of the ones we advertised.
- CBS client_protocol_name_list =
- MakeConstSpan(hs->config->alpn_client_proto_list),
- client_protocol_name;
- while (CBS_len(&client_protocol_name_list) > 0) {
- if (!CBS_get_u8_length_prefixed(&client_protocol_name_list,
- &client_protocol_name)) {
+ return ssl_alpn_list_contains_protocol(hs->config->alpn_client_proto_list,
+ protocol);
+}
+
+bool ssl_alpn_list_contains_protocol(Span<const uint8_t> list,
+ Span<const uint8_t> protocol) {
+ CBS cbs = list, candidate;
+ while (CBS_len(&cbs) > 0) {
+ if (!CBS_get_u8_length_prefixed(&cbs, &candidate)) {
return false;
}
- if (client_protocol_name == protocol) {
+ if (candidate == protocol) {
return true;
}
}
diff --git a/ssl/internal.h b/ssl/internal.h
index a340335..5744dfe 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -2324,6 +2324,11 @@ bool ssl_is_valid_alpn_list(Span<const uint8_t> in);
bool ssl_is_alpn_protocol_allowed(const SSL_HANDSHAKE *hs,
Span<const uint8_t> protocol);
+// ssl_alpn_list_contains_protocol returns whether |list|, a serialized ALPN
+// protocol list, contains |protocol|.
+bool ssl_alpn_list_contains_protocol(Span<const uint8_t> list,
+ Span<const uint8_t> protocol);
+
// ssl_negotiate_alpn negotiates the ALPN extension, if applicable. It returns
// true on successful negotiation or if nothing was negotiated. It returns false
// and sets |*out_alert| to an alert on error.
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index ec0ee89..278c7a9 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -2286,34 +2286,49 @@ int SSL_CTX_set_tlsext_servername_arg(SSL_CTX *ctx, void *arg) {
int SSL_select_next_proto(uint8_t **out, uint8_t *out_len, const uint8_t *peer,
unsigned peer_len, const uint8_t *supported,
unsigned supported_len) {
- const uint8_t *result;
- int status;
+ *out = nullptr;
+ *out_len = 0;
+
+ // Both |peer| and |supported| must be valid protocol lists, but |peer| may be
+ // empty in NPN.
+ auto peer_span = MakeConstSpan(peer, peer_len);
+ auto supported_span = MakeConstSpan(supported, supported_len);
+ if ((!peer_span.empty() && !ssl_is_valid_alpn_list(peer_span)) ||
+ !ssl_is_valid_alpn_list(supported_span)) {
+ return OPENSSL_NPN_NO_OVERLAP;
+ }
// For each protocol in peer preference order, see if we support it.
- for (unsigned i = 0; i < peer_len;) {
- for (unsigned j = 0; j < supported_len;) {
- if (peer[i] == supported[j] &&
- OPENSSL_memcmp(&peer[i + 1], &supported[j + 1], peer[i]) == 0) {
- // We found a match
- result = &peer[i];
- status = OPENSSL_NPN_NEGOTIATED;
- goto found;
- }
- j += supported[j];
- j++;
+ CBS cbs = peer_span, proto;
+ while (CBS_len(&cbs) != 0) {
+ if (!CBS_get_u8_length_prefixed(&cbs, &proto) || CBS_len(&proto) == 0) {
+ return OPENSSL_NPN_NO_OVERLAP;
+ }
+
+ if (ssl_alpn_list_contains_protocol(MakeConstSpan(supported, supported_len),
+ proto)) {
+ // This function is not const-correct for compatibility with existing
+ // callers.
+ *out = const_cast<uint8_t *>(CBS_data(&proto));
+ // A u8 length prefix will fit in |uint8_t|.
+ *out_len = static_cast<uint8_t>(CBS_len(&proto));
+ return OPENSSL_NPN_NEGOTIATED;
}
- i += peer[i];
- i++;
}
- // There's no overlap between our protocols and the peer's list.
- result = supported;
- status = OPENSSL_NPN_NO_OVERLAP;
+ // There's no overlap between our protocols and the peer's list. In ALPN, the
+ // caller is expected to fail the connection with no_application_protocol. In
+ // NPN, the caller is expected to opportunistically select the first protocol.
+ // See draft-agl-tls-nextprotoneg-04, section 6.
+ cbs = supported_span;
+ if (!CBS_get_u8_length_prefixed(&cbs, &proto) || CBS_len(&proto) == 0) {
+ return OPENSSL_NPN_NO_OVERLAP;
+ }
-found:
- *out = (uint8_t *)result + 1;
- *out_len = result[0];
- return status;
+ // See above.
+ *out = const_cast<uint8_t *>(CBS_data(&proto));
+ *out_len = static_cast<uint8_t>(CBS_len(&proto));
+ return OPENSSL_NPN_NO_OVERLAP;
}
void SSL_get0_next_proto_negotiated(const SSL *ssl, const uint8_t **out_data,
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 3cb4998..c344277 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -5335,17 +5335,52 @@ TEST(SSLTest, SelectNextProto) {
(const uint8_t *)"\3ccc\2bb\1a", 9));
EXPECT_EQ(Bytes("a"), Bytes(result, result_len));
- // If there is no overlap, return the first local protocol.
+ // If there is no overlap, opportunistically select the first local protocol.
+ // ALPN callers should ignore this, but NPN callers may use this per
+ // draft-agl-tls-nextprotoneg-03, section 6.
EXPECT_EQ(OPENSSL_NPN_NO_OVERLAP,
SSL_select_next_proto(&result, &result_len,
(const uint8_t *)"\1a\2bb\3ccc", 9,
(const uint8_t *)"\1x\2yy\3zzz", 9));
EXPECT_EQ(Bytes("x"), Bytes(result, result_len));
+ // The peer preference order may be empty in NPN. This should be treated as no
+ // overlap and continue to select an opportunistic protocol.
EXPECT_EQ(OPENSSL_NPN_NO_OVERLAP,
SSL_select_next_proto(&result, &result_len, nullptr, 0,
(const uint8_t *)"\1x\2yy\3zzz", 9));
EXPECT_EQ(Bytes("x"), Bytes(result, result_len));
+
+ // Although calling this function with no local protocols is a caller error,
+ // it should cleanly return an empty protocol.
+ EXPECT_EQ(
+ OPENSSL_NPN_NO_OVERLAP,
+ SSL_select_next_proto(&result, &result_len,
+ (const uint8_t *)"\1a\2bb\3ccc", 9, nullptr, 0));
+ EXPECT_EQ(Bytes(""), Bytes(result, result_len));
+
+ // Syntax errors are similarly caller errors.
+ EXPECT_EQ(
+ OPENSSL_NPN_NO_OVERLAP,
+ SSL_select_next_proto(&result, &result_len, (const uint8_t *)"\4aaa", 4,
+ (const uint8_t *)"\1a\2bb\3ccc", 9));
+ EXPECT_EQ(Bytes(""), Bytes(result, result_len));
+ EXPECT_EQ(OPENSSL_NPN_NO_OVERLAP,
+ SSL_select_next_proto(&result, &result_len,
+ (const uint8_t *)"\1a\2bb\3ccc", 9,
+ (const uint8_t *)"\4aaa", 4));
+ EXPECT_EQ(Bytes(""), Bytes(result, result_len));
+
+ // Protocols in protocol lists may not be empty.
+ EXPECT_EQ(OPENSSL_NPN_NO_OVERLAP,
+ SSL_select_next_proto(&result, &result_len,
+ (const uint8_t *)"\0\2bb\3ccc", 8,
+ (const uint8_t *)"\1a\2bb\3ccc", 9));
+ EXPECT_EQ(OPENSSL_NPN_NO_OVERLAP,
+ SSL_select_next_proto(&result, &result_len,
+ (const uint8_t *)"\1a\2bb\3ccc", 9,
+ (const uint8_t *)"\0\2bb\3ccc", 8));
+ EXPECT_EQ(Bytes(""), Bytes(result, result_len));
}
// The client should gracefully handle no suitable ciphers being enabled.