Implement draft-vvv-tls-alps-01.
(Original CL by svaldez, reworked by davidben.)
Change-Id: I8570808fa5e96a1c9e6e03c4877039a22e73254f
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/42404
Reviewed-by: Steven Valdez <svaldez@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/ssl/handoff.cc b/ssl/handoff.cc
index 977fcc5..16cbdf7 100644
--- a/ssl/handoff.cc
+++ b/ssl/handoff.cc
@@ -24,6 +24,8 @@
constexpr int kHandoffVersion = 0;
constexpr int kHandbackVersion = 0;
+static const unsigned kHandoffTagALPS = CBS_ASN1_CONTEXT_SPECIFIC | 0;
+
// early_data_t represents the state of early data in a more compact way than
// the 3 bits used by the implementation.
enum early_data_t {
@@ -57,6 +59,16 @@
return false;
}
}
+ // ALPS is a draft protocol and may change over time. The handoff structure
+ // contains a [0] IMPLICIT OCTET STRING OPTIONAL, containing a list of u16
+ // ALPS versions that the binary supports. For now we name them by codepoint.
+ // Once ALPS is finalized and past the support horizon, this field can be
+ // removed.
+ CBB alps;
+ if (!CBB_add_asn1(out, &alps, kHandoffTagALPS) ||
+ !CBB_add_u16(&alps, TLSEXT_TYPE_application_settings)) {
+ return false;
+ }
return CBB_flush(out);
}
@@ -189,6 +201,29 @@
new_configured_curves.Shrink(idx);
ssl->config->supported_group_list = std::move(new_configured_curves);
+ CBS alps;
+ CBS_init(&alps, nullptr, 0);
+ if (!CBS_get_optional_asn1(in, &alps, /*out_present=*/nullptr,
+ kHandoffTagALPS)) {
+ return false;
+ }
+ bool supports_alps = false;
+ while (CBS_len(&alps) != 0) {
+ uint16_t id;
+ if (!CBS_get_u16(&alps, &id)) {
+ return false;
+ }
+ // For now, we only support one ALPS code point, so we only need to extract
+ // a boolean signal from the feature list.
+ if (id == TLSEXT_TYPE_application_settings) {
+ supports_alps = true;
+ break;
+ }
+ }
+ if (!supports_alps) {
+ ssl->config->alps_configs.clear();
+ }
+
return true;
}
diff --git a/ssl/internal.h b/ssl/internal.h
index 9dd206e..7420f65 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -389,6 +389,11 @@
T *end() { return array_.data() + size_; }
const T *cend() const { return array_.data() + size_; }
+ void clear() {
+ size_ = 0;
+ array_.Reset();
+ }
+
// Push adds |elem| at the end of the internal array, growing if necessary. It
// returns false when allocation fails.
bool Push(T elem) {
@@ -1482,6 +1487,7 @@
state13_send_half_rtt_ticket,
state13_read_second_client_flight,
state13_process_end_of_early_data,
+ state13_read_client_encrypted_extensions,
state13_read_client_certificate,
state13_read_client_certificate_verify,
state13_read_channel_id,
@@ -1918,6 +1924,12 @@
bool ssl_negotiate_alpn(SSL_HANDSHAKE *hs, uint8_t *out_alert,
const SSL_CLIENT_HELLO *client_hello);
+// ssl_negotiate_alps negotiates the ALPS 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.
+bool ssl_negotiate_alps(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+ const SSL_CLIENT_HELLO *client_hello);
+
struct SSL_EXTENSION_TYPE {
uint16_t type;
bool *out_present;
@@ -2624,6 +2636,12 @@
unsigned timeout_duration_ms = 0;
};
+// An ALPSConfig is a pair of ALPN protocol and settings value to use with ALPS.
+struct ALPSConfig {
+ Array<uint8_t> protocol;
+ Array<uint8_t> settings;
+};
+
// SSL_CONFIG contains configuration bits that can be shed after the handshake
// completes. Objects of this type are not shared; they are unique to a
// particular |SSL|.
@@ -2690,6 +2708,10 @@
// format.
Array<uint8_t> alpn_client_proto_list;
+ // alps_configs contains the list of supported protocols to use with ALPS,
+ // along with their corresponding ALPS values.
+ GrowableArray<ALPSConfig> alps_configs;
+
// Contains a list of supported Token Binding key parameters.
Array<uint8_t> token_binding_params;
@@ -3543,9 +3565,18 @@
// early_alpn is the ALPN protocol from the initial handshake. This is only
// stored for TLS 1.3 and above in order to enforce ALPN matching for 0-RTT
- // resumptions.
+ // resumptions. For the current connection's ALPN protocol, see
+ // |alpn_selected| on |SSL3_STATE|.
bssl::Array<uint8_t> early_alpn;
+ // local_application_settings, if |has_application_settings| is true, is the
+ // local ALPS value for this connection.
+ bssl::Array<uint8_t> local_application_settings;
+
+ // peer_application_settings, if |has_application_settings| is true, is the
+ // peer ALPS value for this connection.
+ bssl::Array<uint8_t> peer_application_settings;
+
// extended_master_secret is whether the master secret in this session was
// generated using EMS and thus isn't vulnerable to the Triple Handshake
// attack.
@@ -3566,6 +3597,10 @@
// is_quic indicates whether this session was created using QUIC.
bool is_quic : 1;
+ // has_application_settings indicates whether ALPS was negotiated in this
+ // session.
+ bool has_application_settings : 1;
+
// quic_early_data_context is used to determine whether early data must be
// rejected when performing a QUIC handshake.
bssl::Array<uint8_t> quic_early_data_context;
diff --git a/ssl/ssl_asn1.cc b/ssl/ssl_asn1.cc
index e6274f1..0e91308 100644
--- a/ssl/ssl_asn1.cc
+++ b/ssl/ssl_asn1.cc
@@ -131,6 +131,10 @@
// earlyALPN [26] OCTET STRING OPTIONAL,
// isQuic [27] BOOLEAN OPTIONAL,
// quicEarlyDataHash [28] OCTET STRING OPTIONAL,
+// localALPS [29] OCTET STRING OPTIONAL,
+// peerALPS [30] OCTET STRING OPTIONAL,
+// -- Either both or none of localALPS and peerALPS must be present. If both
+// -- are present, earlyALPN must be present and non-empty.
// }
//
// Note: historically this serialization has included other optional
@@ -194,6 +198,10 @@
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 27;
static const unsigned kQuicEarlyDataContextTag =
CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 28;
+static const unsigned kLocalALPSTag =
+ CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 29;
+static const unsigned kPeerALPSTag =
+ CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 30;
static int SSL_SESSION_to_bytes_full(const SSL_SESSION *in, CBB *cbb,
int for_ticket) {
@@ -411,6 +419,19 @@
}
}
+ if (in->has_application_settings) {
+ if (!CBB_add_asn1(&session, &child, kLocalALPSTag) ||
+ !CBB_add_asn1_octet_string(&child,
+ in->local_application_settings.data(),
+ in->local_application_settings.size()) ||
+ !CBB_add_asn1(&session, &child, kPeerALPSTag) ||
+ !CBB_add_asn1_octet_string(&child, in->peer_application_settings.data(),
+ in->peer_application_settings.size())) {
+ OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+ return 0;
+ }
+ }
+
return CBB_flush(cbb);
}
@@ -753,13 +774,33 @@
!CBS_get_optional_asn1_bool(&session, &is_quic, kIsQuicTag,
/*default_value=*/false) ||
!SSL_SESSION_parse_octet_string(&session, &ret->quic_early_data_context,
- kQuicEarlyDataContextTag) ||
+ kQuicEarlyDataContextTag)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
+ return nullptr;
+ }
+
+ CBS settings;
+ int has_local_alps, has_peer_alps;
+ if (!CBS_get_optional_asn1_octet_string(&session, &settings, &has_local_alps,
+ kLocalALPSTag) ||
+ !ret->local_application_settings.CopyFrom(settings) ||
+ !CBS_get_optional_asn1_octet_string(&session, &settings, &has_peer_alps,
+ kPeerALPSTag) ||
+ !ret->peer_application_settings.CopyFrom(settings) ||
CBS_len(&session) != 0) {
OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
return nullptr;
}
ret->is_quic = is_quic;
+ // The two ALPS values and ALPN must be consistent.
+ if (has_local_alps != has_peer_alps ||
+ (has_local_alps && ret->early_alpn.empty())) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
+ return nullptr;
+ }
+ ret->has_application_settings = has_local_alps;
+
if (!x509_method->session_cache_objects(ret.get())) {
OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
return nullptr;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 10a97ea..33b9f2f 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -2241,6 +2241,36 @@
ctx->allow_unknown_alpn_protos = !!enabled;
}
+int SSL_add_application_settings(SSL *ssl, const uint8_t *proto,
+ size_t proto_len, const uint8_t *settings,
+ size_t settings_len) {
+ if (!ssl->config) {
+ return 0;
+ }
+ ALPSConfig config;
+ if (!config.protocol.CopyFrom(MakeConstSpan(proto, proto_len)) ||
+ !config.settings.CopyFrom(MakeConstSpan(settings, settings_len)) ||
+ !ssl->config->alps_configs.Push(std::move(config))) {
+ return 0;
+ }
+ return 1;
+}
+
+void SSL_get0_peer_application_settings(const SSL *ssl,
+ const uint8_t **out_data,
+ size_t *out_len) {
+ const SSL_SESSION *session = SSL_get_session(ssl);
+ Span<const uint8_t> settings =
+ session ? session->peer_application_settings : Span<const uint8_t>();
+ *out_data = settings.data();
+ *out_len = settings.size();
+}
+
+int SSL_has_application_settings(const SSL *ssl) {
+ const SSL_SESSION *session = SSL_get_session(ssl);
+ return session && session->has_application_settings;
+}
+
int SSL_CTX_add_cert_compression_alg(SSL_CTX *ctx, uint16_t alg_id,
ssl_cert_compression_func_t compress,
ssl_cert_decompression_func_t decompress) {
diff --git a/ssl/ssl_session.cc b/ssl/ssl_session.cc
index ef902f0..7538a72 100644
--- a/ssl/ssl_session.cc
+++ b/ssl/ssl_session.cc
@@ -264,13 +264,15 @@
new_session->ticket_age_add = session->ticket_age_add;
new_session->ticket_max_early_data = session->ticket_max_early_data;
new_session->extended_master_secret = session->extended_master_secret;
+ new_session->has_application_settings = session->has_application_settings;
- if (!new_session->early_alpn.CopyFrom(session->early_alpn)) {
- return nullptr;
- }
-
- if (!new_session->quic_early_data_context.CopyFrom(
- session->quic_early_data_context)) {
+ if (!new_session->early_alpn.CopyFrom(session->early_alpn) ||
+ !new_session->quic_early_data_context.CopyFrom(
+ session->quic_early_data_context) ||
+ !new_session->local_application_settings.CopyFrom(
+ session->local_application_settings) ||
+ !new_session->peer_application_settings.CopyFrom(
+ session->peer_application_settings)) {
return nullptr;
}
}
@@ -864,7 +866,8 @@
not_resumable(false),
ticket_age_add_valid(false),
is_server(false),
- is_quic(false) {
+ is_quic(false),
+ has_application_settings(false) {
CRYPTO_new_ex_data(&ex_data);
time = ::time(nullptr);
}
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 6584686..e62d6e2 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -754,7 +754,7 @@
"NusdVm/K2rxzY5Dkf3s+Iss9B+1fOHSc4wNQTqGvmO5h8oQ/Eg==";
// kBadSessionExtraField is a custom serialized SSL_SESSION generated by replacing
-// the final (optional) element of |kCustomSession| with tag number 30.
+// the final (optional) element of |kCustomSession| with tag number 99.
static const char kBadSessionExtraField[] =
"MIIBdgIBAQICAwMEAsAvBCAG5Q1ndq4Yfmbeo1zwLkNRKmCXGdNgWvGT3cskV0yQ"
"kAQwJlrlzkAWBOWiLj/jJ76D7l+UXoizP2KI2C7I2FccqMmIfFmmkUy32nIJ0mZH"
@@ -763,7 +763,7 @@
"LwjcDTpsuh3qXEaZ992r1N38VDcyS6P7I6HBYN9BsNHM362zZnY27GpTw+Kwd751"
"CLoXFPoaMOe57dbBpXoro6Pd3BTbf/Tzr88K06yEOTDKPNj3+inbMaVigtK4PLyP"
"q+Topyzvx9USFgRvyuoxn0Hgb+R0A3j6SLRuyOdAi4gv7Y5oliynrSIEIAYGBgYG"
- "BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGrgMEAQevAwQBBL4DBAEF";
+ "BgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGrgMEAQevAwQBBOMDBAEF";
// kBadSessionVersion is a custom serialized SSL_SESSION generated by replacing
// the version of |kCustomSession| with 2.
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 4a2bbcf..e48f1a1 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -125,13 +125,14 @@
#include <openssl/nid.h>
#include <openssl/rand.h>
-#include "internal.h"
#include "../crypto/internal.h"
+#include "internal.h"
BSSL_NAMESPACE_BEGIN
static bool ssl_check_clienthello_tlsext(SSL_HANDSHAKE *hs);
+static bool ssl_check_serverhello_tlsext(SSL_HANDSHAKE *hs);
static int compare_uint16_t(const void *p1, const void *p2) {
uint16_t u1 = *((const uint16_t *)p1);
@@ -512,7 +513,7 @@
};
static bool forbid_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
- CBS *contents) {
+ CBS *contents) {
if (contents != NULL) {
// Servers MUST NOT send this extension.
*out_alert = SSL_AD_UNSUPPORTED_EXTENSION;
@@ -524,7 +525,7 @@
}
static bool ignore_parse_clienthello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
- CBS *contents) {
+ CBS *contents) {
// This extension from the client is handled elsewhere.
return true;
}
@@ -1380,7 +1381,6 @@
CBS protocol_name_list_copy = protocol_name_list;
while (CBS_len(&protocol_name_list_copy) > 0) {
CBS protocol_name;
-
if (!CBS_get_u8_length_prefixed(&protocol_name_list_copy, &protocol_name) ||
// Empty protocol names are forbidden.
CBS_len(&protocol_name) == 0) {
@@ -1946,6 +1946,21 @@
//
// https://tools.ietf.org/html/rfc8446#section-4.2.10
+// ssl_get_local_application_settings looks up the configured ALPS value for
+// |protocol|. If found, it sets |*out_settings| to the value and returns true.
+// Otherwise, it returns false.
+static bool ssl_get_local_application_settings(
+ const SSL_HANDSHAKE *hs, Span<const uint8_t> *out_settings,
+ Span<const uint8_t> protocol) {
+ for (const ALPSConfig &config : hs->config->alps_configs) {
+ if (protocol == config.protocol) {
+ *out_settings = config.settings;
+ return true;
+ }
+ }
+ return false;
+}
+
static bool ext_early_data_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
SSL *const ssl = hs->ssl;
// The second ClientHello never offers early data, and we must have already
@@ -1978,13 +1993,22 @@
return true;
}
- // In case ALPN preferences changed since this session was established, avoid
- // reporting a confusing value in |SSL_get0_alpn_selected| and sending early
- // data we know will be rejected.
- if (!ssl->session->early_alpn.empty() &&
- !ssl_is_alpn_protocol_allowed(hs, ssl->session->early_alpn)) {
- ssl->s3->early_data_reason = ssl_early_data_alpn_mismatch;
- return true;
+ if (!ssl->session->early_alpn.empty()) {
+ if (!ssl_is_alpn_protocol_allowed(hs, ssl->session->early_alpn)) {
+ // Avoid reporting a confusing value in |SSL_get0_alpn_selected|.
+ ssl->s3->early_data_reason = ssl_early_data_alpn_mismatch;
+ return true;
+ }
+
+ Span<const uint8_t> settings;
+ bool has_alps = ssl_get_local_application_settings(
+ hs, &settings, ssl->session->early_alpn);
+ if (has_alps != ssl->session->has_application_settings ||
+ settings != ssl->session->local_application_settings) {
+ // 0-RTT carries ALPS over, so we only offer it when the value matches.
+ ssl->s3->early_data_reason = ssl_early_data_alps_mismatch;
+ return true;
+ }
}
// |early_data_reason| will be filled in later when the server responds.
@@ -2797,6 +2821,144 @@
return true;
}
+// Application-level Protocol Settings
+//
+// https://tools.ietf.org/html/draft-vvv-tls-alps-01
+
+static bool ext_alps_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
+ SSL *const ssl = hs->ssl;
+ if (// ALPS requires TLS 1.3.
+ hs->max_version < TLS1_3_VERSION ||
+ // Do not offer ALPS without ALPN.
+ hs->config->alpn_client_proto_list.empty() ||
+ // Do not offer ALPS if not configured.
+ hs->config->alps_configs.empty() ||
+ // Do not offer ALPS on renegotiation handshakes.
+ ssl->s3->initial_handshake_complete) {
+ return true;
+ }
+
+ CBB contents, proto_list, proto;
+ if (!CBB_add_u16(out, TLSEXT_TYPE_application_settings) ||
+ !CBB_add_u16_length_prefixed(out, &contents) ||
+ !CBB_add_u16_length_prefixed(&contents, &proto_list)) {
+ return false;
+ }
+
+ for (const ALPSConfig &config : hs->config->alps_configs) {
+ if (!CBB_add_u8_length_prefixed(&proto_list, &proto) ||
+ !CBB_add_bytes(&proto, config.protocol.data(),
+ config.protocol.size())) {
+ return false;
+ }
+ }
+
+ return CBB_flush(out);
+}
+
+static bool ext_alps_parse_serverhello(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+ CBS *contents) {
+ SSL *const ssl = hs->ssl;
+ if (contents == nullptr) {
+ return true;
+ }
+
+ assert(!ssl->s3->initial_handshake_complete);
+ assert(!hs->config->alpn_client_proto_list.empty());
+ assert(!hs->config->alps_configs.empty());
+
+ // ALPS requires TLS 1.3.
+ if (ssl_protocol_version(ssl) < TLS1_3_VERSION) {
+ *out_alert = SSL_AD_UNSUPPORTED_EXTENSION;
+ OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+ return false;
+ }
+
+ // Note extension callbacks may run in any order, so we defer checking
+ // consistency with ALPN to |ssl_check_serverhello_tlsext|.
+ if (!hs->new_session->peer_application_settings.CopyFrom(*contents)) {
+ *out_alert = SSL_AD_INTERNAL_ERROR;
+ return false;
+ }
+
+ hs->new_session->has_application_settings = true;
+ return true;
+}
+
+static bool ext_alps_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+ SSL *const ssl = hs->ssl;
+ // If early data is accepted, we omit the ALPS extension. It is implicitly
+ // carried over from the previous connection.
+ if (hs->new_session == nullptr ||
+ !hs->new_session->has_application_settings ||
+ ssl->s3->early_data_accepted) {
+ return true;
+ }
+
+ CBB contents;
+ if (!CBB_add_u16(out, TLSEXT_TYPE_application_settings) ||
+ !CBB_add_u16_length_prefixed(out, &contents) ||
+ !CBB_add_bytes(&contents,
+ hs->new_session->local_application_settings.data(),
+ hs->new_session->local_application_settings.size()) ||
+ !CBB_flush(out)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool ssl_negotiate_alps(SSL_HANDSHAKE *hs, uint8_t *out_alert,
+ const SSL_CLIENT_HELLO *client_hello) {
+ SSL *const ssl = hs->ssl;
+ if (ssl->s3->alpn_selected.empty()) {
+ return true;
+ }
+
+ // If we negotiate ALPN over TLS 1.3, try to negotiate ALPS.
+ CBS alps_contents;
+ Span<const uint8_t> settings;
+ if (ssl_protocol_version(ssl) >= TLS1_3_VERSION &&
+ ssl_get_local_application_settings(hs, &settings,
+ ssl->s3->alpn_selected) &&
+ ssl_client_hello_get_extension(client_hello, &alps_contents,
+ TLSEXT_TYPE_application_settings)) {
+ // Check if the client supports ALPS with the selected ALPN.
+ bool found = false;
+ CBS alps_list;
+ if (!CBS_get_u16_length_prefixed(&alps_contents, &alps_list) ||
+ CBS_len(&alps_contents) != 0 ||
+ CBS_len(&alps_list) == 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ *out_alert = SSL_AD_DECODE_ERROR;
+ return false;
+ }
+ while (CBS_len(&alps_list) > 0) {
+ CBS protocol_name;
+ if (!CBS_get_u8_length_prefixed(&alps_list, &protocol_name) ||
+ // Empty protocol names are forbidden.
+ CBS_len(&protocol_name) == 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ *out_alert = SSL_AD_DECODE_ERROR;
+ return false;
+ }
+ if (protocol_name == MakeConstSpan(ssl->s3->alpn_selected)) {
+ found = true;
+ }
+ }
+
+ // Negotiate ALPS if both client also supports ALPS for this protocol.
+ if (found) {
+ hs->new_session->has_application_settings = true;
+ if (!hs->new_session->local_application_settings.CopyFrom(settings)) {
+ *out_alert = SSL_AD_INTERNAL_ERROR;
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
// kExtensions contains all the supported extensions.
static const struct tls_extension kExtensions[] = {
@@ -2978,6 +3140,15 @@
ext_delegated_credential_parse_clienthello,
dont_add_serverhello,
},
+ {
+ TLSEXT_TYPE_application_settings,
+ NULL,
+ ext_alps_add_clienthello,
+ ext_alps_parse_serverhello,
+ // ALPS is negotiated late in |ssl_negotiate_alpn|.
+ ignore_parse_clienthello,
+ ext_alps_add_serverhello,
+ },
};
#define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))
@@ -3370,6 +3541,36 @@
}
}
+static bool ssl_check_serverhello_tlsext(SSL_HANDSHAKE *hs) {
+ SSL *const ssl = hs->ssl;
+ // ALPS and ALPN have a dependency between each other, so we defer checking
+ // consistency to after the callbacks run.
+ if (hs->new_session != nullptr && hs->new_session->has_application_settings) {
+ // ALPN must be negotiated.
+ if (ssl->s3->alpn_selected.empty()) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_NEGOTIATED_ALPS_WITHOUT_ALPN);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+ return false;
+ }
+
+ // The negotiated protocol must be one of the ones we advertised for ALPS.
+ Span<const uint8_t> settings;
+ if (!ssl_get_local_application_settings(hs, &settings,
+ ssl->s3->alpn_selected)) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_ALPN_PROTOCOL);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+ return false;
+ }
+
+ if (!hs->new_session->local_application_settings.CopyFrom(settings)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+ return false;
+ }
+ }
+
+ return true;
+}
+
bool ssl_parse_serverhello_tlsext(SSL_HANDSHAKE *hs, CBS *cbs) {
SSL *const ssl = hs->ssl;
int alert = SSL_AD_DECODE_ERROR;
@@ -3378,6 +3579,10 @@
return false;
}
+ if (!ssl_check_serverhello_tlsext(hs)) {
+ return false;
+ }
+
return true;
}
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index ebdfeaf..3df861b 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -397,6 +397,11 @@
}
static const char *EarlyDataReasonToString(ssl_early_data_reason_t reason) {
+ if (reason > ssl_early_data_reason_max_value) {
+ fprintf(stderr, "ssl_early_data_reason_max_value is out of date.\n");
+ abort();
+ }
+
switch (reason) {
case ssl_early_data_unknown:
return "unknown";
@@ -426,8 +431,12 @@
return "ticket_age_skew";
case ssl_early_data_quic_parameter_mismatch:
return "quic_parameter_mismatch";
+ case ssl_early_data_alps_mismatch:
+ return "alps_mismatch";
}
+ fprintf(stderr, "Unknown ssl_early_data_reason_t value %d.\n",
+ static_cast<int>(reason));
abort();
}
@@ -538,6 +547,26 @@
return false;
}
+ if (SSL_has_application_settings(ssl) !=
+ (config->expect_peer_application_settings ? 1 : 0)) {
+ fprintf(stderr,
+ "connection %s application settings, but expected the opposite\n",
+ SSL_has_application_settings(ssl) ? "has" : "does not have");
+ return false;
+ }
+ std::string expect_settings = config->expect_peer_application_settings
+ ? *config->expect_peer_application_settings
+ : "";
+ const uint8_t *peer_settings;
+ size_t peer_settings_len;
+ SSL_get0_peer_application_settings(ssl, &peer_settings, &peer_settings_len);
+ if (expect_settings !=
+ std::string(reinterpret_cast<const char *>(peer_settings),
+ peer_settings_len)) {
+ fprintf(stderr, "peer application settings mismatch\n");
+ return false;
+ }
+
if (!config->expect_quic_transport_params.empty() && expect_handshake_done) {
const uint8_t *peer_params;
size_t peer_params_len;
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index b7517e7..8a934b3 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -121,6 +121,7 @@
extensionKeyShare uint16 = 51
extensionCustom uint16 = 1234 // not IANA assigned
extensionNextProtoNeg uint16 = 13172 // not IANA assigned
+ extensionApplicationSettings uint16 = 17513 // not IANA assigned
extensionRenegotiationInfo uint16 = 0xff01
extensionQUICTransportParams uint16 = 0xffa5 // draft-ietf-quic-tls-13
extensionChannelID uint16 = 30032 // not IANA assigned
@@ -260,6 +261,8 @@
PeerSignatureAlgorithm signatureAlgorithm // algorithm used by the peer in the handshake
CurveID CurveID // the curve used in ECDHE
QUICTransportParams []byte // the QUIC transport params received from the peer
+ HasApplicationSettings bool // whether ALPS was negotiated
+ PeerApplicationSettings []byte // application settings received from the peer
}
// ClientAuthType declares the policy the server will follow for
@@ -277,22 +280,25 @@
// ClientSessionState contains the state needed by clients to resume TLS
// sessions.
type ClientSessionState struct {
- sessionId []uint8 // Session ID supplied by the server. nil if the session has a ticket.
- sessionTicket []uint8 // Encrypted ticket used for session resumption with server
- vers uint16 // SSL/TLS version negotiated for the session
- wireVersion uint16 // Wire SSL/TLS version negotiated for the session
- cipherSuite uint16 // Ciphersuite negotiated for the session
- masterSecret []byte // MasterSecret generated by client on a full handshake
- handshakeHash []byte // Handshake hash for Channel ID purposes.
- serverCertificates []*x509.Certificate // Certificate chain presented by the server
- extendedMasterSecret bool // Whether an extended master secret was used to generate the session
- sctList []byte
- ocspResponse []byte
- earlyALPN string
- ticketCreationTime time.Time
- ticketExpiration time.Time
- ticketAgeAdd uint32
- maxEarlyDataSize uint32
+ sessionId []uint8 // Session ID supplied by the server. nil if the session has a ticket.
+ sessionTicket []uint8 // Encrypted ticket used for session resumption with server
+ vers uint16 // SSL/TLS version negotiated for the session
+ wireVersion uint16 // Wire SSL/TLS version negotiated for the session
+ cipherSuite uint16 // Ciphersuite negotiated for the session
+ masterSecret []byte // MasterSecret generated by client on a full handshake
+ handshakeHash []byte // Handshake hash for Channel ID purposes.
+ serverCertificates []*x509.Certificate // Certificate chain presented by the server
+ extendedMasterSecret bool // Whether an extended master secret was used to generate the session
+ sctList []byte
+ ocspResponse []byte
+ earlyALPN string
+ ticketCreationTime time.Time
+ ticketExpiration time.Time
+ ticketAgeAdd uint32
+ maxEarlyDataSize uint32
+ hasApplicationSettings bool
+ localApplicationSettings []byte
+ peerApplicationSettings []byte
}
// ClientSessionCache is a cache of ClientSessionState objects that can be used
@@ -367,6 +373,10 @@
// NextProtos is a list of supported, application level protocols.
NextProtos []string
+ // ApplicationSettings is a set of application settings to use which each
+ // application protocol.
+ ApplicationSettings map[string][]byte
+
// ServerName is used to verify the hostname on the returned
// certificates unless InsecureSkipVerify is given. It is also included
// in the client's handshake to support virtual hosting.
@@ -792,6 +802,33 @@
// return.
ALPNProtocol *string
+ // AlwaysNegotiateApplicationSettings, if true, causes the server to
+ // negotiate ALPS for a protocol even if the client did not support it or
+ // the version is wrong.
+ AlwaysNegotiateApplicationSettings bool
+
+ // SendApplicationSettingsWithEarlyData, if true, causes the client and
+ // server to send the application_settings extension with early data,
+ // rather than letting them implicitly carry over.
+ SendApplicationSettingsWithEarlyData bool
+
+ // AlwaysSendClientEncryptedExtension, if true, causes the client to always
+ // send a, possibly empty, client EncryptedExtensions message.
+ AlwaysSendClientEncryptedExtensions bool
+
+ // OmitClientEncryptedExtensions, if true, causes the client to omit the
+ // client EncryptedExtensions message.
+ OmitClientEncryptedExtensions bool
+
+ // OmitClientApplicationSettings, if true, causes the client to omit the
+ // application_settings extension but still send EncryptedExtensions.
+ OmitClientApplicationSettings bool
+
+ // SendExtraClientEncryptedExtension, if true, causes the client to
+ // include an unsolicited extension in the client EncryptedExtensions
+ // message.
+ SendExtraClientEncryptedExtension bool
+
// AcceptAnySession causes the server to resume sessions regardless of
// the version associated with the session or cipher suite. It also
// causes the server to look in both TLS 1.2 and 1.3 extensions to
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 04fe16c..c0c91d2 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -73,6 +73,9 @@
clientProtocolFallback bool
usedALPN bool
+ localApplicationSettings, peerApplicationSettings []byte
+ hasApplicationSettings bool
+
// verify_data values for the renegotiation extension.
clientVerify []byte
serverVerify []byte
@@ -1390,7 +1393,11 @@
isDTLS: c.isDTLS,
}
case typeEncryptedExtensions:
- m = new(encryptedExtensionsMsg)
+ if c.isClient {
+ m = new(encryptedExtensionsMsg)
+ } else {
+ m = new(clientEncryptedExtensionsMsg)
+ }
case typeCertificate:
m = &certificateMsg{
hasRequestContext: c.vers >= VersionTLS13,
@@ -1608,19 +1615,22 @@
}
session := &ClientSessionState{
- sessionTicket: newSessionTicket.ticket,
- vers: c.vers,
- wireVersion: c.wireVersion,
- cipherSuite: cipherSuite.id,
- masterSecret: c.resumptionSecret,
- serverCertificates: c.peerCertificates,
- sctList: c.sctList,
- ocspResponse: c.ocspResponse,
- ticketCreationTime: c.config.time(),
- ticketExpiration: c.config.time().Add(time.Duration(newSessionTicket.ticketLifetime) * time.Second),
- ticketAgeAdd: newSessionTicket.ticketAgeAdd,
- maxEarlyDataSize: newSessionTicket.maxEarlyDataSize,
- earlyALPN: c.clientProtocol,
+ sessionTicket: newSessionTicket.ticket,
+ vers: c.vers,
+ wireVersion: c.wireVersion,
+ cipherSuite: cipherSuite.id,
+ masterSecret: c.resumptionSecret,
+ serverCertificates: c.peerCertificates,
+ sctList: c.sctList,
+ ocspResponse: c.ocspResponse,
+ ticketCreationTime: c.config.time(),
+ ticketExpiration: c.config.time().Add(time.Duration(newSessionTicket.ticketLifetime) * time.Second),
+ ticketAgeAdd: newSessionTicket.ticketAgeAdd,
+ maxEarlyDataSize: newSessionTicket.maxEarlyDataSize,
+ earlyALPN: c.clientProtocol,
+ hasApplicationSettings: c.hasApplicationSettings,
+ localApplicationSettings: c.localApplicationSettings,
+ peerApplicationSettings: c.peerApplicationSettings,
}
session.masterSecret = deriveSessionPSK(cipherSuite, c.wireVersion, c.resumptionSecret, newSessionTicket.ticketNonce)
@@ -1883,6 +1893,8 @@
state.PeerSignatureAlgorithm = c.peerSignatureAlgorithm
state.CurveID = c.curveID
state.QUICTransportParams = c.quicTransportParams
+ state.HasApplicationSettings = c.hasApplicationSettings
+ state.PeerApplicationSettings = c.peerApplicationSettings
}
return state
@@ -2007,14 +2019,17 @@
}
state := sessionState{
- vers: c.vers,
- cipherSuite: c.cipherSuite.id,
- masterSecret: deriveSessionPSK(c.cipherSuite, c.wireVersion, c.resumptionSecret, nonce),
- certificates: peerCertificatesRaw,
- ticketCreationTime: c.config.time(),
- ticketExpiration: c.config.time().Add(time.Duration(m.ticketLifetime) * time.Second),
- ticketAgeAdd: uint32(addBuffer[3])<<24 | uint32(addBuffer[2])<<16 | uint32(addBuffer[1])<<8 | uint32(addBuffer[0]),
- earlyALPN: []byte(c.clientProtocol),
+ vers: c.vers,
+ cipherSuite: c.cipherSuite.id,
+ masterSecret: deriveSessionPSK(c.cipherSuite, c.wireVersion, c.resumptionSecret, nonce),
+ certificates: peerCertificatesRaw,
+ ticketCreationTime: c.config.time(),
+ ticketExpiration: c.config.time().Add(time.Duration(m.ticketLifetime) * time.Second),
+ ticketAgeAdd: uint32(addBuffer[3])<<24 | uint32(addBuffer[2])<<16 | uint32(addBuffer[1])<<8 | uint32(addBuffer[0]),
+ earlyALPN: []byte(c.clientProtocol),
+ hasApplicationSettings: c.hasApplicationSettings,
+ localApplicationSettings: c.localApplicationSettings,
+ peerApplicationSettings: c.peerApplicationSettings,
}
if !c.config.Bugs.SendEmptySessionTicket {
diff --git a/ssl/test/runner/fuzzer_mode.json b/ssl/test/runner/fuzzer_mode.json
index 3e03a9b..f2dc3fd 100644
--- a/ssl/test/runner/fuzzer_mode.json
+++ b/ssl/test/runner/fuzzer_mode.json
@@ -47,6 +47,7 @@
"CustomExtensions-Server-EarlyDataOffered": "Trial decryption does not work with the NULL cipher.",
"*-TicketAgeSkew-*-Reject*": "Trial decryption does not work with the NULL cipher.",
"*EarlyDataRejected*": "Trial decryption does not work with the NULL cipher.",
+ "ALPS-EarlyData-Mismatch-*": "Trial decryption does not work with the NULL cipher.",
"Renegotiate-Client-BadExt*": "Fuzzer mode does not check renegotiation_info.",
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 241518f..3ebc070 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -207,6 +207,10 @@
hello.secureRenegotiation = nil
}
+ for protocol, _ := range c.config.ApplicationSettings {
+ hello.alpsProtocols = append(hello.alpsProtocols, protocol)
+ }
+
var keyShares map[CurveID]ecdhCurve
if maxVersion >= VersionTLS13 {
keyShares = make(map[CurveID]ecdhCurve)
@@ -1127,6 +1131,26 @@
c.useOutTrafficSecret(c.wireVersion, hs.suite, clientHandshakeTrafficSecret)
+ // The client EncryptedExtensions message is sent if some extension uses it.
+ // (Currently only ALPS does.)
+ hasEncryptedExtensions := c.config.Bugs.AlwaysSendClientEncryptedExtensions
+ clientEncryptedExtensions := new(clientEncryptedExtensionsMsg)
+ if encryptedExtensions.extensions.hasApplicationSettings || (c.config.Bugs.SendApplicationSettingsWithEarlyData && c.hasApplicationSettings) {
+ hasEncryptedExtensions = true
+ if !c.config.Bugs.OmitClientApplicationSettings {
+ clientEncryptedExtensions.hasApplicationSettings = true
+ clientEncryptedExtensions.applicationSettings = c.localApplicationSettings
+ }
+ }
+ if c.config.Bugs.SendExtraClientEncryptedExtension {
+ hasEncryptedExtensions = true
+ clientEncryptedExtensions.customExtension = []byte{0}
+ }
+ if hasEncryptedExtensions && !c.config.Bugs.OmitClientEncryptedExtensions {
+ hs.writeClientHash(clientEncryptedExtensions.marshal())
+ c.writeRecord(recordTypeHandshake, clientEncryptedExtensions.marshal())
+ }
+
if certReq != nil && !c.config.Bugs.SkipClientCertificate {
certMsg := &certificateMsg{
hasRequestContext: true,
@@ -1695,6 +1719,8 @@
c.sendAlert(alertHandshakeFailure)
return errors.New("tls: server accepted early data when not expected")
}
+ } else if serverExtensions.hasEarlyData {
+ return errors.New("tls: server accepted early data when not resuming")
}
if len(serverExtensions.quicTransportParams) > 0 {
@@ -1705,6 +1731,30 @@
c.quicTransportParams = serverExtensions.quicTransportParams
}
+ if serverExtensions.hasApplicationSettings {
+ if c.vers < VersionTLS13 {
+ return errors.New("tls: server sent application settings at invalid version")
+ }
+ if serverExtensions.hasEarlyData {
+ return errors.New("tls: server sent application settings with 0-RTT")
+ }
+ if !serverHasALPN {
+ return errors.New("tls: server sent application settings without ALPN")
+ }
+ settings, ok := c.config.ApplicationSettings[serverExtensions.alpnProtocol]
+ if !ok {
+ return errors.New("tls: server sent application settings for invalid protocol")
+ }
+ c.hasApplicationSettings = true
+ c.localApplicationSettings = settings
+ c.peerApplicationSettings = serverExtensions.applicationSettings
+ } else if serverExtensions.hasEarlyData {
+ // 0-RTT connections inherit application settings from the session.
+ c.hasApplicationSettings = hs.session.hasApplicationSettings
+ c.localApplicationSettings = hs.session.localApplicationSettings
+ c.peerApplicationSettings = hs.session.peerApplicationSettings
+ }
+
return nil
}
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 4378e77..9164819 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -295,6 +295,7 @@
pad int
compressedCertAlgs []uint16
delegatedCredentials bool
+ alpsProtocols []string
prefixExtensions []uint16
}
@@ -574,6 +575,17 @@
body: body.finish(),
})
}
+ if len(m.alpsProtocols) > 0 {
+ body := newByteBuilder()
+ protocolNameList := body.addU16LengthPrefixed()
+ for _, s := range m.alpsProtocols {
+ protocolNameList.addU8LengthPrefixed().addBytes([]byte(s))
+ }
+ extensions = append(extensions, extension{
+ id: extensionApplicationSettings,
+ body: body.finish(),
+ })
+ }
// The PSK extension must be last. See https://tools.ietf.org/html/rfc8446#section-4.2.11
if len(m.pskIdentities) > 0 {
@@ -731,6 +743,7 @@
m.extendedMasterSecret = false
m.customExtension = ""
m.delegatedCredentials = false
+ m.alpsProtocols = nil
if len(reader) == 0 {
// ClientHello is optionally followed by extension data
@@ -889,7 +902,7 @@
}
for len(protocols) > 0 {
var protocol []byte
- if !protocols.readU8LengthPrefixedBytes(&protocol) {
+ if !protocols.readU8LengthPrefixedBytes(&protocol) || len(protocol) == 0 {
return false
}
m.alpnProtocols = append(m.alpnProtocols, string(protocol))
@@ -966,6 +979,18 @@
return false
}
m.delegatedCredentials = true
+ case extensionApplicationSettings:
+ var protocols byteReader
+ if !body.readU16LengthPrefixed(&protocols) || len(body) != 0 {
+ return false
+ }
+ for len(protocols) > 0 {
+ var protocol []byte
+ if !protocols.readU8LengthPrefixedBytes(&protocol) || len(protocol) == 0 {
+ return false
+ }
+ m.alpsProtocols = append(m.alpsProtocols, string(protocol))
+ }
}
if isGREASEValue(extension) {
@@ -1233,6 +1258,8 @@
supportedCurves []CurveID
quicTransportParams []byte
serverNameAck bool
+ applicationSettings []byte
+ hasApplicationSettings bool
}
func (m *serverExtensions) marshal(extensions *byteBuilder) {
@@ -1367,6 +1394,10 @@
extensions.addU16(extensionServerName)
extensions.addU16(0) // zero length
}
+ if m.hasApplicationSettings {
+ extensions.addU16(extensionApplicationSettings)
+ extensions.addU16LengthPrefixed().addBytes(m.applicationSettings)
+ }
}
func (m *serverExtensions) unmarshal(data byteReader, version uint16) bool {
@@ -1475,6 +1506,9 @@
return false
}
m.hasEarlyData = true
+ case extensionApplicationSettings:
+ m.hasApplicationSettings = true
+ m.applicationSettings = body
default:
// Unknown extensions are illegal from the server.
return false
@@ -1484,6 +1518,68 @@
return true
}
+type clientEncryptedExtensionsMsg struct {
+ raw []byte
+ applicationSettings []byte
+ hasApplicationSettings bool
+ customExtension []byte
+}
+
+func (m *clientEncryptedExtensionsMsg) marshal() (x []byte) {
+ if m.raw != nil {
+ return m.raw
+ }
+
+ builder := newByteBuilder()
+ builder.addU8(typeEncryptedExtensions)
+ body := builder.addU24LengthPrefixed()
+ extensions := body.addU16LengthPrefixed()
+ if m.hasApplicationSettings {
+ extensions.addU16(extensionApplicationSettings)
+ extensions.addU16LengthPrefixed().addBytes(m.applicationSettings)
+ }
+ if len(m.customExtension) > 0 {
+ extensions.addU16(extensionCustom)
+ extensions.addU16LengthPrefixed().addBytes(m.customExtension)
+ }
+
+ m.raw = builder.finish()
+ return m.raw
+}
+
+func (m *clientEncryptedExtensionsMsg) unmarshal(data []byte) bool {
+ m.raw = data
+ reader := byteReader(data[4:])
+
+ var extensions byteReader
+ if !reader.readU16LengthPrefixed(&extensions) ||
+ len(reader) != 0 {
+ return false
+ }
+
+ if !checkDuplicateExtensions(extensions) {
+ return false
+ }
+
+ for len(extensions) > 0 {
+ var extension uint16
+ var body byteReader
+ if !extensions.readU16(&extension) ||
+ !extensions.readU16LengthPrefixed(&body) {
+ return false
+ }
+ switch extension {
+ case extensionApplicationSettings:
+ m.hasApplicationSettings = true
+ m.applicationSettings = body
+ default:
+ // Unknown extensions are illegal in EncryptedExtensions.
+ return false
+ }
+ }
+ return true
+}
+
type helloRetryRequestMsg struct {
raw []byte
vers uint16
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 88e186d..1a4beef 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -718,7 +718,10 @@
// Decide whether or not to accept early data.
if !sendHelloRetryRequest && hs.clientHello.hasEarlyData {
if !config.Bugs.AlwaysRejectEarlyData && hs.sessionState != nil {
- if hs.sessionState.cipherSuite == hs.suite.id && c.clientProtocol == string(hs.sessionState.earlyALPN) {
+ if hs.sessionState.cipherSuite == hs.suite.id &&
+ c.clientProtocol == string(hs.sessionState.earlyALPN) &&
+ c.hasApplicationSettings == hs.sessionState.hasApplicationSettings &&
+ bytes.Equal(c.localApplicationSettings, hs.sessionState.localApplicationSettings) {
encryptedExtensions.extensions.hasEarlyData = true
}
if config.Bugs.AlwaysAcceptEarlyData {
@@ -729,6 +732,12 @@
earlyTrafficSecret := hs.finishedHash.deriveSecret(earlyTrafficLabel)
c.earlyExporterSecret = hs.finishedHash.deriveSecret(earlyExporterLabel)
+ // Applications are implicit with early data.
+ if !config.Bugs.SendApplicationSettingsWithEarlyData {
+ encryptedExtensions.extensions.hasApplicationSettings = false
+ encryptedExtensions.extensions.applicationSettings = nil
+ }
+
sessionCipher := cipherSuiteFromID(hs.sessionState.cipherSuite)
if err := c.useInTrafficSecret(c.wireVersion, sessionCipher, earlyTrafficSecret); err != nil {
return err
@@ -1050,6 +1059,29 @@
return err
}
+ // If we sent an ALPS extension, the client must respond with one.
+ if encryptedExtensions.extensions.hasApplicationSettings {
+ msg, err := c.readHandshake()
+ if err != nil {
+ return err
+ }
+ clientEncryptedExtensions, ok := msg.(*clientEncryptedExtensionsMsg)
+ if !ok {
+ c.sendAlert(alertUnexpectedMessage)
+ return unexpectedMessageError(clientEncryptedExtensions, msg)
+ }
+ hs.writeClientHash(clientEncryptedExtensions.marshal())
+
+ if !clientEncryptedExtensions.hasApplicationSettings {
+ c.sendAlert(alertMissingExtension)
+ return errors.New("tls: client didn't provide application settings")
+ }
+ c.peerApplicationSettings = clientEncryptedExtensions.applicationSettings
+ } else if encryptedExtensions.extensions.hasEarlyData {
+ // 0-RTT sessions carry application settings over.
+ c.peerApplicationSettings = hs.sessionState.peerApplicationSettings
+ }
+
// If we requested a client certificate, then the client must send a
// certificate message, even if it's empty.
if config.ClientAuth >= RequestClientCert {
@@ -1367,6 +1399,26 @@
c.clientProtocol = selectedProto
c.usedALPN = true
}
+
+ var alpsAllowed bool
+ if c.vers >= VersionTLS13 {
+ for _, proto := range hs.clientHello.alpsProtocols {
+ if proto == c.clientProtocol {
+ alpsAllowed = true
+ break
+ }
+ }
+ }
+ if c.config.Bugs.AlwaysNegotiateApplicationSettings {
+ alpsAllowed = true
+ }
+ if settings, ok := c.config.ApplicationSettings[c.clientProtocol]; ok && alpsAllowed {
+ c.hasApplicationSettings = true
+ c.localApplicationSettings = settings
+ // Note these fields may later be cleared we accept 0-RTT.
+ serverExtensions.hasApplicationSettings = true
+ serverExtensions.applicationSettings = settings
+ }
}
if len(c.config.Bugs.SendALPN) > 0 {
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 0f34287..bb30e03 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -533,6 +533,9 @@
// quicTransportParams contains the QUIC transport parameters that are to be
// sent by the peer.
quicTransportParams []byte
+ // peerApplicationSettings are the expected application settings for the
+ // connection. If nil, no application settings are expected.
+ peerApplicationSettings []byte
}
type testCase struct {
@@ -894,6 +897,17 @@
}
}
+ if expectations.peerApplicationSettings != nil {
+ if !connState.HasApplicationSettings {
+ return errors.New("application settings should have been negotiated")
+ }
+ if !bytes.Equal(connState.PeerApplicationSettings, expectations.peerApplicationSettings) {
+ return fmt.Errorf("peer application settings mismatch: got %q, wanted %q", connState.PeerApplicationSettings, expectations.peerApplicationSettings)
+ }
+ } else if connState.HasApplicationSettings {
+ return errors.New("application settings unexpectedly negotiated")
+ }
+
if p := connState.SRTPProtectionProfile; p != expectations.srtpProtectionProfile {
return fmt.Errorf("SRTP profile mismatch: got %d, wanted %d", p, expectations.srtpProtectionProfile)
}
@@ -6973,6 +6987,492 @@
})
}
+ // Test ALPS.
+ if ver.version >= VersionTLS13 {
+ // Test that client and server can negotiate ALPS, including
+ // different values on resumption.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-Basic-Client-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
+ },
+ resumeConfig: &Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte("shim1"),
+ },
+ resumeExpectations: &connectionExpectations{
+ peerApplicationSettings: []byte("shim2"),
+ },
+ flags: []string{
+ "-advertise-alpn", "\x05proto",
+ "-expect-alpn", "proto",
+ "-on-initial-application-settings", "proto,shim1",
+ "-on-initial-expect-peer-application-settings", "runner1",
+ "-on-resume-application-settings", "proto,shim2",
+ "-on-resume-expect-peer-application-settings", "runner2",
+ },
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-Basic-Server-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner1")},
+ },
+ resumeConfig: &Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner2")},
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte("shim1"),
+ },
+ resumeExpectations: &connectionExpectations{
+ peerApplicationSettings: []byte("shim2"),
+ },
+ flags: []string{
+ "-select-alpn", "proto",
+ "-on-initial-application-settings", "proto,shim1",
+ "-on-initial-expect-peer-application-settings", "runner1",
+ "-on-resume-application-settings", "proto,shim2",
+ "-on-resume-expect-peer-application-settings", "runner2",
+ },
+ })
+
+ // Test the client and server correctly handle empty settings.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-Empty-Client-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte{}},
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte{},
+ },
+ flags: []string{
+ "-advertise-alpn", "\x05proto",
+ "-expect-alpn", "proto",
+ "-application-settings", "proto,",
+ "-expect-peer-application-settings", "",
+ },
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-Empty-Server-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte{}},
+ },
+ resumeSession: true,
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte{},
+ },
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto,",
+ "-expect-peer-application-settings", "",
+ },
+ })
+
+ // Test the client rejects application settings from the server on
+ // protocols it doesn't have them.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-UnsupportedProtocol-Client-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto1"},
+ ApplicationSettings: map[string][]byte{"proto1": []byte("runner")},
+ Bugs: ProtocolBugs{
+ AlwaysNegotiateApplicationSettings: true,
+ },
+ },
+ // The client supports ALPS with "proto2", but not "proto1".
+ flags: []string{
+ "-advertise-alpn", "\x06proto1\x06proto2",
+ "-application-settings", "proto2,shim",
+ "-expect-alpn", "proto1",
+ },
+ // The server sends ALPS with "proto1", which is invalid.
+ shouldFail: true,
+ expectedError: ":INVALID_ALPN_PROTOCOL:",
+ expectedLocalError: "remote error: illegal parameter",
+ })
+
+ // Test the server declines ALPS if it doesn't support it for the
+ // specified protocol.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-UnsupportedProtocol-Server-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto1"},
+ ApplicationSettings: map[string][]byte{"proto1": []byte("runner")},
+ },
+ // The server supports ALPS with "proto2", but not "proto1".
+ flags: []string{
+ "-select-alpn", "proto1",
+ "-application-settings", "proto2,shim",
+ },
+ })
+
+ // Test that the server rejects a missing application_settings extension.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-OmitClientApplicationSettings-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
+ Bugs: ProtocolBugs{
+ OmitClientApplicationSettings: true,
+ },
+ },
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto,shim",
+ },
+ // The runner is a client, so it only processes the shim's alert
+ // after checking connection state.
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte("shim"),
+ },
+ shouldFail: true,
+ expectedError: ":MISSING_EXTENSION:",
+ expectedLocalError: "remote error: missing extension",
+ })
+
+ // Test that the server rejects a missing EncryptedExtensions message.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-OmitClientEncryptedExtensions-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
+ Bugs: ProtocolBugs{
+ OmitClientEncryptedExtensions: true,
+ },
+ },
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto,shim",
+ },
+ // The runner is a client, so it only processes the shim's alert
+ // after checking connection state.
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte("shim"),
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_MESSAGE:",
+ expectedLocalError: "remote error: unexpected message",
+ })
+
+ // Test that the server rejects an unexpected EncryptedExtensions message.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "UnexpectedClientEncryptedExtensions-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ Bugs: ProtocolBugs{
+ AlwaysSendClientEncryptedExtensions: true,
+ },
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_MESSAGE:",
+ expectedLocalError: "remote error: unexpected message",
+ })
+
+ // Test that the server rejects an unexpected extension in an
+ // expected EncryptedExtensions message.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ExtraClientEncryptedExtension-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
+ Bugs: ProtocolBugs{
+ SendExtraClientEncryptedExtension: true,
+ },
+ },
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto,shim",
+ },
+ // The runner is a client, so it only processes the shim's alert
+ // after checking connection state.
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte("shim"),
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_EXTENSION:",
+ expectedLocalError: "remote error: unsupported extension",
+ })
+
+ // Test that ALPS is carried over on 0-RTT.
+ for _, empty := range []bool{false, true} {
+ suffix := ver.name
+ runnerSettings := "runner"
+ shimSettings := "shim"
+ if empty {
+ suffix = "Empty-" + ver.name
+ runnerSettings = ""
+ shimSettings = ""
+ }
+
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-EarlyData-Client-" + suffix,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
+ },
+ resumeSession: true,
+ earlyData: true,
+ flags: []string{
+ "-advertise-alpn", "\x05proto",
+ "-expect-alpn", "proto",
+ "-application-settings", "proto," + shimSettings,
+ "-expect-peer-application-settings", runnerSettings,
+ },
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte(shimSettings),
+ },
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-EarlyData-Server-" + suffix,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
+ },
+ resumeSession: true,
+ earlyData: true,
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto," + shimSettings,
+ "-expect-peer-application-settings", runnerSettings,
+ },
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte(shimSettings),
+ },
+ })
+
+ // Sending application settings in 0-RTT handshakes is forbidden.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-EarlyData-SendApplicationSettingsWithEarlyData-Client-" + suffix,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
+ Bugs: ProtocolBugs{
+ SendApplicationSettingsWithEarlyData: true,
+ },
+ },
+ resumeSession: true,
+ earlyData: true,
+ flags: []string{
+ "-advertise-alpn", "\x05proto",
+ "-expect-alpn", "proto",
+ "-application-settings", "proto," + shimSettings,
+ "-expect-peer-application-settings", runnerSettings,
+ },
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte(shimSettings),
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_EXTENSION_ON_EARLY_DATA:",
+ expectedLocalError: "remote error: illegal parameter",
+ })
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-EarlyData-SendApplicationSettingsWithEarlyData-Server-" + suffix,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte(runnerSettings)},
+ Bugs: ProtocolBugs{
+ SendApplicationSettingsWithEarlyData: true,
+ },
+ },
+ resumeSession: true,
+ earlyData: true,
+ flags: []string{
+ "-select-alpn", "proto",
+ "-application-settings", "proto," + shimSettings,
+ "-expect-peer-application-settings", runnerSettings,
+ },
+ expectations: connectionExpectations{
+ peerApplicationSettings: []byte(shimSettings),
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_MESSAGE:",
+ expectedLocalError: "remote error: unexpected message",
+ })
+ }
+
+ // Test that the client and server each decline early data if local
+ // ALPS preferences has changed for the current connection.
+ alpsMismatchTests := []struct {
+ name string
+ initialSettings, resumeSettings []byte
+ }{
+ {"DifferentValues", []byte("settings1"), []byte("settings2")},
+ {"OnOff", []byte("settings"), nil},
+ {"OffOn", nil, []byte("settings")},
+ // The empty settings value should not be mistaken for ALPS not
+ // being negotiated.
+ {"OnEmpty", []byte("settings"), []byte{}},
+ {"EmptyOn", []byte{}, []byte("settings")},
+ {"EmptyOff", []byte{}, nil},
+ {"OffEmpty", nil, []byte{}},
+ }
+ for _, test := range alpsMismatchTests {
+ flags := []string{"-on-resume-expect-early-data-reason", "alps_mismatch"}
+ if test.initialSettings != nil {
+ flags = append(flags, "-on-initial-application-settings", "proto,"+string(test.initialSettings))
+ flags = append(flags, "-on-initial-expect-peer-application-settings", "runner")
+ }
+ if test.resumeSettings != nil {
+ flags = append(flags, "-on-resume-application-settings", "proto,"+string(test.resumeSettings))
+ flags = append(flags, "-on-resume-expect-peer-application-settings", "runner")
+ }
+
+ // The client should not offer early data.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: fmt.Sprintf("ALPS-EarlyData-Mismatch-%s-Client-%s", test.name, ver.name),
+ config: Config{
+ MaxVersion: ver.version,
+ MaxEarlyDataSize: 16384,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
+ },
+ resumeSession: true,
+ flags: append([]string{
+ "-enable-early-data",
+ "-expect-ticket-supports-early-data",
+ "-expect-no-offer-early-data",
+ "-advertise-alpn", "\x05proto",
+ "-expect-alpn", "proto",
+ }, flags...),
+ expectations: connectionExpectations{
+ peerApplicationSettings: test.initialSettings,
+ },
+ resumeExpectations: &connectionExpectations{
+ peerApplicationSettings: test.resumeSettings,
+ },
+ })
+
+ // The server should reject early data.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: fmt.Sprintf("ALPS-EarlyData-Mismatch-%s-Server-%s", test.name, ver.name),
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"proto"},
+ ApplicationSettings: map[string][]byte{"proto": []byte("runner")},
+ },
+ resumeSession: true,
+ earlyData: true,
+ expectEarlyDataRejected: true,
+ flags: append([]string{
+ "-select-alpn", "proto",
+ }, flags...),
+ expectations: connectionExpectations{
+ peerApplicationSettings: test.initialSettings,
+ },
+ resumeExpectations: &connectionExpectations{
+ peerApplicationSettings: test.resumeSettings,
+ },
+ })
+ }
+ } else {
+ // Test the client rejects the ALPS extension if the server
+ // negotiated TLS 1.2 or below.
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-Reject-Client-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"foo"},
+ ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
+ Bugs: ProtocolBugs{
+ AlwaysNegotiateApplicationSettings: true,
+ },
+ },
+ flags: []string{
+ "-advertise-alpn", "\x03foo",
+ "-expect-alpn", "foo",
+ "-application-settings", "foo,shim",
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_EXTENSION:",
+ expectedLocalError: "remote error: unsupported extension",
+ })
+ testCases = append(testCases, testCase{
+ testType: clientTest,
+ name: "ALPS-Reject-Client-Resume-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ },
+ resumeConfig: &Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"foo"},
+ ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
+ Bugs: ProtocolBugs{
+ AlwaysNegotiateApplicationSettings: true,
+ },
+ },
+ resumeSession: true,
+ flags: []string{
+ "-on-resume-advertise-alpn", "\x03foo",
+ "-on-resume-expect-alpn", "foo",
+ "-on-resume-application-settings", "foo,shim",
+ },
+ shouldFail: true,
+ expectedError: ":UNEXPECTED_EXTENSION:",
+ expectedLocalError: "remote error: unsupported extension",
+ })
+
+ // Test the server declines ALPS if it negotiates TLS 1.2 or below.
+ testCases = append(testCases, testCase{
+ testType: serverTest,
+ name: "ALPS-Decline-Server-" + ver.name,
+ config: Config{
+ MaxVersion: ver.version,
+ NextProtos: []string{"foo"},
+ ApplicationSettings: map[string][]byte{"foo": []byte("runner")},
+ },
+ // Test both TLS 1.2 full and resumption handshakes.
+ resumeSession: true,
+ flags: []string{
+ "-select-alpn", "foo",
+ "-application-settings", "foo,shim",
+ },
+ // If not specified, runner and shim both implicitly expect ALPS
+ // is not negotiated.
+ })
+ }
+
// Test Token Binding.
const maxTokenBindingVersion = 16
diff --git a/ssl/test/runner/ticket.go b/ssl/test/runner/ticket.go
index af87547..f5163e1 100644
--- a/ssl/test/runner/ticket.go
+++ b/ssl/test/runner/ticket.go
@@ -18,17 +18,20 @@
// sessionState contains the information that is serialized into a session
// ticket in order to later resume a connection.
type sessionState struct {
- vers uint16
- cipherSuite uint16
- masterSecret []byte
- handshakeHash []byte
- certificates [][]byte
- extendedMasterSecret bool
- earlyALPN []byte
- ticketCreationTime time.Time
- ticketExpiration time.Time
- ticketFlags uint32
- ticketAgeAdd uint32
+ vers uint16
+ cipherSuite uint16
+ masterSecret []byte
+ handshakeHash []byte
+ certificates [][]byte
+ extendedMasterSecret bool
+ earlyALPN []byte
+ ticketCreationTime time.Time
+ ticketExpiration time.Time
+ ticketFlags uint32
+ ticketAgeAdd uint32
+ hasApplicationSettings bool
+ localApplicationSettings []byte
+ peerApplicationSettings []byte
}
func (s *sessionState) marshal() []byte {
@@ -61,9 +64,33 @@
earlyALPN := msg.addU16LengthPrefixed()
earlyALPN.addBytes(s.earlyALPN)
+ if s.hasApplicationSettings {
+ msg.addU8(1)
+ msg.addU16LengthPrefixed().addBytes(s.localApplicationSettings)
+ msg.addU16LengthPrefixed().addBytes(s.peerApplicationSettings)
+ } else {
+ msg.addU8(0)
+ }
+
return msg.finish()
}
+func readBool(reader *byteReader, out *bool) bool {
+ var value uint8
+ if !reader.readU8(&value) {
+ return false
+ }
+ if value == 0 {
+ *out = false
+ return true
+ }
+ if value == 1 {
+ *out = true
+ return true
+ }
+ return false
+}
+
func (s *sessionState) unmarshal(data []byte) bool {
reader := byteReader(data)
var numCerts uint16
@@ -82,15 +109,7 @@
}
}
- var extendedMasterSecret uint8
- if !reader.readU8(&extendedMasterSecret) {
- return false
- }
- if extendedMasterSecret == 0 {
- s.extendedMasterSecret = false
- } else if extendedMasterSecret == 1 {
- s.extendedMasterSecret = true
- } else {
+ if !readBool(&reader, &s.extendedMasterSecret) {
return false
}
@@ -107,7 +126,18 @@
}
if !reader.readU16LengthPrefixedBytes(&s.earlyALPN) ||
- len(reader) > 0 {
+ !readBool(&reader, &s.hasApplicationSettings) {
+ return false
+ }
+
+ if s.hasApplicationSettings {
+ if !reader.readU16LengthPrefixedBytes(&s.localApplicationSettings) ||
+ !reader.readU16LengthPrefixedBytes(&s.peerApplicationSettings) {
+ return false
+ }
+ }
+
+ if len(reader) > 0 {
return false
}
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index e015466..ca87c44 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -185,6 +185,13 @@
{"-expect-early-data-reason", &TestConfig::expect_early_data_reason},
};
+// TODO(davidben): When we can depend on C++17 or Abseil, switch this to
+// std::optional or absl::optional.
+const Flag<std::unique_ptr<std::string>> kOptionalStringFlags[] = {
+ {"-expect-peer-application-settings",
+ &TestConfig::expect_peer_application_settings},
+};
+
const Flag<std::string> kBase64Flags[] = {
{"-expect-certificate-types", &TestConfig::expect_certificate_types},
{"-expect-channel-id", &TestConfig::expect_channel_id},
@@ -231,6 +238,11 @@
{"-curves", &TestConfig::curves},
};
+const Flag<std::vector<std::pair<std::string, std::string>>>
+ kStringPairVectorFlags[] = {
+ {"-application-settings", &TestConfig::application_settings},
+};
+
bool ParseFlag(char *flag, int argc, char **argv, int *i,
bool skip, TestConfig *out_config) {
bool *bool_field = FindField(out_config, kBoolFlags, flag);
@@ -254,6 +266,20 @@
return true;
}
+ std::unique_ptr<std::string> *optional_string_field =
+ FindField(out_config, kOptionalStringFlags, flag);
+ if (optional_string_field != NULL) {
+ *i = *i + 1;
+ if (*i >= argc) {
+ fprintf(stderr, "Missing parameter.\n");
+ return false;
+ }
+ if (!skip) {
+ optional_string_field->reset(new std::string(argv[*i]));
+ }
+ return true;
+ }
+
std::string *base64_field = FindField(out_config, kBase64Flags, flag);
if (base64_field != NULL) {
*i = *i + 1;
@@ -309,6 +335,28 @@
return true;
}
+ std::vector<std::pair<std::string, std::string>> *string_pair_vector_field =
+ FindField(out_config, kStringPairVectorFlags, flag);
+ if (string_pair_vector_field) {
+ *i = *i + 1;
+ if (*i >= argc) {
+ fprintf(stderr, "Missing parameter.\n");
+ return false;
+ }
+ const char *comma = strchr(argv[*i], ',');
+ if (!comma) {
+ fprintf(stderr,
+ "Parameter should be a pair of comma-separated strings.\n");
+ return false;
+ }
+ // Each instance of the flag adds to the list.
+ if (!skip) {
+ string_pair_vector_field->push_back(std::make_pair(
+ std::string(argv[*i], comma - argv[*i]), std::string(comma + 1)));
+ }
+ return true;
+ }
+
fprintf(stderr, "Unknown argument: %s.\n", flag);
return false;
}
@@ -1555,10 +1603,20 @@
return nullptr;
}
if (!advertise_alpn.empty() &&
- SSL_set_alpn_protos(ssl.get(), (const uint8_t *)advertise_alpn.data(),
- advertise_alpn.size()) != 0) {
+ SSL_set_alpn_protos(
+ ssl.get(), reinterpret_cast<const uint8_t *>(advertise_alpn.data()),
+ advertise_alpn.size()) != 0) {
return nullptr;
}
+ for (const auto &pair : application_settings) {
+ if (!SSL_add_application_settings(
+ ssl.get(), reinterpret_cast<const uint8_t *>(pair.first.data()),
+ pair.first.size(),
+ reinterpret_cast<const uint8_t *>(pair.second.data()),
+ pair.second.size())) {
+ return nullptr;
+ }
+ }
if (!psk.empty()) {
SSL_set_psk_client_callback(ssl.get(), PskClientCallback);
SSL_set_psk_server_callback(ssl.get(), PskServerCallback);
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 8fe439e..318c733 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -16,6 +16,7 @@
#define HEADER_TEST_CONFIG
#include <string>
+#include <utility>
#include <vector>
#include <openssl/base.h>
@@ -67,6 +68,8 @@
std::string select_alpn;
bool decline_alpn = false;
bool select_empty_alpn = false;
+ std::vector<std::pair<std::string, std::string>> application_settings;
+ std::unique_ptr<std::string> expect_peer_application_settings;
std::string quic_transport_params;
std::string expect_quic_transport_params;
bool expect_session_miss = false;
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index 8cadd73..c77824f 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -44,6 +44,7 @@
state_server_certificate_reverify,
state_read_server_finished,
state_send_end_of_early_data,
+ state_send_client_encrypted_extensions,
state_send_client_certificate,
state_send_client_certificate_verify,
state_complete_second_flight,
@@ -487,12 +488,6 @@
return ssl_hs_error;
}
- // Store the negotiated ALPN in the session.
- if (!hs->new_session->early_alpn.CopyFrom(ssl->s3->alpn_selected)) {
- ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
- return ssl_hs_error;
- }
-
if (ssl->s3->early_data_accepted) {
if (hs->early_session->cipher != hs->new_session->cipher) {
OPENSSL_PUT_ERROR(SSL, SSL_R_CIPHER_MISMATCH_ON_EARLY_DATA);
@@ -505,11 +500,29 @@
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
return ssl_hs_error;
}
- if (ssl->s3->channel_id_valid || ssl->s3->token_binding_negotiated) {
+ // Channel ID and Token Binding are incompatible with 0-RTT. The ALPS
+ // extension should be negotiated implicitly.
+ if (ssl->s3->channel_id_valid || ssl->s3->token_binding_negotiated ||
+ hs->new_session->has_application_settings) {
OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION_ON_EARLY_DATA);
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
return ssl_hs_error;
}
+ hs->new_session->has_application_settings =
+ hs->early_session->has_application_settings;
+ if (!hs->new_session->local_application_settings.CopyFrom(
+ hs->early_session->local_application_settings) ||
+ !hs->new_session->peer_application_settings.CopyFrom(
+ hs->early_session->peer_application_settings)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+ return ssl_hs_error;
+ }
+ }
+
+ // Store the negotiated ALPN in the session.
+ if (!hs->new_session->early_alpn.CopyFrom(ssl->s3->alpn_selected)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+ return ssl_hs_error;
}
if (!ssl_hash_message(hs, msg)) {
@@ -626,8 +639,7 @@
return ssl_hs_ok;
}
-static enum ssl_hs_wait_t do_read_server_certificate_verify(
- SSL_HANDSHAKE *hs) {
+static enum ssl_hs_wait_t do_read_server_certificate_verify(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
SSLMessage msg;
if (!ssl->method->get_message(ssl, &msg)) {
@@ -654,8 +666,7 @@
return ssl_hs_ok;
}
-static enum ssl_hs_wait_t do_server_certificate_reverify(
- SSL_HANDSHAKE *hs) {
+static enum ssl_hs_wait_t do_server_certificate_reverify(SSL_HANDSHAKE *hs) {
switch (ssl_reverify_peer_cert(hs, /*send_alert=*/true)) {
case ssl_verify_ok:
break;
@@ -718,6 +729,32 @@
}
}
+ hs->tls13_state = state_send_client_encrypted_extensions;
+ return ssl_hs_ok;
+}
+
+static enum ssl_hs_wait_t do_send_client_encrypted_extensions(
+ SSL_HANDSHAKE *hs) {
+ SSL *const ssl = hs->ssl;
+ // For now, only one extension uses client EncryptedExtensions. This function
+ // may be generalized if others use it in the future.
+ if (hs->new_session->has_application_settings &&
+ !ssl->s3->early_data_accepted) {
+ ScopedCBB cbb;
+ CBB body, extensions, extension;
+ if (!ssl->method->init_message(ssl, cbb.get(), &body,
+ SSL3_MT_ENCRYPTED_EXTENSIONS) ||
+ !CBB_add_u16_length_prefixed(&body, &extensions) ||
+ !CBB_add_u16(&extensions, TLSEXT_TYPE_application_settings) ||
+ !CBB_add_u16_length_prefixed(&extensions, &extension) ||
+ !CBB_add_bytes(&extension,
+ hs->new_session->local_application_settings.data(),
+ hs->new_session->local_application_settings.size()) ||
+ !ssl_add_message_cbb(ssl, cbb.get())) {
+ return ssl_hs_error;
+ }
+ }
+
hs->tls13_state = state_send_client_certificate;
return ssl_hs_ok;
}
@@ -860,6 +897,9 @@
case state_send_client_certificate:
ret = do_send_client_certificate(hs);
break;
+ case state_send_client_encrypted_extensions:
+ ret = do_send_client_encrypted_extensions(hs);
+ break;
case state_send_client_certificate_verify:
ret = do_send_client_certificate_verify(hs);
break;
@@ -907,6 +947,8 @@
return "TLS 1.3 client read_server_finished";
case state_send_end_of_early_data:
return "TLS 1.3 client send_end_of_early_data";
+ case state_send_client_encrypted_extensions:
+ return "TLS 1.3 client send_client_encrypted_extensions";
case state_send_client_certificate:
return "TLS 1.3 client send_client_certificate";
case state_send_client_certificate_verify:
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 716b7dc..fefb074 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -354,13 +354,6 @@
&offered_ticket, msg, &client_hello)) {
case ssl_ticket_aead_ignore_ticket:
assert(!session);
- if (!ssl->enable_early_data) {
- ssl->s3->early_data_reason = ssl_early_data_disabled;
- } else if (!offered_ticket) {
- ssl->s3->early_data_reason = ssl_early_data_no_session_offered;
- } else {
- ssl->s3->early_data_reason = ssl_early_data_session_not_resumed;
- }
if (!ssl_get_new_session(hs, 1 /* server */)) {
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
return ssl_hs_error;
@@ -377,35 +370,6 @@
return ssl_hs_error;
}
- // |ssl_session_is_resumable| forbids cross-cipher resumptions even if the
- // PRF hashes match.
- assert(hs->new_cipher == session->cipher);
-
- if (!ssl->enable_early_data) {
- ssl->s3->early_data_reason = ssl_early_data_disabled;
- } else if (session->ticket_max_early_data == 0) {
- ssl->s3->early_data_reason = ssl_early_data_unsupported_for_session;
- } else if (!hs->early_data_offered) {
- ssl->s3->early_data_reason = ssl_early_data_peer_declined;
- } else if (ssl->s3->channel_id_valid) {
- // Channel ID is incompatible with 0-RTT.
- ssl->s3->early_data_reason = ssl_early_data_channel_id;
- } else if (ssl->s3->token_binding_negotiated) {
- // Token Binding is incompatible with 0-RTT.
- ssl->s3->early_data_reason = ssl_early_data_token_binding;
- } else if (MakeConstSpan(ssl->s3->alpn_selected) != session->early_alpn) {
- // The negotiated ALPN must match the one in the ticket.
- ssl->s3->early_data_reason = ssl_early_data_alpn_mismatch;
- } else if (ssl->s3->ticket_age_skew < -kMaxTicketAgeSkewSeconds ||
- kMaxTicketAgeSkewSeconds < ssl->s3->ticket_age_skew) {
- ssl->s3->early_data_reason = ssl_early_data_ticket_age_skew;
- } else if (!quic_ticket_compatible(session.get(), hs->config)) {
- ssl->s3->early_data_reason = ssl_early_data_quic_parameter_mismatch;
- } else {
- ssl->s3->early_data_reason = ssl_early_data_accepted;
- ssl->s3->early_data_accepted = true;
- }
-
ssl->s3->session_reused = true;
// Resumption incorporates fresh key material, so refresh the timeout.
@@ -422,15 +386,74 @@
return ssl_hs_pending_ticket;
}
+ // Negotiate ALPS now, after ALPN is negotiated and |hs->new_session| is
+ // initialized.
+ if (!ssl_negotiate_alps(hs, &alert, &client_hello)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+ return ssl_hs_error;
+ }
+
+ // Determine if we're negotiating 0-RTT.
+ if (!ssl->enable_early_data) {
+ ssl->s3->early_data_reason = ssl_early_data_disabled;
+ } else if (!offered_ticket) {
+ ssl->s3->early_data_reason = ssl_early_data_no_session_offered;
+ } else if (!session) {
+ ssl->s3->early_data_reason = ssl_early_data_session_not_resumed;
+ } else if (session->ticket_max_early_data == 0) {
+ ssl->s3->early_data_reason = ssl_early_data_unsupported_for_session;
+ } else if (!hs->early_data_offered) {
+ ssl->s3->early_data_reason = ssl_early_data_peer_declined;
+ } else if (ssl->s3->channel_id_valid) {
+ // Channel ID is incompatible with 0-RTT.
+ ssl->s3->early_data_reason = ssl_early_data_channel_id;
+ } else if (ssl->s3->token_binding_negotiated) {
+ // Token Binding is incompatible with 0-RTT.
+ ssl->s3->early_data_reason = ssl_early_data_token_binding;
+ } else if (MakeConstSpan(ssl->s3->alpn_selected) != session->early_alpn) {
+ // The negotiated ALPN must match the one in the ticket.
+ ssl->s3->early_data_reason = ssl_early_data_alpn_mismatch;
+ } else if (hs->new_session->has_application_settings !=
+ session->has_application_settings ||
+ MakeConstSpan(hs->new_session->local_application_settings) !=
+ session->local_application_settings) {
+ ssl->s3->early_data_reason = ssl_early_data_alps_mismatch;
+ } else if (ssl->s3->ticket_age_skew < -kMaxTicketAgeSkewSeconds ||
+ kMaxTicketAgeSkewSeconds < ssl->s3->ticket_age_skew) {
+ ssl->s3->early_data_reason = ssl_early_data_ticket_age_skew;
+ } else if (!quic_ticket_compatible(session.get(), hs->config)) {
+ ssl->s3->early_data_reason = ssl_early_data_quic_parameter_mismatch;
+ } else {
+ // |ssl_session_is_resumable| forbids cross-cipher resumptions even if the
+ // PRF hashes match.
+ assert(hs->new_cipher == session->cipher);
+
+ ssl->s3->early_data_reason = ssl_early_data_accepted;
+ ssl->s3->early_data_accepted = true;
+ }
+
// Record connection properties in the new session.
hs->new_session->cipher = hs->new_cipher;
- // Store the initial negotiated ALPN in the session.
+ // Store the ALPN and ALPS values in the session for 0-RTT. Note the peer
+ // applications settings are not generally known until client
+ // EncryptedExtensions.
if (!hs->new_session->early_alpn.CopyFrom(ssl->s3->alpn_selected)) {
ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
return ssl_hs_error;
}
+ // The peer applications settings are usually received later, in
+ // EncryptedExtensions. But, in 0-RTT handshakes, we carry over the
+ // values from |session|. Do this now, before |session| is discarded.
+ if (ssl->s3->early_data_accepted &&
+ hs->new_session->has_application_settings &&
+ !hs->new_session->peer_application_settings.CopyFrom(
+ session->peer_application_settings)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+ return ssl_hs_error;
+ }
+
// Copy the QUIC early data context to the session.
if (ssl->enable_early_data && ssl->quic_method) {
if (!hs->new_session->quic_early_data_context.CopyFrom(
@@ -854,6 +877,64 @@
hs->client_handshake_secret())) {
return ssl_hs_error;
}
+ hs->tls13_state = state13_read_client_encrypted_extensions;
+ return ssl_hs_ok;
+}
+
+static enum ssl_hs_wait_t do_read_client_encrypted_extensions(
+ SSL_HANDSHAKE *hs) {
+ SSL *const ssl = hs->ssl;
+ // For now, only one extension uses client EncryptedExtensions. This function
+ // may be generalized if others use it in the future.
+ if (hs->new_session->has_application_settings &&
+ !ssl->s3->early_data_accepted) {
+ SSLMessage msg;
+ if (!ssl->method->get_message(ssl, &msg)) {
+ return ssl_hs_read_message;
+ }
+ if (!ssl_check_message_type(ssl, msg, SSL3_MT_ENCRYPTED_EXTENSIONS)) {
+ return ssl_hs_error;
+ }
+
+ CBS body = msg.body, extensions;
+ if (!CBS_get_u16_length_prefixed(&body, &extensions) ||
+ CBS_len(&body) != 0) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+ return ssl_hs_error;
+ }
+
+ // Parse out the extensions.
+ bool have_application_settings = false;
+ CBS application_settings;
+ SSL_EXTENSION_TYPE ext_types[] = {{TLSEXT_TYPE_application_settings,
+ &have_application_settings,
+ &application_settings}};
+ uint8_t alert = SSL_AD_DECODE_ERROR;
+ if (!ssl_parse_extensions(&extensions, &alert, ext_types,
+ /*ignore_unknown=*/false)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+ return ssl_hs_error;
+ }
+
+ if (!have_application_settings) {
+ OPENSSL_PUT_ERROR(SSL, SSL_R_MISSING_EXTENSION);
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_MISSING_EXTENSION);
+ return ssl_hs_error;
+ }
+
+ // Note that, if 0-RTT was accepted, these values will already have been
+ // initialized earlier.
+ if (!hs->new_session->peer_application_settings.CopyFrom(
+ application_settings) ||
+ !ssl_hash_message(hs, msg)) {
+ ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+ return ssl_hs_error;
+ }
+
+ ssl->method->next_message(ssl);
+ }
+
hs->tls13_state = state13_read_client_certificate;
return ssl_hs_ok;
}
@@ -892,8 +973,7 @@
return ssl_hs_ok;
}
-static enum ssl_hs_wait_t do_read_client_certificate_verify(
- SSL_HANDSHAKE *hs) {
+static enum ssl_hs_wait_t do_read_client_certificate_verify(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl;
if (sk_CRYPTO_BUFFER_num(hs->new_session->certs.get()) == 0) {
// Skip this state.
@@ -1037,6 +1117,9 @@
case state13_process_end_of_early_data:
ret = do_process_end_of_early_data(hs);
break;
+ case state13_read_client_encrypted_extensions:
+ ret = do_read_client_encrypted_extensions(hs);
+ break;
case state13_read_client_certificate:
ret = do_read_client_certificate(hs);
break;
@@ -1093,6 +1176,8 @@
return "TLS 1.3 server read_second_client_flight";
case state13_process_end_of_early_data:
return "TLS 1.3 server process_end_of_early_data";
+ case state13_read_client_encrypted_extensions:
+ return "TLS 1.3 server read_client_encrypted_extensions";
case state13_read_client_certificate:
return "TLS 1.3 server read_client_certificate";
case state13_read_client_certificate_verify: