diff options
Diffstat (limited to 'security/nss/lib/ssl/tls13replay.c')
-rw-r--r-- | security/nss/lib/ssl/tls13replay.c | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/security/nss/lib/ssl/tls13replay.c b/security/nss/lib/ssl/tls13replay.c new file mode 100644 index 0000000000..b090f9bca7 --- /dev/null +++ b/security/nss/lib/ssl/tls13replay.c @@ -0,0 +1,276 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * Anti-replay measures for TLS 1.3. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nss.h" /* for NSS_RegisterShutdown */ +#include "nssilock.h" /* for PZMonitor */ +#include "pk11pub.h" +#include "prinit.h" /* for PR_CallOnce */ +#include "prmon.h" +#include "prtime.h" +#include "secerr.h" +#include "ssl.h" +#include "sslbloom.h" +#include "sslimpl.h" +#include "tls13hkdf.h" + +static struct { + /* Used to ensure that we only initialize the cleanup function once. */ + PRCallOnceType init; + /* Used to serialize access to the filters. */ + PZMonitor *lock; + /* The filters, use of which alternates. */ + sslBloomFilter filters[2]; + /* Which of the two filters is active (0 or 1). */ + PRUint8 current; + /* The time that we will next update. */ + PRTime nextUpdate; + /* The width of the window; i.e., the period of updates. */ + PRTime window; + /* This key ensures that the bloom filter index is unpredictable. */ + PK11SymKey *key; +} ssl_anti_replay; + +/* Clear the current state and free any resources we allocated. The signature + * here is odd to allow this to be called during shutdown. */ +static SECStatus +tls13_AntiReplayReset(void *appData, void *nssData) +{ + if (ssl_anti_replay.key) { + PK11_FreeSymKey(ssl_anti_replay.key); + ssl_anti_replay.key = NULL; + } + if (ssl_anti_replay.lock) { + PZ_DestroyMonitor(ssl_anti_replay.lock); + ssl_anti_replay.lock = NULL; + } + sslBloom_Destroy(&ssl_anti_replay.filters[0]); + sslBloom_Destroy(&ssl_anti_replay.filters[1]); + return SECSuccess; +} + +static PRStatus +tls13_AntiReplayInit(void) +{ + SECStatus rv = NSS_RegisterShutdown(tls13_AntiReplayReset, NULL); + if (rv != SECSuccess) { + return PR_FAILURE; + } + return PR_SUCCESS; +} + +static SECStatus +tls13_AntiReplayKeyGen() +{ + PRUint8 buf[32]; + SECItem keyItem = { siBuffer, buf, sizeof(buf) }; + PK11SlotInfo *slot; + SECStatus rv; + + slot = PK11_GetInternalSlot(); + if (!slot) { + PORT_SetError(SEC_ERROR_LIBRARY_FAILURE); + return SECFailure; + } + rv = PK11_GenerateRandomOnSlot(slot, buf, sizeof(buf)); + if (rv != SECSuccess) { + goto loser; + } + + ssl_anti_replay.key = PK11_ImportSymKey(slot, CKM_NSS_HKDF_SHA256, + PK11_OriginUnwrap, CKA_DERIVE, + &keyItem, NULL); + if (!ssl_anti_replay.key) { + goto loser; + } + + PK11_FreeSlot(slot); + return SECSuccess; + +loser: + PK11_FreeSlot(slot); + return SECFailure; +} + +/* Set a limit on the combination of number of hashes and bits in each hash. */ +#define SSL_MAX_BLOOM_FILTER_SIZE 64 + +/* + * The structures created by this function can be called concurrently on + * multiple threads if the server is multi-threaded. A monitor is used to + * ensure that only one thread can access the structures that change over time, + * but no such guarantee is provided for configuration data. + * + * Functions that read from static configuration data depend on there being a + * memory barrier between the setup and use of this function. + */ +SECStatus +SSLExp_SetupAntiReplay(PRTime window, unsigned int k, unsigned int bits) +{ + SECStatus rv; + + if (k == 0 || bits == 0) { + PORT_SetError(SEC_ERROR_INVALID_ARGS); + return SECFailure; + } + if ((k * (bits + 7) / 8) > SSL_MAX_BLOOM_FILTER_SIZE) { + PORT_SetError(SEC_ERROR_INVALID_ARGS); + return SECFailure; + } + + if (PR_SUCCESS != PR_CallOnce(&ssl_anti_replay.init, + tls13_AntiReplayInit)) { + PORT_SetError(SEC_ERROR_LIBRARY_FAILURE); + return SECFailure; + } + + (void)tls13_AntiReplayReset(NULL, NULL); + + ssl_anti_replay.lock = PZ_NewMonitor(nssILockSSL); + if (!ssl_anti_replay.lock) { + goto loser; /* Code already set. */ + } + + rv = tls13_AntiReplayKeyGen(); + if (rv != SECSuccess) { + goto loser; /* Code already set. */ + } + + rv = sslBloom_Init(&ssl_anti_replay.filters[0], k, bits); + if (rv != SECSuccess) { + goto loser; /* Code already set. */ + } + rv = sslBloom_Init(&ssl_anti_replay.filters[1], k, bits); + if (rv != SECSuccess) { + goto loser; /* Code already set. */ + } + /* When starting out, ensure that 0-RTT is not accepted until the window is + * updated. A ClientHello might have been accepted prior to a restart. */ + sslBloom_Fill(&ssl_anti_replay.filters[1]); + + ssl_anti_replay.current = 0; + ssl_anti_replay.nextUpdate = ssl_TimeUsec() + window; + ssl_anti_replay.window = window; + return SECSuccess; + +loser: + (void)tls13_AntiReplayReset(NULL, NULL); + return SECFailure; +} + +/* This is exposed to tests. Though it could, this doesn't take the lock on the + * basis that those tests use thread confinement. */ +void +tls13_AntiReplayRollover(PRTime now) +{ + ssl_anti_replay.current ^= 1; + ssl_anti_replay.nextUpdate = now + ssl_anti_replay.window; + sslBloom_Zero(ssl_anti_replay.filters + ssl_anti_replay.current); +} + +static void +tls13_AntiReplayUpdate() +{ + PRTime now; + + PR_ASSERT_CURRENT_THREAD_IN_MONITOR(ssl_anti_replay.lock); + + now = ssl_TimeUsec(); + if (now < ssl_anti_replay.nextUpdate) { + return; + } + + tls13_AntiReplayRollover(now); +} + +PRBool +tls13_InWindow(const sslSocket *ss, const sslSessionID *sid) +{ + PRInt32 timeDelta; + + /* Calculate the difference between the client's view of the age of the + * ticket (in |ss->xtnData.ticketAge|) and the server's view, which we now + * calculate. The result should be close to zero. timeDelta is signed to + * make the comparisons below easier. */ + timeDelta = ss->xtnData.ticketAge - + ((ssl_TimeUsec() - sid->creationTime) / PR_USEC_PER_MSEC); + + /* Only allow the time delta to be at most half of our window. This is + * symmetrical, though it doesn't need to be; this assumes that clock errors + * on server and client will tend to cancel each other out. + * + * There are two anti-replay filters that roll over each window. In the + * worst case, immediately after a rollover of the filters, we only have a + * single window worth of recorded 0-RTT attempts. Thus, the period in + * which we can accept 0-RTT is at most one window wide. This uses PR_ABS() + * and half the window so that the first attempt can be up to half a window + * early and then replays will be caught until the attempts are half a + * window late. + * + * For example, a 0-RTT attempt arrives early, but near the end of window 1. + * The attempt is then recorded in window 1. Rollover to window 2 could + * occur immediately afterwards. Window 1 is still checked for new 0-RTT + * attempts for the remainder of window 2. Therefore, attempts to replay + * are detected because the value is recorded in window 1. When rollover + * occurs again, window 1 is erased and window 3 instated. If we allowed an + * attempt to be late by more than half a window, then this check would not + * prevent the same 0-RTT attempt from being accepted during window 1 and + * later window 3. + */ + return PR_ABS(timeDelta) < (ssl_anti_replay.window / 2); +} + +/* Checks for a duplicate in the two filters we have. Performs maintenance on + * the filters as a side-effect. This only detects a probable replay, it's + * possible that this will return true when the 0-RTT attempt is not genuinely a + * replay. In that case, we reject 0-RTT unnecessarily, but that's OK because + * no client expects 0-RTT to work every time. */ +PRBool +tls13_IsReplay(const sslSocket *ss, const sslSessionID *sid) +{ + PRBool replay; + unsigned int size; + PRUint8 index; + SECStatus rv; + static const char *label = "tls13 anti-replay"; + PRUint8 buf[SSL_MAX_BLOOM_FILTER_SIZE]; + + /* If SSL_SetupAntiReplay hasn't been called, then treat all attempts at + * 0-RTT as a replay. */ + if (!ssl_anti_replay.init.initialized) { + return PR_TRUE; + } + + if (!tls13_InWindow(ss, sid)) { + return PR_TRUE; + } + + size = ssl_anti_replay.filters[0].k * + (ssl_anti_replay.filters[0].bits + 7) / 8; + PORT_Assert(size <= SSL_MAX_BLOOM_FILTER_SIZE); + rv = tls13_HkdfExpandLabelRaw(ssl_anti_replay.key, ssl_hash_sha256, + ss->xtnData.pskBinder.data, + ss->xtnData.pskBinder.len, + label, strlen(label), + buf, size); + if (rv != SECSuccess) { + return PR_TRUE; + } + + PZ_EnterMonitor(ssl_anti_replay.lock); + tls13_AntiReplayUpdate(); + + index = ssl_anti_replay.current; + replay = sslBloom_Add(&ssl_anti_replay.filters[index], buf); + if (!replay) { + replay = sslBloom_Check(&ssl_anti_replay.filters[index ^ 1], + buf); + } + + PZ_ExitMonitor(ssl_anti_replay.lock); + return replay; +} |