@@ -260,6 +260,8 @@ struct l_tls {
uint8_t session_id[32];
size_t session_id_size;
+ uint8_t session_id_replaced[32];
+ size_t session_id_size_replaced;
bool session_id_new;
uint8_t session_cipher_suite_id[2];
uint8_t session_compression_method_id;
@@ -209,6 +209,7 @@ static void tls_reset_handshake(struct l_tls *tls)
tls->cert_sent = 0;
tls->session_id_size = 0;
+ tls->session_id_size_replaced = 0;
tls->session_id_new = false;
l_free(l_steal_ptr(tls->session_peer_identity));
}
@@ -392,7 +393,7 @@ static void tls_reset_cipher_spec(struct l_tls *tls, bool txrx)
tls_change_cipher_spec(tls, txrx, NULL);
}
-bool tls_cipher_suite_is_compatible(struct l_tls *tls,
+static bool tls_cipher_suite_is_compatible_no_key_xchg(struct l_tls *tls,
const struct tls_cipher_suite *suite,
const char **error)
{
@@ -477,19 +478,6 @@ bool tls_cipher_suite_is_compatible(struct l_tls *tls,
return false;
}
- if (suite->key_xchg->need_ffdh &&
- !l_key_is_supported(L_KEY_FEATURE_DH)) {
- if (error) {
- *error = error_buf;
- snprintf(error_buf, sizeof(error_buf),
- "Cipher suite %s's key exchange "
- "mechanism needs kernel DH support",
- suite->name);
- }
-
- return false;
- }
-
/*
* If the certificate is compatible with the signature algorithm it
* also must be compatible with the key exchange mechanism because
@@ -510,6 +498,38 @@ bool tls_cipher_suite_is_compatible(struct l_tls *tls,
return false;
}
+ return true;
+}
+
+/*
+ * Assumes that Client Hello extensions have been processed and that we
+ * will want to start a new session using this cipher suite, including the
+ * key exchange. This is unlike tls_cipher_suite_is_compatible_no_key_xchg()
+ * which runs fewer checks and must succeed even for a cipher suite loaded
+ * during session resumption.
+ */
+bool tls_cipher_suite_is_compatible(struct l_tls *tls,
+ const struct tls_cipher_suite *suite,
+ const char **error)
+{
+ static char error_buf[200];
+
+ if (!tls_cipher_suite_is_compatible_no_key_xchg(tls, suite, error))
+ return false;
+
+ if (suite->key_xchg->need_ffdh &&
+ !l_key_is_supported(L_KEY_FEATURE_DH)) {
+ if (error) {
+ *error = error_buf;
+ snprintf(error_buf, sizeof(error_buf),
+ "Cipher suite %s's key exchange "
+ "mechanism needs kernel DH support",
+ suite->name);
+ }
+
+ return false;
+ }
+
/*
* On the server we know what elliptic curve we'll be using as soon
* as we've processed the ClientHello so for EC-based key exchange
@@ -933,8 +953,9 @@ static bool tls_load_cached_session(struct l_tls *tls, const char *group_name,
* It is up to the user to keep multiple cache instances if it needs
* to save multiple sessions.
*/
- if (unlikely(!tls_cipher_suite_is_compatible(tls, cipher_suite,
- &error))) {
+ if (unlikely(!tls_cipher_suite_is_compatible_no_key_xchg(tls,
+ cipher_suite,
+ &error))) {
TLS_DEBUG("Cached session %s cipher suite not compatible: %s",
session_id_str, error);
goto forget;
@@ -1019,6 +1040,118 @@ static bool tls_load_cached_client_session(struct l_tls *tls)
session_id_size, session_id_str);
}
+static bool tls_load_cached_server_session(struct l_tls *tls,
+ const uint8_t *session_id,
+ size_t session_id_size)
+{
+ _auto_(l_free) char *session_id_str =
+ l_util_hexstring(session_id, session_id_size);
+ const char *target_group_name =
+ tls_get_cache_group_name(tls, session_id, session_id_size);
+ _auto_(l_strv_free) char **groups =
+ l_settings_get_groups(tls->session_settings);
+ char **group;
+ unsigned int cnt = 0;
+ size_t prefix_len = strlen(tls->session_prefix);
+ uint64_t now = time_realtime_now();
+ char *oldest_session_group = NULL;
+ uint64_t oldest_session_expiry = UINT64_MAX;
+ bool found = false;
+ bool changed = false;
+ bool loaded = false;
+
+ tls->session_id_size = 0;
+ tls->session_id_new = false;
+
+ /* Clean up expired entries and enforce session count limit */
+ for (group = groups; *group; group++) {
+ uint64_t expiry_time;
+
+ if (memcmp(*group, tls->session_prefix, prefix_len) ||
+ (*group)[prefix_len] != '-')
+ continue;
+
+ /* Group seems to be a session cache entry */
+
+ if (unlikely(!l_settings_get_uint64(tls->session_settings,
+ *group,
+ "SessionExpiryTime",
+ &expiry_time)) ||
+ expiry_time <= now) {
+ TLS_DEBUG("Cached session %s is expired or invalid, "
+ "purging entry",
+ *group + prefix_len + 1);
+ l_settings_remove_group(tls->session_settings, *group);
+ changed = true;
+ }
+
+ cnt++;
+
+ if (!strcmp(*group + prefix_len + 1,
+ target_group_name + prefix_len + 1)) {
+ found = true;
+ continue; /* Don't purge this entry */
+ }
+
+ if (expiry_time < oldest_session_expiry) {
+ oldest_session_group = *group;
+ oldest_session_expiry = expiry_time;
+ }
+ }
+
+ /*
+ * Enforce tls->session_count_max by dropping the entry for the
+ * oldest session (more specifically the one closest to its expiry
+ * time) in the cache, that is not the session we're trying to
+ * load. If the target session was not found, do this as soon as
+ * tls->session_count_max is reached rather than when it's exceeded
+ * so as to make room for a new entry. If we end up not saving
+ * a new session due to an error before the handshake finish, we'll
+ * be left with tls->session_count_max - 1 entries and will have
+ * purged that entry unnecessarily but the cache will have room for
+ * a future session. Same when we did find the target session but
+ * later had to forget it because of a fatal alert during or after
+ * the handshake.
+ *
+ * If we did find the target session but we later find that we
+ * can't resume it, we will forget it so the new session can take
+ * the old session's place and the limit is not exceeded, see
+ * discussion in tls_server_resume_error.
+ */
+ if (tls->session_count_max && oldest_session_group &&
+ cnt >= tls->session_count_max + (found ? 1 : 0)) {
+ l_settings_remove_group(tls->session_settings,
+ oldest_session_group);
+ changed = true;
+ }
+
+ if (!found) {
+ TLS_DEBUG("Requested session %s not found in cache, will "
+ "start a new session", session_id_str);
+ goto call_back;
+ }
+
+ loaded = tls_load_cached_session(tls, target_group_name,
+ session_id, session_id_size,
+ session_id_str);
+
+ /*
+ * If tls_load_cached_session() returned false it will have called
+ * session_update_cb for us.
+ */
+ if (!loaded)
+ changed = false;
+
+call_back:
+ if (changed && tls->session_update_cb) {
+ tls->in_callback = true;
+ tls->session_update_cb(tls->session_update_user_data);
+ tls->in_callback = false;
+ }
+
+ return loaded;
+}
+
#define SWITCH_ENUM_TO_STR(val) \
case (val): \
return L_STRINGIFY(val);
@@ -1068,10 +1201,11 @@ void tls_disconnect(struct l_tls *tls, enum l_tls_alert_desc desc,
enum l_tls_alert_desc local_desc)
{
bool forget_session = false;
+ /* Save session_id_size before tls_reset_handshake() */
+ size_t session_id_size = tls->session_id_size;
- if (!tls->server && (desc || local_desc) &&
- tls->session_settings && tls->session_id_size &&
- !tls->session_id_new)
+ if ((desc || local_desc) && tls->session_settings &&
+ session_id_size && !tls->session_id_new)
/*
* RFC5246 Section 7.2: "Alert messages with a level of fatal
* result in the immediate termination of the connection. In
@@ -1299,7 +1433,12 @@ static bool tls_send_server_hello(struct l_tls *tls, struct l_queue *extensions)
memcpy(ptr, tls->pending.server_random, 32);
ptr += 32;
- *ptr++ = 0; /* Sessions are not cached */
+ if (tls->session_id_size) {
+ *ptr++ = tls->session_id_size;
+ memcpy(ptr, tls->session_id, tls->session_id_size);
+ ptr += tls->session_id_size;
+ } else
+ *ptr++ = 0;
*ptr++ = tls->pending.cipher_suite->id[0];
*ptr++ = tls->pending.cipher_suite->id[1];
@@ -1825,6 +1964,33 @@ decode_error:
return false;
}
+static void tls_server_resume_error(struct l_tls *tls)
+{
+ /*
+ * When Client Hello parameters don't match the parameters of the
+ * cached session that was requested by the client, we'll probably
+ * start and cache a new session. Even though RFC 5246 doesn't
+ * specifically mandate that the requested session be forgotten
+ * (there's no fatal Alert in that case), we overwrite the old
+ * session's entry in the cache with the new session's data to
+ * avoid keeping many sessions related to one client in the cache.
+ * In theory this allows an attacker to connect as a client and
+ * invalidate a legitimate client's session entry in our cache,
+ * DoSing the session resumption mechanism so that clients have
+ * to go through the full handshake. In practice there are many
+ * ways for an attacker to do that even without this.
+ *
+ * Our client mode only caches one last session anyway, other
+ * implementations may work that way too.
+ */
+ memcpy(tls->session_id_replaced, tls->session_id, tls->session_id_size);
+ tls->session_id_size_replaced = tls->session_id_size;
+
+ tls->session_id_size = 0;
+ tls->session_id_new = false;
+ l_free(l_steal_ptr(tls->session_peer_identity));
+}
+
static void tls_handle_client_hello(struct l_tls *tls,
const uint8_t *buf, size_t len)
{
@@ -1835,6 +2001,10 @@ static void tls_handle_client_hello(struct l_tls *tls,
int i;
struct l_queue *extensions_offered = NULL;
enum l_tls_alert_desc alert_desc = TLS_ALERT_HANDSHAKE_FAIL;
+ bool resuming = false;
+ _auto_(l_free) char *session_id_str = NULL;
+ struct tls_cipher_suite *backup_suite = NULL;
+ struct tls_compression_method *backup_cm = NULL;
/* Do we have enough for ProtocolVersion + Random + SessionID size? */
if (len < 2 + 32 + 1)
@@ -1844,6 +2014,9 @@ static void tls_handle_client_hello(struct l_tls *tls,
session_id_size = buf[34];
len -= 35;
+ if (unlikely(session_id_size > 32))
+ goto decode_error;
+
/*
* Do we have enough to hold the actual session ID + 2 byte field for
* cipher_suite len + minimum of a single cipher suite identifier
@@ -1876,6 +2049,24 @@ static void tls_handle_client_hello(struct l_tls *tls,
len -= compression_methods_size;
+ if (session_id_size && tls->session_settings &&
+ tls_load_cached_server_session(tls, buf + 35,
+ session_id_size)) {
+ /*
+ * Attempt a session resumption but don't skip parsing Hello
+ * extensions just yet because we may decide to start a new
+ * session instead after our cipher suite and compression
+ * method checks below and in that case we will need to
+ * handle the extensions and include them in the Server Hello.
+ */
+ resuming = true;
+ session_id_str = l_util_hexstring(tls->session_id,
+ tls->session_id_size);
+ }
+
+ if (tls->pending_destroy)
+ return;
+
extensions_offered = l_queue_new();
if (!tls_handle_hello_extensions(tls, compression_methods +
@@ -1883,13 +2074,6 @@ static void tls_handle_client_hello(struct l_tls *tls,
len, extensions_offered))
goto cleanup;
- /*
- * Note: if the client is supplying a SessionID we know it is false
- * because our server implementation never generates any SessionIDs
- * yet so either the client is attempting something strange or was
- * trying to connect somewhere else. We might want to throw an error.
- */
-
/* Save client_version for Premaster Secret verification */
tls->client_version = l_get_be16(buf);
@@ -1944,6 +2128,16 @@ static void tls_handle_client_hello(struct l_tls *tls,
alert_desc = TLS_ALERT_INSUFFICIENT_SECURITY;
TLS_DEBUG("non-fatal: Cipher suite %s disallowed "
"by config", suite->name);
+ } else if (resuming && memcmp(tls->session_cipher_suite_id,
+ suite->id, 2)) {
+ /*
+ * For now skip this cipher suite because we're trying
+ * to find the one from the cached session state. But
+ * keep it as a backup in case we end up starting a new
+ * session.
+ */
+ if (!backup_suite)
+ backup_suite = suite;
} else {
tls->pending.cipher_suite = suite;
break;
@@ -1953,7 +2147,15 @@ static void tls_handle_client_hello(struct l_tls *tls,
cipher_suites_size -= 2;
}
- if (!cipher_suites_size) {
+ if (unlikely(!cipher_suites_size && backup_suite)) {
+ TLS_DEBUG("Cached session %s's cipher suite %04x "
+ "unavailable, will start a new session",
+ session_id_str,
+ l_get_be16(tls->session_cipher_suite_id));
+ tls->pending.cipher_suite = backup_suite;
+ resuming = false;
+ tls_server_resume_error(tls);
+ } else if (unlikely(!cipher_suites_size)) {
TLS_DISCONNECT(alert_desc, 0,
"No common cipher suites matching negotiated "
"TLS version and our certificate's key type");
@@ -1966,35 +2168,88 @@ static void tls_handle_client_hello(struct l_tls *tls,
goto cleanup;
}
- TLS_DEBUG("Negotiated %s", tls->pending.cipher_suite->name);
-
/* Select a compression method */
/* CompressionMethod.null must be present in the vector */
while (compression_methods_size) {
- tls->pending.compression_method =
+ struct tls_compression_method *cm =
tls_find_compression_method(*compression_methods);
- if (tls->pending.compression_method)
+ if (!cm)
+ TLS_DEBUG("non-fatal: Compression %02x unknown",
+ *compression_methods);
+ else if (resuming && *compression_methods !=
+ tls->session_compression_method_id) {
+ /*
+ * For now skip this compression method because we're
+ * trying to find the one from the cached session state.
+ * But keep it as a backup in case we end up starting
+ * a new * session.
+ */
+ if (!backup_cm)
+ backup_cm = cm;
+ } else {
+ tls->pending.compression_method = cm;
break;
+ }
compression_methods++;
compression_methods_size--;
}
- if (!compression_methods_size) {
+ if (unlikely(!compression_methods_size && backup_cm)) {
+ TLS_DEBUG("Cached session %s's compression method %02x "
+ "unavailable, will start a new session",
+ session_id_str,
+ tls->session_compression_method_id);
+ tls->pending.compression_method = backup_cm;
+
+ if (backup_suite)
+ tls->pending.cipher_suite = backup_suite;
+
+ resuming = false;
+ tls_server_resume_error(tls);
+ } else if (unlikely(!compression_methods_size)) {
TLS_DISCONNECT(TLS_ALERT_HANDSHAKE_FAIL, 0,
"No common compression methods");
goto cleanup;
}
+ if (resuming)
+ TLS_DEBUG("Negotiated resumption of cached session %s",
+ session_id_str);
+
+ TLS_DEBUG("Negotiated %s", tls->pending.cipher_suite->name);
TLS_DEBUG("Negotiated %s", tls->pending.compression_method->name);
- if (!tls_send_server_hello(tls, extensions_offered))
+ if (!resuming && tls->session_settings) {
+ tls->session_id_new = true;
+ tls->session_id_size = 32;
+ l_getrandom(tls->session_id, 32);
+ }
+
+ if (!tls_send_server_hello(tls, resuming ? extensions_offered : NULL))
goto cleanup;
l_queue_destroy(extensions_offered, NULL);
+ if (resuming) {
+ const char *error;
+
+ tls_update_key_block(tls);
+ tls_send_change_cipher_spec(tls);
+
+ if (!tls_change_cipher_spec(tls, 1, &error)) {
+ TLS_DISCONNECT(TLS_ALERT_INTERNAL_ERROR, 0,
+ "change_cipher_spec: %s", error);
+ return;
+ }
+
+ tls_send_finished(tls);
+ TLS_SET_STATE(TLS_HANDSHAKE_WAIT_CHANGE_CIPHER_SPEC);
+ return;
+ }
+
if (tls->pending.cipher_suite->signature && tls->cert)
if (!tls_send_certificate(tls))
return;
@@ -2675,7 +2930,7 @@ static void tls_finished(struct l_tls *tls)
} else if (tls->peer_authenticated && resuming)
peer_identity = tls->session_peer_identity;
- if (!tls->server && tls->session_settings && tls->session_id_new) {
+ if (tls->session_settings && tls->session_id_new) {
_auto_(l_free) char *session_id_str =
l_util_hexstring(tls->session_id, tls->session_id_size);
uint64_t expiry = tls->session_lifetime ?
@@ -2729,6 +2984,14 @@ static void tls_finished(struct l_tls *tls)
TLS_DEBUG("Saving new session %s to cache", session_id_str);
session_update = true;
+
+ if (tls->session_id_size_replaced) {
+ tls_forget_cached_session(tls, NULL,
+ tls->session_id_replaced,
+ tls->session_id_size_replaced,
+ false);
+ tls->session_id_size_replaced = 0;
+ }
}
/* Free up the resources used in the handshake */
@@ -2947,7 +3210,7 @@ static void tls_handle_handshake(struct l_tls *tls, int type,
resuming = tls->session_id_size && !tls->session_id_new;
- if (tls->server || (!tls->server && resuming)) {
+ if ((tls->server && !resuming) || (!tls->server && resuming)) {
const char *error;
tls_send_change_cipher_spec(tls);
@@ -2990,11 +3253,11 @@ static void tls_handle_handshake(struct l_tls *tls, int type,
* pair.
*
* If we're resuming a cached session, we have authenticated
- * this server before and the successful decryption of this
- * message confirms the server identity hasn't changed.
+ * this peer before and the successful decryption of this
+ * message confirms their identity hasn't changed.
*/
- if (!tls->server && tls->cipher_suite[0]->signature &&
- ((!resuming && tls->ca_certs) ||
+ if (tls->cipher_suite[0]->signature &&
+ ((!tls->server && !resuming && tls->ca_certs) ||
(resuming && tls->session_peer_identity)))
tls->peer_authenticated = true;