This week I had to look into CVE-2020-13777, which is a vulnerability in GnuTLS >3.6.4 and <3.6.14. When the projekt introduced a TOTP based way to rotate the session ticket encryption key (STEK), a vulnerability was also introduced which resulted in the session ticket encryption key beeing all-zero until the first rotation happens.
For TLS 1.2 the vulnerability results in full compromise of confidentiality and for TLS 1.3 the impact is reduced to a Man-in-the-middle since a re-keying is done after a session resumption.
Since I read several comments on the cause of the vulnerability which made no sense when I checked the commit diff, I looked into it myself. Maybe the technical details are interesting for anybody else:
Internally the STEK is rotated when the previous valid TOTP time window expires. The current window ID is tracked in a global variable in the session
struct of each connection (session→key.totp.last_result
). The function topt_next()
is used to test whether a rotation should happen. It returns 0 when no rotation is needed (when the current window ID is the same as last_result
) and otherwise the current windows ID. Rotation calculates a new key based on the initial STEK value, which is stored in the session
struct at session→key.initial_stek
. After calculating the new key, it is copied into the session
struct at session→key.session_ticket_key
. When a new session ticket is send to the client, it is encrypted using the key from session→key.session_ticket_key
.
The vulnerability was hidden in how STEK was initialized. The initialization is done in _gnutls_initialize_session_ticket_key_rotation()
, which is called internally once from gnutls_session_ticket_enable\_server()
(which in turn is used by the developer to enable the session ticket system) at TLS session initialization. The vulnerable function:
int _gnutls_initialize_session_ticket_key_rotation(gnutls_session_t session, const gnutls_datum_t *key){
if (unlikely(session == NULL || key == NULL))
return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
if (session->key.totp.last_result == 0) {
int64_t t;
memcpy(session->key.initial_stek, key->data, key->size);
t = totp_next(session);
if (t < 0)
return gnutls_assert_val(t);
session->key.totp.last_result = t;
session->key.totp.was_rotated = 0;
return GNUTLS_E_SUCCESS;
}
return GNUTLS_E_INVALID_REQUEST;
}
At session initialization last_result
is 0. Initial STEK is copied from the user-supplied variable key
into the session struct at session→key.totp.initial_stek
. Next, the current window's ID (returned by totp_next()
) is retrieved and stored into last_result
. Therefore the global state for the STEK is "no rotation needed", so no rotation happens until the the first window expires. The expiration depends on a configurable timeout. Remember: When a session ticket is send to the client, the key is actually retrieved from session→key.session_ticket_key
, which is only updated when a rotation happens! Since the whole session
struct is initialized on startup with 0, the initial STEK is zero until first rotation.
The fix was rather simple. The global "rotation needed" state is not updated in _gnutls_initialize_session_ticket_key_rotation()
, but rather kept "0", so a rotation - and therefore the initialization of session→key.session_ticket_key
- will happen at the first ticket encrypted. The fixed function:
int _gnutls_initialize_session_ticket_key_rotation(gnutls_session_t session, const gnutls_datum_t *key){
if (unlikely(session == NULL || key == NULL))
return gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
if (unlikely(session->key.totp.last_result != 0))
return GNUTLS_E_INVALID_REQUEST;
memcpy(session->key.initial_stek, key->data, key->size);
session->key.totp.was_rotated = 0;
return 0;
}
More information:
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-13777
- https://gitlab.com/gnutls/gnutls/-/issues/1011