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: