summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/config/irmd.cmake2
-rw-r--r--src/irmd/config.h.in1
-rw-r--r--src/irmd/oap/auth.c169
-rw-r--r--src/irmd/oap/tests/oap_test.c147
4 files changed, 265 insertions, 54 deletions
diff --git a/cmake/config/irmd.cmake b/cmake/config/irmd.cmake
index 72463458..2f5e7f02 100644
--- a/cmake/config/irmd.cmake
+++ b/cmake/config/irmd.cmake
@@ -20,6 +20,8 @@ set(FLOW_ALLOC_TIMEOUT 20000 CACHE STRING
# OAP (Ouroboros Authentication Protocol)
set(OAP_REPLAY_TIMER 20 CACHE STRING
"OAP replay protection window (s)")
+set(OAP_REPLAY_MAX 4096 CACHE STRING
+ "Maximum entries in the OAP replay cache (bounds memory/CPU under flood)")
set(OAP_CLIENT_AUTH_DEFAULT TRUE CACHE BOOL
"Client requires the server to authenticate by default (FALSE for testing)")
set(DEBUG_PROTO_OAP FALSE CACHE BOOL
diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in
index 0364e080..84d58130 100644
--- a/src/irmd/config.h.in
+++ b/src/irmd/config.h.in
@@ -42,6 +42,7 @@
#define FLOW_DEALLOC_TIMEOUT @FLOW_DEALLOC_TIMEOUT@
#define OAP_REPLAY_TIMER @OAP_REPLAY_TIMER@
+#define OAP_REPLAY_MAX @OAP_REPLAY_MAX@
#cmakedefine01 OAP_CLIENT_AUTH_DEFAULT
#define BOOTSTRAP_TIMEOUT @BOOTSTRAP_TIMEOUT@
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c
index 60bd5f97..1e39cae6 100644
--- a/src/irmd/oap/auth.c
+++ b/src/irmd/oap/auth.c
@@ -30,7 +30,6 @@
#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
-#include <ouroboros/list.h>
#include <ouroboros/logs.h>
#include <ouroboros/pthread.h>
#include <ouroboros/time.h>
@@ -44,38 +43,99 @@
#include <stdlib.h>
#include <string.h>
-struct oap_replay_entry {
- struct list_head next;
- uint64_t timestamp;
- uint8_t id[OAP_ID_SIZE];
+/*
+ * Replay cache: three timestamp-generation hash buckets. A header's bucket
+ * is gen(T) = T / OAP_REPLAY_TIMER, taken mod 3. Staleness bounds a valid T
+ * to generations {G-1, G, G+1} (G is now's generation; a within-slack future
+ * stamp can reach G+1), which are distinct mod 3; the aliasing generation
+ * G-3 is always rejected as too old first. Each bucket is an open-addressed
+ * hash set whose slots are live iff slot.gen == bucket.gen, so a stale bucket
+ * clears in O(1) by bumping its gen. Overflow fails closed (reject), never
+ * evicts, so a flood cannot displace a genuine entry into a replayable state.
+ */
+#define OAP_REPLAY_GENS 3
+
+struct oap_replay_slot {
+ uint64_t gen; /* live iff == bucket gen; 0 = never used */
+ uint64_t ts;
+ uint8_t id[OAP_ID_SIZE];
+};
+
+struct oap_replay_bucket {
+ uint64_t gen;
+ size_t count;
+ struct oap_replay_slot * slots;
};
static struct {
struct auth_ctx * ca_ctx;
struct {
- struct list_head list;
- pthread_mutex_t mtx;
+ size_t mask; /* slots per bucket - 1 */
+ size_t cap; /* fail-closed threshold */
+ struct oap_replay_bucket bucket[OAP_REPLAY_GENS];
+ pthread_mutex_t mtx;
} replay;
} oap_auth;
+/* FNV-1a over id || ts; the table mask reduces it to a slot index. */
+static size_t replay_hash(const uint8_t * id,
+ uint64_t ts)
+{
+ uint64_t hh = 14695981039346656037ULL;
+ size_t i;
+
+ for (i = 0; i < OAP_ID_SIZE; i++) {
+ hh ^= id[i];
+ hh *= 1099511628211ULL;
+ }
+
+ for (i = 0; i < sizeof(ts); i++) {
+ hh ^= (uint8_t) (ts >> (i * 8));
+ hh *= 1099511628211ULL;
+ }
+
+ return (size_t) hh;
+}
+
int oap_auth_init(void)
{
+ size_t m = 1;
+ int i;
+
oap_auth.ca_ctx = auth_create_ctx();
if (oap_auth.ca_ctx == NULL) {
log_err("Failed to create OAP auth context.");
goto fail_ctx;
}
- list_head_init(&oap_auth.replay.list);
+ while (m < (size_t) OAP_REPLAY_MAX * 2)
+ m <<= 1;
+
+ oap_auth.replay.mask = m - 1;
+ oap_auth.replay.cap = OAP_REPLAY_MAX;
+
+ for (i = 0; i < OAP_REPLAY_GENS; i++) {
+ struct oap_replay_bucket * b = &oap_auth.replay.bucket[i];
+ b->gen = 0;
+ b->count = 0;
+ b->slots = calloc(m, sizeof(*b->slots));
+ if (b->slots == NULL) {
+ log_err("Failed to alloc OAP replay bucket.");
+ goto fail_bucket;
+ }
+ }
if (pthread_mutex_init(&oap_auth.replay.mtx, NULL)) {
log_err("Failed to init OAP replay mutex.");
- goto fail_mtx;
+ goto fail_bucket;
}
return 0;
- fail_mtx:
+ fail_bucket:
+ for (i = 0; i < OAP_REPLAY_GENS; i++)
+ free(oap_auth.replay.bucket[i].slots);
+
auth_destroy_ctx(oap_auth.ca_ctx);
fail_ctx:
return -1;
@@ -83,16 +143,13 @@ int oap_auth_init(void)
void oap_auth_fini(void)
{
- struct list_head * p;
- struct list_head * h;
+ int i;
pthread_mutex_lock(&oap_auth.replay.mtx);
- list_for_each_safe(p, h, &oap_auth.replay.list) {
- struct oap_replay_entry * e;
- e = list_entry(p, struct oap_replay_entry, next);
- list_del(&e->next);
- free(e);
+ for (i = 0; i < OAP_REPLAY_GENS; i++) {
+ free(oap_auth.replay.bucket[i].slots);
+ oap_auth.replay.bucket[i].slots = NULL;
}
pthread_mutex_unlock(&oap_auth.replay.mtx);
@@ -115,14 +172,16 @@ int oap_auth_add_chain_crt(void * crt)
#define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0)
int oap_check_hdr(const struct oap_hdr * hdr)
{
- struct list_head * p;
- struct list_head * h;
- struct timespec now;
- struct oap_replay_entry * new;
- uint64_t stamp;
- uint64_t cur;
- uint8_t * id;
- ssize_t delta;
+ struct oap_replay_bucket * b;
+ struct oap_replay_slot * slots;
+ struct timespec now;
+ uint64_t stamp;
+ uint64_t cur;
+ uint64_t gen;
+ uint8_t * id;
+ size_t h;
+ ssize_t delta;
+ int ret = 0;
assert(hdr != NULL);
@@ -136,52 +195,54 @@ int oap_check_hdr(const struct oap_hdr * hdr)
delta = (ssize_t)(cur - stamp) / MILLION;
if (delta < -TIMESYNC_SLACK) {
log_err_id(id, "OAP header from %zd ms into future.", -delta);
- goto fail_stamp;
+ return -EAUTH;
}
if (delta > OAP_REPLAY_TIMER * 1000) {
log_err_id(id, "OAP header too old (%zd ms).", delta);
- goto fail_stamp;
+ return -EAUTH;
}
- new = malloc(sizeof(*new));
- if (new == NULL) {
- log_err_id(id, "Failed to allocate memory for OAP element.");
- goto fail_stamp;
- }
+ gen = stamp / ((uint64_t) OAP_REPLAY_TIMER * BILLION);
pthread_mutex_lock(&oap_auth.replay.mtx);
- list_for_each_safe(p, h, &oap_auth.replay.list) {
- struct oap_replay_entry * e;
- e = list_entry(p, struct oap_replay_entry, next);
- if (cur > e->timestamp + OAP_REPLAY_TIMER * BILLION) {
- list_del(&e->next);
- free(e);
- continue;
- }
+ b = &oap_auth.replay.bucket[gen % OAP_REPLAY_GENS];
- if (e->timestamp == stamp && ID_IS_EQUAL(e->id, id)) {
- log_warn_id(id, "OAP header already known.");
- goto fail_replay;
- }
+ /* Rotate a stale bucket in O(1): its old-gen slots become free. */
+ if (b->gen != gen) {
+ b->gen = gen;
+ b->count = 0;
}
- memcpy(new->id, id, OAP_ID_SIZE);
- new->timestamp = stamp;
+ slots = b->slots;
- list_add_tail(&new->next, &oap_auth.replay.list);
+ h = replay_hash(id, stamp) & oap_auth.replay.mask;
+ while (slots[h].gen == gen) {
+ if (slots[h].ts == stamp && ID_IS_EQUAL(slots[h].id, id)) {
+ log_warn_id(id, "OAP header already known.");
+ ret = -EREPLAY;
+ goto out;
+ }
- pthread_mutex_unlock(&oap_auth.replay.mtx);
+ h = (h + 1) & oap_auth.replay.mask;
+ }
- return 0;
+ /* Empty slot found; fail closed when the window is at capacity. */
+ if (b->count >= oap_auth.replay.cap) {
+ log_warn_id(id, "OAP replay cache full; rejecting.");
+ ret = -EAUTH;
+ goto out;
+ }
- fail_replay:
+ slots[h].gen = gen;
+ slots[h].ts = stamp;
+ memcpy(slots[h].id, id, OAP_ID_SIZE);
+ b->count++;
+ out:
pthread_mutex_unlock(&oap_auth.replay.mtx);
- free(new);
- return -EREPLAY;
- fail_stamp:
- return -EAUTH;
+
+ return ret;
}
int oap_auth_peer(char * name,
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
index 311177b7..53b525a7 100644
--- a/src/irmd/oap/tests/oap_test.c
+++ b/src/irmd/oap/tests/oap_test.c
@@ -42,6 +42,7 @@
#include <test/certs/ecdsa.h>
#include "oap.h"
+#include "oap/auth.h"
#include "common.h"
#include <stdbool.h>
@@ -1075,6 +1076,150 @@ static int test_oap_replay_packet(void)
return TEST_RC_FAIL;
}
+/* Encode a distinct OAP session ID from an index */
+static void make_id(uint8_t * id,
+ size_t idx)
+{
+ memset(id, 0, OAP_ID_SIZE);
+ memcpy(id, &idx, sizeof(idx));
+}
+
+/*
+ * Replay cache fails closed at capacity: a flood is rejected and no genuine
+ * entry is evicted (so it cannot be replayed).
+ */
+static int test_oap_replay_cap(void)
+{
+ struct oap_hdr h;
+ struct timespec now;
+ uint8_t id[OAP_ID_SIZE];
+ uint64_t stamp;
+ size_t i;
+
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = TS_TO_UINT64(now);
+
+ memset(&h, 0, sizeof(h));
+ h.id.data = id;
+ h.id.len = OAP_ID_SIZE;
+ h.timestamp = stamp;
+
+ /* Fill one generation bucket to capacity with distinct IDs */
+ for (i = 0; i < OAP_REPLAY_MAX; i++) {
+ make_id(id, i);
+ if (oap_check_hdr(&h) != 0) {
+ printf("Distinct header %zu rejected.\n", i);
+ goto fail_fini;
+ }
+ }
+
+ /* One past capacity fails closed (rejected, not evict-oldest) */
+ make_id(id, OAP_REPLAY_MAX);
+ if (oap_check_hdr(&h) != -EAUTH) {
+ printf("Header past capacity not fail-closed.\n");
+ goto fail_fini;
+ }
+
+ /* No genuine entry was evicted: the oldest still reads as a replay */
+ make_id(id, 0);
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Genuine entry evicted under flood.\n");
+ goto fail_fini;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_fini:
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Distinct timestamp generations use separate buckets and are detected
+ * independently (covers the multi-generation / rotation path).
+ */
+static int test_oap_replay_generations(void)
+{
+ struct oap_hdr h;
+ struct timespec now;
+ uint8_t id[OAP_ID_SIZE];
+ uint64_t cur;
+ uint64_t gen_ns;
+ uint64_t stamp_a;
+ uint64_t stamp_b;
+
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ gen_ns = (uint64_t) OAP_REPLAY_TIMER * BILLION;
+
+ /* stamp_a in the current generation, stamp_b one generation older */
+ stamp_a = cur;
+ stamp_b = (cur / gen_ns) * gen_ns - 1;
+
+ memset(&h, 0, sizeof(h));
+ h.id.data = id;
+ h.id.len = OAP_ID_SIZE;
+ make_id(id, 1);
+
+ /* First sighting in each generation is accepted */
+ h.timestamp = stamp_a;
+ if (oap_check_hdr(&h) != 0) {
+ printf("Gen-A header rejected.\n");
+ goto fail_fini;
+ }
+
+ h.timestamp = stamp_b;
+ if (oap_check_hdr(&h) != 0) {
+ printf("Gen-B header rejected.\n");
+ goto fail_fini;
+ }
+
+ /* Each generation independently detects its own replay */
+ h.timestamp = stamp_a;
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Gen-A replay not detected.\n");
+ goto fail_fini;
+ }
+
+ h.timestamp = stamp_b;
+ if (oap_check_hdr(&h) != -EREPLAY) {
+ printf("Gen-B replay not detected.\n");
+ goto fail_fini;
+ }
+
+ oap_auth_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_fini:
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
/* Server rejects client certificate when root CA is missing from store */
static int test_oap_missing_root_ca(void)
{
@@ -1525,6 +1670,8 @@ int oap_test(int argc,
(void) argv;
ret |= test_oap_auth_init_fini();
+ ret |= test_oap_replay_cap();
+ ret |= test_oap_replay_generations();
#ifdef HAVE_OPENSSL
ret |= test_oap_roundtrip_auth_only();