/*1* Copyright (c) 2016 Thomas Pornin <[email protected]>2*3* Permission is hereby granted, free of charge, to any person obtaining4* a copy of this software and associated documentation files (the5* "Software"), to deal in the Software without restriction, including6* without limitation the rights to use, copy, modify, merge, publish,7* distribute, sublicense, and/or sell copies of the Software, and to8* permit persons to whom the Software is furnished to do so, subject to9* the following conditions:10*11* The above copyright notice and this permission notice shall be12* included in all copies or substantial portions of the Software.13*14* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,15* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF16* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND17* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS18* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN19* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN20* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE21* SOFTWARE.22*/2324#include "inner.h"2526/*27* Each entry consists in a fixed number of bytes. Entries are concatenated28* in the store block. "Addresses" are really offsets in the block,29* expressed over 32 bits (so the cache may have size at most 4 GB, which30* "ought to be enough for everyone"). The "null address" is 0xFFFFFFFF.31* Note that since the storage block alignment is in no way guaranteed, we32* perform only accesses that can handle unaligned data.33*34* Two concurrent data structures are maintained:35*36* -- Entries are organised in a doubly-linked list; saved entries are added37* at the head, and loaded entries are moved to the head. Eviction uses38* the list tail (this is the LRU algorithm).39*40* -- Entries are indexed with a binary tree: all left descendants of a41* node have a lower session ID (in lexicographic order), while all42* right descendants have a higher session ID. The tree is heuristically43* balanced.44*45* Entry format:46*47* session ID 32 bytes48* master secret 48 bytes49* protocol version 2 bytes (big endian)50* cipher suite 2 bytes (big endian)51* list prev 4 bytes (big endian)52* list next 4 bytes (big endian)53* tree left child 4 bytes (big endian)54* tree right child 4 bytes (big endian)55*56* If an entry has a protocol version set to 0, then it is "disabled":57* it was a session pushed to the cache at some point, but it has58* been explicitly removed.59*60* We need to keep the tree balanced because an attacker could make61* handshakes, selecting some specific sessions (by reusing them) to62* try to make us make an imbalanced tree that makes lookups expensive63* (a denial-of-service attack that would persist as long as the cache64* remains, i.e. even after the attacker made all his connections).65* To do that, we replace the session ID (or the start of the session ID)66* with a HMAC value computed over the replaced part; the hash function67* implementation and the key are obtained from the server context upon68* first save() call.69*70* Theoretically, an attacker could use the exact timing of the lookup71* to infer the current tree topology, and try to revive entries to make72* it as unbalanced as possible. However, since the session ID are73* chosen randomly by the server, and the attacker cannot see the74* indexing values and must thus rely on blind selection, it should be75* exponentially difficult for the attacker to maintain a large76* imbalance.77*/78#define SESSION_ID_LEN 3279#define MASTER_SECRET_LEN 488081#define SESSION_ID_OFF 082#define MASTER_SECRET_OFF 3283#define VERSION_OFF 8084#define CIPHER_SUITE_OFF 8285#define LIST_PREV_OFF 8486#define LIST_NEXT_OFF 8887#define TREE_LEFT_OFF 9288#define TREE_RIGHT_OFF 968990#define LRU_ENTRY_LEN 1009192#define ADDR_NULL ((uint32_t)-1)9394#define GETSET(name, off) \95static inline uint32_t get_ ## name(br_ssl_session_cache_lru *cc, uint32_t x) \96{ \97return br_dec32be(cc->store + x + (off)); \98} \99static inline void set_ ## name(br_ssl_session_cache_lru *cc, \100uint32_t x, uint32_t val) \101{ \102br_enc32be(cc->store + x + (off), val); \103}104105GETSET(prev, LIST_PREV_OFF)106GETSET(next, LIST_NEXT_OFF)107GETSET(left, TREE_LEFT_OFF)108GETSET(right, TREE_RIGHT_OFF)109110/*111* Transform the session ID by replacing the first N bytes with a HMAC112* value computed over these bytes, using the random key K (the HMAC113* value is truncated if needed). HMAC will use the same hash function114* as the DRBG in the SSL server context, so with SHA-256, SHA-384,115* or SHA-1, depending on what is available.116*117* The risk of collision is considered too small to be a concern; and118* the impact of a collision is low (the handshake won't succeed). This119* risk is much lower than any transmission error, which would lead to120* the same consequences.121*122* Source and destination arrays msut be disjoint.123*/124static void125mask_id(br_ssl_session_cache_lru *cc,126const unsigned char *src, unsigned char *dst)127{128br_hmac_key_context hkc;129br_hmac_context hc;130131memcpy(dst, src, SESSION_ID_LEN);132br_hmac_key_init(&hkc, cc->hash, cc->index_key, sizeof cc->index_key);133br_hmac_init(&hc, &hkc, SESSION_ID_LEN);134br_hmac_update(&hc, src, SESSION_ID_LEN);135br_hmac_out(&hc, dst);136}137138/*139* Find a node by ID. Returned value is the node address, or ADDR_NULL if140* the node is not found.141*142* If addr_link is not NULL, then '*addr_link' is set to the address of the143* last followed link. If the found node is the root, or if the tree is144* empty, then '*addr_link' is set to ADDR_NULL.145*/146static uint32_t147find_node(br_ssl_session_cache_lru *cc, const unsigned char *id,148uint32_t *addr_link)149{150uint32_t x, y;151152x = cc->root;153y = ADDR_NULL;154while (x != ADDR_NULL) {155int r;156157r = memcmp(id, cc->store + x + SESSION_ID_OFF, SESSION_ID_LEN);158if (r < 0) {159y = x + TREE_LEFT_OFF;160x = get_left(cc, x);161} else if (r == 0) {162if (addr_link != NULL) {163*addr_link = y;164}165return x;166} else {167y = x + TREE_RIGHT_OFF;168x = get_right(cc, x);169}170}171if (addr_link != NULL) {172*addr_link = y;173}174return ADDR_NULL;175}176177/*178* For node x, find its replacement upon removal.179*180* -- If node x has no child, then this returns ADDR_NULL.181* -- Otherwise, if node x has a left child, then the replacement is the182* rightmost left-descendent.183* -- Otherwise, the replacement is the leftmost right-descendent.184*185* If a node is returned, then '*al' is set to the address of the field186* that points to that node. Otherwise (node x has no child), '*al' is187* set to ADDR_NULL.188*189* Note that the replacement node, when found, is always a descendent190* of node 'x', so it cannot be the tree root. Thus, '*al' can be set191* to ADDR_NULL only when no node is found and ADDR_NULL is returned.192*/193static uint32_t194find_replacement_node(br_ssl_session_cache_lru *cc, uint32_t x, uint32_t *al)195{196uint32_t y1, y2;197198y1 = get_left(cc, x);199if (y1 != ADDR_NULL) {200y2 = x + TREE_LEFT_OFF;201for (;;) {202uint32_t z;203204z = get_right(cc, y1);205if (z == ADDR_NULL) {206*al = y2;207return y1;208}209y2 = y1 + TREE_RIGHT_OFF;210y1 = z;211}212}213y1 = get_right(cc, x);214if (y1 != ADDR_NULL) {215y2 = x + TREE_RIGHT_OFF;216for (;;) {217uint32_t z;218219z = get_left(cc, y1);220if (z == ADDR_NULL) {221*al = y2;222return y1;223}224y2 = y1 + TREE_LEFT_OFF;225y1 = z;226}227}228*al = ADDR_NULL;229return ADDR_NULL;230}231232/*233* Set the link at address 'alx' to point to node 'x'. If 'alx' is234* ADDR_NULL, then this sets the tree root to 'x'.235*/236static inline void237set_link(br_ssl_session_cache_lru *cc, uint32_t alx, uint32_t x)238{239if (alx == ADDR_NULL) {240cc->root = x;241} else {242br_enc32be(cc->store + alx, x);243}244}245246/*247* Remove node 'x' from the tree. This function shall not be called if248* node 'x' is not part of the tree.249*/250static void251remove_node(br_ssl_session_cache_lru *cc, uint32_t x)252{253uint32_t alx, y, aly;254255/*256* Removal algorithm:257* ------------------258*259* - If we remove the root, then the tree becomes empty.260*261* - If the removed node has no child, then we can simply remove262* it, with nothing else to do.263*264* - Otherwise, the removed node must be replaced by either its265* rightmost left-descendent, or its leftmost right-descendent.266* The replacement node itself must be removed from its current267* place. By definition, that replacement node has either no268* child, or at most a single child that will replace it in the269* tree.270*/271272/*273* Find node back and its ancestor link. If the node was the274* root, then alx is set to ADDR_NULL.275*/276find_node(cc, cc->store + x + SESSION_ID_OFF, &alx);277278/*279* Find replacement node 'y', and 'aly' is set to the address of280* the link to that replacement node. If the removed node has no281* child, then both 'y' and 'aly' are set to ADDR_NULL.282*/283y = find_replacement_node(cc, x, &aly);284285if (y != ADDR_NULL) {286uint32_t z;287288/*289* The unlinked replacement node may have one child (but290* not two) that takes its place.291*/292z = get_left(cc, y);293if (z == ADDR_NULL) {294z = get_right(cc, y);295}296set_link(cc, aly, z);297298/*299* Link the replacement node in its new place, overwriting300* the current link to the node 'x' (which removes 'x').301*/302set_link(cc, alx, y);303304/*305* The replacement node adopts the left and right children306* of the removed node. Note that this also works even if307* the replacement node was a direct descendent of the308* removed node, since we unlinked it previously.309*/310set_left(cc, y, get_left(cc, x));311set_right(cc, y, get_right(cc, x));312} else {313/*314* No replacement, we simply unlink the node 'x'.315*/316set_link(cc, alx, ADDR_NULL);317}318}319320static void321lru_save(const br_ssl_session_cache_class **ctx,322br_ssl_server_context *server_ctx,323const br_ssl_session_parameters *params)324{325br_ssl_session_cache_lru *cc;326unsigned char id[SESSION_ID_LEN];327uint32_t x, alx;328329cc = (br_ssl_session_cache_lru *)ctx;330331/*332* If the buffer is too small, we don't record anything. This333* test avoids problems in subsequent code.334*/335if (cc->store_len < LRU_ENTRY_LEN) {336return;337}338339/*340* Upon the first save in a session cache instance, we obtain341* a random key for our indexing.342*/343if (!cc->init_done) {344br_hmac_drbg_generate(&server_ctx->eng.rng,345cc->index_key, sizeof cc->index_key);346cc->hash = br_hmac_drbg_get_hash(&server_ctx->eng.rng);347cc->init_done = 1;348}349mask_id(cc, params->session_id, id);350351/*352* Look for the node in the tree. If the same ID is already used,353* then reject it. This is a collision event, which should be354* exceedingly rare.355* Note: we do NOT record the emplacement here, because the356* removal of an entry may change the tree topology.357*/358if (find_node(cc, id, NULL) != ADDR_NULL) {359return;360}361362/*363* Find some room for the new parameters. If the cache is not364* full yet, add it to the end of the area and bump the pointer up.365* Otherwise, evict the list tail entry. Note that we already366* filtered out the case of a ridiculously small buffer that367* cannot hold any entry at all; thus, if there is no room for an368* extra entry, then the cache cannot be empty.369*/370if (cc->store_ptr > (cc->store_len - LRU_ENTRY_LEN)) {371/*372* Evict tail. If the buffer has room for a single entry,373* then this may also be the head.374*/375x = cc->tail;376cc->tail = get_prev(cc, x);377if (cc->tail == ADDR_NULL) {378cc->head = ADDR_NULL;379} else {380set_next(cc, cc->tail, ADDR_NULL);381}382383/*384* Remove the node from the tree.385*/386remove_node(cc, x);387} else {388/*389* Allocate room for new node.390*/391x = cc->store_ptr;392cc->store_ptr += LRU_ENTRY_LEN;393}394395/*396* Find the emplacement for the new node, and link it.397*/398find_node(cc, id, &alx);399set_link(cc, alx, x);400set_left(cc, x, ADDR_NULL);401set_right(cc, x, ADDR_NULL);402403/*404* New entry becomes new list head. It may also become the list405* tail if the cache was empty at that point.406*/407if (cc->head == ADDR_NULL) {408cc->tail = x;409} else {410set_prev(cc, cc->head, x);411}412set_prev(cc, x, ADDR_NULL);413set_next(cc, x, cc->head);414cc->head = x;415416/*417* Fill data in the entry.418*/419memcpy(cc->store + x + SESSION_ID_OFF, id, SESSION_ID_LEN);420memcpy(cc->store + x + MASTER_SECRET_OFF,421params->master_secret, MASTER_SECRET_LEN);422br_enc16be(cc->store + x + VERSION_OFF, params->version);423br_enc16be(cc->store + x + CIPHER_SUITE_OFF, params->cipher_suite);424}425426static int427lru_load(const br_ssl_session_cache_class **ctx,428br_ssl_server_context *server_ctx,429br_ssl_session_parameters *params)430{431br_ssl_session_cache_lru *cc;432unsigned char id[SESSION_ID_LEN];433uint32_t x;434435(void)server_ctx;436cc = (br_ssl_session_cache_lru *)ctx;437if (!cc->init_done) {438return 0;439}440mask_id(cc, params->session_id, id);441x = find_node(cc, id, NULL);442if (x != ADDR_NULL) {443unsigned version;444445version = br_dec16be(cc->store + x + VERSION_OFF);446if (version == 0) {447/*448* Entry is disabled, we pretend we did not find it.449* Notably, we don't move it to the front of the450* LRU list.451*/452return 0;453}454params->version = version;455params->cipher_suite = br_dec16be(456cc->store + x + CIPHER_SUITE_OFF);457memcpy(params->master_secret,458cc->store + x + MASTER_SECRET_OFF,459MASTER_SECRET_LEN);460if (x != cc->head) {461/*462* Found node is not at list head, so move463* it to the head.464*/465uint32_t p, n;466467p = get_prev(cc, x);468n = get_next(cc, x);469set_next(cc, p, n);470if (n == ADDR_NULL) {471cc->tail = p;472} else {473set_prev(cc, n, p);474}475set_prev(cc, cc->head, x);476set_next(cc, x, cc->head);477set_prev(cc, x, ADDR_NULL);478cc->head = x;479}480return 1;481}482return 0;483}484485static const br_ssl_session_cache_class lru_class = {486sizeof(br_ssl_session_cache_lru),487&lru_save,488&lru_load489};490491/* see inner.h */492void493br_ssl_session_cache_lru_init(br_ssl_session_cache_lru *cc,494unsigned char *store, size_t store_len)495{496cc->vtable = &lru_class;497cc->store = store;498cc->store_len = store_len;499cc->store_ptr = 0;500cc->init_done = 0;501cc->head = ADDR_NULL;502cc->tail = ADDR_NULL;503cc->root = ADDR_NULL;504}505506/* see bearssl_ssl.h */507void br_ssl_session_cache_lru_forget(508br_ssl_session_cache_lru *cc, const unsigned char *id)509{510unsigned char mid[SESSION_ID_LEN];511uint32_t addr;512513/*514* If the cache is not initialised yet, then it is empty, and515* there is nothing to forget.516*/517if (!cc->init_done) {518return;519}520521/*522* Look for the node in the tree. If found, the entry is marked523* as "disabled"; it will be reused in due course, as it ages524* through the list.525*526* We do not go through the complex moves of actually releasing527* the entry right away because explicitly forgetting sessions528* should be a rare event, meant mostly for testing purposes,529* so this is not worth the extra code size.530*/531mask_id(cc, id, mid);532addr = find_node(cc, mid, NULL);533if (addr != ADDR_NULL) {534br_enc16be(cc->store + addr + VERSION_OFF, 0);535}536}537538539