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()) {