Adding NewSessionTicket.

We will now send tickets as a server and accept them as a
client. Correctly offering and resuming them in the handshake will be
implemented in a follow-up.

Now that we're actually processing draft 14 tickets, bump the draft
version.

Change-Id: I304320a29c4ffe564fa9c00642a4ace96ff8d871
Reviewed-on: https://boringssl-review.googlesource.com/8982
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/include/openssl/ssl.h b/include/openssl/ssl.h
index 0ec3b58..90db2ca 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -572,7 +572,7 @@
 #define DTLS1_VERSION 0xfeff
 #define DTLS1_2_VERSION 0xfefd
 
-#define TLS1_3_DRAFT_VERSION 13
+#define TLS1_3_DRAFT_VERSION 14
 
 /* SSL_CTX_set_min_version sets the minimum protocol version for |ctx| to
  * |version|. */
@@ -3702,7 +3702,10 @@
   uint8_t original_handshake_hash[EVP_MAX_MD_SIZE];
   unsigned original_handshake_hash_len;
 
-  uint32_t tlsext_tick_lifetime_hint; /* Session lifetime hint in seconds */
+  uint32_t ticket_lifetime_hint; /* Session lifetime hint in seconds */
+
+  uint32_t ticket_flags;
+  uint32_t ticket_age_add;
 
   /* extended_master_secret is true if the master secret in this session was
    * generated using EMS and thus isn't vulnerable to the Triple Handshake
@@ -3714,6 +3717,9 @@
 
   /* not_resumable is used to indicate that session resumption is disallowed. */
   unsigned not_resumable:1;
+
+  /* ticket_age_add_valid is non-zero if |ticket_age_add| is valid. */
+  unsigned ticket_age_add_valid:1;
 };
 
 /* ssl_cipher_preference_list_st contains a list of SSL_CIPHERs with
diff --git a/ssl/handshake_client.c b/ssl/handshake_client.c
index 396a66d..38fb3af 100644
--- a/ssl/handshake_client.c
+++ b/ssl/handshake_client.c
@@ -1953,7 +1953,7 @@
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     goto err;
   }
-  session->tlsext_tick_lifetime_hint = ticket_lifetime_hint;
+  session->ticket_lifetime_hint = ticket_lifetime_hint;
 
   /* Generate a session ID for this session based on the session ticket. We use
    * the session ID mechanism for detecting ticket resumption. This also fits in
diff --git a/ssl/handshake_server.c b/ssl/handshake_server.c
index dbf34e9..4e4681a 100644
--- a/ssl/handshake_server.c
+++ b/ssl/handshake_server.c
@@ -1867,107 +1867,22 @@
     return ssl->method->write_message(ssl);
   }
 
-  /* Serialize the SSL_SESSION to be encoded into the ticket. */
-  uint8_t *session = NULL;
-  size_t session_len;
-  if (!SSL_SESSION_to_bytes_for_ticket(
-          ssl->session != NULL ? ssl->session : ssl->s3->new_session,
-          &session, &session_len)) {
-    return -1;
-  }
-
-  EVP_CIPHER_CTX ctx;
-  EVP_CIPHER_CTX_init(&ctx);
-  HMAC_CTX hctx;
-  HMAC_CTX_init(&hctx);
-
-  int ret = -1;
   CBB cbb, body, ticket;
-  if (!ssl->method->init_message(ssl, &cbb, &body, SSL3_MT_NEW_SESSION_TICKET) ||
+  if (!ssl->method->init_message(ssl, &cbb, &body,
+                                 SSL3_MT_NEW_SESSION_TICKET) ||
       /* Ticket lifetime hint (advisory only): We leave this unspecified for
        * resumed session (for simplicity), and guess that tickets for new
        * sessions will live as long as their sessions. */
-      !CBB_add_u32(&body, ssl->session != NULL ? 0 :
-                   ssl->s3->new_session->timeout) ||
-      !CBB_add_u16_length_prefixed(&body, &ticket)) {
-    goto err;
-  }
-
-  /* If the session is too long, emit a dummy value rather than abort the
-   * connection. */
-  const size_t max_ticket_overhead =
-      16 + EVP_MAX_IV_LENGTH + EVP_MAX_BLOCK_LENGTH + EVP_MAX_MD_SIZE;
-  if (session_len > 0xffff - max_ticket_overhead) {
-    static const char kTicketPlaceholder[] = "TICKET TOO LARGE";
-
-    if (!CBB_add_bytes(&ticket, (const uint8_t *)kTicketPlaceholder,
-                       strlen(kTicketPlaceholder)) ||
-        !ssl->method->finish_message(ssl, &cbb)) {
-      goto err;
-    }
-
-    ssl->state = SSL3_ST_SW_SESSION_TICKET_B;
-    ret = 1;
-    goto err;
-  }
-
-  /* Initialize HMAC and cipher contexts. If callback present it does all the
-   * work otherwise use generated values from parent ctx. */
-  SSL_CTX *tctx = ssl->initial_ctx;
-  uint8_t iv[EVP_MAX_IV_LENGTH];
-  uint8_t key_name[16];
-  if (tctx->tlsext_ticket_key_cb != NULL) {
-    if (tctx->tlsext_ticket_key_cb(ssl, key_name, iv, &ctx, &hctx,
-                                   1 /* encrypt */) < 0) {
-      goto err;
-    }
-  } else {
-    if (!RAND_bytes(iv, 16) ||
-        !EVP_EncryptInit_ex(&ctx, EVP_aes_128_cbc(), NULL,
-                            tctx->tlsext_tick_aes_key, iv) ||
-        !HMAC_Init_ex(&hctx, tctx->tlsext_tick_hmac_key, 16, tlsext_tick_md(),
-                      NULL)) {
-      goto err;
-    }
-    memcpy(key_name, tctx->tlsext_tick_key_name, 16);
-  }
-
-  uint8_t *ptr;
-  if (!CBB_add_bytes(&ticket, key_name, 16) ||
-      !CBB_add_bytes(&ticket, iv, EVP_CIPHER_CTX_iv_length(&ctx)) ||
-      !CBB_reserve(&ticket, &ptr, session_len + EVP_MAX_BLOCK_LENGTH)) {
-    goto err;
-  }
-
-  int len;
-  size_t total = 0;
-  if (!EVP_EncryptUpdate(&ctx, ptr + total, &len, session, session_len)) {
-    goto err;
-  }
-  total += len;
-  if (!EVP_EncryptFinal_ex(&ctx, ptr + total, &len)) {
-    goto err;
-  }
-  total += len;
-  if (!CBB_did_write(&ticket, total)) {
-    goto err;
-  }
-
-  unsigned hlen;
-  if (!HMAC_Update(&hctx, CBB_data(&ticket), CBB_len(&ticket)) ||
-      !CBB_reserve(&ticket, &ptr, EVP_MAX_MD_SIZE) ||
-      !HMAC_Final(&hctx, ptr, &hlen) ||
-      !CBB_did_write(&ticket, hlen) ||
+      !CBB_add_u32(&body,
+                   ssl->session != NULL ? 0 : ssl->s3->new_session->timeout) ||
+      !CBB_add_u16_length_prefixed(&body, &ticket) ||
+      !ssl_encrypt_ticket(ssl, &ticket, ssl->session != NULL
+                                            ? ssl->session
+                                            : ssl->s3->new_session) ||
       !ssl->method->finish_message(ssl, &cbb)) {
-    goto err;
+    return 0;
   }
 
   ssl->state = SSL3_ST_SW_SESSION_TICKET_B;
-  ret = ssl->method->write_message(ssl);
-
-err:
-  OPENSSL_free(session);
-  EVP_CIPHER_CTX_cleanup(&ctx);
-  HMAC_CTX_cleanup(&hctx);
-  return ret;
+  return ssl->method->write_message(ssl);
 }
diff --git a/ssl/internal.h b/ssl/internal.h
index 22b5566..360853f 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -891,6 +891,8 @@
 
   uint8_t *cert_context;
   size_t cert_context_len;
+
+  uint8_t session_tickets_sent;
 } /* SSL_HANDSHAKE */;
 
 SSL_HANDSHAKE *ssl_handshake_new(enum ssl_hs_wait_t (*do_handshake)(SSL *ssl));
@@ -925,6 +927,7 @@
 enum ssl_private_key_result_t tls13_prepare_certificate_verify(
     SSL *ssl, int is_first_run);
 int tls13_prepare_finished(SSL *ssl);
+int tls13_process_new_session_ticket(SSL *ssl);
 
 int ssl_ext_key_share_parse_serverhello(SSL *ssl, uint8_t **out_secret,
                                         size_t *out_secret_len,
@@ -1202,12 +1205,18 @@
 extern const SSL3_ENC_METHOD TLSv1_enc_data;
 extern const SSL3_ENC_METHOD SSLv3_enc_data;
 
+/* From draft-ietf-tls-tls13-14, used in determining ticket validity. */
+#define SSL_TICKET_ALLOW_EARLY_DATA 1
+#define SSL_TICKET_ALLOW_DHE_RESUMPTION 2
+#define SSL_TICKET_ALLOW_PSK_RESUMPTION 4
+
 int ssl_clear_bad_session(SSL *ssl);
 CERT *ssl_cert_new(void);
 CERT *ssl_cert_dup(CERT *cert);
 void ssl_cert_clear_certs(CERT *c);
 void ssl_cert_free(CERT *c);
 int ssl_get_new_session(SSL *ssl, int is_server);
+int ssl_encrypt_ticket(SSL *ssl, CBB *out, const SSL_SESSION *session);
 
 enum ssl_session_result_t {
   ssl_session_success,
diff --git a/ssl/ssl_asn1.c b/ssl/ssl_asn1.c
index 41987f8..b1c6a09 100644
--- a/ssl/ssl_asn1.c
+++ b/ssl/ssl_asn1.c
@@ -120,6 +120,8 @@
  *     extendedMasterSecret    [17] BOOLEAN OPTIONAL,
  *     keyExchangeInfo         [18] INTEGER OPTIONAL,
  *     certChain               [19] SEQUENCE OF Certificate OPTIONAL,
+ *     ticketFlags             [20] INTEGER OPTIONAL,
+ *     ticketAgeAdd            [21] OCTET STRING OPTIONAL,
  * }
  *
  * Note: historically this serialization has included other optional
@@ -164,6 +166,10 @@
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 18;
 static const int kCertChainTag =
     CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 19;
+static const int kTicketFlagsTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 20;
+static const int kTicketAgeAddTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 21;
 
 static int SSL_SESSION_to_bytes_full(const SSL_SESSION *in, uint8_t **out_data,
                                      size_t *out_len, int for_ticket) {
@@ -255,9 +261,9 @@
     }
   }
 
-  if (in->tlsext_tick_lifetime_hint > 0) {
+  if (in->ticket_lifetime_hint > 0) {
     if (!CBB_add_asn1(&session, &child, kTicketLifetimeHintTag) ||
-        !CBB_add_asn1_uint64(&child, in->tlsext_tick_lifetime_hint)) {
+        !CBB_add_asn1_uint64(&child, in->ticket_lifetime_hint)) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
       goto err;
     }
@@ -341,6 +347,23 @@
     }
   }
 
+  if (in->ticket_flags > 0) {
+    if (!CBB_add_asn1(&session, &child, kTicketFlagsTag) ||
+        !CBB_add_asn1_uint64(&child, in->ticket_flags)) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+      goto err;
+    }
+  }
+
+  if (in->ticket_age_add_valid) {
+    if (!CBB_add_asn1(&session, &child, kTicketAgeAddTag) ||
+        !CBB_add_asn1(&child, &child2, CBS_ASN1_OCTETSTRING) ||
+        !CBB_add_u32(&child2, in->ticket_age_add)) {
+      OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+      goto err;
+    }
+  }
+
   if (!CBB_finish(&cbb, out_data, out_len)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
     goto err;
@@ -573,7 +596,7 @@
                                 kHostNameTag) ||
       !SSL_SESSION_parse_string(&session, &ret->psk_identity,
                                 kPSKIdentityTag) ||
-      !SSL_SESSION_parse_u32(&session, &ret->tlsext_tick_lifetime_hint,
+      !SSL_SESSION_parse_u32(&session, &ret->ticket_lifetime_hint,
                              kTicketLifetimeHintTag, 0) ||
       !SSL_SESSION_parse_octet_string(&session, &ret->tlsext_tick,
                                       &ret->tlsext_ticklen, kTicketTag)) {
@@ -652,6 +675,19 @@
     }
   }
 
+  CBS age_add;
+  int age_add_present;
+  if (!SSL_SESSION_parse_u32(&session, &ret->ticket_flags,
+                             kTicketFlagsTag, 0) ||
+      !CBS_get_optional_asn1_octet_string(&session, &age_add, &age_add_present,
+                                          kTicketAgeAddTag) ||
+      (age_add_present &&
+       !CBS_get_u32(&age_add, &ret->ticket_age_add)) ||
+      CBS_len(&age_add) != 0) {
+    goto err;
+  }
+  ret->ticket_age_add_valid = age_add_present;
+
   if (CBS_len(&session) != 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_INVALID_SSL_SESSION);
     goto err;
diff --git a/ssl/ssl_session.c b/ssl/ssl_session.c
index 5553400..cb1edb9 100644
--- a/ssl/ssl_session.c
+++ b/ssl/ssl_session.c
@@ -219,8 +219,8 @@
   }
   if (include_ticket) {
     if (session->tlsext_tick != NULL) {
-      new_session->tlsext_tick = BUF_memdup(session->tlsext_tick,
-                                            session->tlsext_ticklen);
+      new_session->tlsext_tick =
+          BUF_memdup(session->tlsext_tick, session->tlsext_ticklen);
       if (new_session->tlsext_tick == NULL) {
         goto err;
       }
@@ -252,7 +252,9 @@
          session->original_handshake_hash_len);
   new_session->original_handshake_hash_len =
       session->original_handshake_hash_len;
-  new_session->tlsext_tick_lifetime_hint = session->tlsext_tick_lifetime_hint;
+  new_session->ticket_lifetime_hint = session->ticket_lifetime_hint;
+  new_session->ticket_flags = session->ticket_flags;
+  new_session->ticket_age_add = session->ticket_age_add;
   new_session->extended_master_secret = session->extended_master_secret;
   new_session->peer_sha256_valid = session->peer_sha256_valid;
   new_session->not_resumable = 1;
@@ -468,6 +470,93 @@
   return 0;
 }
 
+int ssl_encrypt_ticket(SSL *ssl, CBB *out, const SSL_SESSION *session) {
+  int ret = 0;
+
+  /* Serialize the SSL_SESSION to be encoded into the ticket. */
+  uint8_t *session_buf = NULL;
+  size_t session_len;
+  if (!SSL_SESSION_to_bytes_for_ticket(session, &session_buf, &session_len)) {
+    return -1;
+  }
+
+  EVP_CIPHER_CTX ctx;
+  EVP_CIPHER_CTX_init(&ctx);
+  HMAC_CTX hctx;
+  HMAC_CTX_init(&hctx);
+
+  /* If the session is too long, emit a dummy value rather than abort the
+   * connection. */
+  static const size_t kMaxTicketOverhead =
+      16 + EVP_MAX_IV_LENGTH + EVP_MAX_BLOCK_LENGTH + EVP_MAX_MD_SIZE;
+  if (session_len > 0xffff - kMaxTicketOverhead) {
+    static const char kTicketPlaceholder[] = "TICKET TOO LARGE";
+    if (CBB_add_bytes(out, (const uint8_t *)kTicketPlaceholder,
+                      strlen(kTicketPlaceholder))) {
+      ret = 1;
+    }
+    goto err;
+  }
+
+  /* Initialize HMAC and cipher contexts. If callback present it does all the
+   * work otherwise use generated values from parent ctx. */
+  SSL_CTX *tctx = ssl->initial_ctx;
+  uint8_t iv[EVP_MAX_IV_LENGTH];
+  uint8_t key_name[16];
+  if (tctx->tlsext_ticket_key_cb != NULL) {
+    if (tctx->tlsext_ticket_key_cb(ssl, key_name, iv, &ctx, &hctx,
+                                   1 /* encrypt */) < 0) {
+      goto err;
+    }
+  } else {
+    if (!RAND_bytes(iv, 16) ||
+        !EVP_EncryptInit_ex(&ctx, EVP_aes_128_cbc(), NULL,
+                            tctx->tlsext_tick_aes_key, iv) ||
+        !HMAC_Init_ex(&hctx, tctx->tlsext_tick_hmac_key, 16, tlsext_tick_md(),
+                      NULL)) {
+      goto err;
+    }
+    memcpy(key_name, tctx->tlsext_tick_key_name, 16);
+  }
+
+  uint8_t *ptr;
+  if (!CBB_add_bytes(out, key_name, 16) ||
+      !CBB_add_bytes(out, iv, EVP_CIPHER_CTX_iv_length(&ctx)) ||
+      !CBB_reserve(out, &ptr, session_len + EVP_MAX_BLOCK_LENGTH)) {
+    goto err;
+  }
+
+  int len;
+  size_t total = 0;
+  if (!EVP_EncryptUpdate(&ctx, ptr + total, &len, session_buf, session_len)) {
+    goto err;
+  }
+  total += len;
+  if (!EVP_EncryptFinal_ex(&ctx, ptr + total, &len)) {
+    goto err;
+  }
+  total += len;
+  if (!CBB_did_write(out, total)) {
+    goto err;
+  }
+
+  unsigned hlen;
+  if (!HMAC_Update(&hctx, CBB_data(out), CBB_len(out)) ||
+      !CBB_reserve(out, &ptr, EVP_MAX_MD_SIZE) ||
+      !HMAC_Final(&hctx, ptr, &hlen) ||
+      !CBB_did_write(out, hlen)) {
+    goto err;
+  }
+
+  ret = 1;
+
+err:
+  OPENSSL_free(session_buf);
+  EVP_CIPHER_CTX_cleanup(&ctx);
+  HMAC_CTX_cleanup(&hctx);
+  return ret;
+}
+
 /* ssl_lookup_session looks up |session_id| in the session cache and sets
  * |*out_session| to an |SSL_SESSION| object if found. The caller takes
  * ownership of the result. */
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index dd7d3a5..9cc1277 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -1028,6 +1028,14 @@
   return ret;
 }
 
+static uint16_t GetProtocolVersion(const SSL *ssl) {
+  uint16_t version = SSL_version(ssl);
+  if (!SSL_is_dtls(ssl)) {
+    return version;
+  }
+  return 0x0201 + ~version;
+}
+
 // CheckHandshakeProperties checks, immediately after |ssl| completes its
 // initial handshake (or False Starts), whether all the properties are
 // consistent with the test configuration and invariants.
@@ -1057,8 +1065,8 @@
     bool expect_new_session =
         !config->expect_no_session &&
         (!SSL_session_reused(ssl) || config->expect_ticket_renewal) &&
-        /* TODO(svaldez): Implement Session Resumption. */
-        SSL_version(ssl) != TLS1_3_VERSION;
+        // Session tickets are sent post-handshake in TLS 1.3.
+        GetProtocolVersion(ssl) < TLS1_3_VERSION;
     if (expect_new_session != GetTestState(ssl)->got_new_session) {
       fprintf(stderr,
               "new session was%s cached, but we expected the opposite\n",
@@ -1569,11 +1577,24 @@
 
   if (!config->is_server && !config->false_start &&
       !config->implicit_handshake &&
+      // Session tickets are sent post-handshake in TLS 1.3.
+      GetProtocolVersion(ssl.get()) < TLS1_3_VERSION &&
       GetTestState(ssl.get())->got_new_session) {
     fprintf(stderr, "new session was established after the handshake\n");
     return false;
   }
 
+  if (GetProtocolVersion(ssl.get()) >= TLS1_3_VERSION && !config->is_server) {
+    bool expect_new_session =
+        !config->expect_no_session && !config->shim_shuts_down;
+    if (expect_new_session != GetTestState(ssl.get())->got_new_session) {
+      fprintf(stderr,
+              "new session was%s cached, but we expected the opposite\n",
+              GetTestState(ssl.get())->got_new_session ? "" : " not");
+      return false;
+    }
+  }
+
   if (out_session) {
     out_session->reset(SSL_get1_session(ssl.get()));
   }
diff --git a/ssl/tls13_both.c b/ssl/tls13_both.c
index cb3d927..83e0c3b 100644
--- a/ssl/tls13_both.c
+++ b/ssl/tls13_both.c
@@ -458,8 +458,7 @@
 
   if (ssl->s3->tmp.message_type == SSL3_MT_NEW_SESSION_TICKET &&
       !ssl->server) {
-    // TODO(svaldez): Handle NewSessionTicket.
-    return 1;
+    return tls13_process_new_session_ticket(ssl);
   }
 
   // TODO(svaldez): Handle post-handshake authentication.
diff --git a/ssl/tls13_client.c b/ssl/tls13_client.c
index 1ad1780..376e0ac 100644
--- a/ssl/tls13_client.c
+++ b/ssl/tls13_client.c
@@ -566,3 +566,38 @@
 
   return ssl_hs_ok;
 }
+
+int tls13_process_new_session_ticket(SSL *ssl) {
+  SSL_SESSION *session = SSL_SESSION_dup(ssl->s3->established_session,
+                                         0 /* don't include ticket */);
+  if (session == NULL) {
+    return 0;
+  }
+
+  CBS cbs, extensions, ticket;
+  CBS_init(&cbs, ssl->init_msg, ssl->init_num);
+  if (!CBS_get_u32(&cbs, &session->ticket_lifetime_hint) ||
+      !CBS_get_u32(&cbs, &session->ticket_flags) ||
+      !CBS_get_u32(&cbs, &session->ticket_age_add) ||
+      !CBS_get_u16_length_prefixed(&cbs, &extensions) ||
+      !CBS_get_u16_length_prefixed(&cbs, &ticket) ||
+      !CBS_stow(&ticket, &session->tlsext_tick, &session->tlsext_ticklen) ||
+      CBS_len(&cbs) != 0) {
+    SSL_SESSION_free(session);
+    ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+    OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+    return 0;
+  }
+
+  session->ticket_age_add_valid = 1;
+  session->not_resumable = 0;
+
+  if (ssl->ctx->new_session_cb != NULL &&
+      ssl->ctx->new_session_cb(ssl, session)) {
+    /* |new_session_cb|'s return value signals that it took ownership. */
+    return 1;
+  }
+
+  SSL_SESSION_free(session);
+  return 1;
+}
diff --git a/ssl/tls13_server.c b/ssl/tls13_server.c
index d844338..b15d56d 100644
--- a/ssl/tls13_server.c
+++ b/ssl/tls13_server.c
@@ -43,6 +43,8 @@
   state_process_client_certificate,
   state_process_client_certificate_verify,
   state_process_client_finished,
+  state_send_new_session_ticket,
+  state_flush_new_session_ticket,
   state_done,
 };
 
@@ -506,10 +508,55 @@
   }
 
   ssl->method->received_flight(ssl);
-  hs->state = state_done;
+  hs->state = state_send_new_session_ticket;
   return ssl_hs_ok;
 }
 
+static enum ssl_hs_wait_t do_send_new_session_ticket(SSL *ssl,
+                                                     SSL_HANDSHAKE *hs) {
+  SSL_SESSION *session = ssl->s3->new_session;
+  session->ticket_lifetime_hint = session->timeout;
+  session->ticket_flags = SSL_TICKET_ALLOW_DHE_RESUMPTION;
+  if (!RAND_bytes((uint8_t *)&session->ticket_age_add,
+                  sizeof(session->ticket_age_add))) {
+    return 0;
+  }
+  session->ticket_age_add_valid = 1;
+
+  CBB cbb, body, ticket;
+  if (!ssl->method->init_message(ssl, &cbb, &body,
+                                 SSL3_MT_NEW_SESSION_TICKET) ||
+      !CBB_add_u32(&body, session->ticket_lifetime_hint) ||
+      !CBB_add_u32(&body, session->ticket_flags) ||
+      !CBB_add_u32(&body, session->ticket_age_add) ||
+      !CBB_add_u16(&body, 0 /* no ticket extensions */) ||
+      !CBB_add_u16_length_prefixed(&body, &ticket) ||
+      !ssl_encrypt_ticket(ssl, &ticket, session) ||
+      !ssl->method->finish_message(ssl, &cbb)) {
+    CBB_cleanup(&cbb);
+    return ssl_hs_error;
+  }
+
+  hs->session_tickets_sent++;
+
+  hs->state = state_flush_new_session_ticket;
+  return ssl_hs_write_message;
+}
+
+/* TLS 1.3 recommends single-use tickets, so issue multiple tickets in case the
+ * client makes several connections before getting a renewal. */
+static const int kNumTickets = 2;
+
+static enum ssl_hs_wait_t do_flush_new_session_ticket(SSL *ssl,
+                                                      SSL_HANDSHAKE *hs) {
+  if (hs->session_tickets_sent >= kNumTickets) {
+    hs->state = state_done;
+  } else {
+    hs->state = state_send_new_session_ticket;
+  }
+  return ssl_hs_flush;
+}
+
 enum ssl_hs_wait_t tls13_server_handshake(SSL *ssl) {
   SSL_HANDSHAKE *hs = ssl->s3->hs;
 
@@ -562,6 +609,12 @@
       case state_process_client_finished:
         ret = do_process_client_finished(ssl, hs);
         break;
+      case state_send_new_session_ticket:
+        ret = do_send_new_session_ticket(ssl, hs);
+        break;
+      case state_flush_new_session_ticket:
+        ret = do_flush_new_session_ticket(ssl, hs);
+        break;
       case state_done:
         ret = ssl_hs_ok;
         break;