Add SSL_get_early_data_reason. This is to help servers diagnose 0-RTT rejects. (QUIC has a similar feature, and this will help determine if we need to adjust the ticket age skew.) Bug: 113 Change-Id: Icc7e5df326b5fa82e744605021b1205298efba6a Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/35885 Reviewed-by: Adam Langley <agl@google.com>
diff --git a/include/openssl/base.h b/include/openssl/base.h index 41e9305..cb1affa 100644 --- a/include/openssl/base.h +++ b/include/openssl/base.h
@@ -297,13 +297,14 @@ // to do this only for C++. However, the ABI type between C and C++ need to have // equal sizes, which is confirmed in a unittest. #define BORINGSSL_ENUM_INT : int -enum ssl_private_key_result_t BORINGSSL_ENUM_INT; -enum ssl_ticket_aead_result_t BORINGSSL_ENUM_INT; -enum ssl_verify_result_t BORINGSSL_ENUM_INT; +enum ssl_early_data_reason_t BORINGSSL_ENUM_INT; enum ssl_encryption_level_t BORINGSSL_ENUM_INT; +enum ssl_private_key_result_t BORINGSSL_ENUM_INT; enum ssl_renegotiate_mode_t BORINGSSL_ENUM_INT; enum ssl_select_cert_result_t BORINGSSL_ENUM_INT; enum ssl_select_cert_result_t BORINGSSL_ENUM_INT; +enum ssl_ticket_aead_result_t BORINGSSL_ENUM_INT; +enum ssl_verify_result_t BORINGSSL_ENUM_INT; #else #define BORINGSSL_ENUM_INT #endif
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h index 8e02084..b936efa 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h
@@ -3311,6 +3311,44 @@ SSL *ssl, uint8_t *out, size_t out_len, const char *label, size_t label_len, const uint8_t *context, size_t context_len); +// SSL_get_ticket_age_skew returns the difference, in seconds, between the +// client-sent ticket age and the server-computed value in TLS 1.3 server +// connections which resumed a session. +OPENSSL_EXPORT int32_t SSL_get_ticket_age_skew(const SSL *ssl); + +enum ssl_early_data_reason_t BORINGSSL_ENUM_INT { + // The handshake has not progressed far enough for the 0-RTT status to be + // known. + ssl_early_data_unknown, + // 0-RTT is disabled for this connection. + ssl_early_data_disabled, + // 0-RTT was accepted. + ssl_early_data_accepted, + // The negotiated protocol version does not support 0-RTT. + ssl_early_data_protocol_version, + // The peer declined to offer or accept 0-RTT for an unknown reason. + ssl_early_data_peer_declined, + // The client did not offer a session. + ssl_early_data_no_session_offered, + // The server declined to resume the session. + ssl_early_data_session_not_resumed, + // The session does not support 0-RTT. + ssl_early_data_unsupported_for_session, + // The server sent a HelloRetryRequest. + ssl_early_data_hello_retry_request, + // The negotiated ALPN protocol did not match the session. + ssl_early_data_alpn_mismatch, + // The connection negotiated Channel ID, which is incompatible with 0-RTT. + ssl_early_data_channel_id, + // The connection negotiated token binding, which is incompatible with 0-RTT. + ssl_early_data_token_binding, +}; + +// SSL_get_early_data_reason returns details why 0-RTT was accepted or rejected +// on |ssl|. This is primarily useful on the server. +OPENSSL_EXPORT enum ssl_early_data_reason_t SSL_get_early_data_reason( + const SSL *ssl); + // Alerts. // @@ -3815,11 +3853,6 @@ // record with |ssl|. OPENSSL_EXPORT size_t SSL_max_seal_overhead(const SSL *ssl); -// SSL_get_ticket_age_skew returns the difference, in seconds, between the -// client-sent ticket age and the server-computed value in TLS 1.3 server -// connections which resumed a session. -OPENSSL_EXPORT int32_t SSL_get_ticket_age_skew(const SSL *ssl); - // SSL_CTX_set_false_start_allowed_without_alpn configures whether connections // on |ctx| may use False Start (if |SSL_MODE_ENABLE_FALSE_START| is enabled) // without negotiating ALPN.
diff --git a/ssl/handoff.cc b/ssl/handoff.cc index 0928015..db5886a 100644 --- a/ssl/handoff.cc +++ b/ssl/handoff.cc
@@ -450,6 +450,10 @@ s3->aead_write_ctx->SetVersionIfNullCipher(ssl->version); s3->hs->cert_request = cert_request; + // TODO(davidben): When handoff for TLS 1.3 is added, serialize + // |early_data_reason| and stabilize the constants. + s3->early_data_reason = ssl_early_data_protocol_version; + Array<uint8_t> key_block; if ((type == handback_after_session_resumption || type == handback_after_handshake) &&
diff --git a/ssl/handshake.cc b/ssl/handshake.cc index 89be48f..b8e0070 100644 --- a/ssl/handshake.cc +++ b/ssl/handshake.cc
@@ -648,6 +648,7 @@ return -1; case ssl_hs_early_data_rejected: + assert(ssl->s3->early_data_reason != ssl_early_data_unknown); ssl->s3->rwstate = SSL_EARLY_DATA_REJECTED; // Cause |SSL_write| to start failing immediately. hs->can_early_write = false;
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc index 4622ad0..f7e5df7 100644 --- a/ssl/handshake_server.cc +++ b/ssl/handshake_server.cc
@@ -635,6 +635,8 @@ return ssl_hs_ok; } + ssl->s3->early_data_reason = ssl_early_data_protocol_version; + SSL_CLIENT_HELLO client_hello; if (!ssl_client_hello_init(ssl, &client_hello, msg)) { return ssl_hs_error;
diff --git a/ssl/internal.h b/ssl/internal.h index 22e12b5..2a173c1 100644 --- a/ssl/internal.h +++ b/ssl/internal.h
@@ -2266,6 +2266,9 @@ // which resumed a session. int32_t ticket_age_skew = 0; + // ssl_early_data_reason stores details on why 0-RTT was accepted or rejected. + enum ssl_early_data_reason_t early_data_reason = ssl_early_data_unknown; + // aead_read_ctx is the current read cipher state. UniquePtr<SSLAEADContext> aead_read_ctx;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc index f9910f7..95177e0 100644 --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc
@@ -1294,6 +1294,10 @@ ssl->s3->wpend_pending = false; } +enum ssl_early_data_reason_t SSL_get_early_data_reason(const SSL *ssl) { + return ssl->s3->early_data_reason; +} + static int bio_retry_reason_to_error(int reason) { switch (reason) { case BIO_RR_CONNECT:
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc index add2eb7..f79ce9b 100644 --- a/ssl/t1_lib.cc +++ b/ssl/t1_lib.cc
@@ -2057,20 +2057,46 @@ static bool ext_early_data_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) { SSL *const ssl = hs->ssl; - if (!ssl->enable_early_data || - // Session must be 0-RTT capable. - ssl->session == nullptr || - ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION || - ssl->session->ticket_max_early_data == 0 || - // The second ClientHello never offers early data. - hs->received_hello_retry_request || - // In case ALPN preferences changed since this session was established, - // avoid reporting a confusing value in |SSL_get0_alpn_selected|. - (!ssl->session->early_alpn.empty() && - !ssl_is_alpn_protocol_allowed(hs, ssl->session->early_alpn))) { + // The second ClientHello never offers early data, and we must have already + // filled in |early_data_reason| by this point. + if (hs->received_hello_retry_request) { + assert(ssl->s3->early_data_reason != ssl_early_data_unknown); return true; } + if (!ssl->enable_early_data) { + ssl->s3->early_data_reason = ssl_early_data_disabled; + return true; + } + + if (hs->max_version < TLS1_3_VERSION) { + // We discard inapplicable sessions, so this is redundant with the session + // checks below, but we check give a more useful reason. + ssl->s3->early_data_reason = ssl_early_data_protocol_version; + return true; + } + + if (ssl->session == nullptr) { + ssl->s3->early_data_reason = ssl_early_data_no_session_offered; + return true; + } + + if (ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION || + ssl->session->ticket_max_early_data == 0) { + ssl->s3->early_data_reason = ssl_early_data_unsupported_for_session; + 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; + } + + // |early_data_reason| will be filled in later when the server responds. hs->early_data_offered = true; if (!CBB_add_u16(out, TLSEXT_TYPE_early_data) || @@ -2083,12 +2109,27 @@ } static bool ext_early_data_parse_serverhello(SSL_HANDSHAKE *hs, - uint8_t *out_alert, CBS *contents) { + uint8_t *out_alert, + CBS *contents) { SSL *const ssl = hs->ssl; if (contents == NULL) { + if (hs->early_data_offered && !hs->received_hello_retry_request) { + ssl->s3->early_data_reason = ssl->s3->session_reused + ? ssl_early_data_peer_declined + : ssl_early_data_session_not_resumed; + } else { + // We already filled in |early_data_reason| when declining to offer 0-RTT + // or handling the implicit HelloRetryRequest reject. + assert(ssl->s3->early_data_reason != ssl_early_data_unknown); + } return true; } + // If we received an HRR, the second ClientHello never offers early data, so + // the extensions logic will automatically reject early data extensions as + // unsolicited. This covered by the ServerAcceptsEarlyDataOnHRR test. + assert(!hs->received_hello_retry_request); + if (CBS_len(contents) != 0) { *out_alert = SSL_AD_DECODE_ERROR; return false; @@ -2100,6 +2141,7 @@ return false; } + ssl->s3->early_data_reason = ssl_early_data_accepted; ssl->s3->early_data_accepted = true; return true; } @@ -3061,6 +3103,9 @@ return false; } + // Note we may send multiple ClientHellos for DTLS HelloVerifyRequest and TLS + // 1.3 HelloRetryRequest. For the latter, the extensions may change, so it is + // important to reset this value. hs->extensions.sent = 0; for (size_t i = 0; i < kNumExtensions; i++) {
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc index d61a2a6..e7e5dec 100644 --- a/ssl/test/bssl_shim.cc +++ b/ssl/test/bssl_shim.cc
@@ -389,6 +389,37 @@ return true; } +static const char *EarlyDataReasonToString(ssl_early_data_reason_t reason) { + switch (reason) { + case ssl_early_data_unknown: + return "unknown"; + case ssl_early_data_disabled: + return "disabled"; + case ssl_early_data_accepted: + return "accepted"; + case ssl_early_data_protocol_version: + return "protocol_version"; + case ssl_early_data_peer_declined: + return "peer_declined"; + case ssl_early_data_no_session_offered: + return "no_session_offered"; + case ssl_early_data_session_not_resumed: + return "session_not_resumed"; + case ssl_early_data_unsupported_for_session: + return "unsupported_for_session"; + case ssl_early_data_hello_retry_request: + return "hello_retry_request"; + case ssl_early_data_alpn_mismatch: + return "alpn_mismatch"; + case ssl_early_data_channel_id: + return "channel_id"; + case ssl_early_data_token_binding: + return "token_binding"; + } + + abort(); +} + // CheckHandshakeProperties checks, immediately after |ssl| completes its // initial handshake (or False Starts), whether all the properties are // consistent with the test configuration and invariants. @@ -596,6 +627,15 @@ SSL_early_data_accepted(ssl) ? "" : " not"); return false; } + + const char *early_data_reason = + EarlyDataReasonToString(SSL_get_early_data_reason(ssl)); + if (!config->expect_early_data_reason.empty() && + config->expect_early_data_reason != early_data_reason) { + fprintf(stderr, "Early data reason was \"%s\", expected \"%s\"\n", + early_data_reason, config->expect_early_data_reason.c_str()); + return false; + } } if (!config->psk.empty()) { @@ -702,6 +742,16 @@ return false; } + // Client pre- and post-0-RTT reject states are considered logically + // different connections with different test expections. Check that the test + // did not mistakenly configure reason expectations on the wrong one. + if (!config->expect_early_data_reason.empty()) { + fprintf(stderr, + "Test error: client reject -expect-early-data-reason flags " + "should be configured with -on-retry, not -on-resume.\n"); + return false; + } + // Reset the connection and try again at 1-RTT. SSL_reset_early_data_reject(ssl.get()); GetTestState(ssl.get())->cert_verified = false;
diff --git a/ssl/test/runner/fuzzer_mode.json b/ssl/test/runner/fuzzer_mode.json index 1a154c2..78639e2 100644 --- a/ssl/test/runner/fuzzer_mode.json +++ b/ssl/test/runner/fuzzer_mode.json
@@ -30,6 +30,7 @@ "Resume-Server-DeclineCrossVersion*": "Fuzzer mode does not encrypt tickets.", "TicketCallback-SingleCall-*": "Fuzzer mode does not encrypt tickets.", "CorruptTicket-*": "Fuzzer mode does not encrypt tickets.", + "*RejectTicket-Server-*": "Fuzzer mode does not encrypt tickets.", "ShimTicketRewritable*": "Fuzzer mode does not encrypt tickets.", "Resume-Server-*Binder*": "Fuzzer mode does not check binders.",
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go index d27b094..f5f0285 100644 --- a/ssl/test/runner/runner.go +++ b/ssl/test/runner/runner.go
@@ -4466,6 +4466,8 @@ }, resumeSession: true, resumeRenewedSession: true, + // 0-RTT being disabled overrides all other 0-RTT reasons. + flags: []string{"-expect-early-data-reason", "disabled"}, }) tests = append(tests, testCase{ @@ -4477,9 +4479,13 @@ }, resumeSession: true, resumeRenewedSession: true, - // TLS 1.3 uses tickets, so the session should not be - // cached statefully. - flags: []string{"-expect-no-session-id"}, + flags: []string{ + // TLS 1.3 uses tickets, so the session should not be + // cached statefully. + "-expect-no-session-id", + // 0-RTT being disabled overrides all other 0-RTT reasons. + "-expect-early-data-reason", "disabled", + }, }) tests = append(tests, testCase{ @@ -7018,6 +7024,8 @@ "-expect-ticket-supports-early-data", "-token-binding-params", base64.StdEncoding.EncodeToString([]byte{2, 1, 0}), + "-expect-reject-early-data", + "-on-retry-expect-early-data-reason", "token_binding", }, }) } @@ -12329,7 +12337,9 @@ flags: []string{ "-enable-early-data", "-expect-ticket-supports-early-data", + "-on-initial-expect-early-data-reason", "no_session_offered", "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", "-on-resume-shim-writes-first", }, }) @@ -12354,6 +12364,7 @@ "-expect-ticket-supports-early-data", "-expect-reject-early-data", "-on-resume-shim-writes-first", + "-on-retry-expect-early-data-reason", "peer_declined", }, }) @@ -12373,10 +12384,14 @@ resumeSession: true, flags: []string{ "-enable-early-data", + "-on-initial-expect-early-data-reason", "no_session_offered", "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", }, }) + // The above tests the most recent ticket. Additionally test that 0-RTT + // works on the first ticket issued by the server. testCases = append(testCases, testCase{ testType: serverTest, name: "EarlyData-FirstTicket-Server-TLS13", @@ -12395,6 +12410,7 @@ flags: []string{ "-enable-early-data", "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", }, }) @@ -12463,6 +12479,9 @@ }, DefaultCurves: []CurveID{}, }, + // Though the session is not resumed and we send HelloRetryRequest, + // early data being disabled takes priority as the reject reason. + flags: []string{"-expect-early-data-reason", "disabled"}, }) testCases = append(testCases, testCase{ @@ -12979,6 +12998,8 @@ }, }, }) + + // Test the client handles 0-RTT being rejected by a full handshake. testCases = append(testCases, testCase{ testType: clientTest, name: "EarlyData-RejectTicket-Client-TLS13", @@ -13000,6 +13021,9 @@ "-expect-ticket-supports-early-data", "-expect-reject-early-data", "-on-resume-shim-writes-first", + "-on-retry-expect-early-data-reason", "session_not_resumed", + // Test the peer certificate is reported correctly in each of the + // three logical connections. "-on-initial-expect-peer-cert-file", path.Join(*resourceDir, rsaCertificateFile), "-on-resume-expect-peer-cert-file", path.Join(*resourceDir, rsaCertificateFile), "-on-retry-expect-peer-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile), @@ -13008,6 +13032,34 @@ }, }) + // Test the server rejects 0-RTT if it does not recognize the ticket. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "EarlyData-RejectTicket-Server-TLS13", + config: Config{ + MaxVersion: VersionTLS13, + MinVersion: VersionTLS13, + Bugs: ProtocolBugs{ + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: false, + // Corrupt the ticket. + FilterTicket: func(in []byte) ([]byte, error) { + in[len(in)-1] ^= 1 + return in, nil + }, + }, + }, + messageCount: 2, + resumeSession: true, + expectResumeRejected: true, + flags: []string{ + "-enable-early-data", + "-on-resume-expect-reject-early-data", + "-on-resume-expect-early-data-reason", "session_not_resumed", + }, + }) + + // Test the client handles 0-RTT being rejected via a HelloRetryRequest. testCases = append(testCases, testCase{ testType: clientTest, name: "EarlyData-HRR-Client-TLS13", @@ -13027,6 +13079,99 @@ "-enable-early-data", "-expect-ticket-supports-early-data", "-expect-reject-early-data", + "-on-retry-expect-early-data-reason", "hello_retry_request", + }, + }) + + // Test the server rejects 0-RTT if it needs to send a HelloRetryRequest. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "EarlyData-HRR-Server-TLS13", + config: Config{ + MaxVersion: VersionTLS13, + MinVersion: VersionTLS13, + // Require a HelloRetryRequest for every curve. + DefaultCurves: []CurveID{}, + Bugs: ProtocolBugs{ + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: false, + }, + }, + messageCount: 2, + resumeSession: true, + flags: []string{ + "-enable-early-data", + "-on-resume-expect-reject-early-data", + "-on-resume-expect-early-data-reason", "hello_retry_request", + }, + }) + + // Test the client handles a 0-RTT reject from both ticket rejection and + // HelloRetryRequest. + testCases = append(testCases, testCase{ + testType: clientTest, + name: "EarlyData-HRR-RejectTicket-Client-TLS13", + config: Config{ + MaxVersion: VersionTLS13, + MaxEarlyDataSize: 16384, + Certificates: []Certificate{rsaCertificate}, + }, + resumeConfig: &Config{ + MaxVersion: VersionTLS13, + MaxEarlyDataSize: 16384, + Certificates: []Certificate{ecdsaP256Certificate}, + SessionTicketsDisabled: true, + Bugs: ProtocolBugs{ + SendHelloRetryRequestCookie: []byte{1, 2, 3, 4}, + }, + }, + resumeSession: true, + expectResumeRejected: true, + flags: []string{ + "-enable-early-data", + "-expect-ticket-supports-early-data", + "-expect-reject-early-data", + // The client sees HelloRetryRequest before the resumption result, + // though neither value is inherently preferable. + "-on-retry-expect-early-data-reason", "hello_retry_request", + // Test the peer certificate is reported correctly in each of the + // three logical connections. + "-on-initial-expect-peer-cert-file", path.Join(*resourceDir, rsaCertificateFile), + "-on-resume-expect-peer-cert-file", path.Join(*resourceDir, rsaCertificateFile), + "-on-retry-expect-peer-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile), + // Session tickets are disabled, so the runner will not send a ticket. + "-on-retry-expect-no-session", + }, + }) + + // Test the server rejects 0-RTT if it needs to send a HelloRetryRequest. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "EarlyData-HRR-RejectTicket-Server-TLS13", + config: Config{ + MaxVersion: VersionTLS13, + MinVersion: VersionTLS13, + // Require a HelloRetryRequest for every curve. + DefaultCurves: []CurveID{}, + Bugs: ProtocolBugs{ + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: false, + // Corrupt the ticket. + FilterTicket: func(in []byte) ([]byte, error) { + in[len(in)-1] ^= 1 + return in, nil + }, + }, + }, + messageCount: 2, + resumeSession: true, + expectResumeRejected: true, + flags: []string{ + "-enable-early-data", + "-on-resume-expect-reject-early-data", + // The server sees the missed resumption before HelloRetryRequest, + // though neither value is inherently preferable. + "-on-resume-expect-early-data-reason", "session_not_resumed", }, }) @@ -13192,6 +13337,10 @@ "-enable-early-data", "-expect-ticket-supports-early-data", "-expect-reject-early-data", + // The client does not learn ALPN was the cause. + "-on-retry-expect-early-data-reason", "peer_declined", + // In the 0-RTT state, we surface the predicted ALPN. After + // processing the reject, we surface the real one. "-on-initial-expect-alpn", "foo", "-on-resume-expect-alpn", "foo", "-on-retry-expect-alpn", "bar", @@ -13218,6 +13367,10 @@ "-enable-early-data", "-expect-ticket-supports-early-data", "-expect-reject-early-data", + // The client does not learn ALPN was the cause. + "-on-retry-expect-early-data-reason", "peer_declined", + // In the 0-RTT state, we surface the predicted ALPN. After + // processing the reject, we surface the real one. "-on-initial-expect-alpn", "", "-on-resume-expect-alpn", "", "-on-retry-expect-alpn", "foo", @@ -13245,6 +13398,10 @@ "-enable-early-data", "-expect-ticket-supports-early-data", "-expect-reject-early-data", + // The client does not learn ALPN was the cause. + "-on-retry-expect-early-data-reason", "peer_declined", + // In the 0-RTT state, we surface the predicted ALPN. After + // processing the reject, we surface the real one. "-on-initial-expect-alpn", "foo", "-on-resume-expect-alpn", "foo", "-on-retry-expect-alpn", "", @@ -13299,10 +13456,31 @@ "-enable-early-data", "-expect-ticket-supports-early-data", "-expect-no-offer-early-data", + // Offer different ALPN values in the initial and resumption. "-on-initial-advertise-alpn", "\x03foo", - "-on-resume-advertise-alpn", "\x03bar", "-on-initial-expect-alpn", "foo", + "-on-resume-advertise-alpn", "\x03bar", "-on-resume-expect-alpn", "bar", + // The ALPN mismatch comes from the client, so it reports it as the + // reason. + "-on-resume-expect-early-data-reason", "alpn_mismatch", + }, + }) + + // Test that the client does not offer 0-RTT to servers which never + // advertise it. + testCases = append(testCases, testCase{ + testType: clientTest, + name: "EarlyData-NonZeroRTTSession-Client-TLS13", + config: Config{ + MaxVersion: VersionTLS13, + }, + resumeSession: true, + flags: []string{ + "-enable-early-data", + "-on-resume-expect-no-offer-early-data", + // The client declines to offer 0-RTT because of the session. + "-on-resume-expect-early-data-reason", "unsupported_for_session", }, }) @@ -13325,6 +13503,8 @@ flags: []string{ "-on-resume-enable-early-data", "-expect-reject-early-data", + // The server rejects 0-RTT because of the session. + "-on-resume-expect-early-data-reason", "unsupported_for_session", }, }) @@ -13350,6 +13530,7 @@ "-enable-early-data", "-on-initial-select-alpn", "", "-on-resume-select-alpn", "foo", + "-on-resume-expect-early-data-reason", "alpn_mismatch", }, }) @@ -13375,6 +13556,7 @@ "-enable-early-data", "-on-initial-select-alpn", "foo", "-on-resume-select-alpn", "", + "-on-resume-expect-early-data-reason", "alpn_mismatch", }, }) @@ -13399,6 +13581,7 @@ "-enable-early-data", "-on-initial-select-alpn", "foo", "-on-resume-select-alpn", "bar", + "-on-resume-expect-early-data-reason", "alpn_mismatch", }, }) @@ -13443,6 +13626,8 @@ "-expect-ticket-supports-early-data", "-send-channel-id", path.Join(*resourceDir, channelIDKeyFile), "-expect-reject-early-data", + // The client never learns the reason was Channel ID. + "-on-retry-expect-early-data-reason", "peer_declined", }, }) @@ -13461,6 +13646,7 @@ "-expect-ticket-supports-early-data", "-send-channel-id", path.Join(*resourceDir, channelIDKeyFile), "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", }, }) @@ -13484,6 +13670,7 @@ "-expect-reject-early-data", "-expect-channel-id", base64.StdEncoding.EncodeToString(channelIDBytes), + "-on-resume-expect-early-data-reason", "channel_id", }, }) @@ -13506,6 +13693,7 @@ "-enable-early-data", "-on-resume-expect-accept-early-data", "-enable-channel-id", + "-on-resume-expect-early-data-reason", "accept", }, }) @@ -13687,6 +13875,45 @@ expectedLocalError: "remote error: unexpected message", }) + // If the client or server has 0-RTT enabled but disabled TLS 1.3, it should + // report a reason of protocol_version. + testCases = append(testCases, testCase{ + testType: clientTest, + name: "EarlyDataEnabled-Client-MaxTLS12", + expectedVersion: VersionTLS12, + flags: []string{ + "-enable-early-data", + "-max-version", strconv.Itoa(VersionTLS12), + "-expect-early-data-reason", "protocol_version", + }, + }) + testCases = append(testCases, testCase{ + testType: serverTest, + name: "EarlyDataEnabled-Server-MaxTLS12", + expectedVersion: VersionTLS12, + flags: []string{ + "-enable-early-data", + "-max-version", strconv.Itoa(VersionTLS12), + "-expect-early-data-reason", "protocol_version", + }, + }) + + // The server additionally reports protocol_version if it enabled TLS 1.3, + // but the peer negotiated TLS 1.2. (The corresponding situation does not + // exist on the client because negotiating TLS 1.2 with a 0-RTT ClientHello + // is a fatal error.) + testCases = append(testCases, testCase{ + testType: serverTest, + name: "EarlyDataEnabled-Server-NegotiateTLS12", + config: Config{ + MaxVersion: VersionTLS12, + }, + expectedVersion: VersionTLS12, + flags: []string{ + "-enable-early-data", + "-expect-early-data-reason", "protocol_version", + }, + }) } func addTLS13CipherPreferenceTests() {
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc index 70e061b..4c43be8 100644 --- a/ssl/test/test_config.cc +++ b/ssl/test/test_config.cc
@@ -179,6 +179,7 @@ { "-expect-msg-callback", &TestConfig::expect_msg_callback }, { "-handshaker-path", &TestConfig::handshaker_path }, { "-delegated-credential", &TestConfig::delegated_credential }, + { "-expect-early-data-reason", &TestConfig::expect_early_data_reason }, }; const Flag<std::string> kBase64Flags[] = {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h index 9221d6f..e0ea7a7 100644 --- a/ssl/test/test_config.h +++ b/ssl/test/test_config.h
@@ -174,6 +174,7 @@ bool export_traffic_secrets = false; bool key_update = false; std::string delegated_credential; + std::string expect_early_data_reason; int argc; char **argv;
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc index ac97165..80918ad 100644 --- a/ssl/tls13_client.cc +++ b/ssl/tls13_client.cc
@@ -188,6 +188,7 @@ hs->tls13_state = state_send_second_client_hello; // 0-RTT is rejected if we receive a HelloRetryRequest. if (hs->in_early_data) { + ssl->s3->early_data_reason = ssl_early_data_hello_retry_request; return ssl_hs_early_data_rejected; } return ssl_hs_ok;
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc index 13a89c0..82c9689 100644 --- a/ssl/tls13_server.cc +++ b/ssl/tls13_server.cc
@@ -307,16 +307,15 @@ static enum ssl_ticket_aead_result_t select_session( SSL_HANDSHAKE *hs, uint8_t *out_alert, UniquePtr<SSL_SESSION> *out_session, - int32_t *out_ticket_age_skew, const SSLMessage &msg, - const SSL_CLIENT_HELLO *client_hello) { + int32_t *out_ticket_age_skew, bool *out_offered_ticket, + const SSLMessage &msg, const SSL_CLIENT_HELLO *client_hello) { SSL *const ssl = hs->ssl; - *out_session = NULL; + *out_session = nullptr; - // Decode the ticket if we agreed on a PSK key exchange mode. CBS pre_shared_key; - if (!hs->accept_psk_mode || - !ssl_client_hello_get_extension(client_hello, &pre_shared_key, - TLSEXT_TYPE_pre_shared_key)) { + *out_offered_ticket = ssl_client_hello_get_extension( + client_hello, &pre_shared_key, TLSEXT_TYPE_pre_shared_key); + if (!*out_offered_ticket) { return ssl_ticket_aead_ignore_ticket; } @@ -337,6 +336,11 @@ return ssl_ticket_aead_error; } + // If the peer did not offer psk_dhe, ignore the resumption. + if (!hs->accept_psk_mode) { + return ssl_ticket_aead_ignore_ticket; + } + // TLS 1.3 session tickets are renewed separately as part of the // NewSessionTicket. bool unused_renew; @@ -406,10 +410,18 @@ uint8_t alert = SSL_AD_DECODE_ERROR; UniquePtr<SSL_SESSION> session; - switch (select_session(hs, &alert, &session, &ssl->s3->ticket_age_skew, msg, - &client_hello)) { + bool offered_ticket = false; + switch (select_session(hs, &alert, &session, &ssl->s3->ticket_age_skew, + &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; @@ -422,17 +434,23 @@ hs->new_session = SSL_SESSION_dup(session.get(), SSL_SESSION_DUP_AUTH_ONLY); - if (ssl->enable_early_data && - // Early data must be acceptable for this ticket. - session->ticket_max_early_data != 0 && - // The client must have offered early data. - hs->early_data_offered && + 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->channel_id_valid && - // If Token Binding is negotiated, reject 0-RTT. - !ssl->s3->token_binding_negotiated && - // The negotiated ALPN must match the one in the ticket. - MakeConstSpan(ssl->s3->alpn_selected) == session->early_alpn) { + 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 { + ssl->s3->early_data_reason = ssl_early_data_accepted; ssl->s3->early_data_accepted = true; } @@ -499,7 +517,10 @@ bool need_retry; if (!resolve_ecdhe_secret(hs, &need_retry, &client_hello)) { if (need_retry) { - ssl->s3->early_data_accepted = false; + if (ssl->s3->early_data_accepted) { + ssl->s3->early_data_reason = ssl_early_data_hello_retry_request; + ssl->s3->early_data_accepted = false; + } ssl->s3->skip_early_data = true; ssl->method->next_message(ssl); if (!hs->transcript.UpdateForHelloRetryRequest()) {