Enforce the ticket_age parameter for 0-RTT. For now just hard-code a tolerance of 1 minute. SSL_get_early_data_reason and SSL_get_ticket_age_skew will allow us to tune this. Bug: 113 Change-Id: I85a530494d5405a3e11198d49bfa9cfd355f4f35 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/35886 Reviewed-by: Adam Langley <agl@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h index b936efa..63350ea 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h
@@ -3342,6 +3342,8 @@ ssl_early_data_channel_id, // The connection negotiated token binding, which is incompatible with 0-RTT. ssl_early_data_token_binding, + // The client and server ticket age were too far apart. + ssl_early_data_ticket_age_skew, }; // SSL_get_early_data_reason returns details why 0-RTT was accepted or rejected
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc index e7e5dec..ea9516b 100644 --- a/ssl/test/bssl_shim.cc +++ b/ssl/test/bssl_shim.cc
@@ -415,6 +415,8 @@ return "channel_id"; case ssl_early_data_token_binding: return "token_binding"; + case ssl_early_data_ticket_age_skew: + return "ticket_age_skew"; } abort();
diff --git a/ssl/test/runner/fuzzer_mode.json b/ssl/test/runner/fuzzer_mode.json index 78639e2..836b7ba 100644 --- a/ssl/test/runner/fuzzer_mode.json +++ b/ssl/test/runner/fuzzer_mode.json
@@ -47,6 +47,7 @@ "*-EarlyData-RejectUnfinishedWrite-Client-*": "Trial decryption does not work with the NULL cipher.", "EarlyData-Reject*-Client-*": "Trial decryption does not work with the NULL cipher.", "CustomExtensions-Server-EarlyDataOffered": "Trial decryption does not work with the NULL cipher.", + "*-TicketAgeSkew-*-Reject": "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/runner.go b/ssl/test/runner/runner.go index f5f0285..de65b28 100644 --- a/ssl/test/runner/runner.go +++ b/ssl/test/runner/runner.go
@@ -11330,6 +11330,96 @@ }, }) + // Test that ticket age skew up to 60 seconds in either direction is accepted. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "TLS13-TicketAgeSkew-Forward-60-Accept", + config: Config{ + MaxVersion: VersionTLS13, + Bugs: ProtocolBugs{ + SendTicketAge: 70 * time.Second, + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: true, + ExpectHalfRTTData: [][]byte{{254, 253, 252, 251}}, + }, + }, + resumeSession: true, + flags: []string{ + "-resumption-delay", "10", + "-expect-ticket-age-skew", "60", + // 0-RTT is accepted. + "-enable-early-data", + "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", + }, + }) + testCases = append(testCases, testCase{ + testType: serverTest, + name: "TLS13-TicketAgeSkew-Backward-60-Accept", + config: Config{ + MaxVersion: VersionTLS13, + Bugs: ProtocolBugs{ + SendTicketAge: 10 * time.Second, + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: true, + ExpectHalfRTTData: [][]byte{{254, 253, 252, 251}}, + }, + }, + resumeSession: true, + flags: []string{ + "-resumption-delay", "70", + "-expect-ticket-age-skew", "-60", + // 0-RTT is accepted. + "-enable-early-data", + "-on-resume-expect-accept-early-data", + "-on-resume-expect-early-data-reason", "accept", + }, + }) + + // Test that ticket age skew beyond 60 seconds in either direction is rejected. + testCases = append(testCases, testCase{ + testType: serverTest, + name: "TLS13-TicketAgeSkew-Forward-61-Reject", + config: Config{ + MaxVersion: VersionTLS13, + Bugs: ProtocolBugs{ + SendTicketAge: 71 * time.Second, + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: false, + }, + }, + resumeSession: true, + flags: []string{ + "-resumption-delay", "10", + "-expect-ticket-age-skew", "61", + // 0-RTT is rejected. + "-enable-early-data", + "-expect-reject-early-data", + "-on-resume-expect-early-data-reason", "ticket_age_skew", + }, + }) + testCases = append(testCases, testCase{ + testType: serverTest, + name: "TLS13-TicketAgeSkew-Backward-61-Reject", + config: Config{ + MaxVersion: VersionTLS13, + Bugs: ProtocolBugs{ + SendTicketAge: 10 * time.Second, + SendEarlyData: [][]byte{{1, 2, 3, 4}}, + ExpectEarlyDataAccepted: false, + }, + }, + resumeSession: true, + flags: []string{ + "-resumption-delay", "71", + "-expect-ticket-age-skew", "-61", + // 0-RTT is rejected. + "-enable-early-data", + "-expect-reject-early-data", + "-on-resume-expect-early-data-reason", "ticket_age_skew", + }, + }) + testCases = append(testCases, testCase{ testType: clientTest, name: "TLS13-SendTicketEarlyDataInfo",
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc index 82c9689..8f31704 100644 --- a/ssl/tls13_server.cc +++ b/ssl/tls13_server.cc
@@ -53,6 +53,12 @@ static const uint8_t kZeroes[EVP_MAX_MD_SIZE] = {0}; +// Allow a minute of ticket age skew in either direction. This covers +// transmission delays in ClientHello and NewSessionTicket, as well as +// drift between client and server clock rate since the ticket was issued. +// See RFC 8446, section 8.3. +static const int32_t kMaxTicketAgeSkewSeconds = 60; + static int resolve_ecdhe_secret(SSL_HANDSHAKE *hs, bool *out_need_retry, SSL_CLIENT_HELLO *client_hello) { SSL *const ssl = hs->ssl; @@ -433,6 +439,10 @@ // a fresh session. hs->new_session = SSL_SESSION_dup(session.get(), SSL_SESSION_DUP_AUTH_ONLY); + if (hs->new_session == nullptr) { + ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR); + return ssl_hs_error; + } if (!ssl->enable_early_data) { ssl->s3->early_data_reason = ssl_early_data_disabled; @@ -449,16 +459,14 @@ } 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 { ssl->s3->early_data_reason = ssl_early_data_accepted; ssl->s3->early_data_accepted = true; } - if (hs->new_session == NULL) { - ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR); - return ssl_hs_error; - } - ssl->s3->session_reused = true; // Resumption incorporates fresh key material, so refresh the timeout.