Repository: aws/s2n-tls (C)

Issue:
More accurate TLS1.3 ticket_lifetime
### Problem:

At the moment, ticket_lifetime is calculated as the minimum of:
* the user-set session lifetime
* the user-set encryption key decryption lifetime
* 1 week

This isn't /exactly/ accurate. Which is fine-- the RFC allows for an inaccurate ticket_lifetime. But if could lead to customers trying to use expired tickets (due to keying material lifetime) or discarding valid tickets (due to encryption key lifetime).

### Solution:

A more accurate (but more complicated) ticket_lifetime would be the minimum of:
* the user-set session lifetime
* the remaining total lifetime of the encryption key, aka ((encryption lifetime + decryption lifetime) + intro time) - now
* the remaining keying material lifetime, aka (keying material expiration - now).
* 1 week

This is a little fun bc we'll likely want to make sure that the encryption key used for the calculation is the encryption key we use to encrypt, and that "now" is consistent and equal to the ticket issue time. We should also probably handle the case where the result is 0-- should we still send the ticket?

* **Does this change what S2N sends over the wire?** No
* **Does this change any public APIs?** No
* **Which versions of TLS will this impact?** TLS1.3

### Requirements / Acceptance Criteria:

What must a solution address in order to solve the problem? How do we know the solution is complete?

* **RFC links:** Links to relevant RFC(s)
* **Related Issues:** Link any relevant issues
* **Will the Usage Guide or other documentation need to be updated?**
* **Testing:** How will this change be tested? Call out new integration tests, functional tests, or particularly interesting/important unit tests.
  * **Will this change trigger SAW changes?** Changes to the state machine, the s2n_handshake_io code that controls state transitions, the DRBG, or the corking/uncorking logic could trigger SAW failures.
  * **Should this change be fuzz tested?** Will it handle untrusted input? Create a separate issue to track the fuzzing work.

### Out of scope:

Is there anything the solution will intentionally NOT address?

[//]: #  (NOTE: If you believe this might be a security issue, please email aws-security@amazon.com instead of creating a GitHub issue. For more details, see the AWS Vulnerability Reporting Guide: https://aws.amazon.com/security/vulnerability-reporting/ )

improper calculation of session ticket age
### Problem:

s2n-tls configures STEK lifetime through two parameters
1. encrypt lifetime: how long the material can be used for both encryption and decryption
2. decrypt lifetime: how long the material can only be used for decryption.

The session ticket lifetime is then set to the sum of these values: 
https://github.com/aws/s2n-tls/blob/7ec1446ac1d836cefb6a5363b9bfb438a4faa792/tls/s2n_server_new_session_ticket.c#L193-L199

However, this fails to account for the fact that the STEK has likely already been in use for some amount of time.

Consider the following scenario

0. `encrypt_lifetime = 1000`, `decrypt_lifetime = 1000`
1. `time=0`: stek added to s2n-tls
2. `time=500`: a client handshakes with the server
3. `time=500`: the client receives a session ticket with session ticket lifetime set to `2000`.
4. `time=2000`: the stek is expired by s2n-tls. 
5. `time=2100`: the client attempts resumption, which is expects to succeed, because only `1600` seconds have elapsed
6. `time=2100`: the resumption attempt fails, because the STEK has expired.

While the above scenario is simplest to understand, the real-world scenario is a bit more complicated. 

s2n-tls will choose a STEK based upon the remaining encryption lifetime of the key. The maximum probability of selection occurs once `encryption_lifetime / 2` has elapsed. 
https://github.com/aws/s2n-tls/blob/7ec1446ac1d836cefb6a5363b9bfb438a4faa792/tls/s2n_resume.c#L656-L665
Therefore the expected key material lifetime for a session_ticket is `encryption_lifetime / 2 + decryption_lifetime`, but the `session_ticket_lifetime` will always be set to `encryption_lifetime + decryption_lifetime`.

I wrote a simulation to confirm this behavior, which can be viewed [here](https://github.com/jmayclin/s2n-tls/blob/resumption-example/bindings/rust-examples/session-resumption/src/lib.rs)

It shows the following output.
```
2024-06-04T23:37:43.655801Z ERROR node{name="client"}: session_resumption: 251: Ticket supposed to be valid for 360s, only 310.017s elapsed
2024-06-04T23:37:43.664387Z DEBUG node{name="client"}: session_resumption: 110: stek name is 7
2024-06-04T23:37:44.395429Z ERROR node{name="client"}: session_resumption: 251: Ticket supposed to be valid for 360s, only 309.867s elapsed
2024-06-04T23:37:44.399744Z DEBUG node{name="client"}: session_resumption: 110: stek name is 8
2024-06-04T23:37:45.146100Z ERROR node{name="client"}: session_resumption: 251: Ticket supposed to be valid for 360s, only 309.916s elapsed
2024-06-04T23:37:45.149128Z DEBUG node{name="client"}: session_resumption: 110: stek name is 9
2024-06-04T23:37:45.892796Z DEBUG node{name="client"}: session_resumption: 110: stek name is 9
2024-06-04T23:37:46.627179Z  INFO node{name="client"}: session_resumption: 273: session resumption attempts: 29, successes: 10
```


### Solution:

#### Option 1: STEK lifetime calculation
`s2n_generate_ticket_lifetime` should be made aware of the specific STEK that the ticket is being encrypted under. Then the actual ticket lifetime could be accurately set to the remaining lifetime of the STEK.

#### Option 2: Bigger Overhaul
We don't have great documentation around it, but I am a little dubious of the "weighted key selection" logic. To get an effective lifetime of `$TARGET`, the STEK lifetime has to be set strictly larger than `$TARGET`, which is non-ideal because it forces sensitive cryptographic secrets to be kept around longer.

This might just be an opportunity to do a larger overhaul of our STEK selection logic 😄 .

### Requirements / Acceptance Criteria:

s2n-tls should return session tickets with accurate session ticket lifetimes.



Discussion: 

Similar to #2756. Gonna keep this issue open since it has a bit more information and isn't TLS 1.3 specific.



Fix (patch):
diff --git a/error/s2n_errno.c b/error/s2n_errno.c
index 4bbe1a6d4e6..764184d4a1e 100644
--- a/error/s2n_errno.c
+++ b/error/s2n_errno.c
@@ -270,6 +270,7 @@ static const char *no_such_error = "Internal s2n error";
     ERR_ENTRY(S2N_ERR_DUPLICATE_PSK_IDENTITIES, "The list of pre-shared keys provided contains duplicate psk identities") \
     ERR_ENTRY(S2N_ERR_OFFERED_PSKS_TOO_LONG, "The total pre-shared key data is too long to send over the wire") \
     ERR_ENTRY(S2N_ERR_INVALID_SESSION_TICKET, "Session ticket data is not valid") \
+    ERR_ENTRY(S2N_ERR_ZERO_LIFETIME_TICKET, "Calculated session lifetime is zero") \
     ERR_ENTRY(S2N_ERR_REENTRANCY, "Original execution must complete before method can be called again") \
     ERR_ENTRY(S2N_ERR_INVALID_CERT_STATE, "Certificate validation entered an invalid state and is not able to continue") \
     ERR_ENTRY(S2N_ERR_INVALID_EARLY_DATA_STATE, "Early data in invalid state") \
diff --git a/error/s2n_errno.h b/error/s2n_errno.h
index a6de3704e74..e9985729cec 100644
--- a/error/s2n_errno.h
+++ b/error/s2n_errno.h
@@ -240,6 +240,7 @@ typedef enum {
     S2N_ERR_BAD_HEX,
     S2N_ERR_TEST_ASSERTION,
     S2N_ERR_CONFIG_NULL_BEFORE_CH_CALLBACK,
+    S2N_ERR_ZERO_LIFETIME_TICKET,
     S2N_ERR_T_INTERNAL_END,
 
     /* S2N_ERR_T_USAGE */
diff --git a/tls/s2n_resume.c b/tls/s2n_resume.c
index abca18fcc10..a8b368e9d0b 100644
--- a/tls/s2n_resume.c
+++ b/tls/s2n_resume.c
@@ -495,7 +495,9 @@ S2N_RESULT s2n_store_to_cache(struct s2n_connection *conn)
     RESULT_ENSURE(conn->session_id_len <= S2N_TLS_SESSION_ID_MAX_LEN, S2N_ERR_SESSION_ID_TOO_LONG);
 
     RESULT_GUARD_POSIX(s2n_stuffer_init(&to, &entry));
-    RESULT_GUARD(s2n_resume_encrypt_session_ticket(conn, &to));
+
+    struct s2n_ticket_key *key = s2n_get_ticket_encrypt_decrypt_key(conn->config);
+    RESULT_GUARD(s2n_resume_encrypt_session_ticket(conn, key, &to));
 
     /* Store to the cache */
     conn->config->cache_store(conn, conn->config->cache_store_data, S2N_TLS_SESSION_CACHE_TTL, conn->session_id, conn->session_id_len, entry.data, entry.size);
@@ -642,7 +644,7 @@ S2N_RESULT s2n_config_is_encrypt_key_available(struct s2n_config *config)
         RESULT_GUARD(s2n_set_get(config->ticket_keys, idx, (void **) &ticket_key));
         uint64_t key_intro_time = ticket_key->intro_timestamp;
 
-        if (key_intro_time < now
+        if (key_intro_time <= now
                 && now < key_intro_time + config->encrypt_decrypt_key_lifetime_in_nanos) {
             return S2N_RESULT_OK;
         }
@@ -815,13 +817,12 @@ static S2N_RESULT s2n_resume_generate_unique_ticket_key(struct s2n_unique_ticket
     return S2N_RESULT_OK;
 }
 
-S2N_RESULT s2n_resume_encrypt_session_ticket(struct s2n_connection *conn, struct s2n_stuffer *to)
+S2N_RESULT s2n_resume_encrypt_session_ticket(struct s2n_connection *conn,
+        struct s2n_ticket_key *key, struct s2n_stuffer *to)
 {
     RESULT_ENSURE_REF(conn);
     RESULT_ENSURE_REF(to);
 
-    struct s2n_ticket_key *key = s2n_get_ticket_encrypt_decrypt_key(conn->config);
-    /* No keys loaded by the user or the keys are either in decrypt-only or expired state */
     RESULT_ENSURE(key != NULL, S2N_ERR_NO_TICKET_ENCRYPT_DECRYPT_KEY);
 
     /* Generate unique per-ticket encryption key */
diff --git a/tls/s2n_resume.h b/tls/s2n_resume.h
index 2a65e651fc5..4264d7cdf42 100644
--- a/tls/s2n_resume.h
+++ b/tls/s2n_resume.h
@@ -77,7 +77,8 @@ struct s2n_session_ticket {
 };
 
 struct s2n_ticket_key *s2n_find_ticket_key(struct s2n_config *config, const uint8_t name[S2N_TICKET_KEY_NAME_LEN]);
-S2N_RESULT s2n_resume_encrypt_session_ticket(struct s2n_connection *conn, struct s2n_stuffer *to);
+struct s2n_ticket_key *s2n_get_ticket_encrypt_decrypt_key(struct s2n_config *config);
+S2N_RESULT s2n_resume_encrypt_session_ticket(struct s2n_connection *conn, struct s2n_ticket_key *key, struct s2n_stuffer *to);
 S2N_RESULT s2n_resume_decrypt_session_ticket(struct s2n_connection *conn, struct s2n_stuffer *from);
 S2N_RESULT s2n_resume_decrypt_session_cache(struct s2n_connection *conn, struct s2n_stuffer *from);
 S2N_RESULT s2n_config_is_encrypt_key_available(struct s2n_config *config);
diff --git a/tls/s2n_server_new_session_ticket.c b/tls/s2n_server_new_session_ticket.c
index f0cb82ab473..d4ad8754475 100644
--- a/tls/s2n_server_new_session_ticket.c
+++ b/tls/s2n_server_new_session_ticket.c
@@ -72,15 +72,61 @@ int s2n_server_nst_recv(struct s2n_connection *conn)
     return S2N_SUCCESS;
 }
 
+static S2N_RESULT s2n_generate_ticket_lifetime(struct s2n_connection *conn, uint64_t key_intro_time,
+        uint32_t *ticket_lifetime)
+{
+    RESULT_ENSURE_REF(conn);
+    RESULT_ENSURE_REF(conn->config);
+    RESULT_ENSURE_MUT(ticket_lifetime);
+
+    uint64_t now = 0;
+    RESULT_GUARD(s2n_config_wall_clock(conn->config, &now));
+
+    /* Calculate ticket key age */
+    RESULT_ENSURE_GTE(now, key_intro_time);
+    uint64_t ticket_key_age_in_nanos = now - key_intro_time;
+
+    /* Calculate remaining key lifetime */
+    uint64_t key_lifetime_in_nanos = conn->config->encrypt_decrypt_key_lifetime_in_nanos + conn->config->decrypt_key_lifetime_in_nanos;
+    RESULT_ENSURE_GTE(key_lifetime_in_nanos, ticket_key_age_in_nanos);
+    uint32_t remaining_key_lifetime = (key_lifetime_in_nanos - ticket_key_age_in_nanos) / ONE_SEC_IN_NANOS;
+
+    uint32_t session_lifetime = conn->config->session_state_lifetime_in_nanos / ONE_SEC_IN_NANOS;
+
+    /* Min of remaining key lifetime and session */
+    uint32_t min_lifetime = MIN(remaining_key_lifetime, session_lifetime);
+
+    /* In TLS1.3 we take into account keying material lifetime */
+    if (conn->actual_protocol_version == S2N_TLS13) {
+        uint32_t key_material_lifetime = conn->server_keying_material_lifetime;
+        struct s2n_psk *chosen_psk = conn->psk_params.chosen_psk;
+        if (chosen_psk) {
+            RESULT_ENSURE_GTE(chosen_psk->keying_material_expiration, now);
+            uint32_t psk_key_material_lifetime = (chosen_psk->keying_material_expiration - now) / ONE_SEC_IN_NANOS;
+            key_material_lifetime = MIN(key_material_lifetime, psk_key_material_lifetime);
+        }
+        min_lifetime = MIN(min_lifetime, key_material_lifetime);
+    }
+
+    /**
+     *= https://www.rfc-editor.org/rfc/rfc8446#section-4.6.1
+     *# Servers MUST NOT use any value greater than
+     *# 604800 seconds (7 days).
+     **/
+    *ticket_lifetime = MIN(min_lifetime, ONE_WEEK_IN_SEC);
+
+    return S2N_RESULT_OK;
+}
+
 int s2n_server_nst_send(struct s2n_connection *conn)
 {
-    uint16_t session_ticket_len = S2N_TLS12_TICKET_SIZE_IN_BYTES;
+    POSIX_ENSURE_REF(conn);
+
     uint8_t data[S2N_TLS12_TICKET_SIZE_IN_BYTES] = { 0 };
-    struct s2n_blob entry = { 0 };
-    POSIX_GUARD(s2n_blob_init(&entry, data, sizeof(data)));
-    struct s2n_stuffer to = { 0 };
-    uint32_t lifetime_hint_in_secs =
-            (conn->config->encrypt_decrypt_key_lifetime_in_nanos + conn->config->decrypt_key_lifetime_in_nanos) / ONE_SEC_IN_NANOS;
+    struct s2n_blob session_ticket = { 0 };
+    POSIX_GUARD(s2n_blob_init(&session_ticket, data, sizeof(data)));
+
+    uint32_t lifetime_hint_in_secs = 0;
 
     /* Send a zero-length ticket in the NewSessionTicket message if the server changes 
      * its mind mid-handshake or if there are no valid encrypt keys currently available. 
@@ -91,22 +137,15 @@ int s2n_server_nst_send(struct s2n_connection *conn)
      *# ServerHello, then it sends a zero-length ticket in the
      *# NewSessionTicket handshake message.
      **/
-    POSIX_GUARD(s2n_stuffer_init(&to, &entry));
-    if (!conn->config->use_tickets || s2n_result_is_error(s2n_resume_encrypt_session_ticket(conn, &to))) {
+    if (s2n_result_is_error(s2n_server_nst_write(conn, &lifetime_hint_in_secs, &session_ticket))) {
         POSIX_GUARD(s2n_stuffer_write_uint32(&conn->handshake.io, 0));
         POSIX_GUARD(s2n_stuffer_write_uint16(&conn->handshake.io, 0));
-
         return S2N_SUCCESS;
     }
 
-    if (!s2n_server_sending_nst(conn)) {
-        POSIX_BAIL(S2N_ERR_SENDING_NST);
-    }
-
     POSIX_GUARD(s2n_stuffer_write_uint32(&conn->handshake.io, lifetime_hint_in_secs));
-    POSIX_GUARD(s2n_stuffer_write_uint16(&conn->handshake.io, session_ticket_len));
-
-    POSIX_GUARD(s2n_stuffer_write(&conn->handshake.io, &to.blob));
+    POSIX_GUARD(s2n_stuffer_write_uint16(&conn->handshake.io, session_ticket.size));
+    POSIX_GUARD(s2n_stuffer_write(&conn->handshake.io, &session_ticket));
 
     /* For parity with TLS1.3, track the single ticket sent.
      * This simplifies s2n_connection_get_tickets_sent.
@@ -115,6 +154,24 @@ int s2n_server_nst_send(struct s2n_connection *conn)
     return S2N_SUCCESS;
 }
 
+S2N_RESULT s2n_server_nst_write(struct s2n_connection *conn, uint32_t *lifetime_hint_in_secs,
+        struct s2n_blob *session_ticket)
+{
+    RESULT_ENSURE_REF(conn);
+    RESULT_ENSURE(s2n_server_sending_nst(conn), S2N_ERR_SENDING_NST);
+
+    struct s2n_stuffer output = { 0 };
+    RESULT_GUARD_POSIX(s2n_stuffer_init(&output, session_ticket));
+
+    struct s2n_ticket_key *key = s2n_get_ticket_encrypt_decrypt_key(conn->config);
+    RESULT_ENSURE(key != NULL, S2N_ERR_NO_TICKET_ENCRYPT_DECRYPT_KEY);
+
+    RESULT_GUARD(s2n_generate_ticket_lifetime(conn, key->intro_timestamp, lifetime_hint_in_secs));
+    RESULT_GUARD(s2n_resume_encrypt_session_ticket(conn, key, &output));
+
+    return S2N_RESULT_OK;
+}
+
 S2N_RESULT s2n_tls13_server_nst_send(struct s2n_connection *conn, s2n_blocked_status *blocked)
 {
     RESULT_ENSURE_REF(conn);
@@ -184,31 +241,6 @@ S2N_RESULT s2n_tls13_server_nst_send(struct s2n_connection *conn, s2n_blocked_st
     return S2N_RESULT_OK;
 }
 
-/** 
- *= https://www.rfc-editor.org/rfc/rfc8446#section-4.6.1
- *# Indicates the lifetime in seconds as a 32-bit
- *# unsigned integer in network byte order from the time of ticket
- *# issuance. 
- **/
-static S2N_RESULT s2n_generate_ticket_lifetime(struct s2n_connection *conn, uint32_t *ticket_lifetime)
-{
-    RESULT_ENSURE_REF(conn);
-    RESULT_ENSURE_MUT(ticket_lifetime);
-
-    uint32_t key_lifetime_in_secs =
-            (conn->config->encrypt_decrypt_key_lifetime_in_nanos + conn->config->decrypt_key_lifetime_in_nanos) / ONE_SEC_IN_NANOS;
-    uint32_t session_lifetime_in_secs = conn->config->session_state_lifetime_in_nanos / ONE_SEC_IN_NANOS;
-    uint32_t key_and_session_min_lifetime = MIN(key_lifetime_in_secs, session_lifetime_in_secs);
-    /** 
-     *= https://www.rfc-editor.org/rfc/rfc8446#section-4.6.1
-     *# Servers MUST NOT use any value greater than
-     *# 604800 seconds (7 days).
-     **/
-    *ticket_lifetime = MIN(key_and_session_min_lifetime, ONE_WEEK_IN_SEC);
-
-    return S2N_RESULT_OK;
-}
-
 /** 
  *= https://www.rfc-editor.org/rfc/rfc8446#section-4.6.1
  *# A per-ticket value that is unique across all tickets
@@ -271,6 +303,9 @@ S2N_RESULT s2n_tls13_server_nst_write(struct s2n_connection *conn, struct s2n_st
     RESULT_ENSURE_REF(conn);
     RESULT_ENSURE_REF(output);
 
+    struct s2n_ticket_key *key = s2n_get_ticket_encrypt_decrypt_key(conn->config);
+    RESULT_ENSURE(key != NULL, S2N_ERR_NO_TICKET_ENCRYPT_DECRYPT_KEY);
+
     struct s2n_ticket_fields *ticket_fields = &conn->tls13_ticket_fields;
 
     /* Write message type because session resumption in TLS13 is a post-handshake message */
@@ -280,7 +315,9 @@ S2N_RESULT s2n_tls13_server_nst_write(struct s2n_connection *conn, struct s2n_st
     RESULT_GUARD_POSIX(s2n_stuffer_reserve_uint24(output, &message_size));
 
     uint32_t ticket_lifetime_in_secs = 0;
-    RESULT_GUARD(s2n_generate_ticket_lifetime(conn, &ticket_lifetime_in_secs));
+    RESULT_GUARD(s2n_generate_ticket_lifetime(conn, key->intro_timestamp, &ticket_lifetime_in_secs));
+
+    RESULT_ENSURE(ticket_lifetime_in_secs > 0, S2N_ERR_ZERO_LIFETIME_TICKET);
     RESULT_GUARD_POSIX(s2n_stuffer_write_uint32(output, ticket_lifetime_in_secs));
 
     /* Get random data to use as ticket_age_add value */
@@ -310,7 +347,7 @@ S2N_RESULT s2n_tls13_server_nst_write(struct s2n_connection *conn, struct s2n_st
     /* Write ticket */
     struct s2n_stuffer_reservation ticket_size = { 0 };
     RESULT_GUARD_POSIX(s2n_stuffer_reserve_uint16(output, &ticket_size));
-    RESULT_GUARD(s2n_resume_encrypt_session_ticket(conn, output));
+    RESULT_GUARD(s2n_resume_encrypt_session_ticket(conn, key, output));
     RESULT_GUARD_POSIX(s2n_stuffer_write_vector_size(&ticket_size));
 
     RESULT_GUARD_POSIX(s2n_extension_list_send(S2N_EXTENSION_LIST_NST, conn, output));
diff --git a/tls/s2n_tls.h b/tls/s2n_tls.h
index 1aa8f43fc9d..827de21303b 100644
--- a/tls/s2n_tls.h
+++ b/tls/s2n_tls.h
@@ -60,6 +60,8 @@ int s2n_client_cert_verify_send(struct s2n_connection *conn);
 int s2n_tls13_cert_verify_recv(struct s2n_connection *conn);
 int s2n_tls13_cert_verify_send(struct s2n_connection *conn);
 int s2n_server_nst_send(struct s2n_connection *conn);
+S2N_RESULT s2n_server_nst_write(struct s2n_connection *conn, uint32_t *lifetime_hint_in_secs,
+        struct s2n_blob *session_ticket);
 int s2n_server_nst_recv(struct s2n_connection *conn);
 S2N_RESULT s2n_tls13_server_nst_send(struct s2n_connection *conn, s2n_blocked_status *blocked);
 S2N_RESULT s2n_tls13_server_nst_write(struct s2n_connection *conn, struct s2n_stuffer *output);