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;