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/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 3759c69..cd6f87a 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -1,4 +1,5 @@
 SSL,277,ALPN_MISMATCH_ON_EARLY_DATA
+SSL,309,ALPS_MISMATCH_ON_EARLY_DATA
 SSL,281,APPLICATION_DATA_INSTEAD_OF_HANDSHAKE
 SSL,291,APPLICATION_DATA_ON_SHUTDOWN
 SSL,100,APP_DATA_IN_HANDSHAKE
@@ -94,6 +95,7 @@
 SSL,167,MISSING_TMP_ECDH_KEY
 SSL,168,MIXED_SPECIAL_OPERATOR_WITH_GROUPS
 SSL,169,MTU_TOO_SMALL
+SSL,308,NEGOTIATED_ALPS_WITHOUT_ALPN
 SSL,170,NEGOTIATED_BOTH_NPN_AND_ALPN
 SSL,285,NEGOTIATED_TB_WITHOUT_EMS_OR_RI
 SSL,171,NESTED_GROUP
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 3e4b638..4db6afc 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -2776,6 +2776,51 @@
                                                           int enabled);
 
 
+// Application-layer protocol settings
+//
+// The ALPS extension (draft-vvv-tls-alps) allows exchanging application-layer
+// settings in the TLS handshake for applications negotiated with ALPN. Note
+// that, when ALPS is negotiated, the client and server each advertise their own
+// settings, so there are functions to both configure setting to send and query
+// received settings.
+
+// SSL_add_application_settings configures |ssl| to enable ALPS with ALPN
+// protocol |proto|, sending an ALPS value of |settings|. It returns one on
+// success and zero on error. If |proto| is negotiated via ALPN and the peer
+// supports ALPS, |settings| will be sent to the peer. The peer's ALPS value can
+// be retrieved with |SSL_get0_peer_application_settings|.
+//
+// On the client, this function should be called before the handshake, once for
+// each supported ALPN protocol which uses ALPS. |proto| must be included in the
+// client's ALPN configuration (see |SSL_CTX_set_alpn_protos| and
+// |SSL_set_alpn_protos|). On the server, ALPS can be preconfigured for each
+// protocol as in the client, or configuration can be deferred to the ALPN
+// callback (see |SSL_CTX_set_alpn_select_cb|), in which case only the selected
+// protocol needs to be configured.
+//
+// ALPS can be independently configured from 0-RTT, however changes in protocol
+// settings will fallback to 1-RTT to negotiate the new value, so it is
+// recommended for |settings| to be relatively stable.
+OPENSSL_EXPORT int SSL_add_application_settings(SSL *ssl, const uint8_t *proto,
+                                                size_t proto_len,
+                                                const uint8_t *settings,
+                                                size_t settings_len);
+
+// SSL_get0_peer_application_settings sets |*out_data| and |*out_len| to a
+// buffer containing the peer's ALPS value, or the empty string if ALPS was not
+// negotiated. Note an empty string could also indicate the peer sent an empty
+// settings value. Use |SSL_has_application_settings| to check if ALPS was
+// negotiated. The output buffer is owned by |ssl| and is valid until the next
+// time |ssl| is modified.
+OPENSSL_EXPORT void SSL_get0_peer_application_settings(const SSL *ssl,
+                                                       const uint8_t **out_data,
+                                                       size_t *out_len);
+
+// SSL_has_application_settings returns one if ALPS was negotiated on this
+// connection and zero otherwise.
+OPENSSL_EXPORT int SSL_has_application_settings(const SSL *ssl);
+
+
 // Certificate compression.
 //
 // Certificates in TLS 1.3 can be compressed[1]. BoringSSL supports this as both
@@ -3493,8 +3538,10 @@
   ssl_early_data_ticket_age_skew = 12,
   // QUIC parameters differ between this connection and the original.
   ssl_early_data_quic_parameter_mismatch = 13,
+  // The application settings did not match the session.
+  ssl_early_data_alps_mismatch = 14,
   // The value of the largest entry.
-  ssl_early_data_reason_max_value = ssl_early_data_quic_parameter_mismatch,
+  ssl_early_data_reason_max_value = ssl_early_data_alps_mismatch,
 };
 
 // SSL_get_early_data_reason returns details why 0-RTT was accepted or rejected
@@ -5217,6 +5264,8 @@
 #define SSL_R_QUIC_TRANSPORT_PARAMETERS_MISCONFIGURED 305
 #define SSL_R_UNEXPECTED_COMPATIBILITY_MODE 306
 #define SSL_R_MISSING_ALPN 307
+#define SSL_R_NEGOTIATED_ALPS_WITHOUT_ALPN 308
+#define SSL_R_ALPS_MISMATCH_ON_EARLY_DATA 309
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 13545dd..1514eca 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -235,6 +235,10 @@
 // ExtensionType value from draft-ietf-tls-subcerts.
 #define TLSEXT_TYPE_delegated_credential 0x22
 
+// ExtensionType value from draft-vvv-tls-alps. This is not an IANA defined
+// extension number.
+#define TLSEXT_TYPE_application_settings 17513
+
 // ExtensionType value from RFC6962
 #define TLSEXT_TYPE_certificate_timestamp 18
 
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: