Support asynchronous ticket decryption with TLS 1.0–1.2.

This change adds support for setting an |SSL_TICKET_AEAD_METHOD| which
allows a caller to control ticket encryption and decryption to a greater
extent than previously possible and also permits asynchronous ticket
decryption.

This change only includes partial support: TLS 1.3 work remains to be
done.

Change-Id: Ia2e10ebb3257e1a119630c463b6bf389cf20ef18
Reviewed-on: https://boringssl-review.googlesource.com/14144
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/handshake_server.c b/ssl/handshake_server.c
index e3a4e51..81e45ef 100644
--- a/ssl/handshake_server.c
+++ b/ssl/handshake_server.c
@@ -930,6 +930,9 @@
     case ssl_session_retry:
       ssl->rwstate = SSL_PENDING_SESSION;
       goto err;
+    case ssl_session_ticket_retry:
+      ssl->rwstate = SSL_PENDING_TICKET;
+      goto err;
   }
 
   if (session != NULL) {
diff --git a/ssl/internal.h b/ssl/internal.h
index bc72239..b93a3e4 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1991,13 +1991,15 @@
   ssl_session_success,
   ssl_session_error,
   ssl_session_retry,
+  ssl_session_ticket_retry,
 };
 
 /* ssl_get_prev_session looks up the previous session based on |client_hello|.
  * On success, it sets |*out_session| to the session or NULL if none was found.
  * If the session could not be looked up synchronously, it returns
- * |ssl_session_retry| and should be called again. Otherwise, it returns
- * |ssl_session_error|.  */
+ * |ssl_session_retry| and should be called again. If a ticket could not be
+ * decrypted immediately it returns |ssl_session_ticket_retry| and should also
+ * be called again. Otherwise, it returns |ssl_session_error|.  */
 enum ssl_session_result_t ssl_get_prev_session(
     SSL *ssl, SSL_SESSION **out_session, int *out_tickets_supported,
     int *out_renew_ticket, const SSL_CLIENT_HELLO *client_hello);
@@ -2165,15 +2167,19 @@
 
 #define tlsext_tick_md EVP_sha256
 
-/* tls_process_ticket processes a session ticket from the client. On success,
- * it sets |*out_session| to the decrypted session or NULL if the ticket was
- * rejected. If the ticket was valid, it sets |*out_renew_ticket| to whether
- * the ticket should be renewed. It returns one on success and zero on fatal
- * error. */
-int tls_process_ticket(SSL *ssl, SSL_SESSION **out_session,
-                       int *out_renew_ticket, const uint8_t *ticket,
-                       size_t ticket_len, const uint8_t *session_id,
-                       size_t session_id_len);
+/* ssl_process_ticket processes a session ticket from the client. It returns
+ * one of:
+ *   |ssl_ticket_aead_success|: |*out_session| is set to the parsed session and
+ *       |*out_renew_ticket| is set to whether the ticket should be renewed.
+ *   |ssl_ticket_aead_ignore_ticket|: |*out_renew_ticket| is set to whether a
+ *       fresh ticket should be sent, but the given ticket cannot be used.
+ *   |ssl_ticket_aead_retry|: the ticket could not be immediately decrypted.
+ *       Retry later.
+ *   |ssl_ticket_aead_error|: an error occured that is fatal to the connection. */
+enum ssl_ticket_aead_result_t ssl_process_ticket(
+    SSL *ssl, SSL_SESSION **out_session, int *out_renew_ticket,
+    const uint8_t *ticket, size_t ticket_len, const uint8_t *session_id,
+    size_t session_id_len);
 
 /* tls1_verify_channel_id processes the current message as a Channel ID message,
  * and verifies the signature. If the key is valid, it saves the Channel ID and
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 95ea170..d16c952 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -910,6 +910,9 @@
 
     case SSL_PRIVATE_KEY_OPERATION:
       return SSL_ERROR_WANT_PRIVATE_KEY_OPERATION;
+
+    case SSL_PENDING_TICKET:
+      return SSL_ERROR_PENDING_TICKET;
   }
 
   return SSL_ERROR_SYSCALL;
@@ -2699,3 +2702,8 @@
 int SSL_set_max_version(SSL *ssl, uint16_t version) {
   return SSL_set_max_proto_version(ssl, version);
 }
+
+void SSL_CTX_set_ticket_aead_method(SSL_CTX *ctx,
+                                    const SSL_TICKET_AEAD_METHOD *aead_method) {
+  ctx->ticket_aead_method = aead_method;
+}
diff --git a/ssl/ssl_session.c b/ssl/ssl_session.c
index e11238f..05ae059 100644
--- a/ssl/ssl_session.c
+++ b/ssl/ssl_session.c
@@ -581,16 +581,11 @@
   return 0;
 }
 
-int ssl_encrypt_ticket(SSL *ssl, CBB *out, const SSL_SESSION *session) {
+static int ssl_encrypt_ticket_with_cipher_ctx(SSL *ssl, CBB *out,
+                                              const uint8_t *session_buf,
+                                              size_t session_len) {
   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;
@@ -667,12 +662,60 @@
   ret = 1;
 
 err:
-  OPENSSL_free(session_buf);
   EVP_CIPHER_CTX_cleanup(&ctx);
   HMAC_CTX_cleanup(&hctx);
   return ret;
 }
 
+static int ssl_encrypt_ticket_with_method(SSL *ssl, CBB *out,
+                                          const uint8_t *session_buf,
+                                          size_t session_len) {
+  const SSL_TICKET_AEAD_METHOD *method = ssl->session_ctx->ticket_aead_method;
+  const size_t max_overhead = method->max_overhead(ssl);
+  const size_t max_out = session_len + max_overhead;
+  if (max_out < max_overhead) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_OVERFLOW);
+    return 0;
+  }
+
+  uint8_t *ptr;
+  if (!CBB_reserve(out, &ptr, max_out)) {
+    return 0;
+  }
+
+  size_t out_len;
+  if (!method->seal(ssl, ptr, &out_len, max_out, session_buf, session_len)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_TICKET_ENCRYPTION_FAILED);
+    return 0;
+  }
+
+  if (!CBB_did_write(out, out_len)) {
+    return 0;
+  }
+
+  return 1;
+}
+
+int ssl_encrypt_ticket(SSL *ssl, CBB *out, const SSL_SESSION *session) {
+  /* 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;
+  }
+
+  int ret = 0;
+  if (ssl->session_ctx->ticket_aead_method) {
+    ret = ssl_encrypt_ticket_with_method(ssl, out, session_buf, session_len);
+  } else {
+    ret =
+        ssl_encrypt_ticket_with_cipher_ctx(ssl, out, session_buf, session_len);
+  }
+
+  OPENSSL_free(session_buf);
+  return ret;
+}
+
 int ssl_session_is_context_valid(const SSL *ssl, const SSL_SESSION *session) {
   if (session == NULL) {
     return 0;
@@ -811,10 +854,18 @@
       SSL_early_callback_ctx_extension_get(
           client_hello, TLSEXT_TYPE_session_ticket, &ticket, &ticket_len);
   if (tickets_supported && ticket_len > 0) {
-    if (!tls_process_ticket(ssl, &session, &renew_ticket, ticket, ticket_len,
-                            client_hello->session_id,
-                            client_hello->session_id_len)) {
-      return ssl_session_error;
+    switch (ssl_process_ticket(ssl, &session, &renew_ticket, ticket, ticket_len,
+                               client_hello->session_id,
+                               client_hello->session_id_len)) {
+      case ssl_ticket_aead_success:
+        break;
+      case ssl_ticket_aead_ignore_ticket:
+        assert(session == NULL);
+        break;
+      case ssl_ticket_aead_error:
+        return ssl_session_error;
+      case ssl_ticket_aead_retry:
+        return ssl_session_ticket_retry;
     }
   } else {
     /* The client didn't send a ticket, so the session ID is a real ID. */
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 5f89b81..719bdab 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -1352,7 +1352,8 @@
     int client_err = SSL_get_error(client, client_ret);
     if (client_err != SSL_ERROR_NONE &&
         client_err != SSL_ERROR_WANT_READ &&
-        client_err != SSL_ERROR_WANT_WRITE) {
+        client_err != SSL_ERROR_WANT_WRITE &&
+        client_err != SSL_ERROR_PENDING_TICKET) {
       fprintf(stderr, "Client error: %d\n", client_err);
       return false;
     }
@@ -1361,7 +1362,8 @@
     int server_err = SSL_get_error(server, server_ret);
     if (server_err != SSL_ERROR_NONE &&
         server_err != SSL_ERROR_WANT_READ &&
-        server_err != SSL_ERROR_WANT_WRITE) {
+        server_err != SSL_ERROR_WANT_WRITE &&
+        server_err != SSL_ERROR_PENDING_TICKET) {
       fprintf(stderr, "Server error: %d\n", server_err);
       return false;
     }
@@ -3246,6 +3248,238 @@
   EXPECT_EQ(0u, sk_SSL_CIPHER_num(SSL_CTX_get_ciphers(ctx.get())));
 }
 
+// ssl_test_ticket_aead_failure_mode enumerates the possible ways in which the
+// test |SSL_TICKET_AEAD_METHOD| can fail.
+enum ssl_test_ticket_aead_failure_mode {
+  ssl_test_ticket_aead_ok = 0,
+  ssl_test_ticket_aead_seal_fail,
+  ssl_test_ticket_aead_open_soft_fail,
+  ssl_test_ticket_aead_open_hard_fail,
+};
+
+struct ssl_test_ticket_aead_state {
+  unsigned retry_count;
+  ssl_test_ticket_aead_failure_mode failure_mode;
+};
+
+static int ssl_test_ticket_aead_ex_index_dup(CRYPTO_EX_DATA *to,
+                                             const CRYPTO_EX_DATA *from,
+                                             void **from_d, int index,
+                                             long argl, void *argp) {
+  abort();
+}
+
+static void ssl_test_ticket_aead_ex_index_free(void *parent, void *ptr,
+                                               CRYPTO_EX_DATA *ad, int index,
+                                               long argl, void *argp) {
+  auto state = reinterpret_cast<ssl_test_ticket_aead_state*>(ptr);
+  if (state == nullptr) {
+    return;
+  }
+
+  OPENSSL_free(state);
+}
+
+static CRYPTO_once_t g_ssl_test_ticket_aead_ex_index_once = CRYPTO_ONCE_INIT;
+static int g_ssl_test_ticket_aead_ex_index;
+
+static int ssl_test_ticket_aead_get_ex_index() {
+  CRYPTO_once(&g_ssl_test_ticket_aead_ex_index_once, [] {
+    g_ssl_test_ticket_aead_ex_index = SSL_get_ex_new_index(
+        0, nullptr, nullptr, ssl_test_ticket_aead_ex_index_dup,
+        ssl_test_ticket_aead_ex_index_free);
+  });
+  return g_ssl_test_ticket_aead_ex_index;
+}
+
+static size_t ssl_test_ticket_aead_max_overhead(SSL *ssl) {
+  return 1;
+}
+
+static int ssl_test_ticket_aead_seal(SSL *ssl, uint8_t *out, size_t *out_len,
+                                     size_t max_out_len, const uint8_t *in,
+                                     size_t in_len) {
+  auto state = reinterpret_cast<ssl_test_ticket_aead_state *>(
+      SSL_get_ex_data(ssl, ssl_test_ticket_aead_get_ex_index()));
+
+  if (state->failure_mode == ssl_test_ticket_aead_seal_fail ||
+      max_out_len < in_len + 1) {
+    return 0;
+  }
+
+  OPENSSL_memmove(out, in, in_len);
+  out[in_len] = 0xff;
+  *out_len = in_len + 1;
+
+  return 1;
+}
+
+static ssl_ticket_aead_result_t ssl_test_ticket_aead_open(
+    SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out_len,
+    const uint8_t *in, size_t in_len) {
+  auto state = reinterpret_cast<ssl_test_ticket_aead_state *>(
+      SSL_get_ex_data(ssl, ssl_test_ticket_aead_get_ex_index()));
+
+  if (state->retry_count > 0) {
+    state->retry_count--;
+    return ssl_ticket_aead_retry;
+  }
+
+  switch (state->failure_mode) {
+    case ssl_test_ticket_aead_ok:
+      break;
+    case ssl_test_ticket_aead_seal_fail:
+      // If |seal| failed then there shouldn't be any ticket to try and
+      // decrypt.
+      abort();
+      break;
+    case ssl_test_ticket_aead_open_soft_fail:
+      return ssl_ticket_aead_ignore_ticket;
+    case ssl_test_ticket_aead_open_hard_fail:
+      return ssl_ticket_aead_error;
+  }
+
+  if (in_len == 0 || in[in_len - 1] != 0xff) {
+    return ssl_ticket_aead_ignore_ticket;
+  }
+
+  if (max_out_len < in_len - 1) {
+    return ssl_ticket_aead_error;
+  }
+
+  OPENSSL_memmove(out, in, in_len - 1);
+  *out_len = in_len - 1;
+  return ssl_ticket_aead_success;
+}
+
+static const SSL_TICKET_AEAD_METHOD kSSLTestTicketMethod = {
+  ssl_test_ticket_aead_max_overhead,
+  ssl_test_ticket_aead_seal,
+  ssl_test_ticket_aead_open,
+};
+
+static void ConnectClientAndServerWithTicketMethod(
+    bssl::UniquePtr<SSL> *out_client, bssl::UniquePtr<SSL> *out_server,
+    SSL_CTX *client_ctx, SSL_CTX *server_ctx, unsigned retry_count,
+    ssl_test_ticket_aead_failure_mode failure_mode, SSL_SESSION *session) {
+  bssl::UniquePtr<SSL> client(SSL_new(client_ctx)), server(SSL_new(server_ctx));
+  ASSERT_TRUE(client);
+  ASSERT_TRUE(server);
+  SSL_set_connect_state(client.get());
+  SSL_set_accept_state(server.get());
+
+  auto state = reinterpret_cast<ssl_test_ticket_aead_state *>(
+      OPENSSL_malloc(sizeof(ssl_test_ticket_aead_state)));
+  ASSERT_TRUE(state);
+  OPENSSL_memset(state, 0, sizeof(ssl_test_ticket_aead_state));
+  state->retry_count = retry_count;
+  state->failure_mode = failure_mode;
+
+  ASSERT_TRUE(SSL_set_ex_data(server.get(), ssl_test_ticket_aead_get_ex_index(),
+                              state));
+
+  SSL_set_session(client.get(), session);
+
+  BIO *bio1, *bio2;
+  ASSERT_TRUE(BIO_new_bio_pair(&bio1, 0, &bio2, 0));
+
+  // SSL_set_bio takes ownership.
+  SSL_set_bio(client.get(), bio1, bio1);
+  SSL_set_bio(server.get(), bio2, bio2);
+
+  if (CompleteHandshakes(client.get(), server.get())) {
+    *out_client = std::move(client);
+    *out_server = std::move(server);
+  } else {
+    out_client->reset();
+    out_server->reset();
+  }
+}
+
+class TicketAEADMethodTest
+    : public ::testing::TestWithParam<testing::tuple<
+          uint16_t, unsigned, ssl_test_ticket_aead_failure_mode>> {};
+
+TEST_P(TicketAEADMethodTest, Resume) {
+  bssl::UniquePtr<X509> cert = GetTestCertificate();
+  ASSERT_TRUE(cert);
+  bssl::UniquePtr<EVP_PKEY> key = GetTestKey();
+  ASSERT_TRUE(key);
+
+  bssl::UniquePtr<SSL_CTX> server_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(server_ctx);
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(client_ctx);
+
+  const uint16_t version = testing::get<0>(GetParam());
+  const unsigned retry_count = testing::get<1>(GetParam());
+  const ssl_test_ticket_aead_failure_mode failure_mode =
+      testing::get<2>(GetParam());
+
+  ASSERT_TRUE(SSL_CTX_use_certificate(server_ctx.get(), cert.get()));
+  ASSERT_TRUE(SSL_CTX_use_PrivateKey(server_ctx.get(), key.get()));
+  ASSERT_TRUE(SSL_CTX_set_min_proto_version(client_ctx.get(), version));
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(client_ctx.get(), version));
+  ASSERT_TRUE(SSL_CTX_set_min_proto_version(server_ctx.get(), version));
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(server_ctx.get(), version));
+
+  SSL_CTX_set_session_cache_mode(client_ctx.get(), SSL_SESS_CACHE_BOTH);
+  SSL_CTX_set_session_cache_mode(server_ctx.get(), SSL_SESS_CACHE_BOTH);
+  SSL_CTX_set_current_time_cb(client_ctx.get(), FrozenTimeCallback);
+  SSL_CTX_set_current_time_cb(server_ctx.get(), FrozenTimeCallback);
+
+  SSL_CTX_set_ticket_aead_method(server_ctx.get(), &kSSLTestTicketMethod);
+
+  bssl::UniquePtr<SSL> client, server;
+  ConnectClientAndServerWithTicketMethod(&client, &server, client_ctx.get(),
+                                         server_ctx.get(), retry_count,
+                                         failure_mode, nullptr);
+  switch (failure_mode) {
+    case ssl_test_ticket_aead_ok:
+    case ssl_test_ticket_aead_open_hard_fail:
+    case ssl_test_ticket_aead_open_soft_fail:
+      ASSERT_TRUE(client);
+      break;
+    case ssl_test_ticket_aead_seal_fail:
+      EXPECT_FALSE(client);
+      return;
+  }
+  EXPECT_FALSE(SSL_session_reused(client.get()));
+  EXPECT_FALSE(SSL_session_reused(server.get()));
+
+  SSL_SESSION *session = SSL_get_session(client.get());
+  ConnectClientAndServerWithTicketMethod(&client, &server, client_ctx.get(),
+                                         server_ctx.get(), retry_count,
+                                         failure_mode, session);
+  switch (failure_mode) {
+    case ssl_test_ticket_aead_ok:
+      ASSERT_TRUE(client);
+      EXPECT_TRUE(SSL_session_reused(client.get()));
+      EXPECT_TRUE(SSL_session_reused(server.get()));
+      break;
+    case ssl_test_ticket_aead_seal_fail:
+      abort();
+      break;
+    case ssl_test_ticket_aead_open_hard_fail:
+      EXPECT_FALSE(client);
+      break;
+    case ssl_test_ticket_aead_open_soft_fail:
+      ASSERT_TRUE(client);
+      EXPECT_FALSE(SSL_session_reused(client.get()));
+      EXPECT_FALSE(SSL_session_reused(server.get()));
+  }
+}
+
+INSTANTIATE_TEST_CASE_P(
+    TicketAEADMethodTests, TicketAEADMethodTest,
+    testing::Combine(
+        testing::Values(TLS1_2_VERSION /*, TLS1_3_VERSION TODO: enable */),
+        testing::Values(0, 1, 2),
+        testing::Values(ssl_test_ticket_aead_ok,
+                        ssl_test_ticket_aead_seal_fail,
+                        ssl_test_ticket_aead_open_soft_fail,
+                        ssl_test_ticket_aead_open_hard_fail)));
+
 // TODO(davidben): Convert this file to GTest properly.
 TEST(SSLTest, AllTests) {
   if (!TestCipherRules() ||
diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index 34478ed..e3d0a9e 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -2017,10 +2017,18 @@
   /* TLS 1.3 session tickets are renewed separately as part of the
    * NewSessionTicket. */
   int unused_renew;
-  if (!tls_process_ticket(ssl, out_session, &unused_renew, CBS_data(&ticket),
-                          CBS_len(&ticket), NULL, 0)) {
-    *out_alert = SSL_AD_INTERNAL_ERROR;
-    return 0;
+  switch (ssl_process_ticket(ssl, out_session, &unused_renew, CBS_data(&ticket),
+                             CBS_len(&ticket), NULL, 0)) {
+    case ssl_ticket_aead_success:
+      break;
+    case ssl_ticket_aead_ignore_ticket:
+      assert(*out_session == NULL);
+      break;
+    case ssl_ticket_aead_retry:
+    /* TODO: async tickets for TLS 1.3. */
+    case ssl_ticket_aead_error:
+      *out_alert = SSL_AD_INTERNAL_ERROR;
+      return 0;
   }
 
   return 1;
@@ -3043,12 +3051,12 @@
   return 1;
 }
 
-int tls_process_ticket(SSL *ssl, SSL_SESSION **out_session,
-                       int *out_renew_ticket, const uint8_t *ticket,
-                       size_t ticket_len, const uint8_t *session_id,
-                       size_t session_id_len) {
-  int ret = 1; /* Most errors are non-fatal. */
-  SSL_CTX *ssl_ctx = ssl->session_ctx;
+static enum ssl_ticket_aead_result_t
+ssl_decrypt_ticket_with_cipher_ctx(SSL *ssl, uint8_t **out, size_t *out_len,
+                                   int *out_renew_ticket, const uint8_t *ticket,
+                                   size_t ticket_len) {
+  enum ssl_ticket_aead_result_t ret = ssl_ticket_aead_ignore_ticket;
+  const SSL_CTX *const ssl_ctx = ssl->session_ctx;
   uint8_t *plaintext = NULL;
 
   HMAC_CTX hmac_ctx;
@@ -3056,23 +3064,12 @@
   EVP_CIPHER_CTX cipher_ctx;
   EVP_CIPHER_CTX_init(&cipher_ctx);
 
-  *out_renew_ticket = 0;
-  *out_session = NULL;
-
-  if (SSL_get_options(ssl) & SSL_OP_NO_TICKET) {
-    goto done;
-  }
-
-  if (session_id_len > SSL_MAX_SSL_SESSION_ID_LENGTH) {
-    goto done;
-  }
-
   /* Ensure there is room for the key name and the largest IV
    * |tlsext_ticket_key_cb| may try to consume. The real limit may be lower, but
    * the maximum IV length should be well under the minimum size for the
    * session material and HMAC. */
   if (ticket_len < SSL_TICKET_KEY_NAME_LEN + EVP_MAX_IV_LENGTH) {
-    goto done;
+    goto out;
   }
   const uint8_t *iv = ticket + SSL_TICKET_KEY_NAME_LEN;
 
@@ -3081,28 +3078,26 @@
         ssl, (uint8_t *)ticket /* name */, (uint8_t *)iv, &cipher_ctx,
         &hmac_ctx, 0 /* decrypt */);
     if (cb_ret < 0) {
-      ret = 0;
-      goto done;
-    }
-    if (cb_ret == 0) {
-      goto done;
-    }
-    if (cb_ret == 2) {
+      ret = ssl_ticket_aead_error;
+      goto out;
+    } else if (cb_ret == 0) {
+      goto out;
+    } else if (cb_ret == 2) {
       *out_renew_ticket = 1;
     }
   } else {
     /* Check the key name matches. */
     if (OPENSSL_memcmp(ticket, ssl_ctx->tlsext_tick_key_name,
                        SSL_TICKET_KEY_NAME_LEN) != 0) {
-      goto done;
+      goto out;
     }
     if (!HMAC_Init_ex(&hmac_ctx, ssl_ctx->tlsext_tick_hmac_key,
                       sizeof(ssl_ctx->tlsext_tick_hmac_key), tlsext_tick_md(),
                       NULL) ||
         !EVP_DecryptInit_ex(&cipher_ctx, EVP_aes_128_cbc(), NULL,
                             ssl_ctx->tlsext_tick_aes_key, iv)) {
-      ret = 0;
-      goto done;
+      ret = ssl_ticket_aead_error;
+      goto out;
     }
   }
   size_t iv_len = EVP_CIPHER_CTX_iv_length(&cipher_ctx);
@@ -3112,7 +3107,7 @@
   size_t mac_len = HMAC_size(&hmac_ctx);
   if (ticket_len < SSL_TICKET_KEY_NAME_LEN + iv_len + 1 + mac_len) {
     /* The ticket must be large enough for key name, IV, data, and MAC. */
-    goto done;
+    goto out;
   }
   HMAC_Update(&hmac_ctx, ticket, ticket_len - mac_len);
   HMAC_Final(&hmac_ctx, mac, NULL);
@@ -3122,7 +3117,7 @@
   mac_ok = 1;
 #endif
   if (!mac_ok) {
-    goto done;
+    goto out;
   }
 
   /* Decrypt the session data. */
@@ -3131,8 +3126,8 @@
                           mac_len;
   plaintext = OPENSSL_malloc(ciphertext_len);
   if (plaintext == NULL) {
-    ret = 0;
-    goto done;
+    ret = ssl_ticket_aead_error;
+    goto out;
   }
   size_t plaintext_len;
 #if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
@@ -3140,24 +3135,89 @@
   plaintext_len = ciphertext_len;
 #else
   if (ciphertext_len >= INT_MAX) {
-    goto done;
+    goto out;
   }
   int len1, len2;
   if (!EVP_DecryptUpdate(&cipher_ctx, plaintext, &len1, ciphertext,
                          (int)ciphertext_len) ||
       !EVP_DecryptFinal_ex(&cipher_ctx, plaintext + len1, &len2)) {
-    ERR_clear_error(); /* Don't leave an error on the queue. */
-    goto done;
+    ERR_clear_error();
+    goto out;
   }
-  plaintext_len = (size_t)(len1 + len2);
+  plaintext_len = (size_t)(len1) + len2;
 #endif
 
+  *out = plaintext;
+  plaintext = NULL;
+  *out_len = plaintext_len;
+  ret = ssl_ticket_aead_success;
+
+out:
+  OPENSSL_free(plaintext);
+  HMAC_CTX_cleanup(&hmac_ctx);
+  EVP_CIPHER_CTX_cleanup(&cipher_ctx);
+  return ret;
+}
+
+static enum ssl_ticket_aead_result_t ssl_decrypt_ticket_with_method(
+    SSL *ssl, uint8_t **out, size_t *out_len, int *out_renew_ticket,
+    const uint8_t *ticket, size_t ticket_len) {
+  uint8_t *plaintext = OPENSSL_malloc(ticket_len);
+  if (plaintext == NULL) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_MALLOC_FAILURE);
+    return ssl_ticket_aead_error;
+  }
+
+  size_t plaintext_len;
+  const enum ssl_ticket_aead_result_t result =
+      ssl->session_ctx->ticket_aead_method->open(
+          ssl, plaintext, &plaintext_len, ticket_len, ticket, ticket_len);
+
+  if (result == ssl_ticket_aead_success) {
+    *out = plaintext;
+    plaintext = NULL;
+    *out_len = plaintext_len;
+  }
+
+  OPENSSL_free(plaintext);
+  return result;
+}
+
+enum ssl_ticket_aead_result_t ssl_process_ticket(
+    SSL *ssl, SSL_SESSION **out_session, int *out_renew_ticket,
+    const uint8_t *ticket, size_t ticket_len, const uint8_t *session_id,
+    size_t session_id_len) {
+  *out_renew_ticket = 0;
+  *out_session = NULL;
+
+  if ((SSL_get_options(ssl) & SSL_OP_NO_TICKET) ||
+      session_id_len > SSL_MAX_SSL_SESSION_ID_LENGTH) {
+    return ssl_ticket_aead_ignore_ticket;
+  }
+
+  uint8_t *plaintext = NULL;
+  size_t plaintext_len;
+  enum ssl_ticket_aead_result_t result;
+  if (ssl->session_ctx->ticket_aead_method != NULL) {
+    result = ssl_decrypt_ticket_with_method(
+        ssl, &plaintext, &plaintext_len, out_renew_ticket, ticket, ticket_len);
+  } else {
+    result = ssl_decrypt_ticket_with_cipher_ctx(
+        ssl, &plaintext, &plaintext_len, out_renew_ticket, ticket, ticket_len);
+  }
+
+  if (result != ssl_ticket_aead_success) {
+    return result;
+  }
+
   /* Decode the session. */
   SSL_SESSION *session =
       SSL_SESSION_from_bytes(plaintext, plaintext_len, ssl->ctx);
+  OPENSSL_free(plaintext);
+
   if (session == NULL) {
     ERR_clear_error(); /* Don't leave an error on the queue. */
-    goto done;
+    return ssl_ticket_aead_ignore_ticket;
   }
 
   /* Copy the client's session ID into the new session, to denote the ticket has
@@ -3166,12 +3226,7 @@
   session->session_id_length = session_id_len;
 
   *out_session = session;
-
-done:
-  OPENSSL_free(plaintext);
-  HMAC_CTX_cleanup(&hmac_ctx);
-  EVP_CIPHER_CTX_cleanup(&cipher_ctx);
-  return ret;
+  return ssl_ticket_aead_success;
 }
 
 int tls1_parse_peer_sigalgs(SSL_HANDSHAKE *hs, const CBS *in_sigalgs) {