Exercise SSL_TICKET_AEAD_METHOD in runner

We never actually tested it in runner, though I think we did write
ssl_test.cc tests. Adding some tests now to prepare for adding and
testing some new behavior shortly.

Also fix the documentation for SSL_CTX_set_tlsext_ticket_key_cb
returning zero in the encrypt case.

Change-Id: Ic7b8d51b2433d56cc524f62565946b906d515257
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73027
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 0a40b84..b198e0c 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -2469,9 +2469,9 @@
 // When decrypting a ticket, |encrypt| will be zero. |key_name| will point to a
 // 16-byte key name and |iv| points to an IV. The length of the IV consumed must
 // match |EVP_CIPHER_CTX_iv_length| of the cipher selected. In this mode,
-// |callback| returns -1 to abort the handshake, 0 if decrypting the ticket
-// failed, and 1 or 2 on success. If it returns 2, the ticket will be renewed.
-// This may be used to re-key the ticket.
+// |callback| returns -1 to abort the handshake, 0 if the ticket key was
+// unrecognized, and 1 or 2 on success. If it returns 2, the ticket will be
+// renewed. This may be used to re-key the ticket.
 //
 // WARNING: |callback| wildly breaks the usual return value convention and is
 // called in two different modes.
diff --git a/ssl/test/handshake_util.cc b/ssl/test/handshake_util.cc
index b6d8280..65219d8 100644
--- a/ssl/test/handshake_util.cc
+++ b/ssl/test/handshake_util.cc
@@ -101,6 +101,9 @@
     case SSL_ERROR_WANT_CERTIFICATE_VERIFY:
       test_state->custom_verify_ready = true;
       return true;
+    case SSL_ERROR_PENDING_TICKET:
+      test_state->async_ticket_decrypt_ready = true;
+      return true;
     default:
       return false;
   }
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 682ca23..532fa5f 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -8574,54 +8574,65 @@
 				resumeSession:        true,
 				expectResumeRejected: true,
 			})
-			// Test the ticket callback, with and without renewal.
-			testCases = append(testCases, testCase{
-				protocol: protocol,
-				testType: serverTest,
-				name:     "TicketCallback-" + suffix,
-				config: Config{
-					MaxVersion: ver.version,
-				},
-				resumeSession: true,
-				flags:         []string{"-use-ticket-callback"},
-			})
-			testCases = append(testCases, testCase{
-				protocol: protocol,
-				testType: serverTest,
-				name:     "TicketCallback-Renew-" + suffix,
-				config: Config{
-					MaxVersion: ver.version,
-					Bugs: ProtocolBugs{
-						ExpectNewTicket: true,
+			// Test the ticket callbacks.
+			for _, aeadCallback := range []bool{false, true} {
+				flag := "-use-ticket-callback"
+				callbackSuffix := suffix
+				if aeadCallback {
+					flag = "-use-ticket-aead-callback"
+					callbackSuffix += "-AEAD"
+				}
+				testCases = append(testCases, testCase{
+					protocol: protocol,
+					testType: serverTest,
+					name:     "TicketCallback-" + callbackSuffix,
+					config: Config{
+						MaxVersion: ver.version,
 					},
-				},
-				flags:         []string{"-use-ticket-callback", "-renew-ticket"},
-				resumeSession: true,
-			})
+					resumeSession: true,
+					flags:         []string{flag},
+				})
+				// Only the old callback supports renewal.
+				if !aeadCallback {
+					testCases = append(testCases, testCase{
+						protocol: protocol,
+						testType: serverTest,
+						name:     "TicketCallback-Renew-" + callbackSuffix,
+						config: Config{
+							MaxVersion: ver.version,
+							Bugs: ProtocolBugs{
+								ExpectNewTicket: true,
+							},
+						},
+						flags:         []string{flag, "-renew-ticket"},
+						resumeSession: true,
+					})
+				}
 
-			// Test that the ticket callback is only called once when everything before
-			// it in the ClientHello is asynchronous. This corrupts the ticket so
-			// certificate selection callbacks run.
-			testCases = append(testCases, testCase{
-				protocol: protocol,
-				testType: serverTest,
-				name:     "TicketCallback-SingleCall-" + suffix,
-				config: Config{
-					MaxVersion: ver.version,
-					Bugs: ProtocolBugs{
-						FilterTicket: func(in []byte) ([]byte, error) {
-							in[len(in)-1] ^= 1
-							return in, nil
+				// Test that the ticket callback is only called once when everything before
+				// it in the ClientHello is asynchronous. This corrupts the ticket so
+				// certificate selection callbacks run.
+				testCases = append(testCases, testCase{
+					protocol: protocol,
+					testType: serverTest,
+					name:     "TicketCallback-SingleCall-" + callbackSuffix,
+					config: Config{
+						MaxVersion: ver.version,
+						Bugs: ProtocolBugs{
+							FilterTicket: func(in []byte) ([]byte, error) {
+								in[len(in)-1] ^= 1
+								return in, nil
+							},
 						},
 					},
-				},
-				resumeSession:        true,
-				expectResumeRejected: true,
-				flags: []string{
-					"-use-ticket-callback",
-					"-async",
-				},
-			})
+					resumeSession:        true,
+					expectResumeRejected: true,
+					flags: []string{
+						flag,
+						"-async",
+					},
+				})
+			}
 
 			// Resume with various lengths of ticket session id.
 			if ver.version < VersionTLS13 {
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index abd66ed..cf47abc 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -28,6 +28,7 @@
 #include <memory>
 #include <type_traits>
 
+#include <openssl/aead.h>
 #include <openssl/base64.h>
 #include <openssl/hmac.h>
 #include <openssl/hpke.h>
@@ -354,7 +355,8 @@
         BoolFlag("-implicit-handshake", &TestConfig::implicit_handshake),
         BoolFlag("-use-early-callback", &TestConfig::use_early_callback),
         BoolFlag("-fail-early-callback", &TestConfig::fail_early_callback),
-        BoolFlag("-fail-early-callback-ech-rewind", &TestConfig::fail_early_callback_ech_rewind),
+        BoolFlag("-fail-early-callback-ech-rewind",
+                 &TestConfig::fail_early_callback_ech_rewind),
         BoolFlag("-install-ddos-callback", &TestConfig::install_ddos_callback),
         BoolFlag("-fail-ddos-callback", &TestConfig::fail_ddos_callback),
         BoolFlag("-fail-cert-callback", &TestConfig::fail_cert_callback),
@@ -375,9 +377,10 @@
                  &TestConfig::expect_reject_early_data),
         BoolFlag("-expect-no-offer-early-data",
                  &TestConfig::expect_no_offer_early_data),
-        BoolFlag("-expect-no-server-name",
-                 &TestConfig::expect_no_server_name),
+        BoolFlag("-expect-no-server-name", &TestConfig::expect_no_server_name),
         BoolFlag("-use-ticket-callback", &TestConfig::use_ticket_callback),
+        BoolFlag("-use-ticket-aead-callback",
+                 &TestConfig::use_ticket_aead_callback),
         BoolFlag("-renew-ticket", &TestConfig::renew_ticket),
         BoolFlag("-enable-early-data", &TestConfig::enable_early_data),
         Base64Flag("-expect-ocsp-response", &TestConfig::expect_ocsp_response),
@@ -1214,6 +1217,88 @@
     AsyncPrivateKeyComplete,
 };
 
+static size_t AsyncTicketMaxOverhead(SSL *ssl) {
+  const EVP_AEAD *aead = EVP_aead_aes_128_gcm_siv();
+  return EVP_AEAD_max_overhead(aead) + EVP_AEAD_nonce_length(aead);
+}
+
+static int AsyncTicketSeal(SSL *ssl, uint8_t *out, size_t *out_len,
+                           size_t max_out_len, const uint8_t *in,
+                           size_t in_len) {
+  auto out_span = bssl::MakeSpan(out, max_out_len);
+  // Encrypt the ticket with the all zero key and a random nonce.
+  static const uint8_t kKey[16] = {0};
+  const EVP_AEAD *aead = EVP_aead_aes_128_gcm_siv();
+  size_t nonce_len = EVP_AEAD_nonce_length(aead);
+  if (out_span.size() < nonce_len) {
+    return 0;
+  }
+  auto nonce = out_span.first(nonce_len);
+  out_span = out_span.subspan(nonce_len);
+  RAND_bytes(nonce.data(), nonce.size());
+  bssl::ScopedEVP_AEAD_CTX ctx;
+  size_t len;
+  if (!EVP_AEAD_CTX_init(ctx.get(), EVP_aead_aes_128_gcm_siv(), kKey,
+                         sizeof(kKey), EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr) ||
+      !EVP_AEAD_CTX_seal(ctx.get(), out_span.data(), &len, out_span.size(),
+                         nonce.data(), nonce.size(), in, in_len,
+                         /*ad=*/nullptr, /*ad_len=*/0)) {
+    return 0;
+  }
+  *out_len = nonce.size() + len;
+  return 1;
+}
+
+static ssl_ticket_aead_result_t AsyncTicketOpen(SSL *ssl, uint8_t *out,
+                                                size_t *out_len,
+                                                size_t max_out_len,
+                                                const uint8_t *in,
+                                                size_t in_len) {
+  auto in_span = bssl::MakeSpan(in, in_len);
+  const TestConfig *test_config = GetTestConfig(ssl);
+  TestState *test_state = GetTestState(ssl);
+  if (test_state->ticket_decrypt_done) {
+    fprintf(stderr, "AsyncTicketOpen called after completion.\n");
+    return ssl_ticket_aead_error;
+  }
+  if (test_config->renew_ticket) {
+    fprintf(stderr, "-renew-ticket not supported with async tickets.\n");
+    return ssl_ticket_aead_error;
+  }
+  if (test_config->async && !test_state->async_ticket_decrypt_ready) {
+    return ssl_ticket_aead_retry;
+  }
+
+  const EVP_AEAD *aead = EVP_aead_aes_128_gcm_siv();
+  size_t nonce_len = EVP_AEAD_nonce_length(aead);
+  if (in_span.size() < nonce_len) {
+    return ssl_ticket_aead_error;
+  }
+  auto nonce = in_span.first(nonce_len);
+  in_span = in_span.subspan(nonce_len);
+
+  static const uint8_t kKey[16] = {0};
+  bssl::ScopedEVP_AEAD_CTX ctx;
+  if (!EVP_AEAD_CTX_init(ctx.get(), EVP_aead_aes_128_gcm_siv(), kKey,
+                         sizeof(kKey), EVP_AEAD_DEFAULT_TAG_LENGTH, nullptr)) {
+    return ssl_ticket_aead_error;
+  }
+  if (!EVP_AEAD_CTX_open(ctx.get(), out, out_len, max_out_len, nonce.data(),
+                         nonce.size(), in_span.data(), in_span.size(),
+                         /*ad=*/nullptr, /*ad_len=*/0)) {
+    ERR_clear_error();
+    return ssl_ticket_aead_ignore_ticket;
+  }
+  test_state->ticket_decrypt_done = true;
+  return ssl_ticket_aead_success;
+}
+
+static const SSL_TICKET_AEAD_METHOD g_async_ticket_aead_method = {
+    AsyncTicketMaxOverhead,
+    AsyncTicketSeal,
+    AsyncTicketOpen,
+};
+
 static bssl::UniquePtr<SSL_CREDENTIAL> CredentialFromConfig(
     const TestConfig &config, const CredentialConfig &cred_config, int number) {
   bssl::UniquePtr<SSL_CREDENTIAL> cred;
@@ -1758,8 +1843,10 @@
   SSL_CTX_set_info_callback(ssl_ctx.get(), InfoCallback);
   SSL_CTX_sess_set_new_cb(ssl_ctx.get(), NewSessionCallback);
 
-  if (use_ticket_callback || handshake_hints) {
-    // If using handshake hints, always enable the ticket callback, so we can
+  if (use_ticket_aead_callback) {
+    SSL_CTX_set_ticket_aead_method(ssl_ctx.get(), &g_async_ticket_aead_method);
+  } else if (use_ticket_callback || handshake_hints) {
+    // If using handshake hints, always enable some ticket callback, so we can
     // check that hints only mismatch when allowed. The ticket callback also
     // uses a constant key, which simplifies the test.
     SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx.get(), TicketKeyCallback);
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 0afeb49..36f9701 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -137,6 +137,7 @@
   bool expect_no_offer_early_data = false;
   bool expect_no_server_name = false;
   bool use_ticket_callback = false;
+  bool use_ticket_aead_callback = false;
   bool renew_ticket = false;
   bool enable_early_data = false;
   std::string ocsp_response;
diff --git a/ssl/test/test_state.h b/ssl/test/test_state.h
index 14b4e05..c5a3f9c 100644
--- a/ssl/test/test_state.h
+++ b/ssl/test/test_state.h
@@ -56,6 +56,7 @@
   unsigned private_key_retries = 0;
   bool got_new_session = false;
   bssl::UniquePtr<SSL_SESSION> new_session;
+  bool async_ticket_decrypt_ready = false;
   bool ticket_decrypt_done = false;
   bool alpn_select_done = false;
   bool early_callback_ready = false;