Add Data-less Zero-RTT support.

This adds support on the server and client to accept data-less early
data. The server will still fail to parse early data with any
contents, so this should remain disabled.

BUG=76

Change-Id: Id85d192d8e0360b8de4b6971511b5e8a0e8012f7
Reviewed-on: https://boringssl-review.googlesource.com/12921
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
CQ-Verified: CQ bot account: commit-bot@chromium.org <commit-bot@chromium.org>
diff --git a/ssl/dtls_method.c b/ssl/dtls_method.c
index 6084789..806212d 100644
--- a/ssl/dtls_method.c
+++ b/ssl/dtls_method.c
@@ -145,6 +145,7 @@
     dtls1_release_current_message,
     dtls1_read_app_data,
     dtls1_read_change_cipher_spec,
+    NULL,
     dtls1_read_close_notify,
     dtls1_write_app_data,
     dtls1_dispatch_alert,
diff --git a/ssl/handshake_client.c b/ssl/handshake_client.c
index 1feb7d8..3898c1b 100644
--- a/ssl/handshake_client.c
+++ b/ssl/handshake_client.c
@@ -208,6 +208,18 @@
         }
 
         if (!SSL_is_dtls(ssl) || ssl->d1->send_cookie) {
+          if (hs->early_data_offered) {
+            if (!tls13_init_early_key_schedule(hs) ||
+                !tls13_advance_key_schedule(hs, ssl->session->master_key,
+                                            ssl->session->master_key_length) ||
+                !tls13_derive_early_secrets(hs) ||
+                !tls13_set_traffic_key(ssl, evp_aead_seal,
+                                       hs->early_traffic_secret,
+                                       hs->hash_len)) {
+              ret = -1;
+              goto end;
+            }
+          }
           hs->next_state = SSL3_ST_CR_SRVR_HELLO_A;
         } else {
           hs->next_state = DTLS1_ST_CR_HELLO_VERIFY_REQUEST_A;
@@ -875,6 +887,12 @@
     return 1;
   }
 
+  if (hs->early_data_offered) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_WRONG_VERSION_ON_EARLY_DATA);
+    al = SSL_AD_PROTOCOL_VERSION;
+    goto f_err;
+  }
+
   ssl_clear_tls13_state(hs);
 
   if (!ssl_check_message_type(ssl, SSL3_MT_SERVER_HELLO)) {
diff --git a/ssl/internal.h b/ssl/internal.h
index 99980d8..ed5172c 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -865,6 +865,11 @@
  * It returns one on success and zero on error. */
 int tls13_init_key_schedule(SSL_HANDSHAKE *hs);
 
+/* tls13_init_early_key_schedule initializes the handshake hash and key
+ * derivation state from the resumption secret to derive the early secrets. It
+ * returns one on success and zero on error. */
+int tls13_init_early_key_schedule(SSL_HANDSHAKE *hs);
+
 /* tls13_advance_key_schedule incorporates |in| into the key schedule with
  * HKDF-Extract. It returns one on success and zero on error. */
 int tls13_advance_key_schedule(SSL_HANDSHAKE *hs, const uint8_t *in,
@@ -876,6 +881,10 @@
                           const uint8_t *traffic_secret,
                           size_t traffic_secret_len);
 
+/* tls13_derive_early_secrets derives the early traffic secret. It returns one
+ * on success and zero on error. */
+int tls13_derive_early_secrets(SSL_HANDSHAKE *hs);
+
 /* tls13_derive_handshake_secrets derives the handshake traffic secret. It
  * returns one on success and zero on error. */
 int tls13_derive_handshake_secrets(SSL_HANDSHAKE *hs);
@@ -930,6 +939,7 @@
   ssl_hs_channel_id_lookup,
   ssl_hs_private_key_operation,
   ssl_hs_pending_ticket,
+  ssl_hs_read_end_of_early_data,
 };
 
 struct ssl_handshake_st {
@@ -957,6 +967,7 @@
 
   size_t hash_len;
   uint8_t secret[EVP_MAX_MD_SIZE];
+  uint8_t early_traffic_secret[EVP_MAX_MD_SIZE];
   uint8_t client_handshake_secret[EVP_MAX_MD_SIZE];
   uint8_t server_handshake_secret[EVP_MAX_MD_SIZE];
   uint8_t client_traffic_secret_0[EVP_MAX_MD_SIZE];
@@ -1100,6 +1111,9 @@
    * Start. The client may write data at this point. */
   unsigned in_false_start:1;
 
+  /* early_data_offered is one if the client sent the early_data extension. */
+  unsigned early_data_offered:1;
+
   /* next_proto_neg_seen is one of NPN was negotiated. */
   unsigned next_proto_neg_seen:1;
 
@@ -1398,6 +1412,7 @@
   int (*read_app_data)(SSL *ssl, int *out_got_handshake, uint8_t *buf, int len,
                        int peek);
   int (*read_change_cipher_spec)(SSL *ssl);
+  int (*read_end_of_early_data)(SSL *ssl);
   void (*read_close_notify)(SSL *ssl);
   int (*write_app_data)(SSL *ssl, const uint8_t *buf, int len);
   int (*dispatch_alert)(SSL *ssl);
@@ -1629,9 +1644,11 @@
   uint8_t write_traffic_secret[EVP_MAX_MD_SIZE];
   uint8_t read_traffic_secret[EVP_MAX_MD_SIZE];
   uint8_t exporter_secret[EVP_MAX_MD_SIZE];
+  uint8_t early_exporter_secret[EVP_MAX_MD_SIZE];
   uint8_t write_traffic_secret_len;
   uint8_t read_traffic_secret_len;
   uint8_t exporter_secret_len;
+  uint8_t early_exporter_secret_len;
 
   /* Connection binding to prevent renegotiation attacks */
   uint8_t previous_client_finished[12];
@@ -1933,6 +1950,9 @@
    * hash of the peer's certificate and then discard it to save memory and
    * session space. Only effective on the server side. */
   unsigned retain_only_sha256_of_client_certs:1;
+
+  /* early_data_accepted is true if early data was accepted by the server. */
+  unsigned early_data_accepted:1;
 };
 
 /* From draft-ietf-tls-tls13-18, used in determining PSK modes. */
@@ -2049,6 +2069,7 @@
 int ssl3_read_app_data(SSL *ssl, int *out_got_handshake, uint8_t *buf, int len,
                        int peek);
 int ssl3_read_change_cipher_spec(SSL *ssl);
+int ssl3_read_end_of_early_data(SSL *ssl);
 void ssl3_read_close_notify(SSL *ssl);
 int ssl3_read_handshake_bytes(SSL *ssl, uint8_t *buf, int len);
 int ssl3_write_app_data(SSL *ssl, const uint8_t *buf, int len);
diff --git a/ssl/s3_both.c b/ssl/s3_both.c
index 8fa51e9..7ef400a 100644
--- a/ssl/s3_both.c
+++ b/ssl/s3_both.c
@@ -153,6 +153,7 @@
   }
 
   OPENSSL_cleanse(hs->secret, sizeof(hs->secret));
+  OPENSSL_cleanse(hs->early_traffic_secret, sizeof(hs->early_traffic_secret));
   OPENSSL_cleanse(hs->client_handshake_secret,
                   sizeof(hs->client_handshake_secret));
   OPENSSL_cleanse(hs->server_handshake_secret,
diff --git a/ssl/s3_pkt.c b/ssl/s3_pkt.c
index 2f919ca..69696ed 100644
--- a/ssl/s3_pkt.c
+++ b/ssl/s3_pkt.c
@@ -407,6 +407,30 @@
   return 1;
 }
 
+int ssl3_read_end_of_early_data(SSL *ssl) {
+  SSL3_RECORD *rr = &ssl->s3->rrec;
+
+  if (rr->length == 0) {
+    int ret = ssl3_get_record(ssl);
+    if (ret <= 0) {
+      return ret;
+    }
+  }
+
+  if (rr->type != SSL3_RT_ALERT ||
+      rr->length != 2 ||
+      rr->data[0] != SSL3_AL_WARNING ||
+      rr->data[1] != TLS1_AD_END_OF_EARLY_DATA) {
+    ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNEXPECTED_MESSAGE);
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_RECORD);
+    return -1;
+  }
+
+  rr->length = 0;
+  ssl_read_buffer_discard(ssl);
+  return 1;
+}
+
 void ssl3_read_close_notify(SSL *ssl) {
   /* Read records until an error or close_notify. */
   while (ssl3_get_record(ssl) > 0) {
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index e3bcb91..88d5f29 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -821,6 +821,10 @@
   ctx->enable_early_data = !!enabled;
 }
 
+int SSL_early_data_accepted(const SSL *ssl) {
+  return ssl->early_data_accepted;
+}
+
 static int bio_retry_reason_to_error(int reason) {
   switch (reason) {
     case BIO_RR_CONNECT:
diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index 759d87b..ef4a889 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -2086,11 +2086,30 @@
  * https://tools.ietf.org/html/draft-ietf-tls-tls13-18#section-4.2.8 */
 
 static int ext_early_data_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
-  /* TODO(svaldez): Support 0RTT. */
+  SSL *const ssl = hs->ssl;
+  uint16_t session_version;
+  if (ssl->session == NULL ||
+      !ssl->method->version_from_wire(&session_version,
+                                      ssl->session->ssl_version) ||
+      session_version < TLS1_3_VERSION ||
+      ssl->session->ticket_max_early_data == 0 ||
+      hs->received_hello_retry_request ||
+      !ssl->ctx->enable_early_data) {
+    return 1;
+  }
+
+  hs->early_data_offered = 1;
+
+  if (!CBB_add_u16(out, TLSEXT_TYPE_early_data) ||
+      !CBB_add_u16(out, 0) ||
+      !CBB_flush(out)) {
+    return 0;
+  }
+
   return 1;
 }
 
-static int ext_early_data_parse_clienthello(SSL_HANDSHAKE *hs,
+static int ext_early_data_parse_serverhello(SSL_HANDSHAKE *hs,
                                             uint8_t *out_alert, CBS *contents) {
   SSL *const ssl = hs->ssl;
   if (contents == NULL) {
@@ -2102,11 +2121,44 @@
     return 0;
   }
 
-  /* Since we don't currently accept 0-RTT, we have to skip past any early data
-   * the client might have sent. */
-  if (ssl3_protocol_version(ssl) >= TLS1_3_VERSION) {
-    ssl->s3->skip_early_data = 1;
+  if (!ssl->s3->session_reused) {
+    *out_alert = SSL_AD_UNSUPPORTED_EXTENSION;
+    OPENSSL_PUT_ERROR(SSL, SSL_R_UNEXPECTED_EXTENSION);
+    return 0;
   }
+
+  ssl->early_data_accepted = 1;
+  return 1;
+}
+
+static int ext_early_data_parse_clienthello(SSL_HANDSHAKE *hs,
+                                            uint8_t *out_alert, CBS *contents) {
+  SSL *const ssl = hs->ssl;
+  if (contents == NULL ||
+      ssl3_protocol_version(ssl) < TLS1_3_VERSION) {
+    return 1;
+  }
+
+  if (CBS_len(contents) != 0) {
+    *out_alert = SSL_AD_DECODE_ERROR;
+    return 0;
+  }
+
+  hs->early_data_offered = 1;
+  return 1;
+}
+
+static int ext_early_data_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  if (!hs->ssl->early_data_accepted) {
+    return 1;
+  }
+
+  if (!CBB_add_u16(out, TLSEXT_TYPE_early_data) ||
+      !CBB_add_u16(out, 0) ||
+      !CBB_flush(out)) {
+    return 0;
+  }
+
   return 1;
 }
 
@@ -2597,9 +2649,9 @@
     TLSEXT_TYPE_early_data,
     NULL,
     ext_early_data_add_clienthello,
-    forbid_parse_serverhello,
+    ext_early_data_parse_serverhello,
     ext_early_data_parse_clienthello,
-    dont_add_serverhello,
+    ext_early_data_add_serverhello,
   },
   {
     TLSEXT_TYPE_supported_versions,
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 2c70361..11d0d19 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -108,6 +108,7 @@
   bssl::UniquePtr<SSL_SESSION> new_session;
   bool ticket_decrypt_done = false;
   bool alpn_select_done = false;
+  bool is_resume = false;
   bool early_callback_ready = false;
 };
 
@@ -764,6 +765,10 @@
 
   *out = (const uint8_t*)config->select_alpn.data();
   *outlen = config->select_alpn.size();
+  if (GetTestState(ssl)->is_resume && config->select_resume_alpn.size() > 0) {
+    *out = (const uint8_t*)config->select_resume_alpn.data();
+    *outlen = config->select_resume_alpn.size();
+  }
   return SSL_TLSEXT_ERR_OK;
 }
 
@@ -1109,7 +1114,8 @@
                                      NULL);
   }
 
-  if (!config->select_alpn.empty() || config->decline_alpn) {
+  if (!config->select_alpn.empty() || !config->select_resume_alpn.empty() ||
+      config->decline_alpn) {
     SSL_CTX_set_alpn_select_cb(ssl_ctx.get(), AlpnSelectCallback, NULL);
   }
 
@@ -1422,13 +1428,22 @@
     }
   }
 
-  if (!config->expected_alpn.empty()) {
+  std::string expected_alpn = config->expected_alpn;
+  if (is_resume && !config->expected_resume_alpn.empty()) {
+    expected_alpn = config->expected_resume_alpn;
+  }
+  bool expect_no_alpn = (!is_resume && config->expect_no_alpn) ||
+      (is_resume && config->expect_no_resume_alpn);
+  if (expect_no_alpn) {
+    expected_alpn.clear();
+  }
+
+  if (!expected_alpn.empty() || expect_no_alpn) {
     const uint8_t *alpn_proto;
     unsigned alpn_proto_len;
     SSL_get0_alpn_selected(ssl, &alpn_proto, &alpn_proto_len);
-    if (alpn_proto_len != config->expected_alpn.size() ||
-        OPENSSL_memcmp(alpn_proto, config->expected_alpn.data(),
-                       alpn_proto_len) != 0) {
+    if (alpn_proto_len != expected_alpn.size() ||
+        OPENSSL_memcmp(alpn_proto, expected_alpn.data(), alpn_proto_len) != 0) {
       fprintf(stderr, "negotiated alpn proto mismatch\n");
       return false;
     }
@@ -1540,6 +1555,15 @@
     return false;
   }
 
+  if (is_resume) {
+    if ((config->expect_accept_early_data && !SSL_early_data_accepted(ssl)) ||
+        (config->expect_reject_early_data && SSL_early_data_accepted(ssl))) {
+      fprintf(stderr,
+              "Early data was%s accepted, but we expected the opposite\n",
+              SSL_early_data_accepted(ssl) ? "" : " not");
+      return false;
+    }
+  }
 
   if (!config->psk.empty()) {
     if (SSL_get_peer_cert_chain(ssl) != nullptr) {
@@ -1629,6 +1653,10 @@
 static bool DoExchange(bssl::UniquePtr<SSL_SESSION> *out_session,
                        SSL_CTX *ssl_ctx, const TestConfig *config,
                        bool is_resume, SSL_SESSION *session) {
+  if (is_resume && config->enable_resume_early_data) {
+    SSL_CTX_set_early_data_enabled(ssl_ctx, 1);
+  }
+
   bssl::UniquePtr<SSL> ssl(SSL_new(ssl_ctx));
   if (!ssl) {
     return false;
@@ -1639,6 +1667,8 @@
     return false;
   }
 
+  GetTestState(ssl.get())->is_resume = is_resume;
+
   if (config->fallback_scsv &&
       !SSL_set_mode(ssl.get(), SSL_MODE_SEND_FALLBACK_SCSV)) {
     return false;
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 167e872..65a8797 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -249,6 +249,7 @@
 	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
@@ -1146,10 +1147,26 @@
 
 	// SendEarlyData causes a TLS 1.3 client to send the provided data
 	// in application data records immediately after the ClientHello,
-	// provided that the client has a PSK that is appropriate for sending
-	// early data and includes that PSK in its ClientHello.
+	// provided that the client offers a TLS 1.3 session. It will do this
+	// whether or not the server advertised early data for the ticket.
 	SendEarlyData [][]byte
 
+	// ExpectEarlyDataAccepted causes a TLS 1.3 client to check that early data
+	// was accepted by the server.
+	ExpectEarlyDataAccepted bool
+
+	// AlwaysAcceptEarlyData causes a TLS 1.3 server to always accept early data
+	// regardless of ALPN mismatch.
+	AlwaysAcceptEarlyData bool
+
+	// AlwaysRejectEarlyData causes a TLS 1.3 server to always reject early data.
+	AlwaysRejectEarlyData bool
+
+	// SendEarlyDataExtension, if true, causes a TLS 1.3 server to send the
+	// early_data extension in EncryptedExtensions, independent of whether
+	// it was accepted.
+	SendEarlyDataExtension bool
+
 	// ExpectEarlyData causes a TLS 1.3 server to read application
 	// data after the ClientHello (assuming the server is able to
 	// derive the key under which the data is encrypted) before it
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 1bdca84..ffe4f34 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -1477,6 +1477,7 @@
 				ticketExpiration:   c.config.time().Add(time.Duration(newSessionTicket.ticketLifetime) * time.Second),
 				ticketAgeAdd:       newSessionTicket.ticketAgeAdd,
 				maxEarlyDataSize:   newSessionTicket.maxEarlyDataSize,
+				earlyALPN:          c.clientProtocol,
 			}
 
 			cacheKey := clientSessionCacheKey(c.conn.RemoteAddr(), c.config)
@@ -1789,6 +1790,7 @@
 		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),
 	}
 
 	if !c.config.Bugs.SendEmptySessionTicket {
@@ -1798,7 +1800,6 @@
 			return err
 		}
 	}
-
 	c.out.Lock()
 	defer c.out.Unlock()
 	_, err = c.writeRecord(recordTypeHandshake, m.marshal())
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index bf38c1a..a3fc14a 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -330,7 +330,7 @@
 	}
 
 	var sendEarlyData bool
-	if len(hello.pskIdentities) > 0 && session.maxEarlyDataSize > 0 && c.config.Bugs.SendEarlyData != nil {
+	if len(hello.pskIdentities) > 0 && c.config.Bugs.SendEarlyData != nil {
 		hello.hasEarlyData = true
 		sendEarlyData = true
 	}
@@ -1336,6 +1336,18 @@
 		c.srtpProtectionProfile = serverExtensions.srtpProtectionProfile
 	}
 
+	if c.vers >= VersionTLS13 && c.didResume {
+		if c.config.Bugs.ExpectEarlyDataAccepted && !serverExtensions.hasEarlyData {
+			c.sendAlert(alertHandshakeFailure)
+			return errors.New("tls: server did not accept early data when expected")
+		}
+
+		if !c.config.Bugs.ExpectEarlyDataAccepted && serverExtensions.hasEarlyData {
+			c.sendAlert(alertHandshakeFailure)
+			return errors.New("tls: server accepted early data when not expected")
+		}
+	}
+
 	return nil
 }
 
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 64edd01..b1afc03 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -451,7 +451,7 @@
 
 	var pskIndex int
 	foundKEMode := bytes.IndexByte(pskKEModes, pskDHEKEMode) >= 0
-	if foundKEMode {
+	if foundKEMode && !config.SessionTicketsDisabled {
 		for i, pskIdentity := range pskIdentities {
 			// TODO(svaldez): Check the obfuscatedTicketAge before accepting 0-RTT.
 			sessionState, ok := c.decryptTicket(pskIdentity.ticket)
@@ -579,6 +579,10 @@
 		c.writeRecord(recordTypeHandshake, helloRetryRequest.marshal())
 		c.flushHandshake()
 
+		if hs.clientHello.hasEarlyData {
+			c.skipEarlyData = true
+		}
+
 		// Read new ClientHello.
 		newMsg, err := c.readHandshake()
 		if err != nil {
@@ -591,6 +595,10 @@
 		}
 		hs.writeClientHash(newClientHello.marshal())
 
+		if newClientHello.hasEarlyData {
+			return errors.New("tls: EarlyData sent in new ClientHello")
+		}
+
 		applyBugsToClientHello(newClientHello, config)
 
 		// Check that the new ClientHello matches the old ClientHello,
@@ -628,6 +636,7 @@
 			newClientHelloCopy.pskIdentities[i].obfuscatedTicketAge = identity.obfuscatedTicketAge
 		}
 		newClientHelloCopy.pskBinders = oldClientHelloCopy.pskBinders
+		newClientHelloCopy.hasEarlyData = oldClientHelloCopy.hasEarlyData
 
 		if !oldClientHelloCopy.equal(&newClientHelloCopy) {
 			return errors.New("tls: new ClientHello does not match")
@@ -650,10 +659,13 @@
 	}
 
 	// Decide whether or not to accept early data.
-	// TODO(nharper): This does not check that ALPN or SNI matches.
-	if hs.clientHello.hasEarlyData {
-		if !sendHelloRetryRequest && hs.sessionState != nil {
-			encryptedExtensions.extensions.hasEarlyData = true
+	if !sendHelloRetryRequest && hs.clientHello.hasEarlyData {
+		if !config.Bugs.AlwaysRejectEarlyData && hs.sessionState != nil {
+			if c.clientProtocol == string(hs.sessionState.earlyALPN) || config.Bugs.AlwaysAcceptEarlyData {
+				encryptedExtensions.extensions.hasEarlyData = true
+			}
+		}
+		if encryptedExtensions.extensions.hasEarlyData {
 			earlyTrafficSecret := hs.finishedHash.deriveSecret(earlyTrafficLabel)
 			c.in.useTrafficSecret(c.vers, hs.suite, earlyTrafficSecret, clientWrite)
 
@@ -673,6 +685,10 @@
 		}
 	}
 
+	if config.Bugs.SendEarlyDataExtension {
+		encryptedExtensions.extensions.hasEarlyData = true
+	}
+
 	// Resolve ECDHE and compute the handshake secret.
 	if hs.hello.hasKeyShare {
 		// Once a curve has been selected and a key share identified,
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 563d4b2..262ac28 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -3562,6 +3562,40 @@
 			// Cover HelloRetryRequest during an ECDHE-PSK resumption.
 			resumeSession: true,
 		})
+
+		// TODO(svaldez): Send data on early data once implemented.
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "TLS13-EarlyData-Client",
+			config: Config{
+				MaxVersion:       VersionTLS13,
+				MinVersion:       VersionTLS13,
+				MaxEarlyDataSize: 16384,
+			},
+			resumeSession: true,
+			flags: []string{
+				"-enable-early-data",
+				"-expect-early-data-info",
+				"-expect-accept-early-data",
+			},
+		})
+
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "TLS13-EarlyData-Server",
+			config: Config{
+				MaxVersion: VersionTLS13,
+				MinVersion: VersionTLS13,
+				Bugs: ProtocolBugs{
+					SendEarlyData:           [][]byte{},
+					ExpectEarlyDataAccepted: true,
+				},
+			},
+			resumeSession: true,
+			flags: []string{
+				"-enable-early-data",
+			},
+		})
 	}
 
 	// TLS client auth.
@@ -5957,8 +5991,8 @@
 
 	// In TLS 1.3, clients may advertise a cipher list which does not
 	// include the selected cipher. Test that we tolerate this. Servers may
-	// resume at another cipher if the PRF matches, but BoringSSL will
-	// always decline.
+	// resume at another cipher if the PRF matches and are not doing 0-RTT, but
+	// BoringSSL will always decline.
 	testCases = append(testCases, testCase{
 		testType:      serverTest,
 		name:          "Resume-Server-UnofferedCipher-TLS13",
@@ -9916,6 +9950,364 @@
 			},
 		},
 	})
+
+	// Test that we accept data-less early data.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TLS13-DataLessEarlyData-Server",
+		config: Config{
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				SendEarlyData:           [][]byte{},
+				ExpectEarlyDataAccepted: true,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-accept-early-data",
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-accept-early-data",
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-Reject-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				AlwaysRejectEarlyData: true,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-reject-early-data",
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-HRR-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				SendHelloRetryRequestCookie: []byte{1, 2, 3, 4},
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-reject-early-data",
+		},
+	})
+
+	// The client must check the server does not send the early_data
+	// extension while rejecting the session.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-EarlyDataWithoutResume-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion:             VersionTLS13,
+			SessionTicketsDisabled: true,
+			Bugs: ProtocolBugs{
+				SendEarlyDataExtension: true,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+		},
+		shouldFail:    true,
+		expectedError: ":UNEXPECTED_EXTENSION:",
+	})
+
+	// The client must fail with a dedicated error code if the server
+	// responds with TLS 1.2 when offering 0-RTT.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-EarlyDataVersionDowngrade-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion: VersionTLS12,
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+		},
+		shouldFail:    true,
+		expectedError: ":WRONG_VERSION_ON_EARLY_DATA:",
+	})
+
+	// Test that the client rejects an (unsolicited) early_data extension if
+	// the server sent an HRR.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-ServerAcceptsEarlyDataOnHRR-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				SendHelloRetryRequestCookie: []byte{1, 2, 3, 4},
+				SendEarlyDataExtension:      true,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-expect-early-data-info",
+		},
+		shouldFail:    true,
+		expectedError: ":UNEXPECTED_EXTENSION:",
+	})
+
+	fooString := "foo"
+	barString := "bar"
+
+	// Test that the client reports the correct ALPN after a 0-RTT reject
+	// that changed it.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-ALPNMismatch-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				ALPNProtocol: &fooString,
+			},
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				ALPNProtocol: &barString,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-advertise-alpn", "\x03foo\x03bar",
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-reject-early-data",
+			"-expect-alpn", "foo",
+			"-expect-resume-alpn", "bar",
+		},
+	})
+
+	// Test that the client reports the correct ALPN after a 0-RTT reject if
+	// ALPN was omitted from the first connection.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-ALPNOmitted1-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			NextProtos:       []string{"foo"},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-advertise-alpn", "\x03foo\x03bar",
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-reject-early-data",
+			"-expect-no-alpn",
+			"-expect-resume-alpn", "foo",
+		},
+	})
+
+	// Test that the client reports the correct ALPN after a 0-RTT reject if
+	// ALPN was omitted from the second connection.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-ALPNOmitted2-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			NextProtos:       []string{"foo"},
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+		},
+		resumeSession: true,
+		flags: []string{
+			"-advertise-alpn", "\x03foo\x03bar",
+			"-enable-early-data",
+			"-expect-early-data-info",
+			"-expect-reject-early-data",
+			"-expect-alpn", "foo",
+			"-expect-no-resume-alpn",
+		},
+	})
+
+	// Test that the client enforces ALPN match on 0-RTT accept.
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "TLS13-DataLessEarlyData-BadALPNMismatch-Client",
+		config: Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				ALPNProtocol: &fooString,
+			},
+		},
+		resumeConfig: &Config{
+			MaxVersion:       VersionTLS13,
+			MaxEarlyDataSize: 16384,
+			Bugs: ProtocolBugs{
+				AlwaysAcceptEarlyData: true,
+				ALPNProtocol:          &barString,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-advertise-alpn", "\x03foo\x03bar",
+			"-enable-early-data",
+			"-expect-early-data-info",
+		},
+		shouldFail:    true,
+		expectedError: ":ALPN_MISMATCH_ON_EARLY_DATA:",
+	})
+
+	// Test that the server correctly rejects 0-RTT when the previous
+	// session did not allow early data on resumption.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TLS13-EarlyData-NonZeroRTTSession-Server",
+		config: Config{
+			MaxVersion: VersionTLS13,
+		},
+		resumeConfig: &Config{
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				SendEarlyData:           [][]byte{{}},
+				ExpectEarlyDataAccepted: false,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-resume-early-data",
+			"-expect-reject-early-data",
+		},
+	})
+
+	// Test that we reject early data where ALPN is omitted from the first
+	// connection.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TLS13-EarlyData-ALPNOmitted1-Server",
+		config: Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{},
+		},
+		resumeConfig: &Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{"foo"},
+			Bugs: ProtocolBugs{
+				SendEarlyData:           [][]byte{{}},
+				ExpectEarlyDataAccepted: false,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-select-alpn", "",
+			"-select-resume-alpn", "foo",
+		},
+	})
+
+	// Test that we reject early data where ALPN is omitted from the second
+	// connection.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TLS13-EarlyData-ALPNOmitted2-Server",
+		config: Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{"foo"},
+		},
+		resumeConfig: &Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{},
+			Bugs: ProtocolBugs{
+				SendEarlyData:           [][]byte{{}},
+				ExpectEarlyDataAccepted: false,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-select-alpn", "foo",
+			"-select-resume-alpn", "",
+		},
+	})
+
+	// Test that we reject early data with mismatched ALPN.
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "TLS13-EarlyData-ALPNMismatch-Server",
+		config: Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{"foo"},
+		},
+		resumeConfig: &Config{
+			MaxVersion: VersionTLS13,
+			NextProtos: []string{"bar"},
+			Bugs: ProtocolBugs{
+				SendEarlyData:           [][]byte{{}},
+				ExpectEarlyDataAccepted: false,
+			},
+		},
+		resumeSession: true,
+		flags: []string{
+			"-enable-early-data",
+			"-select-alpn", "foo",
+			"-select-resume-alpn", "bar",
+		},
+	})
+
 }
 
 func addTLS13CipherPreferenceTests() {
diff --git a/ssl/test/runner/ticket.go b/ssl/test/runner/ticket.go
index 4a4540c..10ac54f 100644
--- a/ssl/test/runner/ticket.go
+++ b/ssl/test/runner/ticket.go
@@ -25,6 +25,7 @@
 	handshakeHash        []byte
 	certificates         [][]byte
 	extendedMasterSecret bool
+	earlyALPN            []byte
 	ticketCreationTime   time.Time
 	ticketExpiration     time.Time
 	ticketFlags          uint32
@@ -58,6 +59,9 @@
 		msg.addU32(s.ticketAgeAdd)
 	}
 
+	earlyALPN := msg.addU16LengthPrefixed()
+	earlyALPN.addBytes(s.earlyALPN)
+
 	return msg.finish()
 }
 
@@ -138,6 +142,14 @@
 		data = data[4:]
 	}
 
+	earlyALPNLen := int(data[0])<<8 | int(data[1])
+	data = data[2:]
+	if len(data) < earlyALPNLen {
+		return false
+	}
+	s.earlyALPN = data[:earlyALPNLen]
+	data = data[earlyALPNLen:]
+
 	if len(data) > 0 {
 		return false
 	}
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index e581581..7e57543 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -85,6 +85,7 @@
   { "-use-ticket-callback", &TestConfig::use_ticket_callback },
   { "-renew-ticket", &TestConfig::renew_ticket },
   { "-enable-early-data", &TestConfig::enable_early_data },
+  { "-enable-resume-early-data", &TestConfig::enable_resume_early_data },
   { "-enable-client-custom-extension",
     &TestConfig::enable_client_custom_extension },
   { "-enable-server-custom-extension",
@@ -123,6 +124,10 @@
     &TestConfig::expect_no_secure_renegotiation },
   { "-expect-session-id", &TestConfig::expect_session_id },
   { "-expect-no-session-id", &TestConfig::expect_no_session_id },
+  { "-expect-accept-early-data", &TestConfig::expect_accept_early_data },
+  { "-expect-reject-early-data", &TestConfig::expect_reject_early_data },
+  { "-expect-no-alpn", &TestConfig::expect_no_alpn },
+  { "-expect-no-resume-alpn", &TestConfig::expect_no_resume_alpn },
 };
 
 const Flag<std::string> kStringFlags[] = {
@@ -137,8 +142,10 @@
   { "-host-name", &TestConfig::host_name },
   { "-advertise-alpn", &TestConfig::advertise_alpn },
   { "-expect-alpn", &TestConfig::expected_alpn },
+  { "-expect-resume-alpn", &TestConfig::expected_resume_alpn },
   { "-expect-advertised-alpn", &TestConfig::expected_advertised_alpn },
   { "-select-alpn", &TestConfig::select_alpn },
+  { "-select-resume-alpn", &TestConfig::select_resume_alpn },
   { "-psk", &TestConfig::psk },
   { "-psk-identity", &TestConfig::psk_identity },
   { "-srtp-profiles", &TestConfig::srtp_profiles },
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 7057b48..fadd05e 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -52,8 +52,12 @@
   std::string host_name;
   std::string advertise_alpn;
   std::string expected_alpn;
+  std::string expected_resume_alpn;
+  bool expect_no_alpn = false;
+  bool expect_no_resume_alpn = false;
   std::string expected_advertised_alpn;
   std::string select_alpn;
+  std::string select_resume_alpn;
   bool decline_alpn = false;
   bool expect_session_miss = false;
   bool expect_extended_master_secret = false;
@@ -84,9 +88,12 @@
   bool expect_ticket_renewal = false;
   bool expect_no_session = false;
   bool expect_early_data_info = false;
+  bool expect_accept_early_data = false;
+  bool expect_reject_early_data = false;
   bool use_ticket_callback = false;
   bool renew_ticket = false;
   bool enable_early_data = false;
+  bool enable_resume_early_data = false;
   bool enable_client_custom_extension = false;
   bool enable_server_custom_extension = false;
   bool custom_extension_skip = false;
diff --git a/ssl/tls13_both.c b/ssl/tls13_both.c
index 6243923..4a16437 100644
--- a/ssl/tls13_both.c
+++ b/ssl/tls13_both.c
@@ -64,6 +64,14 @@
         break;
       }
 
+      case ssl_hs_read_end_of_early_data: {
+        int ret = ssl->method->read_end_of_early_data(ssl);
+        if (ret <= 0) {
+          return ret;
+        }
+        break;
+      }
+
       case ssl_hs_x509_lookup:
         ssl->rwstate = SSL_X509_LOOKUP;
         hs->wait = ssl_hs_ok;
diff --git a/ssl/tls13_client.c b/ssl/tls13_client.c
index f13a4f7..16aedc6 100644
--- a/ssl/tls13_client.c
+++ b/ssl/tls13_client.c
@@ -37,6 +37,7 @@
   state_process_server_certificate,
   state_process_server_certificate_verify,
   state_process_server_finished,
+  state_send_end_of_early_data,
   state_send_client_certificate,
   state_send_client_certificate_verify,
   state_complete_client_certificate_verify,
@@ -144,7 +145,11 @@
 }
 
 static enum ssl_hs_wait_t do_send_second_client_hello(SSL_HANDSHAKE *hs) {
-  if (!ssl_write_client_hello(hs)) {
+  SSL *const ssl = hs->ssl;
+  /* TODO(svaldez): Ensure that we set can_early_write to false since 0-RTT is
+   * rejected if we receive a HelloRetryRequest. */
+  if (!ssl->method->set_write_state(ssl, NULL) ||
+      !ssl_write_client_hello(hs)) {
     return ssl_hs_error;
   }
 
@@ -254,7 +259,6 @@
       ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
       return ssl_hs_error;
     }
-    ssl_set_session(ssl, NULL);
 
     /* Resumption incorporates fresh key material, so refresh the timeout. */
     ssl_session_renew_timeout(ssl, hs->new_session,
@@ -267,17 +271,6 @@
   hs->new_session->cipher = cipher;
   hs->new_cipher = cipher;
 
-  /* Store the initial negotiated ALPN in the session. */
-  if (ssl->s3->alpn_selected != NULL) {
-    hs->new_session->early_alpn =
-        BUF_memdup(ssl->s3->alpn_selected, ssl->s3->alpn_selected_len);
-    if (hs->new_session->early_alpn == NULL) {
-      ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
-      return ssl_hs_error;
-    }
-    hs->new_session->early_alpn_len = ssl->s3->alpn_selected_len;
-  }
-
   /* The PRF hash is now known. Set up the key schedule. */
   if (!tls13_init_key_schedule(hs)) {
     return ssl_hs_error;
@@ -319,7 +312,13 @@
   if (!ssl_hash_current_message(hs) ||
       !tls13_derive_handshake_secrets(hs) ||
       !tls13_set_traffic_key(ssl, evp_aead_open, hs->server_handshake_secret,
-                             hs->hash_len) ||
+                             hs->hash_len)) {
+    return ssl_hs_error;
+  }
+
+  /* If not sending early data, set client traffic keys now so that alerts are
+   * encrypted. */
+  if (!hs->early_data_offered &&
       !tls13_set_traffic_key(ssl, evp_aead_seal, hs->client_handshake_secret,
                              hs->hash_len)) {
     return ssl_hs_error;
@@ -347,6 +346,32 @@
     return ssl_hs_error;
   }
 
+  /* Store the negotiated ALPN in the session. */
+  if (ssl->s3->alpn_selected != NULL) {
+    hs->new_session->early_alpn =
+        BUF_memdup(ssl->s3->alpn_selected, ssl->s3->alpn_selected_len);
+    if (hs->new_session->early_alpn == NULL) {
+      ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+      return ssl_hs_error;
+    }
+    hs->new_session->early_alpn_len = ssl->s3->alpn_selected_len;
+  }
+
+  if (ssl->early_data_accepted) {
+    if (ssl->session->cipher != hs->new_session->cipher ||
+        ssl->session->early_alpn_len != ssl->s3->alpn_selected_len ||
+        OPENSSL_memcmp(ssl->session->early_alpn, ssl->s3->alpn_selected,
+                       ssl->s3->alpn_selected_len) != 0) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_ALPN_MISMATCH_ON_EARLY_DATA);
+      return ssl_hs_error;
+    }
+  }
+
+  /* Release offered session now that it is no longer needed. */
+  if (ssl->s3->session_reused) {
+    ssl_set_session(ssl, NULL);
+  }
+
   if (!ssl_hash_current_message(hs)) {
     return ssl_hs_error;
   }
@@ -450,12 +475,32 @@
   }
 
   ssl->method->received_flight(ssl);
+  hs->tls13_state = state_send_end_of_early_data;
+  return ssl_hs_ok;
+}
+
+static enum ssl_hs_wait_t do_send_end_of_early_data(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  /* TODO(svaldez): Stop sending early data. */
+  if (ssl->early_data_accepted &&
+      !ssl->method->add_alert(ssl, SSL3_AL_WARNING,
+                              TLS1_AD_END_OF_EARLY_DATA)) {
+    return ssl_hs_error;
+  }
+
+  if (hs->early_data_offered &&
+      !tls13_set_traffic_key(ssl, evp_aead_seal, hs->client_handshake_secret,
+                             hs->hash_len)) {
+    return ssl_hs_error;
+  }
+
   hs->tls13_state = state_send_client_certificate;
   return ssl_hs_ok;
 }
 
 static enum ssl_hs_wait_t do_send_client_certificate(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+
   /* The peer didn't request a certificate. */
   if (!hs->cert_request) {
     hs->tls13_state = state_complete_second_flight;
@@ -581,6 +626,9 @@
       case state_process_server_finished:
         ret = do_process_server_finished(hs);
         break;
+      case state_send_end_of_early_data:
+        ret = do_send_end_of_early_data(hs);
+        break;
       case state_send_client_certificate:
         ret = do_send_client_certificate(hs);
         break;
diff --git a/ssl/tls13_enc.c b/ssl/tls13_enc.c
index 412705d..16efd85 100644
--- a/ssl/tls13_enc.c
+++ b/ssl/tls13_enc.c
@@ -28,22 +28,43 @@
 #include "internal.h"
 
 
-int tls13_init_key_schedule(SSL_HANDSHAKE *hs) {
-  if (!SSL_TRANSCRIPT_init_hash(&hs->transcript, ssl3_protocol_version(hs->ssl),
-                                hs->new_cipher->algorithm_prf)) {
+static int init_key_schedule(SSL_HANDSHAKE *hs, uint16_t version,
+                              int algorithm_prf) {
+  if (!SSL_TRANSCRIPT_init_hash(&hs->transcript, version, algorithm_prf)) {
     return 0;
   }
 
-
   hs->hash_len = SSL_TRANSCRIPT_digest_len(&hs->transcript);
 
   /* Initialize the secret to the zero key. */
   OPENSSL_memset(hs->secret, 0, hs->hash_len);
 
+  return 1;
+}
+
+int tls13_init_key_schedule(SSL_HANDSHAKE *hs) {
+  if (!init_key_schedule(hs, ssl3_protocol_version(hs->ssl),
+                         hs->new_cipher->algorithm_prf)) {
+    return 0;
+  }
+
   SSL_TRANSCRIPT_free_buffer(&hs->transcript);
   return 1;
 }
 
+int tls13_init_early_key_schedule(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  uint16_t session_version;
+  if (!ssl->method->version_from_wire(&session_version,
+                                      ssl->session->ssl_version) ||
+      !init_key_schedule(hs, session_version,
+                         ssl->session->cipher->algorithm_prf)) {
+    return 0;
+  }
+
+  return 1;
+}
+
 int tls13_advance_key_schedule(SSL_HANDSHAKE *hs, const uint8_t *in,
                                size_t len) {
   return HKDF_extract(hs->secret, &hs->hash_len,
@@ -100,6 +121,13 @@
 int tls13_set_traffic_key(SSL *ssl, enum evp_aead_direction_t direction,
                           const uint8_t *traffic_secret,
                           size_t traffic_secret_len) {
+  const SSL_SESSION *session = SSL_get_session(ssl);
+  uint16_t version;
+  if (!ssl->method->version_from_wire(&version, session->ssl_version)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return 0;
+  }
+
   if (traffic_secret_len > 0xff) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_OVERFLOW);
     return 0;
@@ -108,14 +136,13 @@
   /* Look up cipher suite properties. */
   const EVP_AEAD *aead;
   size_t discard;
-  if (!ssl_cipher_get_evp_aead(&aead, &discard, &discard,
-                               SSL_get_session(ssl)->cipher,
-                               ssl3_protocol_version(ssl))) {
+  if (!ssl_cipher_get_evp_aead(&aead, &discard, &discard, session->cipher,
+                               version)) {
     return 0;
   }
 
   const EVP_MD *digest = ssl_get_handshake_digest(
-      SSL_get_session(ssl)->cipher->algorithm_prf, ssl3_protocol_version(ssl));
+      session->cipher->algorithm_prf, version);
 
   /* Derive the key. */
   size_t key_len = EVP_AEAD_key_length(aead);
@@ -134,8 +161,7 @@
   }
 
   SSL_AEAD_CTX *traffic_aead = SSL_AEAD_CTX_new(
-      direction, ssl3_protocol_version(ssl), SSL_get_session(ssl)->cipher, key,
-      key_len, NULL, 0, iv, iv_len);
+      direction, version, session->cipher, key, key_len, NULL, 0, iv, iv_len);
   if (traffic_aead == NULL) {
     return 0;
   }
@@ -164,6 +190,11 @@
   return 1;
 }
 
+static const char kTLS13LabelExporter[] = "exporter master secret";
+static const char kTLS13LabelEarlyExporter[] = "early exporter master secret";
+
+static const char kTLS13LabelClientEarlyTraffic[] =
+    "client early traffic secret";
 static const char kTLS13LabelClientHandshakeTraffic[] =
     "client handshake traffic secret";
 static const char kTLS13LabelServerHandshakeTraffic[] =
@@ -173,6 +204,18 @@
 static const char kTLS13LabelServerApplicationTraffic[] =
     "server application traffic secret";
 
+int tls13_derive_early_secrets(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  return derive_secret(hs, hs->early_traffic_secret, hs->hash_len,
+                       (const uint8_t *)kTLS13LabelClientEarlyTraffic,
+                       strlen(kTLS13LabelClientEarlyTraffic)) &&
+         ssl_log_secret(ssl, "CLIENT_EARLY_TRAFFIC_SECRET",
+                        hs->early_traffic_secret, hs->hash_len) &&
+         derive_secret(hs, ssl->s3->early_exporter_secret, hs->hash_len,
+                       (const uint8_t *)kTLS13LabelEarlyExporter,
+                       strlen(kTLS13LabelEarlyExporter));
+}
+
 int tls13_derive_handshake_secrets(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   return derive_secret(hs, hs->client_handshake_secret, hs->hash_len,
@@ -187,8 +230,6 @@
                         hs->server_handshake_secret, hs->hash_len);
 }
 
-static const char kTLS13LabelExporter[] = "exporter master secret";
-
 int tls13_derive_application_secrets(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   ssl->s3->exporter_secret_len = hs->hash_len;
diff --git a/ssl/tls13_server.c b/ssl/tls13_server.c
index 9c8d1a1..4854002 100644
--- a/ssl/tls13_server.c
+++ b/ssl/tls13_server.c
@@ -43,6 +43,8 @@
   state_send_server_certificate_verify,
   state_complete_server_certificate_verify,
   state_send_server_finished,
+  state_read_second_client_flight,
+  state_process_end_of_early_data,
   state_process_client_certificate,
   state_process_client_certificate_verify,
   state_process_channel_id,
@@ -289,6 +291,19 @@
       /* Carry over authentication information from the previous handshake into
        * a fresh session. */
       hs->new_session = SSL_SESSION_dup(session, SSL_SESSION_DUP_AUTH_ONLY);
+
+      if (/* Early data must be acceptable for this ticket. */
+          ssl->ctx->enable_early_data &&
+          session->ticket_max_early_data != 0 &&
+          /* The client must have offered early data. */
+          hs->early_data_offered &&
+          /* The negotiated ALPN must match the one in the ticket. */
+          ssl->s3->alpn_selected_len == session->early_alpn_len &&
+          OPENSSL_memcmp(ssl->s3->alpn_selected, session->early_alpn,
+                         ssl->s3->alpn_selected_len) == 0) {
+        ssl->early_data_accepted = 1;
+      }
+
       SSL_SESSION_free(session);
       if (hs->new_session == NULL) {
         ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
@@ -323,6 +338,7 @@
     }
   }
 
+  /* Store the initial negotiated ALPN in the session. */
   if (ssl->s3->alpn_selected != NULL) {
     hs->new_session->early_alpn =
         BUF_memdup(ssl->s3->alpn_selected, ssl->s3->alpn_selected_len);
@@ -351,12 +367,22 @@
     return ssl_hs_error;
   }
 
+  if (ssl->early_data_accepted) {
+    if (!tls13_derive_early_secrets(hs)) {
+      return ssl_hs_error;
+    }
+  } else if (hs->early_data_offered) {
+    ssl->s3->skip_early_data = 1;
+  }
+
   ssl->method->received_flight(ssl);
 
   /* Resolve ECDHE and incorporate it into the secret. */
   int need_retry;
   if (!resolve_ecdhe_secret(hs, &need_retry, &client_hello)) {
     if (need_retry) {
+      ssl->early_data_accepted = 0;
+      ssl->s3->skip_early_data = 1;
       hs->tls13_state = state_send_hello_retry_request;
       return ssl_hs_ok;
     }
@@ -440,8 +466,6 @@
 
   /* Derive and enable the handshake traffic secrets. */
   if (!tls13_derive_handshake_secrets(hs) ||
-      !tls13_set_traffic_key(ssl, evp_aead_open, hs->client_handshake_secret,
-                             hs->hash_len) ||
       !tls13_set_traffic_key(ssl, evp_aead_seal, hs->server_handshake_secret,
                              hs->hash_len)) {
     goto err;
@@ -543,8 +567,33 @@
     return ssl_hs_error;
   }
 
+  hs->tls13_state = state_read_second_client_flight;
+  return ssl_hs_flush;
+}
+
+static enum ssl_hs_wait_t do_read_second_client_flight(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  if (ssl->early_data_accepted) {
+    if (!tls13_set_traffic_key(ssl, evp_aead_open, hs->early_traffic_secret,
+                               hs->hash_len)) {
+      return ssl_hs_error;
+    }
+    hs->tls13_state = state_process_end_of_early_data;
+    return ssl_hs_read_end_of_early_data;
+  }
+
+  hs->tls13_state = state_process_end_of_early_data;
+  return ssl_hs_ok;
+}
+
+static enum ssl_hs_wait_t do_process_end_of_early_data(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  if (!tls13_set_traffic_key(ssl, evp_aead_open, hs->client_handshake_secret,
+                             hs->hash_len)) {
+    return ssl_hs_error;
+  }
   hs->tls13_state = state_process_client_certificate;
-  return ssl_hs_flush_and_read_message;
+  return ssl_hs_read_message;
 }
 
 static enum ssl_hs_wait_t do_process_client_certificate(SSL_HANDSHAKE *hs) {
@@ -720,10 +769,16 @@
       break;
       case state_complete_server_certificate_verify:
         ret = do_send_server_certificate_verify(hs, 0 /* complete */);
-      break;
+        break;
       case state_send_server_finished:
         ret = do_send_server_finished(hs);
         break;
+      case state_read_second_client_flight:
+        ret = do_read_second_client_flight(hs);
+        break;
+      case state_process_end_of_early_data:
+        ret = do_process_end_of_early_data(hs);
+        break;
       case state_process_client_certificate:
         ret = do_process_client_certificate(hs);
         break;
diff --git a/ssl/tls_method.c b/ssl/tls_method.c
index 6144f86..2af4f2c 100644
--- a/ssl/tls_method.c
+++ b/ssl/tls_method.c
@@ -141,6 +141,7 @@
     ssl3_release_current_message,
     ssl3_read_app_data,
     ssl3_read_change_cipher_spec,
+    ssl3_read_end_of_early_data,
     ssl3_read_close_notify,
     ssl3_write_app_data,
     ssl3_dispatch_alert,
diff --git a/ssl/tls_record.c b/ssl/tls_record.c
index aafb6f5..0f9720c 100644
--- a/ssl/tls_record.c
+++ b/ssl/tls_record.c
@@ -327,6 +327,14 @@
   }
 
   if (type == SSL3_RT_ALERT) {
+    /* Return end_of_early_data alerts as-is for the caller to process. */
+    if (CBS_len(out) == 2 &&
+        CBS_data(out)[0] == SSL3_AL_WARNING &&
+        CBS_data(out)[1] == TLS1_AD_END_OF_EARLY_DATA) {
+      *out_type = type;
+      return ssl_open_record_success;
+    }
+
     return ssl_process_alert(ssl, out_alert, CBS_data(out), CBS_len(out));
   }