summaryrefslogtreecommitdiff
path: root/src/irmd
diff options
context:
space:
mode:
Diffstat (limited to 'src/irmd')
-rw-r--r--src/irmd/CMakeLists.txt6
-rw-r--r--src/irmd/config.h.in6
-rw-r--r--src/irmd/configfile.c12
-rw-r--r--src/irmd/ipcp.c32
-rw-r--r--src/irmd/ipcp.h3
-rw-r--r--src/irmd/main.c1078
-rw-r--r--src/irmd/oap.c130
-rw-r--r--src/irmd/oap.h25
-rw-r--r--src/irmd/oap/auth.c444
-rw-r--r--src/irmd/oap/auth.h41
-rw-r--r--src/irmd/oap/cli.c230
-rw-r--r--src/irmd/oap/hdr.c476
-rw-r--r--src/irmd/oap/hdr.h54
-rw-r--r--src/irmd/oap/internal.h25
-rw-r--r--src/irmd/oap/io.c36
-rw-r--r--src/irmd/oap/io.h2
-rw-r--r--src/irmd/oap/srv.c224
-rw-r--r--src/irmd/oap/tests/common.c286
-rw-r--r--src/irmd/oap/tests/common.h34
-rw-r--r--src/irmd/oap/tests/oap_test.c1084
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c348
-rw-r--r--src/irmd/reg/flow.c11
-rw-r--r--src/irmd/reg/flow.h16
-rw-r--r--src/irmd/reg/reg.c532
-rw-r--r--src/irmd/reg/reg.h67
-rw-r--r--src/irmd/reg/tests/flow_test.c20
-rw-r--r--src/irmd/reg/tests/reg_test.c276
27 files changed, 4807 insertions, 691 deletions
diff --git a/src/irmd/CMakeLists.txt b/src/irmd/CMakeLists.txt
index 9aa747ca..5aa457ff 100644
--- a/src/irmd/CMakeLists.txt
+++ b/src/irmd/CMakeLists.txt
@@ -7,11 +7,11 @@ if(HAVE_TOML)
set(INSTALL_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}")
configure_file("${CMAKE_SOURCE_DIR}/irmd.conf.in"
"${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example" @ONLY)
- configure_file("${CMAKE_SOURCE_DIR}/enc.conf.in"
- "${CMAKE_BINARY_DIR}/enc.conf.example" @ONLY)
+ configure_file("${CMAKE_SOURCE_DIR}/sec.conf.in"
+ "${CMAKE_BINARY_DIR}/sec.conf.example" @ONLY)
install(FILES "${CMAKE_BINARY_DIR}/${OUROBOROS_CONFIG_FILE}.example"
DESTINATION "${OUROBOROS_CONFIG_DIR}")
- install(FILES "${CMAKE_BINARY_DIR}/enc.conf.example"
+ install(FILES "${CMAKE_BINARY_DIR}/sec.conf.example"
DESTINATION "${OUROBOROS_CONFIG_DIR}")
install(CODE "
if(NOT EXISTS \"${OUROBOROS_CONFIG_DIR}/${OUROBOROS_CONFIG_FILE}\")
diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in
index df0cd718..53f80ca4 100644
--- a/src/irmd/config.h.in
+++ b/src/irmd/config.h.in
@@ -42,6 +42,9 @@
#define FLOW_DEALLOC_TIMEOUT @FLOW_DEALLOC_TIMEOUT@
#define OAP_REPLAY_TIMER @OAP_REPLAY_TIMER@
+#define OAP_REPLAY_MAX @OAP_REPLAY_MAX@
+#define OAP_REKEY_TIMER @OAP_REKEY_TIMER@
+#cmakedefine01 OAP_CLIENT_AUTH_DEFAULT
#define BOOTSTRAP_TIMEOUT @BOOTSTRAP_TIMEOUT@
#define ENROLL_TIMEOUT @ENROLL_TIMEOUT@
@@ -79,8 +82,7 @@
#cmakedefine HAVE_LIBGCRYPT
#cmakedefine HAVE_OPENSSL
#ifdef HAVE_OPENSSL
-#cmakedefine HAVE_OPENSSL_ML_KEM
-#cmakedefine HAVE_OPENSSL_ML_DSA
+#cmakedefine HAVE_ML
#endif
#define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@
#ifdef CONFIG_OUROBOROS_DEBUG
diff --git a/src/irmd/configfile.c b/src/irmd/configfile.c
index 53608eee..35cf4292 100644
--- a/src/irmd/configfile.c
+++ b/src/irmd/configfile.c
@@ -922,10 +922,10 @@ static int toml_name(toml_table_t * table,
toml_array_t * progs;
toml_array_t * args;
toml_datum_t lb;
- toml_datum_t senc;
+ toml_datum_t ssec;
toml_datum_t scrt;
toml_datum_t skey;
- toml_datum_t cenc;
+ toml_datum_t csec;
toml_datum_t ccrt;
toml_datum_t ckey;
@@ -957,8 +957,8 @@ static int toml_name(toml_table_t * table,
log_err("Invalid load-balancing policy for %s.", name);
return -1;
}
- senc = toml_string_in(table, "server_enc_file");
- if (senc.ok && cp_chk_path(info.s.enc, senc.u.s) < 0)
+ ssec = toml_string_in(table, "server_sec_file");
+ if (ssec.ok && cp_chk_path(info.s.sec, ssec.u.s) < 0)
return -1;
scrt = toml_string_in(table, "server_crt_file");
@@ -969,8 +969,8 @@ static int toml_name(toml_table_t * table,
if (skey.ok && cp_chk_path(info.s.key, skey.u.s) < 0)
return -1;
- cenc = toml_string_in(table, "client_enc_file");
- if (cenc.ok && cp_chk_path(info.c.enc, cenc.u.s) < 0)
+ csec = toml_string_in(table, "client_sec_file");
+ if (csec.ok && cp_chk_path(info.c.sec, csec.u.s) < 0)
return -1;
ccrt = toml_string_in(table, "client_crt_file");
diff --git a/src/irmd/ipcp.c b/src/irmd/ipcp.c
index a7da186c..7eccfc80 100644
--- a/src/irmd/ipcp.c
+++ b/src/irmd/ipcp.c
@@ -444,6 +444,38 @@ int ipcp_flow_join(const struct flow_info * flow,
return ret;
}
+int ipcp_flow_update(const struct flow_info * flow,
+ const buffer_t data)
+{
+ ipcp_msg_t msg = IPCP_MSG__INIT;
+ ipcp_msg_t * recv_msg;
+ int ret;
+
+ msg.code = IPCP_MSG_CODE__IPCP_FLOW_UPDATE;
+ msg.has_flow_id = true;
+ msg.flow_id = flow->id;
+ msg.has_pk = true;
+ msg.pk.data = data.data;
+ msg.pk.len = data.len;
+
+ recv_msg = send_recv_ipcp_msg(flow->n_1_pid, &msg);
+ if (recv_msg == NULL) {
+ log_err("Did not receive message.");
+ return -EIPCP;
+ }
+
+ if (!recv_msg->has_result) {
+ log_err("Message has no result");
+ ipcp_msg__free_unpacked(recv_msg, NULL);
+ return -EIPCP;
+ }
+
+ ret = recv_msg->result;
+ ipcp_msg__free_unpacked(recv_msg, NULL);
+
+ return ret;
+}
+
int ipcp_flow_alloc(const struct flow_info * flow,
const buffer_t dst,
const buffer_t data)
diff --git a/src/irmd/ipcp.h b/src/irmd/ipcp.h
index f1025096..8d06623c 100644
--- a/src/irmd/ipcp.h
+++ b/src/irmd/ipcp.h
@@ -68,4 +68,7 @@ int ipcp_flow_dealloc(pid_t pid,
int flow_id,
time_t timeo);
+int ipcp_flow_update(const struct flow_info * flow,
+ const buffer_t data);
+
#endif /* OUROBOROS_IRMD_IPCP_H */
diff --git a/src/irmd/main.c b/src/irmd/main.c
index e610a015..19be4ab9 100644
--- a/src/irmd/main.c
+++ b/src/irmd/main.c
@@ -36,6 +36,7 @@
#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
#include <ouroboros/flow.h>
+#include <ouroboros/fqueue.h>
#include <ouroboros/hash.h>
#include <ouroboros/irm.h>
#include <ouroboros/list.h>
@@ -86,7 +87,11 @@
#define TIMESYNC_SLACK 100 /* ms */
#define OAP_SEEN_TIMER 20 /* s */
#define DEALLOC_TIME 300 /* s */
-#define DIRECT_MPL 1 /* s */
+#define REKEY_BATCH 64 /* flows re-keyed per timer pass */
+#define REKEY_RESP_TIMEO 20 /* s; give-up on a re-key RESPONSE */
+#define DIRECT_MPL 20 /* ms */
+/* bytes; in-process, bounded only by PUP/GSPP. */
+#define DIRECT_MTU 65000
enum irm_state {
IRMD_NULL = 0,
@@ -103,13 +108,38 @@ struct cmd {
int fd;
};
+/* In-flight Tier-2 re-key, owned solely by the re-key worker thread. */
+struct rekey_ctx {
+ struct list_head next;
+
+ int flow_id;
+ void * ctx; /* OAP client ctx (opaque) */
+ struct timespec deadline; /* reap if no RESPONSE by then */
+};
+
+enum rekey_evt_type {
+ REKEY_INIT = 0, /* start an exchange for flow_id */
+ REKEY_REQ, /* a REQUEST arrived for flow_id */
+ REKEY_RESP, /* a RESPONSE arrived for flow_id */
+ REKEY_DIRECT /* in-process re-key, direct flow */
+};
+
+struct rekey_evt {
+ struct list_head next;
+
+ enum rekey_evt_type type;
+ int flow_id;
+ pid_t n_1_pid; /* INIT: flow's lower IPCP */
+ buffer_t buf; /* RESP: owned RESPONSE payload */
+};
+
struct {
bool log_stdout; /* log to stdout */
#ifdef HAVE_TOML
char * cfg_file; /* configuration file path */
#endif
struct lockfile * lf; /* single irmd per system */
- struct ssm_pool * gspp; /* pool for packets */
+ struct ssm_pool * gspp; /* pool for packets */
int sockfd; /* UNIX socket */
@@ -124,6 +154,13 @@ struct {
pthread_t irm_sanitize; /* clean up irmd resources */
pthread_t acceptor; /* accept new commands */
+
+ struct {
+ pthread_t worker; /* Tier-2 re-key orchestrator */
+ struct list_head inbox; /* re-key events for worker */
+ pthread_cond_t cond; /* inbox signal condvar */
+ pthread_mutex_t mtx; /* inbox lock */
+ } rk;
} irmd;
static enum irm_state irmd_get_state(void)
@@ -452,8 +489,8 @@ static void name_update_sec_paths(struct name_info * info)
assert(info != NULL);
- if (strlen(info->s.enc) == 0)
- sprintf(info->s.enc, "%s/%s/enc.conf", srv_dir, info->name);
+ if (strlen(info->s.sec) == 0)
+ sprintf(info->s.sec, "%s/%s/sec.conf", srv_dir, info->name);
if (strlen(info->s.crt) == 0)
sprintf(info->s.crt, "%s/%s/crt.pem", srv_dir, info->name);
@@ -461,8 +498,8 @@ static void name_update_sec_paths(struct name_info * info)
if (strlen(info->s.key) == 0)
sprintf(info->s.key, "%s/%s/key.pem", srv_dir, info->name);
- if (strlen(info->c.enc) == 0)
- sprintf(info->c.enc, "%s/%s/enc.conf", cli_dir, info->name);
+ if (strlen(info->c.sec) == 0)
+ sprintf(info->c.sec, "%s/%s/sec.conf", cli_dir, info->name);
if (strlen(info->c.crt) == 0)
sprintf(info->c.crt, "%s/%s/crt.pem", cli_dir, info->name);
@@ -782,7 +819,8 @@ static int name_unreg(const char * name,
static int get_peer_ids(int fd,
uid_t * uid,
- gid_t * gid)
+ gid_t * gid,
+ pid_t * pid)
{
#if defined(__linux__)
struct ucred ucred;
@@ -795,9 +833,14 @@ static int get_peer_ids(int fd,
*uid = ucred.uid;
*gid = ucred.gid;
+ if (pid != NULL)
+ *pid = ucred.pid;
#else
if (getpeereid(fd, uid, gid) < 0)
goto fail;
+
+ if (pid != NULL)
+ *pid = -1; /* no portable SO_PEERCRED.pid equivalent */
#endif
return 0;
fail:
@@ -846,6 +889,7 @@ static int flow_accept(struct flow_info * flow,
{
buffer_t req_hdr;
buffer_t resp_hdr;
+ buffer_t peer_crt = BUF_INIT;
char name[NAME_SIZE + 1];
struct name_info info;
int err;
@@ -909,7 +953,12 @@ static int flow_accept(struct flow_info * flow,
flow->uid = reg_get_proc_uid(flow->n_pid);
- err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk);
+ err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk,
+ false, NULL, &peer_crt);
+ if (err == -EREPLAY) {
+ log_warn("Dropping replayed alloc request for %s.", name);
+ goto fail_replay;
+ }
if (err < 0) {
log_err("OAP processing failed for %s.", name);
goto fail_oap;
@@ -920,16 +969,21 @@ static int flow_accept(struct flow_info * flow,
log_err("Failed to respond to direct flow.");
goto fail_resp;
}
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, false, peer_crt);
log_info("Flow %d accepted (direct) by %d for %s.",
flow->id, flow->n_pid, name);
} else if (ipcp_flow_alloc_resp(flow, 0, resp_hdr) < 0) {
log_err("Failed to respond to flow allocation.");
goto fail_resp;
} else {
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, false, peer_crt);
log_info("Flow %d accepted by %d for %s (uid %d).",
flow->id, flow->n_pid, name, flow->uid);
}
+ freebuf(peer_crt);
freebuf(req_hdr);
freebuf(resp_hdr);
@@ -938,6 +992,10 @@ static int flow_accept(struct flow_info * flow,
fail_oap:
if (!reg_flow_is_direct(flow->id))
ipcp_flow_alloc_resp(flow, err, resp_hdr);
+ fail_replay:
+ freebuf(peer_crt);
+ freebuf(req_hdr);
+ freebuf(resp_hdr);
fail_wait:
reg_destroy_flow(flow->id);
fail_flow:
@@ -945,6 +1003,7 @@ static int flow_accept(struct flow_info * flow,
fail_resp:
flow->state = FLOW_NULL;
+ freebuf(peer_crt);
freebuf(req_hdr);
freebuf(resp_hdr);
reg_destroy_flow(flow->id);
@@ -1193,6 +1252,7 @@ static int flow_alloc_direct(const char * dst,
struct flow_info acc; /* server side flow */
buffer_t req_hdr = BUF_INIT;
buffer_t resp_hdr = BUF_INIT;
+ buffer_t no_crt = BUF_INIT;
void * ctx;
int err;
@@ -1202,13 +1262,14 @@ static int flow_alloc_direct(const char * dst,
return -EAGAIN;
}
- if (oap_cli_prepare(&ctx, info, &req_hdr, *data) < 0) {
+ if (oap_cli_prepare(&ctx, info, &req_hdr, *data, false) < 0) {
log_err("Failed to prepare OAP for %s.", dst);
return -EBADF;
}
acc.n_1_pid = flow->n_pid;
acc.mpl = DIRECT_MPL;
+ acc.mtu = DIRECT_MTU;
acc.qs = flow->qs;
acc.state = FLOW_ALLOCATED;
@@ -1234,7 +1295,7 @@ static int flow_alloc_direct(const char * dst,
return -ETIMEDOUT;
}
- err = oap_cli_complete(ctx, info, resp_hdr, data, sk);
+ err = oap_cli_complete(ctx, info, resp_hdr, data, sk, NULL, NULL);
if (err < 0) {
log_err("OAP completion failed for %s.", dst);
freebuf(resp_hdr);
@@ -1244,8 +1305,13 @@ static int flow_alloc_direct(const char * dst,
flow->id = acc.id;
flow->n_1_pid = acc.n_pid;
flow->mpl = DIRECT_MPL;
+ flow->mtu = DIRECT_MTU;
flow->state = FLOW_ALLOCATED;
+ /* Mark encrypted for re-key; the acceptor caches the cert. */
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(acc.id, true, no_crt);
+
log_info("Flow %d allocated (direct) for %d to %s.",
flow->id, flow->n_pid, dst);
@@ -1264,6 +1330,7 @@ static int flow_alloc(const char * dst,
buffer_t req_hdr = BUF_INIT;
buffer_t resp_hdr = BUF_INIT;
buffer_t hash = BUF_INIT;
+ buffer_t peer_crt = BUF_INIT;
struct name_info info;
void * ctx;
int err;
@@ -1297,6 +1364,8 @@ static int flow_alloc(const char * dst,
goto fail_flow;
}
+ reg_set_name_for_flow_id(dst, flow->id);
+
if (get_ipcp_by_dst(dst, &flow->n_1_pid, &hash) < 0) {
log_err("Failed to find IPCP for %s.", dst);
err = -EIPCP;
@@ -1309,7 +1378,7 @@ static int flow_alloc(const char * dst,
goto fail_prepare;
}
- if (oap_cli_prepare(&ctx, &info, &req_hdr, *data) < 0) {
+ if (oap_cli_prepare(&ctx, &info, &req_hdr, *data, false) < 0) {
log_err("Failed to prepare OAP request for %s.", dst);
err = -EBADF;
goto fail_prepare;
@@ -1341,12 +1410,16 @@ static int flow_alloc(const char * dst,
goto fail_peer;
}
- err = oap_cli_complete(ctx, &info, resp_hdr, data, sk);
+ err = oap_cli_complete(ctx, &info, resp_hdr, data, sk, NULL, &peer_crt);
if (err < 0) {
log_err("OAP completion failed for %s.", dst);
goto fail_complete;
}
+ if (sk->nid != NID_undef)
+ reg_flow_set_rekey(flow->id, true, peer_crt);
+
+ freebuf(peer_crt);
freebuf(req_hdr);
freebuf(resp_hdr);
freebuf(hash);
@@ -1354,7 +1427,8 @@ static int flow_alloc(const char * dst,
return 0;
fail_complete:
- ctx = NULL; /* freee'd on complete */
+ freebuf(peer_crt);
+ ctx = NULL; /* free'd on complete */
fail_peer:
flow->state = FLOW_DEALLOCATED;
fail_wait:
@@ -1421,6 +1495,741 @@ static int flow_dealloc_resp(struct flow_info * flow)
return 0;
}
+/*
+ * Inbox producers. Any thread may post; the worker drains. INIT carries
+ * the flow's lower IPCP pid; RESP transfers ownership of buf.
+ */
+static void rekey_post(enum rekey_evt_type type,
+ int flow_id,
+ pid_t n_1_pid,
+ buffer_t * buf)
+{
+ struct rekey_evt * evt;
+
+ evt = malloc(sizeof(*evt));
+ if (evt == NULL) {
+ log_err("Failed to malloc re-key event for flow %d.", flow_id);
+ if (type == REKEY_INIT || type == REKEY_DIRECT)
+ reg_flow_clear_in_flight(flow_id);
+ else
+ reg_flow_rekey_arr_done(flow_id, type == REKEY_REQ);
+
+ if (buf != NULL)
+ freebuf(*buf);
+
+ return;
+ }
+
+ list_head_init(&evt->next);
+ evt->type = type;
+ evt->flow_id = flow_id;
+ evt->n_1_pid = n_1_pid;
+ clrbuf(evt->buf);
+ if (buf != NULL) {
+ evt->buf = *buf;
+ clrbuf(*buf);
+ }
+
+ pthread_mutex_lock(&irmd.rk.mtx);
+
+ list_add_tail(&evt->next, &irmd.rk.inbox);
+ pthread_cond_signal(&irmd.rk.cond);
+
+ pthread_mutex_unlock(&irmd.rk.mtx);
+}
+
+static void rekey_post_init(int flow_id,
+ pid_t n_1_pid)
+{
+ rekey_post(REKEY_INIT, flow_id, n_1_pid, NULL);
+}
+
+static void rekey_post_resp(int flow_id,
+ buffer_t * buf)
+{
+ rekey_post(REKEY_RESP, flow_id, 0, buf);
+}
+
+static void rekey_post_req(int flow_id,
+ pid_t n_1_pid,
+ buffer_t * buf)
+{
+ rekey_post(REKEY_REQ, flow_id, n_1_pid, buf);
+}
+
+static void rekey_post_direct(int flow_id)
+{
+ rekey_post(REKEY_DIRECT, flow_id, 0, NULL);
+}
+
+/* Worker-only: find an in-flight entry by flow_id. */
+static struct rekey_ctx * rekey_find(struct list_head * tbl,
+ int flow_id)
+{
+ struct list_head * p;
+
+ list_for_each(p, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ if (e->flow_id == flow_id)
+ return e;
+ }
+
+ return NULL;
+}
+
+/* Worker-only: drop an entry, freeing its OAP ctx. */
+static void rekey_drop(struct rekey_ctx * e)
+{
+ if (e->ctx != NULL)
+ oap_ctx_free(e->ctx);
+
+ list_del(&e->next);
+ free(e);
+}
+
+/* Resolve a flow's registered name info; < 0 if the flow or name is gone. */
+static int rekey_name_info(int flow_id,
+ struct name_info * info)
+{
+ char name[NAME_SIZE + 1];
+
+ if (reg_get_name_for_flow_id(name, flow_id) < 0)
+ return -1;
+
+ return reg_get_name_info(name, info);
+}
+
+/* Flow-update relay payload: a 1-byte type prefix on an opaque body. */
+enum flow_upd_type {
+ FLOW_UPD_REKEY_REQ = 0,
+ FLOW_UPD_REKEY_RESP = 1,
+};
+
+/* Prepend the update type to body; caller frees out on success. */
+static int flow_upd_wrap(buffer_t * out,
+ uint8_t type,
+ const buffer_t * body)
+{
+ out->len = body->len + 1;
+ out->data = malloc(out->len);
+ if (out->data == NULL)
+ return -ENOMEM;
+
+ out->data[0] = type;
+ memcpy(out->data + 1, body->data, body->len);
+
+ return 0;
+}
+
+/* Cleanup handlers — the re-key worker is cancelled at shutdown. */
+static void rk_free_evt(void * o)
+{
+ struct rekey_evt * evt = o;
+
+ freebuf(evt->buf);
+ free(evt);
+}
+
+static void rk_freebuf(void * o)
+{
+ freebuf(*(buffer_t *) o);
+}
+
+static void rk_clear_in_flight(void * o)
+{
+ reg_flow_clear_in_flight(*(int *) o);
+}
+
+static void rk_clear_key(void * o)
+{
+ crypt_secure_clear(o, SYMMKEYSZ);
+}
+
+static void rekey_do_initiate(struct list_head * tbl,
+ int flow_id,
+ pid_t n_1_pid)
+{
+ struct rekey_ctx * e;
+ struct flow_info info;
+ struct name_info name;
+ buffer_t req = BUF_INIT;
+ buffer_t upd = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ void * ctx = NULL;
+ int ret;
+
+ e = rekey_find(tbl, flow_id);
+ if (e != NULL)
+ rekey_drop(e); /* Replace in-flight entries */
+
+ if (rekey_name_info(flow_id, &name) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ goto fail;
+ }
+
+ if (oap_cli_prepare(&ctx, &name, &req, data, true) < 0) {
+ log_err("Failed to prepare re-key for flow %d.", flow_id);
+ goto fail;
+ }
+
+ memset(&info, 0, sizeof(info));
+ info.id = flow_id;
+ info.n_1_pid = n_1_pid;
+
+ if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_REQ, &req) < 0) {
+ log_err("Failed to wrap re-key request for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ pthread_cleanup_push(rk_clear_in_flight, &flow_id);
+ pthread_cleanup_push(oap_ctx_free, ctx);
+ pthread_cleanup_push(rk_freebuf, &req);
+ pthread_cleanup_push(rk_freebuf, &upd);
+ ret = ipcp_flow_update(&info, upd);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ freebuf(upd);
+ if (ret < 0) {
+ log_err("Failed to send re-key request for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ e = malloc(sizeof(*e));
+ if (e == NULL) {
+ log_err("Failed to malloc re-key ctx for flow %d.", flow_id);
+ goto fail_ctx;
+ }
+
+ list_head_init(&e->next);
+ e->flow_id = flow_id;
+ e->ctx = ctx;
+ clock_gettime(PTHREAD_COND_CLOCK, &e->deadline);
+ e->deadline.tv_sec += REKEY_RESP_TIMEO;
+
+ list_add(&e->next, tbl);
+
+ log_dbg("Re-key request sent for flow %d.", flow_id);
+
+ freebuf(req);
+
+ return;
+
+ fail_ctx:
+ oap_ctx_free(ctx);
+ freebuf(req);
+ fail:
+ reg_flow_clear_in_flight(flow_id);
+}
+
+/* Worker-only: complete the exchange, install the pending seed. */
+static void rekey_do_complete(struct list_head * tbl,
+ int flow_id,
+ buffer_t buf)
+{
+ struct rekey_ctx * e;
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ uint8_t newgen;
+
+ e = rekey_find(tbl, flow_id);
+ if (e == NULL) {
+ log_dbg("Stale re-key RESPONSE for flow %d.", flow_id);
+ return;
+ }
+
+ /* A concurrent responder already parked a seed; don't overwrite. */
+ if (reg_flow_rekey_pending(flow_id)) {
+ log_dbg("Re-key already pending for flow %d.", flow_id);
+ goto finish;
+ }
+
+ if (rekey_name_info(flow_id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ goto finish;
+ }
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow_id, &crt);
+
+ /* oap_cli_complete frees the ctx on every path. */
+ if (oap_cli_complete(e->ctx, &info, buf, &data, &sk, &crt, NULL) < 0) {
+ log_warn("Failed to complete re-key for flow %d.", flow_id);
+ e->ctx = NULL;
+ goto finish_clear;
+ }
+
+ e->ctx = NULL;
+
+ if (data.len != 1) {
+ log_warn("Re-key reply malformed for flow %d.", flow_id);
+ goto finish_clear;
+ }
+
+ newgen = *(uint8_t *) data.data;
+
+ if (newgen >= 16) {
+ log_warn("Re-key gen %u out of range for flow %d.",
+ newgen, flow_id);
+ goto finish_clear;
+ }
+
+ if (reg_flow_store_pending(flow_id, kbuf, newgen, true) < 0)
+ log_warn("Flow %d gone during re-key.", flow_id);
+ else
+ reg_notify_flow(flow_id, FLOW_UPD);
+
+ log_dbg("Re-key completed for flow %d (gen %u).", flow_id, newgen);
+
+ finish_clear:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ freebuf(data);
+ finish:
+ freebuf(crt);
+ rekey_drop(e);
+ reg_flow_clear_in_flight(flow_id);
+}
+
+/* Worker-only: reap entries whose RESPONSE never arrived. */
+static void rekey_reap_expired(struct list_head * tbl)
+{
+ struct list_head * p;
+ struct list_head * h;
+ struct timespec now;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ list_for_each_safe(p, h, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ if (ts_diff_ns(&e->deadline, &now) > 0)
+ continue;
+
+ log_warn("Re-key timed out for flow %d.", e->flow_id);
+ reg_flow_clear_in_flight(e->flow_id);
+ rekey_drop(e);
+ }
+}
+
+/* Responder side: process request, install pending seed, send response. */
+static int rekey_respond(struct flow_info * flow,
+ buffer_t * pk)
+{
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t rsp = BUF_INIT;
+ buffer_t upd = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ uint8_t newgen;
+ int epoch;
+ int err;
+
+ epoch = reg_flow_get_epoch(flow->id);
+ if (epoch < 0) {
+ log_warn("Re-key for unknown flow %d.", flow->id);
+ return -EBADF;
+ }
+
+ /* Collision: we are driving our own exchange; let it win. */
+ if (reg_flow_rekey_should_yield(flow->id)) {
+ log_dbg("Yielding to own re-key for flow %d.", flow->id);
+ return 0;
+ }
+
+ if (rekey_name_info(flow->id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow->id);
+ return -ENAME;
+ }
+
+ if (reg_flow_rekey_pending(flow->id)) {
+ log_dbg("Duplicate re-key request for flow %d.", flow->id);
+ return 0;
+ }
+
+ newgen = (uint8_t) ((epoch + 1) & 0x0F);
+ data.data = &newgen;
+ data.len = 1;
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow->id, &crt);
+
+ err = oap_srv_process(&info, *pk, &rsp, &data, &sk, true, &crt, NULL);
+ if (err < 0) {
+ /* data still points to stack newgen; don't free it. */
+ log_err("Failed to process re-key OAP for flow %d.", flow->id);
+ goto finish;
+ }
+
+ /* On success oap_srv_process repointed data to client output. */
+ freebuf(data);
+
+ if (reg_flow_store_pending(flow->id, kbuf, newgen, false) < 0) {
+ log_warn("Flow %d gone during re-key.", flow->id);
+ err = -EBADF;
+ goto finish;
+ }
+
+ reg_notify_flow(flow->id, FLOW_UPD);
+
+ if (flow_upd_wrap(&upd, FLOW_UPD_REKEY_RESP, &rsp) == 0) {
+ pthread_cleanup_push(rk_clear_key, kbuf);
+ pthread_cleanup_push(rk_freebuf, &rsp);
+ pthread_cleanup_push(rk_freebuf, &crt);
+ pthread_cleanup_push(rk_freebuf, &upd);
+ if (ipcp_flow_update(flow, upd) < 0)
+ log_err("Failed to send re-key response for flow %d.",
+ flow->id);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ pthread_cleanup_pop(false);
+ freebuf(upd);
+ }
+
+ err = 0;
+ finish:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ freebuf(rsp);
+ freebuf(crt);
+
+ return err;
+}
+
+/*
+ * Worker-only: re-key a direct (loopback) flow, the exchange runs in-process:
+ * build a client request, then derive the shared seed, and hand the one seed
+ * to both apps with RB_REKEY.
+ */
+static void rekey_do_direct(int flow_id)
+{
+ struct name_info info;
+ struct crypt_sk sk;
+ uint8_t kbuf[SYMMKEYSZ];
+ buffer_t req = BUF_INIT;
+ buffer_t rsp = BUF_INIT;
+ buffer_t data = BUF_INIT;
+ buffer_t crt = BUF_INIT;
+ void * ctx = NULL;
+ uint8_t newgen;
+ int epoch;
+
+ epoch = reg_flow_get_epoch(flow_id);
+ if (epoch < 0) {
+ log_warn("Re-key for unknown flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ if (rekey_name_info(flow_id, &info) < 0) {
+ log_err("Failed to get name info to re-key flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ if (oap_cli_prepare(&ctx, &info, &req, data, true) < 0) {
+ log_err("Failed to prepare re-key for flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ return;
+ }
+
+ newgen = (uint8_t) ((epoch + 1) & 0x0F);
+ data.data = &newgen;
+ data.len = 1;
+
+ sk.key = kbuf;
+
+ reg_flow_get_peer_crt(flow_id, &crt);
+
+ if (oap_srv_process(&info, req, &rsp, &data, &sk, true,
+ &crt, NULL) < 0) {
+ /* data still points to stack newgen; don't free it. */
+ log_err("Failed to process re-key OAP for flow %d.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ goto out;
+ }
+
+ /* On success oap_srv_process repointed data to its output. */
+ freebuf(data);
+
+ if (reg_flow_store_pending_direct(flow_id, kbuf, newgen) < 0) {
+ log_warn("Flow %d gone during re-key.", flow_id);
+ reg_flow_clear_in_flight(flow_id);
+ goto out;
+ }
+
+ reg_notify_flow_peers(flow_id, FLOW_UPD);
+
+ log_dbg("Re-key completed (direct) for flow %d (gen %u).",
+ flow_id, newgen);
+ out:
+ crypt_secure_clear(kbuf, SYMMKEYSZ);
+ oap_ctx_free(ctx);
+ freebuf(req);
+ freebuf(rsp);
+ freebuf(crt);
+}
+
+/* Route one snapshot entry to the wire or in-process re-key path. */
+static void rekey_dispatch(struct list_head * tbl,
+ const struct rekey_info * ri)
+{
+ if (ri->direct)
+ rekey_do_direct(ri->flow_id);
+ else
+ rekey_do_initiate(tbl, ri->flow_id, ri->n_1_pid);
+}
+
+static int flow_update_arr(struct flow_info * flow,
+ buffer_t * pk)
+{
+ uint8_t type;
+ bool is_req;
+
+ if (pk->len < 1)
+ return -EINVAL;
+
+ type = pk->data[0];
+
+ switch (type) {
+ case FLOW_UPD_REKEY_REQ:
+ is_req = true;
+ break;
+ case FLOW_UPD_REKEY_RESP:
+ is_req = false;
+ break;
+ default:
+ log_warn("Unknown flow update type %u.", type);
+ return -EINVAL;
+ }
+
+ /* Drop floods/spoofs before allocating a worker event. */
+ if (!reg_flow_rekey_arr_admit(flow->id, flow->n_1_pid, is_req))
+ return 0;
+
+ /* Strip the type byte, keeping the malloc base for hand-off. */
+ memmove(pk->data, pk->data + 1, pk->len - 1);
+ pk->len -= 1;
+
+ /* Defer to worker; an inline RESP send deadlocks loopback. */
+ if (is_req)
+ rekey_post_req(flow->id, flow->n_1_pid, pk);
+ else
+ rekey_post_resp(flow->id, pk);
+
+ return 0;
+}
+
+static int flow_update(struct flow_info * flow,
+ uid_t uid,
+ pid_t cpid,
+ bool rekey,
+ struct crypt_sk * sk,
+ bool * has_key,
+ bool * initiator)
+{
+ uint8_t seed[SYMMKEYSZ];
+ uint8_t epoch;
+ int rc;
+
+ *has_key = false;
+ *initiator = false;
+
+ if (rekey) {
+ pid_t n_1_pid;
+
+ if (!reg_flow_owned_by(flow->id, uid))
+ return -EPERM;
+
+ /* Direct flows re-key in-process; no lower IPCP carrier. */
+ if (reg_flow_is_direct(flow->id)) {
+ if (reg_flow_rekey_begin(flow->id))
+ rekey_post_direct(flow->id);
+
+ return 0;
+ }
+
+ /* Watermark re-key: the app can't know its lower IPCP. */
+ n_1_pid = reg_flow_get_n_1_pid(flow->id);
+ if (n_1_pid <= 0)
+ return 0;
+
+ /* One exchange per flow; the latch arbitrates collisions. */
+ if (reg_flow_rekey_begin(flow->id))
+ rekey_post_init(flow->id, n_1_pid);
+
+ return 0;
+ }
+
+ rc = reg_flow_take_pending(flow->id, uid, cpid, seed, &epoch,
+ initiator);
+ if (rc == -EPERM)
+ return -EPERM;
+
+ if (rc != 0)
+ return 0;
+
+ memcpy(sk->key, seed, SYMMKEYSZ);
+ sk->epoch = epoch;
+ *has_key = true;
+
+ crypt_secure_clear(seed, SYMMKEYSZ);
+
+ log_dbg("Delivered re-key seed for flow %d (gen %u).",
+ flow->id, epoch);
+
+ return 0;
+}
+
+static void rekey_table_cleanup(void * o)
+{
+ struct list_head * tbl = o;
+ struct list_head * p;
+ struct list_head * h;
+
+ list_for_each_safe(p, h, tbl) {
+ struct rekey_ctx * e = list_entry(p, struct rekey_ctx, next);
+ rekey_drop(e);
+ }
+}
+
+static struct rekey_evt * rekey_event_wait(const struct timespec * dl)
+{
+ struct rekey_evt * evt = NULL;
+ int ret = 0;
+
+ pthread_mutex_lock(&irmd.rk.mtx);
+ pthread_cleanup_push(__cleanup_mutex_unlock, &irmd.rk.mtx);
+
+ while (list_is_empty(&irmd.rk.inbox) && ret != -ETIMEDOUT)
+ ret = -pthread_cond_timedwait(&irmd.rk.cond, &irmd.rk.mtx, dl);
+
+ if (!list_is_empty(&irmd.rk.inbox)) {
+ evt = list_first_entry(&irmd.rk.inbox, struct rekey_evt, next);
+ list_del(&evt->next);
+ }
+
+ pthread_cleanup_pop(true);
+
+ return evt;
+}
+
+static struct timespec rekey_deadline(struct list_head * tbl,
+ struct timespec next)
+{
+ struct timespec deadline = next;
+ struct list_head * p;
+
+ list_for_each(p, tbl) {
+ struct rekey_ctx * e;
+ e = list_entry(p, struct rekey_ctx, next);
+ if (ts_diff_ns(&e->deadline, &deadline) < 0)
+ deadline = e->deadline;
+ }
+
+ return deadline;
+}
+
+static void rekey_handle_evt(struct list_head * tbl,
+ struct rekey_evt * evt)
+{
+ struct flow_info rinfo;
+
+ pthread_cleanup_push(rk_free_evt, evt);
+
+ switch (evt->type) {
+ case REKEY_INIT:
+ rekey_do_initiate(tbl, evt->flow_id, evt->n_1_pid);
+ break;
+ case REKEY_REQ:
+ memset(&rinfo, 0, sizeof(rinfo));
+ rinfo.id = evt->flow_id;
+ rinfo.n_1_pid = evt->n_1_pid;
+ rekey_respond(&rinfo, &evt->buf);
+ reg_flow_rekey_arr_done(evt->flow_id, true);
+ break;
+ case REKEY_RESP:
+ rekey_do_complete(tbl, evt->flow_id, evt->buf);
+ reg_flow_rekey_arr_done(evt->flow_id, false);
+ break;
+ case REKEY_DIRECT:
+ rekey_do_direct(evt->flow_id);
+ break;
+ default:
+ break;
+ }
+
+ pthread_cleanup_pop(true);
+}
+
+/* On the periodic tick, dispatch all flows due for re-keying. */
+static void rekey_run_periodic(struct list_head * tbl,
+ struct timespec * next)
+{
+ struct rekey_info snap[REKEY_BATCH];
+ struct timespec now;
+ int n;
+ int i;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &now);
+
+ if (ts_diff_ns(next, &now) > 0)
+ return;
+
+ n = reg_flow_snapshot_rekey_due(snap, REKEY_BATCH);
+ for (i = 0; i < n; ++i)
+ rekey_dispatch(tbl, &snap[i]);
+
+ clock_gettime(PTHREAD_COND_CLOCK, next);
+ next->tv_sec += OAP_REKEY_TIMER;
+}
+
+/*
+ * Single worker owning all in-flight Tier-2 re-keys. It drains the
+ * inbox, runs the periodic snapshot, and reaps timed-out exchanges.
+ * The table is touched only here, so it needs no lock.
+ */
+static void * rekey_worker(void * o)
+{
+ struct list_head table;
+ struct timespec next;
+
+ (void) o;
+
+ list_head_init(&table);
+
+ clock_gettime(PTHREAD_COND_CLOCK, &next);
+ next.tv_sec += OAP_REKEY_TIMER;
+
+ pthread_cleanup_push(rekey_table_cleanup, &table);
+
+ while (true) {
+ struct rekey_evt * evt;
+ struct timespec deadline;
+
+ deadline = rekey_deadline(&table, next);
+
+ evt = rekey_event_wait(&deadline);
+
+ if (evt != NULL)
+ rekey_handle_evt(&table, evt);
+
+ rekey_run_periodic(&table, &next);
+
+ rekey_reap_expired(&table);
+ }
+
+ pthread_cleanup_pop(true);
+
+ return (void *) 0;
+}
+
static void * acceptloop(void * o)
{
int csockfd;
@@ -1491,6 +2300,11 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
struct timespec now;
struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */
int res;
+ bool has_key = false;
+ bool initiator = false;
+ uid_t uid;
+ gid_t gid;
+ pid_t cpid;
irm_msg_t * ret_msg;
buffer_t data;
@@ -1557,7 +2371,7 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
case IRM_MSG_CODE__IRM_PROC_ANNOUNCE:
proc.pid = msg->pid;
strcpy(proc.prog, msg->prog);
- res = get_peer_ids(fd, &proc.uid, &proc.gid);
+ res = get_peer_ids(fd, &proc.uid, &proc.gid, NULL);
if (res < 0)
log_err("Failed to get UID/GID for pid %d.", msg->pid);
else
@@ -1600,26 +2414,29 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
flow = flow_info_msg_to_s(msg->flow_info);
sk.key = kbuf;
res = flow_accept(&flow, &data, abstime, &sk);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_pk = data.len != 0;
- ret_msg->pk.data = data.data;
- ret_msg->pk.len = data.len;
- ret_msg->has_cipher_nid = true;
- ret_msg->cipher_nid = sk.nid;
- if (sk.nid != NID_undef) {
- hbuf = malloc(SYMMKEYSZ);
- if (hbuf == NULL) {
- log_err("Failed to malloc key buf");
- return NULL;
- }
-
- memcpy(hbuf, kbuf, SYMMKEYSZ);
- ret_msg->sym_key.data = hbuf;
- ret_msg->sym_key.len = SYMMKEYSZ;
- ret_msg->has_sym_key = true;
- }
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ ret_msg->has_pk = data.len != 0;
+ ret_msg->pk.data = data.data;
+ ret_msg->pk.len = data.len;
+ ret_msg->has_cipher_nid = true;
+ ret_msg->cipher_nid = sk.nid;
+ if (sk.nid == NID_undef)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
}
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
break;
case IRM_MSG_CODE__IRM_FLOW_ALLOC:
data.len = msg->pk.len;
@@ -1630,25 +2447,29 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
abstime = abstime == NULL ? &max : abstime;
sk.key = kbuf;
res = flow_alloc(msg->dst, &flow, &data, abstime, &sk);
- if (res == 0) {
- ret_msg->flow_info = flow_info_s_to_msg(&flow);
- ret_msg->has_pk = data.len != 0;
- ret_msg->pk.data = data.data;
- ret_msg->pk.len = data.len;
- ret_msg->has_cipher_nid = true;
- ret_msg->cipher_nid = sk.nid;
- if (sk.nid != NID_undef) {
- hbuf = malloc(SYMMKEYSZ);
- if (hbuf == NULL) {
- log_err("Failed to malloc key buf");
- return NULL;
- }
- memcpy(hbuf, kbuf, SYMMKEYSZ);
- ret_msg->sym_key.data = hbuf;
- ret_msg->sym_key.len = SYMMKEYSZ;
- ret_msg->has_sym_key = true;
- }
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ ret_msg->has_pk = data.len != 0;
+ ret_msg->pk.data = data.data;
+ ret_msg->pk.len = data.len;
+ ret_msg->has_cipher_nid = true;
+ ret_msg->cipher_nid = sk.nid;
+ if (sk.nid == NID_undef)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
}
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
break;
case IRM_MSG_CODE__IRM_FLOW_JOIN:
assert(msg->pk.len == 0 && msg->pk.data == NULL);
@@ -1687,6 +2508,51 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
flow = flow_info_msg_to_s(msg->flow_info);
res = flow_alloc_reply(&flow, msg->response, &data);
break;
+ case IRM_MSG_CODE__IPCP_FLOW_UPDATE_ARR:
+ data.len = msg->pk.len;
+ data.data = msg->pk.data;
+ msg->pk.data = NULL; /* pass data */
+ msg->pk.len = 0;
+ flow = flow_info_msg_to_s(msg->flow_info);
+ res = flow_update_arr(&flow, &data);
+ freebuf(data);
+ break;
+ case IRM_MSG_CODE__IRM_FLOW_UPDATE:
+ flow = flow_info_msg_to_s(msg->flow_info);
+ if (get_peer_ids(fd, &uid, &gid, &cpid) < 0) {
+ res = -EPERM;
+ break;
+ }
+
+ if (cpid <= 0) /* non-Linux: fall back to asserted pid */
+ cpid = flow.n_pid;
+
+ sk.key = kbuf;
+ res = flow_update(&flow, uid, cpid, msg->rekey, &sk, &has_key,
+ &initiator);
+ if (res != 0)
+ break;
+
+ ret_msg->flow_info = flow_info_s_to_msg(&flow);
+ if (!has_key)
+ break;
+
+ hbuf = malloc(SYMMKEYSZ);
+ if (hbuf == NULL) {
+ log_err("Failed to malloc key buf");
+ res = -ENOMEM;
+ break;
+ }
+
+ memcpy(hbuf, kbuf, SYMMKEYSZ);
+ ret_msg->sym_key.data = hbuf;
+ ret_msg->sym_key.len = SYMMKEYSZ;
+ ret_msg->has_sym_key = true;
+ ret_msg->has_generation = true;
+ ret_msg->generation = sk.epoch;
+ ret_msg->has_rk_initiator = true;
+ ret_msg->rk_initiator = initiator;
+ break;
default:
log_err("Don't know that message code.");
res = -1;
@@ -1706,6 +2572,13 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg,
return ret_msg;
}
+/* Wipe the session key from a reply before its buffers are freed. */
+static void clear_msg_key(irm_msg_t * msg)
+{
+ if (msg != NULL && msg->has_sym_key)
+ crypt_secure_clear(msg->sym_key.data, msg->sym_key.len);
+}
+
static void * mainloop(void * o)
{
int sfd;
@@ -1717,6 +2590,7 @@ static void * mainloop(void * o)
while (true) {
irm_msg_t * ret_msg;
struct cmd * cmd;
+ bool had_key;
pthread_mutex_lock(&irmd.cmd_lock);
@@ -1780,6 +2654,9 @@ static void * mainloop(void * o)
irm_msg__pack(ret_msg, buffer.data);
+ had_key = ret_msg->has_sym_key;
+ clear_msg_key(ret_msg);
+
irm_msg__free_unpacked(ret_msg, NULL);
pthread_cleanup_push(__cleanup_close_ptr, &sfd);
@@ -1794,6 +2671,9 @@ static void * mainloop(void * o)
strerror(errno));
}
+ if (had_key)
+ crypt_secure_clear(buffer.data, buffer.len);
+
pthread_cleanup_pop(true);
pthread_cleanup_pop(true);
@@ -1801,6 +2681,7 @@ static void * mainloop(void * o)
continue;
fail:
+ clear_msg_key(ret_msg);
irm_msg__free_unpacked(ret_msg, NULL);
fail_msg:
close(sfd);
@@ -1884,12 +2765,14 @@ void * irm_sanitize(void * o)
return (void *) 0;
}
-static int irm_load_store(char * dpath)
+static int irm_load_store(char * dpath,
+ bool anchor)
{
struct stat st;
struct dirent * dent;
DIR * dir;
void * crt;
+ int ret;
if (stat(dpath, &st) == -1) {
log_dbg("Store directory %s not found.", dpath);
@@ -1933,7 +2816,9 @@ static int irm_load_store(char * dpath)
goto fail_file;
}
- if (oap_auth_add_ca_crt(crt) < 0) {
+ ret = anchor ? oap_auth_add_ca_crt(crt)
+ : oap_auth_add_chain_crt(crt);
+ if (ret < 0) {
log_err("Failed to add certificate from %s to store.",
path);
goto fail_crt_add;
@@ -2030,6 +2915,29 @@ static int irm_init(void)
list_head_init(&irmd.cmds);
+ if (pthread_mutex_init(&irmd.rk.mtx, NULL)) {
+ log_err("Failed to initialize mutex.");
+ goto fail_rk_mtx;
+ }
+
+ if (pthread_condattr_init(&cattr)) {
+ log_err("Failed to initialize condattr.");
+ goto fail_rk_mtx;
+ }
+
+#ifndef __APPLE__
+ pthread_condattr_setclock(&cattr, PTHREAD_COND_CLOCK);
+#endif
+ if (pthread_cond_init(&irmd.rk.cond, &cattr)) {
+ log_err("Failed to initialize condvar.");
+ pthread_condattr_destroy(&cattr);
+ goto fail_rk_cond;
+ }
+
+ pthread_condattr_destroy(&cattr);
+
+ list_head_init(&irmd.rk.inbox);
+
if (stat(SOCK_PATH, &st) == -1) {
if (mkdir(SOCK_PATH, 0777)) {
log_err("Failed to create sockets directory.");
@@ -2077,12 +2985,12 @@ static int irm_init(void)
goto fail_oap;
}
- if (irm_load_store(OUROBOROS_CA_CRT_DIR) < 0) {
+ if (irm_load_store(OUROBOROS_CA_CRT_DIR, true) < 0) {
log_err("Failed to load CA certificates.");
goto fail_load_store;
}
- if (irm_load_store(OUROBOROS_CHAIN_DIR) < 0) {
+ if (irm_load_store(OUROBOROS_CHAIN_DIR, false) < 0) {
log_err("Failed to load intermediate certificates.");
goto fail_load_store;
}
@@ -2133,6 +3041,10 @@ static int irm_init(void)
fail_sock_path:
unlink(IRM_SOCK_PATH);
fail_stat:
+ pthread_cond_destroy(&irmd.rk.cond);
+ fail_rk_cond:
+ pthread_mutex_destroy(&irmd.rk.mtx);
+ fail_rk_mtx:
pthread_cond_destroy(&irmd.cmd_cond);
fail_cmd_cond:
pthread_mutex_destroy(&irmd.cmd_lock);
@@ -2181,13 +3093,28 @@ static void irm_fini(void)
pthread_mutex_unlock(&irmd.cmd_lock);
+ pthread_mutex_lock(&irmd.rk.mtx);
+
+ list_for_each_safe(p, h, &irmd.rk.inbox) {
+ struct rekey_evt * evt;
+ evt = list_entry(p, struct rekey_evt, next);
+ list_del(&evt->next);
+ freebuf(evt->buf);
+ free(evt);
+ }
+
+ pthread_mutex_unlock(&irmd.rk.mtx);
+
pthread_mutex_destroy(&irmd.cmd_lock);
pthread_cond_destroy(&irmd.cmd_cond);
+ pthread_mutex_destroy(&irmd.rk.mtx);
+ pthread_cond_destroy(&irmd.rk.cond);
pthread_rwlock_destroy(&irmd.state_lock);
#ifdef HAVE_FUSE
while (rmdir(FUSE_PREFIX) < 0 && retries-- > 0)
nanosleep(&wait, NULL);
+
if (retries < 0)
log_err("Failed to remove " FUSE_PREFIX);
#endif
@@ -2220,10 +3147,18 @@ static int irm_start(void)
if (pthread_create(&irmd.acceptor, NULL, acceptloop, NULL))
goto fail_acceptor;
+ if (OAP_REKEY_TIMER > 0) {
+ if (pthread_create(&irmd.rk.worker, NULL, rekey_worker, NULL))
+ goto fail_rekey_worker;
+ }
+
log_info("Ouroboros IPC Resource Manager daemon started...");
return 0;
+ fail_rekey_worker:
+ pthread_cancel(irmd.acceptor);
+ pthread_join(irmd.acceptor, NULL);
fail_acceptor:
pthread_cancel(irmd.irm_sanitize);
pthread_join(irmd.irm_sanitize, NULL);
@@ -2263,6 +3198,11 @@ static void irm_sigwait(sigset_t sigset)
static void irm_stop(void)
{
+ if (OAP_REKEY_TIMER > 0) {
+ pthread_cancel(irmd.rk.worker);
+ pthread_join(irmd.rk.worker, NULL);
+ }
+
pthread_cancel(irmd.acceptor);
pthread_cancel(irmd.irm_sanitize);
@@ -2383,26 +3323,31 @@ int main(int argc,
goto fail_irm_init;
}
- if (irm_init() < 0)
+ if (crypt_secure_malloc_init(IRMD_SECMEM_MAX) < 0) {
+ log_err("Failed to initialize secure memory allocation.");
+ goto fail_secmem;
+ }
+
+ if (irm_init() < 0) {
+ log_err("Failed to initialize IRMd.");
goto fail_irm_init;
+ }
if (reg_init() < 0) {
log_err("Failed to initialize registry.");
goto fail_reg;
}
- if (crypt_secure_malloc_init(IRMD_SECMEM_MAX) < 0) {
- log_err("Failed to initialize secure memory allocation.");
- goto fail_reg;
- }
-
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
- if (irm_start() < 0)
+ if (irm_start() < 0) {
+ log_err("Failed to start IRMd.");
goto fail_irm_start;
+ }
#ifdef HAVE_TOML
if (irm_configure(irmd.cfg_file) < 0) {
+ log_err("Failed to load IRMd configuration.");
irmd_set_state(IRMD_SHUTDOWN);
ret = EXIT_FAILURE;
}
@@ -2415,14 +3360,16 @@ int main(int argc,
pthread_sigmask(SIG_UNBLOCK, &sigset, NULL);
- crypt_secure_malloc_fini();
-
reg_clear();
reg_fini();
irm_fini();
+ crypt_secure_malloc_fini();
+
+ crypt_cleanup();
+
log_info("Ouroboros IPC Resource Manager daemon exited. Bye.");
log_fini();
@@ -2434,5 +3381,8 @@ int main(int argc,
fail_reg:
irm_fini();
fail_irm_init:
+ crypt_secure_malloc_fini();
+ crypt_cleanup();
+ fail_secmem:
exit(EXIT_FAILURE);
}
diff --git a/src/irmd/oap.c b/src/irmd/oap.c
deleted file mode 100644
index 1831f533..00000000
--- a/src/irmd/oap.c
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Ouroboros - Copyright (C) 2016 - 2026
- *
- * OAP - Shared credential and configuration loading
- *
- * Dimitri Staessens <dimitri@ouroboros.rocks>
- * Sander Vrijders <sander@ouroboros.rocks>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., http://www.fsf.org/about/contact/.
- */
-
-#if defined(__linux__) || defined(__CYGWIN__)
- #define _DEFAULT_SOURCE
-#else
- #define _POSIX_C_SOURCE 200809L
-#endif
-
-#define OUROBOROS_PREFIX "irmd/oap"
-
-#include <ouroboros/crypt.h>
-#include <ouroboros/errno.h>
-#include <ouroboros/logs.h>
-
-#include "config.h"
-
-#include <assert.h>
-#include <string.h>
-#include <sys/stat.h>
-
-/*
- * Shared credential and configuration loading helpers
- */
-
-#ifndef OAP_TEST_MODE
-
-static bool file_exists(const char * path)
-{
- struct stat s;
-
- if (stat(path, &s) < 0 && errno == ENOENT) {
- log_dbg("File %s does not exist.", path);
- return false;
- }
-
- return true;
-}
-
-int load_credentials(const char * name,
- const struct name_sec_paths * paths,
- void ** pkp,
- void ** crt)
-{
- assert(paths != NULL);
- assert(pkp != NULL);
- assert(crt != NULL);
-
- *pkp = NULL;
- *crt = NULL;
-
- if (!file_exists(paths->crt) || !file_exists(paths->key)) {
- log_info("No authentication certificates for %s.", name);
- return 0;
- }
-
- if (crypt_load_crt_file(paths->crt, crt) < 0) {
- log_err("Failed to load %s for %s.", paths->crt, name);
- goto fail_crt;
- }
-
- if (crypt_load_privkey_file(paths->key, pkp) < 0) {
- log_err("Failed to load %s for %s.", paths->key, name);
- goto fail_key;
- }
-
- log_info("Loaded authentication certificates for %s.", name);
-
- return 0;
-
- fail_key:
- crypt_free_crt(*crt);
- *crt = NULL;
- fail_crt:
- return -EAUTH;
-}
-
-int load_kex_config(const char * name,
- const char * path,
- struct sec_config * cfg)
-{
- assert(name != NULL);
- assert(cfg != NULL);
-
- memset(cfg, 0, sizeof(*cfg));
-
- /* Load encryption config */
- if (!file_exists(path))
- log_dbg("No encryption %s for %s.", path, name);
-
- if (load_sec_config_file(cfg, path) < 0) {
- log_warn("Failed to load %s for %s.", path, name);
- return -1;
- }
-
- if (!IS_KEX_ALGO_SET(cfg)) {
- log_info("Key exchange not configured for %s.", name);
- return 0;
- }
-
- if (cfg->c.nid == NID_undef || crypt_nid_to_str(cfg->c.nid) == NULL) {
- log_err("Invalid cipher NID %d for %s.", cfg->c.nid, name);
- return -ECRYPT;
- }
-
- log_info("Encryption enabled for %s.", name);
-
- return 0;
-}
-
-#endif /* OAP_TEST_MODE */
diff --git a/src/irmd/oap.h b/src/irmd/oap.h
index d6d8dfe2..86f11e21 100644
--- a/src/irmd/oap.h
+++ b/src/irmd/oap.h
@@ -28,6 +28,8 @@
#include <ouroboros/name.h>
#include <ouroboros/utils.h>
+#include <stdbool.h>
+
/* OAP authentication state (in oap/auth.c) */
int oap_auth_init(void);
@@ -35,31 +37,46 @@ void oap_auth_fini(void);
int oap_auth_add_ca_crt(void * crt);
+int oap_auth_add_chain_crt(void * crt);
+
/*
* Prepare OAP request header for server, returns context
* Passes client data for srv, returns srv data for client
+* rekey forces ephemeral server-encap KEX (no client-encap; preserves FS/PCS)
*/
int oap_cli_prepare(void ** ctx,
const struct name_info * info,
buffer_t * req_buf,
- buffer_t data);
+ buffer_t data,
+ bool rekey);
/*
* Server processes header, creates response header, returns secret key.
* data is in/out: input=srv data to send, output=cli data received.
+ * rekey drops the cert and verifies against cached_crt; peer_crt (or NULL)
+ * receives a copy of the peer cert to cache at the initial handshake.
*/
int oap_srv_process(const struct name_info * info,
buffer_t req_buf,
buffer_t * rsp_buf,
buffer_t * data,
- struct crypt_sk * sk);
+ struct crypt_sk * sk,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
-/* Complete OAP, returns secret key and server data, frees ctx */
+/*
+ * Complete OAP, returns secret key and server data, frees ctx.
+ * cached_crt verifies a cert-less re-key; peer_crt (or NULL) receives a
+ * copy of the peer cert to cache at the initial handshake.
+ */
int oap_cli_complete(void * ctx,
const struct name_info * info,
buffer_t rsp_buf,
buffer_t * data,
- struct crypt_sk * sk);
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt);
/* Free OAP state (on failure before complete) */
void oap_ctx_free(void * ctx);
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c
index a11ab158..f70f9df1 100644
--- a/src/irmd/oap/auth.c
+++ b/src/irmd/oap/auth.c
@@ -29,8 +29,8 @@
#define OUROBOROS_PREFIX "irmd/oap"
#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
#include <ouroboros/errno.h>
-#include <ouroboros/list.h>
#include <ouroboros/logs.h>
#include <ouroboros/pthread.h>
#include <ouroboros/time.h>
@@ -44,38 +44,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 +144,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);
@@ -106,18 +164,214 @@ int oap_auth_add_ca_crt(void * crt)
return auth_add_crt_to_store(oap_auth.ca_ctx, crt);
}
+int oap_auth_add_chain_crt(void * crt)
+{
+ return auth_add_crt_to_chain(oap_auth.ca_ctx, crt);
+}
+
+/* HKDF info = LABEL (incl. NUL separator) || request-hash [|| response-hash] */
+#define OAP_BIND_LABEL "o7s-oap-bind"
+#define OAP_KC_LABEL "o7s-oap-kc"
+#define OAP_HS_LABEL "o7s-oap-hs"
+
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t cat = BUF_INIT;
+ uint8_t * p;
+ ssize_t len;
+
+ assert(out != NULL);
+ assert(out->data != NULL);
+
+ cat.len = kex.len + data.len + crt.len;
+ if (cat.len == 0)
+ return -EINVAL;
+
+ cat.data = malloc(cat.len);
+ if (cat.data == NULL)
+ return -ENOMEM;
+
+ p = cat.data;
+ if (kex.len > 0) {
+ memcpy(p, kex.data, kex.len);
+ p += kex.len;
+ }
+
+ if (data.len > 0) {
+ memcpy(p, data.data, data.len);
+ p += data.len;
+ }
+
+ if (crt.len > 0)
+ memcpy(p, crt.data, crt.len);
+
+ len = md_digest(md_nid, cat, out->data);
+
+ freebuf(cat);
+
+ if (len < 0)
+ return -ECRYPT;
+
+ out->len = (size_t) len;
+
+ return 0;
+}
+
+/* HKDF-expand sk->key with info into out; -ECRYPT on failure. */
+static int oap_hkdf_expand(const struct crypt_sk * sk,
+ buffer_t info,
+ uint8_t * out,
+ size_t outlen)
+{
+ buffer_t prk;
+ buffer_t okm;
+
+ prk.len = SYMMKEYSZ;
+ prk.data = sk->key;
+ okm.len = outlen;
+ okm.data = out;
+
+ if (crypt_hkdf_expand(prk, info, okm) < 0)
+ return -ECRYPT;
+
+ return 0;
+}
+
+/* info = label || H(req) */
+#define OAP_HS_INFO_SZ (sizeof(OAP_HS_LABEL) + MAX_HASH_SIZE)
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out)
+{
+ uint8_t info_buf[OAP_HS_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_HS_LABEL);
+ memcpy(info_buf, OAP_HS_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, SYMMKEYSZ);
+}
+
+/* info = label || H(req) || H(resp) || cipher_nid || kdf_nid */
+#define OAP_BIND_INFO_SZ \
+ (sizeof(OAP_BIND_LABEL) + 2 * MAX_HASH_SIZE + 2 * sizeof(uint16_t))
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid)
+{
+ uint8_t info_buf[OAP_BIND_INFO_SZ];
+ uint8_t tmp[SYMMKEYSZ];
+ uint16_t suite[2];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_BIND_LABEL);
+ memcpy(info_buf, OAP_BIND_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ suite[0] = hton16((uint16_t) sk->nid);
+ suite[1] = hton16((uint16_t) kdf_nid);
+ memcpy(info_buf + len, suite, sizeof(suite));
+ len += sizeof(suite);
+
+ info.len = len;
+ info.data = info_buf;
+
+ if (oap_hkdf_expand(sk, info, tmp, SYMMKEYSZ) < 0)
+ return -ECRYPT;
+
+ memcpy(sk->key, tmp, SYMMKEYSZ);
+ crypt_secure_clear(tmp, SYMMKEYSZ);
+
+ return 0;
+}
+
+/* info = label || H(req) || H(resp) */
+#define OAP_KC_INFO_SZ (sizeof(OAP_KC_LABEL) + 2 * MAX_HASH_SIZE)
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen)
+{
+ uint8_t info_buf[OAP_KC_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (outlen > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_KC_LABEL);
+ memcpy(info_buf, OAP_KC_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, outlen);
+}
+
#define TIMESYNC_SLACK 100 /* ms */
#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);
@@ -131,63 +385,72 @@ 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);
- fail_stamp:
- return -EAUTH;
+
+ return ret;
}
-int oap_auth_peer(char * name,
- const struct oap_hdr * local_hdr,
- const struct oap_hdr * peer_hdr)
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt)
{
void * crt;
- void * pk;
- buffer_t sign; /* Signed region */
+ void * pk = NULL;
+ void * pin = NULL;
+ buffer_t crt_der; /* cert source: wire, else cached (re-key) */
+ buffer_t sign; /* Signed region */
uint8_t * id = peer_hdr->id.data;
+ int ret;
assert(name != NULL);
+ assert(cfg != NULL);
assert(local_hdr != NULL);
assert(peer_hdr != NULL);
@@ -196,13 +459,22 @@ int oap_auth_peer(char * name,
goto fail_check;
}
- if (peer_hdr->crt.len == 0) {
+ /* Re-key drops the wire cert; fall back to the cached peer cert. */
+ crt_der = peer_hdr->crt;
+ if (crt_der.len == 0 && cached_crt != NULL)
+ crt_der = *cached_crt;
+
+ if (crt_der.len == 0) {
+ if (cfg->a.req) {
+ log_err_id(id, "Peer did not provide a certificate.");
+ goto fail_check;
+ }
log_dbg_id(id, "No crt provided.");
name[0] = '\0';
return 0;
}
- if (crypt_load_crt_der(peer_hdr->crt, &crt) < 0) {
+ if (crypt_load_crt_der(crt_der, &crt) < 0) {
log_err_id(id, "Failed to load crt.");
goto fail_check;
}
@@ -216,26 +488,58 @@ int oap_auth_peer(char * name,
log_dbg_id(id, "Got public key from crt.");
- if (auth_verify_crt(oap_auth.ca_ctx, crt) < 0) {
+ if (cfg->a.cacert[0] != '\0') {
+ if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
+ log_err_id(id, "Failed to load pinned CA %s.",
+ cfg->a.cacert);
+ goto fail_crt;
+ }
+ }
+
+ ret = auth_verify_crt_pin(oap_auth.ca_ctx, crt, pin);
+ if (ret == -ENOENT) {
+ log_err_id(id, "Peer crt not issued by pinned CA %s.",
+ cfg->a.cacert);
+ goto fail_pin;
+ }
+
+ if (ret < 0) {
log_err_id(id, "Failed to verify peer with CA store.");
- goto fail_crt;
+ goto fail_pin;
}
log_dbg_id(id, "Successfully verified peer crt.");
- sign = peer_hdr->hdr;
+ /* Digest pin: peer must sign with the configured digest */
+ if (crypt_pk_requires_md(pk) &&
+ cfg->d.nid != NID_undef && peer_hdr->md_nid != cfg->d.nid) {
+ log_err_id(id, "Peer did not sign with %s.",
+ md_nid_to_str(cfg->d.nid));
+ goto fail_pin;
+ }
+
+ /* Sealed responses verify over the reconstructed plaintext. */
+ sign = peer_hdr->sealed_pt.data != NULL ?
+ peer_hdr->sealed_pt : peer_hdr->hdr;
sign.len -= peer_hdr->sig.len;
if (auth_verify_sig(pk, peer_hdr->md_nid, sign, peer_hdr->sig) < 0) {
log_err_id(id, "Failed to verify signature.");
- goto fail_check_sig;
+ goto fail_pin;
}
- if (crypt_get_crt_name(crt, name) < 0) {
- log_warn_id(id, "Failed to extract name from certificate.");
- name[0] = '\0';
+ ret = crypt_get_crt_name(crt, name);
+ if (ret < 0) {
+ if (ret == -ENAME)
+ log_err_id(id, "Certificate CN too long.");
+ else
+ log_err_id(id, "No name in certificate.");
+ goto fail_pin;
}
+ if (pin != NULL)
+ crypt_free_crt(pin);
+
crypt_free_key(pk);
crypt_free_crt(crt);
@@ -243,9 +547,11 @@ int oap_auth_peer(char * name,
return 0;
- fail_check_sig:
- crypt_free_key(pk);
+ fail_pin:
+ if (pin != NULL)
+ crypt_free_crt(pin);
fail_crt:
+ crypt_free_key(pk);
crypt_free_crt(crt);
fail_check:
return -EAUTH;
diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h
index 4f748750..72938b53 100644
--- a/src/irmd/oap/auth.h
+++ b/src/irmd/oap/auth.h
@@ -23,13 +23,46 @@
#ifndef OUROBOROS_IRMD_OAP_AUTH_H
#define OUROBOROS_IRMD_OAP_AUTH_H
+#include <ouroboros/crypt.h>
+
#include "hdr.h"
int oap_check_hdr(const struct oap_hdr * hdr);
-/* name is updated with the peer's certificate name if available */
-int oap_auth_peer(char * name,
- const struct oap_hdr * local_hdr,
- const struct oap_hdr * peer_hdr);
+/*
+ * name is set to the peer crt CN, "" if no crt was presented.
+ * cached_crt (or NULL) is the peer cert from the initial handshake, used
+ * to verify a cert-less re-key.
+ */
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt);
+
+/* Derive the handshake key that seals the response identity block. */
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out);
+
+/* resp_hash = H(kex || data || crt): binds the server response transcript. */
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out);
+
+/* Fold request + response transcript + negotiated suite into the key. */
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid);
+
+/* Server->client key-confirmation tag derived from the bound key. */
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen);
#endif /* OUROBOROS_IRMD_OAP_AUTH_H */
diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c
index 8ecd317d..3518b4d1 100644
--- a/src/irmd/oap/cli.c
+++ b/src/irmd/oap/cli.c
@@ -50,11 +50,11 @@
struct oap_cli_ctx {
uint8_t __id[OAP_ID_SIZE];
buffer_t id;
- uint8_t kex_buf[MSGBUFSZ];
+ uint8_t kex_buf[CRYPT_KEY_BUFSZ];
uint8_t req_hash[MAX_HASH_SIZE];
size_t req_hash_len;
int req_md_nid;
- struct sec_config kcfg;
+ struct sec_config scfg;
struct oap_hdr local_hdr;
void * pkp; /* Ephemeral keypair */
uint8_t * key; /* For client-encap KEM */
@@ -69,7 +69,7 @@ struct oap_cli_ctx {
extern int load_cli_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-extern int load_cli_kex_config(const struct name_info * info,
+extern int load_cli_sec_config(const struct name_info * info,
struct sec_config * cfg);
extern int load_server_kem_pk(const char * name,
struct sec_config * cfg,
@@ -87,13 +87,18 @@ int load_cli_credentials(const struct name_info * info,
return load_credentials(info->name, &info->c, pkp, crt);
}
-int load_cli_kex_config(const struct name_info * info,
+int load_cli_sec_config(const struct name_info * info,
struct sec_config * cfg)
{
assert(info != NULL);
assert(cfg != NULL);
- return load_kex_config(info->name, info->c.enc, cfg);
+ memset(cfg, 0, sizeof(*cfg));
+
+ /* A client authenticates the server by default, like an https client */
+ cfg->a.req = OAP_CLIENT_AUTH_DEFAULT;
+
+ return load_sec_config(info->name, info->c.sec, cfg);
}
int load_server_kem_pk(const char * name,
@@ -133,13 +138,13 @@ int load_server_kem_pk(const char * name,
static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
buffer_t * kex = &s->local_hdr.kex;
uint8_t * id = s->id.data;
ssize_t len;
/* Generate ephemeral keypair, send PK */
- len = kex_pkp_create(kcfg, &s->pkp, kex->data);
+ len = kex_pkp_create(scfg, &s->pkp, kex->data);
if (len < 0) {
log_err_id(id, "Failed to generate DHE keypair.");
return -ECRYPT;
@@ -147,7 +152,7 @@ static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s)
kex->len = (size_t) len;
log_dbg_id(id, "Generated ephemeral %s keys (%zd bytes).",
- kcfg->x.str, len);
+ scfg->x.str, len);
return 0;
}
@@ -155,24 +160,24 @@ static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s)
static int do_client_kex_prepare_kem_encap(const char * server_name,
struct oap_cli_ctx * s)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
buffer_t * kex = &s->local_hdr.kex;
uint8_t * id = s->id.data;
buffer_t server_pk = BUF_INIT;
uint8_t key_buf[SYMMKEYSZ];
ssize_t len;
- if (load_server_kem_pk(server_name, kcfg, &server_pk) < 0) {
+ if (load_server_kem_pk(server_name, scfg, &server_pk) < 0) {
log_err_id(id, "Failed to load server KEM pk.");
return -ECRYPT;
}
- if (IS_HYBRID_KEM(kcfg->x.str))
+ if (IS_HYBRID_KEM(scfg->x.str))
len = kex_kem_encap_raw(server_pk, kex->data,
- kcfg->k.nid, key_buf);
+ scfg->k.nid, key_buf);
else
len = kex_kem_encap(server_pk, kex->data,
- kcfg->k.nid, key_buf);
+ scfg->k.nid, key_buf);
freebuf(server_pk);
@@ -198,13 +203,13 @@ static int do_client_kex_prepare_kem_encap(const char * server_name,
static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
buffer_t * kex = &s->local_hdr.kex;
uint8_t * id = s->id.data;
ssize_t len;
/* Server encaps: generate keypair, send PK */
- len = kex_pkp_create(kcfg, &s->pkp, kex->data);
+ len = kex_pkp_create(scfg, &s->pkp, kex->data);
if (len < 0) {
log_err_id(id, "Failed to generate KEM keypair.");
return -ECRYPT;
@@ -219,13 +224,13 @@ static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s)
static int do_client_kex_prepare(const char * server_name,
struct oap_cli_ctx * s)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
- if (!IS_KEX_ALGO_SET(kcfg))
+ if (!IS_KEX_ALGO_SET(scfg))
return 0;
- if (IS_KEM_ALGORITHM(kcfg->x.str)) {
- if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP)
+ if (IS_KEM_ALGORITHM(scfg->x.str)) {
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP)
return do_client_kex_prepare_kem_encap(server_name, s);
else
return do_client_kex_prepare_kem_decap(s);
@@ -237,12 +242,15 @@ static int do_client_kex_prepare(const char * server_name,
int oap_cli_prepare(void ** ctx,
const struct name_info * info,
buffer_t * req_buf,
- buffer_t data)
+ buffer_t data,
+ bool rekey)
{
struct oap_cli_ctx * s;
void * pkp = NULL;
void * crt = NULL;
+ buffer_t no_tag = BUF_INIT;
ssize_t ret;
+ int enc_flags = 0;
assert(ctx != NULL);
assert(info != NULL);
@@ -276,22 +284,37 @@ int oap_cli_prepare(void ** ctx,
goto fail_id;
}
- /* Load KEX config */
- if (load_cli_kex_config(info, &s->kcfg) < 0) {
- log_err_id(s->id.data, "Failed to load KEX config for %s.",
+ /* Load security config */
+ if (load_cli_sec_config(info, &s->scfg) < 0) {
+ log_err_id(s->id.data, "Failed to load security config for %s.",
info->name);
goto fail_kex;
}
- oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->kcfg.c.nid);
+ /* Re-key forces server-encap: client-encap forfeits FS/PCS. */
+ if (rekey && s->scfg.x.mode == KEM_MODE_CLIENT_ENCAP) {
+ s->scfg.x.mode = KEM_MODE_SERVER_ENCAP;
+ log_dbg_id(s->id.data, "Re-key forcing ephemeral server KEX.");
+ }
+
+ /* Re-key omits the cert; the server verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
+ if (rekey)
+ enc_flags = OAP_ENC_REKEY;
+
+ oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->scfg.c.nid);
if (do_client_kex_prepare(info->name, s) < 0) {
log_err_id(s->id.data, "Failed to prepare client KEX.");
goto fail_kex;
}
- if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->kcfg,
- (buffer_t) BUF_INIT, NID_undef)) {
+ if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->scfg,
+ no_tag, NID_undef, NULL, enc_flags)) {
log_err_id(s->id.data, "Failed to create OAP request header.");
goto fail_hdr;
}
@@ -299,7 +322,7 @@ int oap_cli_prepare(void ** ctx,
debug_oap_hdr_snd(&s->local_hdr);
/* Compute and store hash of request for verification in complete */
- s->req_md_nid = s->kcfg.d.nid != NID_undef ? s->kcfg.d.nid : NID_sha384;
+ s->req_md_nid = s->scfg.d.nid != NID_undef ? s->scfg.d.nid : NID_sha384;
ret = md_digest(s->req_md_nid, s->local_hdr.hdr, s->req_hash);
if (ret < 0) {
log_err_id(s->id.data, "Failed to hash request.");
@@ -324,6 +347,7 @@ int oap_cli_prepare(void ** ctx,
return 0;
fail_hash:
+ oap_hdr_fini(&s->local_hdr);
fail_hdr:
crypt_secure_free(s->key, SYMMKEYSZ);
crypt_free_key(s->pkp);
@@ -358,11 +382,11 @@ static int do_client_kex_complete_kem(struct oap_cli_ctx * s,
const struct oap_hdr * peer_hdr,
struct crypt_sk * sk)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
uint8_t * id = s->id.data;
uint8_t key_buf[SYMMKEYSZ];
- if (kcfg->x.mode == KEM_MODE_SERVER_ENCAP) {
+ if (scfg->x.mode == KEM_MODE_SERVER_ENCAP) {
buffer_t ct;
if (peer_hdr->kex.len == 0) {
@@ -373,27 +397,27 @@ static int do_client_kex_complete_kem(struct oap_cli_ctx * s,
ct.data = peer_hdr->kex.data;
ct.len = peer_hdr->kex.len;
- if (kex_kem_decap(s->pkp, ct, kcfg->k.nid, key_buf) < 0) {
+ if (kex_kem_decap(s->pkp, ct, scfg->k.nid, key_buf) < 0) {
log_err_id(id, "Failed to decapsulate KEM.");
return -ECRYPT;
}
log_dbg_id(id, "Client decapsulated server CT.");
- } else if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
+ } else if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
/* Key already derived during prepare */
memcpy(sk->key, s->key, SYMMKEYSZ);
- sk->nid = kcfg->c.nid;
- log_info_id(id, "Negotiated %s + %s.", kcfg->x.str,
- kcfg->c.str);
+ sk->nid = scfg->c.nid;
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str,
+ scfg->c.str);
return 0;
}
memcpy(sk->key, key_buf, SYMMKEYSZ);
- sk->nid = kcfg->c.nid;
+ sk->nid = scfg->c.nid;
crypt_secure_clear(key_buf, SYMMKEYSZ);
- log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str);
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
return 0;
}
@@ -402,7 +426,7 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s,
const struct oap_hdr * peer_hdr,
struct crypt_sk * sk)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
uint8_t * id = s->id.data;
uint8_t key_buf[SYMMKEYSZ];
@@ -412,7 +436,7 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s,
return -ECRYPT;
}
- if (kex_dhe_derive(kcfg, s->pkp, peer_hdr->kex, key_buf) < 0) {
+ if (kex_dhe_derive(scfg, s->pkp, peer_hdr->kex, key_buf) < 0) {
log_err_id(id, "Failed to derive DHE secret.");
return -ECRYPT;
}
@@ -420,10 +444,10 @@ static int do_client_kex_complete_dhe(struct oap_cli_ctx * s,
log_dbg_id(id, "DHE: derived shared secret.");
memcpy(sk->key, key_buf, SYMMKEYSZ);
- sk->nid = kcfg->c.nid;
+ sk->nid = scfg->c.nid;
crypt_secure_clear(key_buf, SYMMKEYSZ);
- log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str);
+ log_info_id(id, "Negotiated %s + %s.", scfg->x.str, scfg->c.str);
return 0;
}
@@ -433,17 +457,17 @@ static int do_client_kex_complete(struct oap_cli_ctx * s,
const struct oap_hdr * peer_hdr,
struct crypt_sk * sk)
{
- struct sec_config * kcfg = &s->kcfg;
+ struct sec_config * scfg = &s->scfg;
uint8_t * id = s->id.data;
int cipher_nid;
int kdf_nid;
- if (!IS_KEX_ALGO_SET(kcfg))
+ if (!IS_KEX_ALGO_SET(scfg))
return 0;
/* Save client's configured minimums */
- cipher_nid = kcfg->c.nid;
- kdf_nid = kcfg->k.nid;
+ cipher_nid = scfg->c.nid;
+ kdf_nid = scfg->k.nid;
/* Accept server's cipher choice */
if (peer_hdr->cipher_str == NULL) {
@@ -451,15 +475,15 @@ static int do_client_kex_complete(struct oap_cli_ctx * s,
return -ECRYPT;
}
- SET_KEX_CIPHER(kcfg, peer_hdr->cipher_str);
- if (crypt_validate_nid(kcfg->c.nid) < 0) {
+ SET_KEX_CIPHER(scfg, peer_hdr->cipher_str);
+ if (crypt_validate_nid(scfg->c.nid) < 0) {
log_err_id(id, "Server cipher '%s' not supported.",
peer_hdr->cipher_str);
return -ENOTSUP;
}
/* Verify server cipher >= client's minimum */
- if (crypt_cipher_rank(kcfg->c.nid) < crypt_cipher_rank(cipher_nid)) {
+ if (crypt_cipher_rank(scfg->c.nid) < crypt_cipher_rank(cipher_nid)) {
log_err_id(id, "Server cipher %s too weak.",
peer_hdr->cipher_str);
return -ECRYPT;
@@ -469,20 +493,20 @@ static int do_client_kex_complete(struct oap_cli_ctx * s,
peer_hdr->cipher_str);
/* Accept server's KDF for non-client-encap modes */
- if (kcfg->x.mode != KEM_MODE_CLIENT_ENCAP
+ if (scfg->x.mode != KEM_MODE_CLIENT_ENCAP
&& peer_hdr->kdf_nid != NID_undef) {
if (crypt_kdf_rank(peer_hdr->kdf_nid)
< crypt_kdf_rank(kdf_nid)) {
log_err_id(id, "Server KDF too weak.");
return -ECRYPT;
}
- SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid);
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
log_dbg_id(id, "Accepted server KDF %s.",
- md_nid_to_str(kcfg->k.nid));
+ md_nid_to_str(scfg->k.nid));
}
/* Derive shared secret */
- if (IS_KEM_ALGORITHM(kcfg->x.str))
+ if (IS_KEM_ALGORITHM(scfg->x.str))
return do_client_kex_complete_kem(s, peer_hdr, sk);
return do_client_kex_complete_dhe(s, peer_hdr, sk);
@@ -492,12 +516,20 @@ int oap_cli_complete(void * ctx,
const struct name_info * info,
buffer_t rsp_buf,
buffer_t * data,
- struct crypt_sk * sk)
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
{
struct oap_cli_ctx * s = ctx;
struct oap_hdr peer_hdr;
char peer[NAME_SIZE + 1];
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
uint8_t * id;
+ int rc;
assert(ctx != NULL);
assert(info != NULL);
@@ -515,7 +547,7 @@ int oap_cli_complete(void * ctx,
log_dbg_id(id, "Completing OAP for %s.", info->name);
/* Decode response header using client's md_nid for hash length */
- if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid) < 0) {
+ if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid, false) < 0) {
log_err_id(id, "Failed to decode OAP response header.");
goto fail_oap;
}
@@ -528,20 +560,52 @@ int oap_cli_complete(void * ctx,
goto fail_oap;
}
- /* Authenticate server */
- if (oap_auth_peer(peer, &s->local_hdr, &peer_hdr) < 0) {
- log_err_id(id, "Failed to authenticate server.");
+ /* Complete key exchange first; the sealed identity needs the secret */
+ if (do_client_kex_complete(s, &peer_hdr, sk) < 0) {
+ log_err_id(id, "Failed to complete key exchange.");
goto fail_oap;
}
- /* Verify request hash in authenticated response */
- if (peer_hdr.req_hash.len == 0) {
- log_err_id(id, "Response missing req_hash.");
+ req_hash.data = s->req_hash;
+ req_hash.len = s->req_hash_len;
+
+ /* Decrypt the sealed server identity (data+cert+sig) before auth */
+ if (sk->nid != NID_undef && peer_hdr.sealed.data != NULL) {
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_oap;
+ }
+
+ rc = oap_hdr_unseal(&peer_hdr, hs_key);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (rc < 0) {
+ log_err_id(id, "Failed to unseal server identity.");
+ goto fail_oap;
+ }
+ }
+
+ /* Authenticate server (cert + signature now in cleartext) */
+ if (oap_auth_peer(peer, &s->scfg, &s->local_hdr, &peer_hdr,
+ cached_crt) < 0) {
+ log_err_id(id, "Failed to authenticate server.");
goto fail_oap;
}
- if (memcmp(peer_hdr.req_hash.data, s->req_hash, s->req_hash_len) != 0) {
- log_err_id(id, "Response req_hash mismatch.");
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_oap;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
+ /* Response must carry a transcript tag of the expected length */
+ if (peer_hdr.rsp_tag.len != s->req_hash_len) {
+ log_err_id(id, "Response transcript tag mismatch.");
goto fail_oap;
}
@@ -552,10 +616,43 @@ int oap_cli_complete(void * ctx,
goto fail_oap;
}
- /* Complete key exchange */
- if (do_client_kex_complete(s, &peer_hdr, sk) < 0) {
- log_err_id(id, "Failed to complete key exchange.");
- goto fail_oap;
+ if (sk->nid != NID_undef) {
+ /* Encrypted: bind the key and verify key confirmation */
+ resp_hash.data = resp_hash_buf;
+
+ if (oap_resp_hash(s->req_md_nid, peer_hdr.kex,
+ peer_hdr.data, peer_hdr.crt,
+ &resp_hash) < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_oap;
+ }
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ s->scfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_oap;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ s->req_hash_len) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_oap;
+ }
+
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, kc_buf,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Key confirmation mismatch.");
+ goto fail_oap;
+ }
+ } else {
+ /* Cleartext path is config-driven, never a wire downgrade */
+ assert(!IS_KEX_ALGO_SET(&s->scfg));
+ /* Unencrypted: verify request-echo integrity */
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, s->req_hash,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Response tag mismatch.");
+ goto fail_oap;
+ }
}
/* Copy piggybacked data from server response */
@@ -566,11 +663,14 @@ int oap_cli_complete(void * ctx,
log_info_id(id, "OAP completed for %s.", info->name);
+ freebuf(peer_hdr.sealed_pt);
+
oap_ctx_free(s);
return 0;
fail_oap:
+ freebuf(peer_hdr.sealed_pt);
oap_ctx_free(s);
return -ECRYPT;
}
diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c
index 5465dd2a..6f355133 100644
--- a/src/irmd/oap/hdr.c
+++ b/src/irmd/oap/hdr.c
@@ -30,6 +30,7 @@
#include <ouroboros/crypt.h>
#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
#include <ouroboros/hash.h>
#include <ouroboros/logs.h>
#include <ouroboros/rib.h>
@@ -45,9 +46,17 @@
#include <string.h>
#include <time.h>
+#define OAP_SEAL_TAGSZ 16 /* AEAD tag on the sealed identity block */
+/* Sealed length prefix: data_len ‖ crt_len. */
+#define OAP_SEAL_LENSZ (sizeof(uint16_t) + sizeof(uint16_t))
+
+/* hs_key is single-use per handshake, so a fixed nonce is reuse-safe. */
+static const uint8_t oap_seal_nonce[12];
+
int oap_hdr_decode(struct oap_hdr * oap_hdr,
buffer_t hdr,
- int req_md_nid)
+ int req_md_nid,
+ bool rekey)
{
off_t offset;
uint16_t kex_len;
@@ -88,11 +97,13 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
oap_hdr->md_str = md_nid_to_str(oap_hdr->md_nid);
offset += sizeof(uint16_t);
- /* Validate NIDs: NID_undef is valid at parse time, else must be known.
+ /*
+ * Validate NIDs: NID_undef is valid at parse time, else must be known.
* Note: md_nid=NID_undef only valid for PQC; enforced at sign/verify.
*/
if (ciph_nid != NID_undef && crypt_validate_nid(ciph_nid) < 0)
goto fail_decode;
+
if (oap_hdr->kdf_nid != NID_undef &&
md_validate_nid(oap_hdr->kdf_nid) < 0)
goto fail_decode;
@@ -115,10 +126,37 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
offset += sizeof(uint16_t);
- /* Response includes req_hash when md_nid is set */
+ assert((size_t) offset == OAP_HDR_MIN_SIZE);
+
+ /* Response includes rsp_tag when md_nid is set */
hash_len = (req_md_nid != NID_undef) ?
(size_t) md_len(req_md_nid) : 0;
+ /* Encrypted response: sealed block is data_len‖crt_len‖data‖crt‖sig. */
+ if (req_md_nid != NID_undef && ciph_nid != NID_undef) {
+ if (hdr.len < (size_t) offset + oap_hdr->kex.len + hash_len +
+ OAP_SEAL_TAGSZ + OAP_SEAL_LENSZ)
+ goto fail_decode;
+
+ oap_hdr->kex.data = hdr.data + offset;
+ offset += oap_hdr->kex.len;
+
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
+ offset += hash_len;
+
+ oap_hdr->sealed.data = hdr.data + offset;
+ oap_hdr->sealed.len = hdr.len - offset;
+
+ /* crt/data/sig lengths are sealed; set by oap_hdr_unseal. */
+ oap_hdr->crt.len = crt_len;
+ oap_hdr->data.len = data_len;
+
+ oap_hdr->hdr = hdr;
+
+ return 0;
+ }
+
/* Validate total length */
if (hdr.len < (size_t) offset + crt_len + oap_hdr->kex.len +
data_len + hash_len)
@@ -128,8 +166,12 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
sig_len = hdr.len - offset - crt_len - oap_hdr->kex.len -
data_len - hash_len;
- /* Unsigned packets must not have trailing bytes */
- if (crt_len == 0 && sig_len != 0)
+ /*
+ * Unsigned packets must not have trailing bytes. A re-key request
+ * is signed but cert-less (verified against the cached peer cert),
+ * so the rekey caller permits crt_len==0 with a signature.
+ */
+ if (crt_len == 0 && sig_len != 0 && !rekey)
goto fail_decode;
/* Parse variable fields */
@@ -144,8 +186,8 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
oap_hdr->data.len = data_len;
offset += data_len;
- oap_hdr->req_hash.data = hdr.data + offset;
- oap_hdr->req_hash.len = hash_len;
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
offset += hash_len;
oap_hdr->sig.data = hdr.data + offset;
@@ -164,6 +206,7 @@ void oap_hdr_fini(struct oap_hdr * oap_hdr)
{
assert(oap_hdr != NULL);
+ freebuf(oap_hdr->sealed_pt);
freebuf(oap_hdr->hdr);
memset(oap_hdr, 0, sizeof(*oap_hdr));
}
@@ -207,12 +250,232 @@ void oap_hdr_init(struct oap_hdr * hdr,
hdr->nid = nid;
}
+/* Write the 36-byte fixed header; stamp is already in network order. */
+static void write_oap_fixed(uint8_t * buf,
+ const struct oap_hdr * hdr,
+ const struct sec_config * scfg,
+ size_t crt_len,
+ size_t data_len,
+ uint64_t stamp)
+{
+ uint16_t v;
+ uint16_t kex_len;
+ off_t offset = 0;
+
+ memcpy(buf + offset, hdr->id.data, hdr->id.len);
+ offset += hdr->id.len;
+
+ memcpy(buf + offset, &stamp, sizeof(stamp));
+ offset += sizeof(stamp);
+
+ v = hton16(hdr->nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(scfg->k.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(scfg->d.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16((uint16_t) crt_len);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ kex_len = (uint16_t) hdr->kex.len;
+ if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(scfg->x.str)) {
+ if (IS_HYBRID_KEM(scfg->x.str))
+ kex_len |= OAP_KEX_FMT_BIT;
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP)
+ kex_len |= OAP_KEX_ROLE_BIT;
+ }
+
+ kex_len = hton16(kex_len);
+ memcpy(buf + offset, &kex_len, sizeof(kex_len));
+ offset += sizeof(kex_len);
+
+ v = hton16((uint16_t) data_len);
+ memcpy(buf + offset, &v, sizeof(v));
+}
+
+/*
+ * Pack lens ‖ data ‖ crt, sign prefix ‖ body, append the signature, then
+ * AEAD-seal lens ‖ data ‖ crt ‖ sig under prefix as AAD. The cert, app data
+ * and their sizes stay confidential; *out is the opaque sealed block. The
+ * signature rides inside the seal so it can't deanonymise the server.
+ */
+static int oap_seal_body(int nid,
+ const uint8_t * seal_key,
+ void * pkp,
+ int md_nid,
+ buffer_t prefix,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t sig = BUF_INIT;
+ buffer_t sign;
+ buffer_t aad;
+ buffer_t plain;
+ uint8_t * buf;
+ uint8_t * tmp;
+ uint16_t datalen;
+ uint16_t crtlen;
+ size_t body_len;
+ off_t offset;
+
+ datalen = hton16((uint16_t) data.len);
+ crtlen = hton16((uint16_t) crt.len);
+
+ body_len = OAP_SEAL_LENSZ + data.len + crt.len;
+
+ buf = malloc(prefix.len + body_len);
+ if (buf == NULL)
+ return -1;
+
+ memcpy(buf, prefix.data, prefix.len);
+ offset = (off_t) prefix.len;
+
+ memcpy(buf + offset, &datalen, sizeof(datalen));
+ offset += sizeof(datalen);
+
+ memcpy(buf + offset, &crtlen, sizeof(crtlen));
+ offset += sizeof(crtlen);
+
+ if (data.len != 0)
+ memcpy(buf + offset, data.data, data.len);
+
+ offset += data.len;
+
+ if (crt.len != 0)
+ memcpy(buf + offset, crt.data, crt.len);
+
+ /* Sign prefix ‖ lens ‖ data ‖ crt (plaintext, before sealing). */
+ sign.data = buf;
+ sign.len = prefix.len + body_len;
+
+ if (pkp != NULL && auth_sign(pkp, md_nid, sign, &sig) < 0)
+ goto fail_buf;
+
+ /* Append the signature so the seal covers lens ‖ data ‖ crt ‖ sig. */
+ if (sig.len != 0) {
+ tmp = realloc(buf, prefix.len + body_len + sig.len);
+ if (tmp == NULL)
+ goto fail_sig;
+
+ buf = tmp;
+ memcpy(buf + prefix.len + body_len, sig.data, sig.len);
+ }
+
+ aad.data = buf;
+ aad.len = prefix.len;
+ plain.data = buf + prefix.len;
+ plain.len = body_len + sig.len;
+
+ if (crypt_oneshot_seal(nid, seal_key, oap_seal_nonce,
+ aad, plain, out) < 0)
+ goto fail_sig;
+
+ free(buf);
+ freebuf(sig);
+
+ return 0;
+
+ fail_sig:
+ freebuf(sig);
+ fail_buf:
+ free(buf);
+ return -1;
+}
+
+/* Encode an identity-hidden response: wire = prefix ‖ oap_seal_body(...). */
+static int oap_hdr_encode_sealed(struct oap_hdr * hdr,
+ void * pkp,
+ void * crt,
+ struct sec_config * scfg,
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key,
+ int flags)
+{
+ struct timespec now;
+ uint64_t stamp;
+ buffer_t der = BUF_INIT;
+ buffer_t sealed = BUF_INIT;
+ buffer_t prefix;
+ off_t offset;
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = hton64(TS_TO_UINT64(now));
+
+ if (crt != NULL && crypt_crt_der(crt, &der) < 0)
+ goto fail_der;
+
+ prefix.len = OAP_HDR_MIN_SIZE + hdr->kex.len + rsp_tag.len;
+ prefix.data = malloc(prefix.len);
+ if (prefix.data == NULL)
+ goto fail_der;
+
+ /* Cleartext crt_len/data_len are 0; real lengths prefix the seal. */
+ write_oap_fixed(prefix.data, hdr, scfg, 0, 0, stamp);
+ offset = OAP_HDR_MIN_SIZE;
+
+ if (hdr->kex.len != 0)
+ memcpy(prefix.data + offset, hdr->kex.data, hdr->kex.len);
+
+ offset += hdr->kex.len;
+
+ if (rsp_tag.len != 0)
+ memcpy(prefix.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
+
+ assert((size_t) offset == prefix.len);
+
+ if (oap_seal_body(hdr->nid, seal_key, pkp, scfg->d.nid,
+ prefix, hdr->data, der, &sealed) < 0)
+ goto fail_prefix;
+
+ hdr->hdr.len = prefix.len + sealed.len;
+ hdr->hdr.data = malloc(hdr->hdr.len);
+ if (hdr->hdr.data == NULL)
+ goto fail_sealed;
+
+ memcpy(hdr->hdr.data, prefix.data, prefix.len);
+ memcpy(hdr->hdr.data + prefix.len, sealed.data, sealed.len);
+
+ freebuf(sealed);
+ free(prefix.data);
+ freebuf(der);
+
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid,
+ flags & OAP_ENC_REKEY) < 0)
+ goto fail_decode;
+
+ return 0;
+
+ fail_decode:
+ oap_hdr_fini(hdr);
+ return -1;
+ fail_sealed:
+ freebuf(sealed);
+ fail_prefix:
+ free(prefix.data);
+ fail_der:
+ freebuf(der);
+ return -1;
+}
+
int oap_hdr_encode(struct oap_hdr * hdr,
void * pkp,
void * crt,
- struct sec_config * kcfg,
- buffer_t req_hash,
- int req_md_nid)
+ struct sec_config * scfg,
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key,
+ int flags)
{
struct timespec now;
uint64_t stamp;
@@ -220,16 +483,15 @@ int oap_hdr_encode(struct oap_hdr * hdr,
buffer_t der = BUF_INIT;
buffer_t sig = BUF_INIT;
buffer_t sign;
- uint16_t len;
- uint16_t ciph_nid;
- uint16_t kdf_nid;
- uint16_t md_nid;
- uint16_t kex_len;
off_t offset;
assert(hdr != NULL);
assert(hdr->id.data != NULL && hdr->id.len == OAP_ID_SIZE);
- assert(kcfg != NULL);
+ assert(scfg != NULL);
+
+ if (seal_key != NULL)
+ return oap_hdr_encode_sealed(hdr, pkp, crt, scfg, rsp_tag,
+ req_md_nid, seal_key, flags);
clock_gettime(CLOCK_REALTIME, &now);
stamp = hton64(TS_TO_UINT64(now));
@@ -237,86 +499,40 @@ int oap_hdr_encode(struct oap_hdr * hdr,
if (crt != NULL && crypt_crt_der(crt, &der) < 0)
goto fail_der;
- ciph_nid = hton16(hdr->nid);
- kdf_nid = hton16(kcfg->k.nid);
- md_nid = hton16(kcfg->d.nid);
-
- /* Build kex_len with flags */
- kex_len = (uint16_t) hdr->kex.len;
- if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(kcfg->x.str)) {
- if (IS_HYBRID_KEM(kcfg->x.str))
- kex_len |= OAP_KEX_FMT_BIT;
- if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP)
- kex_len |= OAP_KEX_ROLE_BIT;
- }
- kex_len = hton16(kex_len);
-
- /* Fixed header (36 bytes) + variable fields + req_hash (if auth) */
+ /* Fixed header (36 bytes) + variable fields + rsp_tag (rsp only) */
out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len +
- req_hash.len;
+ rsp_tag.len;
out.data = malloc(out.len);
if (out.data == NULL)
goto fail_out;
- offset = 0;
-
- /* id (16 bytes) */
- memcpy(out.data + offset, hdr->id.data, hdr->id.len);
- offset += hdr->id.len;
-
- /* timestamp (8 bytes) */
- memcpy(out.data + offset, &stamp, sizeof(stamp));
- offset += sizeof(stamp);
-
- /* cipher_nid (2 bytes) */
- memcpy(out.data + offset, &ciph_nid, sizeof(ciph_nid));
- offset += sizeof(ciph_nid);
-
- /* kdf_nid (2 bytes) */
- memcpy(out.data + offset, &kdf_nid, sizeof(kdf_nid));
- offset += sizeof(kdf_nid);
-
- /* md_nid (2 bytes) */
- memcpy(out.data + offset, &md_nid, sizeof(md_nid));
- offset += sizeof(md_nid);
-
- /* crt_len (2 bytes) */
- len = hton16((uint16_t) der.len);
- memcpy(out.data + offset, &len, sizeof(len));
- offset += sizeof(len);
-
- /* kex_len + flags (2 bytes) */
- memcpy(out.data + offset, &kex_len, sizeof(kex_len));
- offset += sizeof(kex_len);
-
- /* data_len (2 bytes) */
- len = hton16((uint16_t) hdr->data.len);
- memcpy(out.data + offset, &len, sizeof(len));
- offset += sizeof(len);
-
- /* Fixed header complete (36 bytes) */
- assert((size_t) offset == OAP_HDR_MIN_SIZE);
+ write_oap_fixed(out.data, hdr, scfg, der.len, hdr->data.len, stamp);
+ offset = OAP_HDR_MIN_SIZE;
/* certificate (variable) */
if (der.len != 0)
memcpy(out.data + offset, der.data, der.len);
+
offset += der.len;
/* kex data (variable) */
if (hdr->kex.len != 0)
memcpy(out.data + offset, hdr->kex.data, hdr->kex.len);
+
offset += hdr->kex.len;
/* data (variable) */
if (hdr->data.len != 0)
memcpy(out.data + offset, hdr->data.data, hdr->data.len);
+
offset += hdr->data.len;
- /* req_hash (variable, only for authenticated responses) */
- if (req_hash.len != 0)
- memcpy(out.data + offset, req_hash.data, req_hash.len);
- offset += req_hash.len;
+ /* rsp_tag (variable, response only) */
+ if (rsp_tag.len != 0)
+ memcpy(out.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
assert((size_t) offset == out.len);
@@ -324,7 +540,7 @@ int oap_hdr_encode(struct oap_hdr * hdr,
sign.data = out.data;
sign.len = out.len;
- if (pkp != NULL && auth_sign(pkp, kcfg->d.nid, sign, &sig) < 0)
+ if (pkp != NULL && auth_sign(pkp, scfg->d.nid, sign, &sig) < 0)
goto fail_sig;
hdr->hdr = out;
@@ -337,10 +553,13 @@ int oap_hdr_encode(struct oap_hdr * hdr,
goto fail_realloc;
memcpy(hdr->hdr.data + offset, sig.data, sig.len);
- clrbuf(out);
}
- if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid) < 0)
+ /* Ownership moved to hdr->hdr; drop the alias to avoid double-free. */
+ clrbuf(out);
+
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid,
+ flags & OAP_ENC_REKEY) < 0)
goto fail_decode;
freebuf(der);
@@ -360,28 +579,99 @@ int oap_hdr_encode(struct oap_hdr * hdr,
return -1;
}
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key)
+{
+ buffer_t pt = BUF_INIT;
+ buffer_t prefix;
+ uint8_t * recon;
+ size_t body_len;
+ size_t pt_len;
+ size_t data_len;
+ size_t crt_len;
+
+ assert(hdr != NULL);
+ assert(key != NULL);
+
+ if (hdr->sealed.data == NULL || hdr->sealed.len == 0)
+ return -EINVAL;
+
+ /* AAD prefix is fixed‖kex‖rsp_tag; sealed starts right after. */
+ prefix.data = hdr->hdr.data;
+ prefix.len = (size_t) (hdr->sealed.data - hdr->hdr.data);
+
+ if (crypt_oneshot_open(hdr->nid, key, oap_seal_nonce, prefix,
+ hdr->sealed, &pt) < 0)
+ return -ECRYPT;
+
+ pt_len = pt.len;
+
+ /* Plaintext = data_len ‖ crt_len ‖ data ‖ crt ‖ sig. */
+ if (pt_len < OAP_SEAL_LENSZ)
+ goto fail_auth;
+
+ data_len = (size_t) ntoh16(*(uint16_t *) pt.data);
+ crt_len = (size_t) ntoh16(*(uint16_t *)(pt.data + sizeof(uint16_t)));
+
+ body_len = OAP_SEAL_LENSZ + data_len + crt_len;
+ if (pt_len < body_len)
+ goto fail_auth;
+
+ /* Rebuild prefix ‖ lens ‖ data ‖ crt ‖ sig (whole signed region). */
+ recon = malloc(prefix.len + pt_len);
+ if (recon == NULL)
+ goto fail_mem;
+
+ memcpy(recon, prefix.data, prefix.len);
+ memcpy(recon + prefix.len, pt.data, pt_len);
+
+ freebuf(pt);
+
+ hdr->sealed_pt.data = recon;
+ hdr->sealed_pt.len = prefix.len + pt_len;
+
+ hdr->data.data = recon + prefix.len + OAP_SEAL_LENSZ;
+ hdr->data.len = data_len;
+ hdr->crt.data = recon + prefix.len + OAP_SEAL_LENSZ + data_len;
+ hdr->crt.len = crt_len;
+ hdr->sig.data = recon + prefix.len + body_len;
+ hdr->sig.len = pt_len - body_len;
+
+ return 0;
+
+ fail_mem:
+ freebuf(pt);
+ return -ENOMEM;
+ fail_auth:
+ freebuf(pt);
+ return -EAUTH;
+}
+
#ifdef DEBUG_PROTO_OAP
#define OAP_KEX_IS_KEM(hdr) ((hdr)->kex_flags.role | (hdr)->kex_flags.fmt)
static void debug_oap_hdr(const struct oap_hdr * hdr)
{
assert(hdr);
+ if (hdr->sealed.len > 0)
+ log_proto(" Sealed block: [%zu bytes] on wire",
+ hdr->sealed.len);
+
if (hdr->crt.len > 0)
log_proto(" crt: [%zu bytes]", hdr->crt.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" crt: <sealed>");
else
log_proto(" crt: <none>");
if (hdr->kex.len > 0) {
if (OAP_KEX_IS_KEM(hdr))
- log_proto(" Key Exchange Data:"
- " [%zu bytes] [%s]",
+ log_proto(" Key Exchange Data: [%zu bytes] [%s]",
hdr->kex.len,
hdr->kex_flags.role ?
- "Client encaps" :
- "Server encaps");
+ "Client encaps" : "Server encaps");
else
- log_proto(" Key Exchange Data:"
- " [%zu bytes]",
+ log_proto(" Key Exchange Data: [%zu bytes]",
hdr->kex.len);
} else
log_proto(" Key Exchange Data: <none>");
@@ -403,16 +693,20 @@ static void debug_oap_hdr(const struct oap_hdr * hdr)
if (hdr->data.len > 0)
log_proto(" Data: [%zu bytes]", hdr->data.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Data: <sealed>");
else
log_proto(" Data: <none>");
- if (hdr->req_hash.len > 0)
- log_proto(" Req Hash: [%zu bytes]", hdr->req_hash.len);
+ if (hdr->rsp_tag.len > 0)
+ log_proto(" Rsp Tag: [%zu bytes]", hdr->rsp_tag.len);
else
- log_proto(" Req Hash: <none>");
+ log_proto(" Rsp Tag: <none>");
if (hdr->sig.len > 0)
log_proto(" Signature: [%zu bytes]", hdr->sig.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Signature: <sealed>");
else
log_proto(" Signature: <none>");
}
@@ -432,8 +726,9 @@ void debug_oap_hdr_rcv(const struct oap_hdr * hdr)
tm = gmtime(&stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--",
- HASH_VAL64(hdr->id.data), tmstr);
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s <--",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
debug_oap_hdr(hdr);
#else
@@ -455,8 +750,9 @@ void debug_oap_hdr_snd(const struct oap_hdr * hdr)
tm = gmtime(&stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->",
- HASH_VAL64(hdr->id.data), tmstr);
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s -->",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
debug_oap_hdr(hdr);
#else
diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h
index 6016452c..66fbac9a 100644
--- a/src/irmd/oap/hdr.h
+++ b/src/irmd/oap/hdr.h
@@ -43,6 +43,9 @@
#define OAP_KEX_IS_RAW_FMT(hdr) (((hdr)->kex_flags.fmt) == 1)
/*
+ * Plaintext layout (request, and unencrypted/signed response). The
+ * signature covers the whole packet except itself.
+ *
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
@@ -83,8 +86,8 @@
* | | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
* | | |
- * + req_hash (variable, response only) + |
- * | H(request) using req md_nid / sha384 | |
+ * + rsp_tag (variable, response only) + |
+ * | key-confirm tag (enc), else H(request) | |
* | | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
* | |
@@ -92,6 +95,25 @@
* | DSA signature over signed region |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
+ * Encrypted response - wire layout. The certificate, application data and
+ * signature are AEAD-sealed - hiding the server identity and the cert/data
+ * sizes; kex and rsp_tag move ahead of the sealed block as cleartext AAD.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | fixed header (36 bytes, see above) | |
+ * + id, timestamp, NIDs, crt_len=0, kex_len, data_len=0 + | AAD
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | kex_data (variable) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | rsp_tag (variable, response only) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | SEAL( data_len ‖ crt_len ‖ data ‖ crt ‖ sig ) | |
+ * + encrypted cert, app data and signature + | Sealed
+ * | + AEAD tag (128 bits) | | area
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ *
* cipher_nid: NID value for symmetric cipher (0 = none)
* kdf_nid: NID value for KDF function (0 = none)
* md_nid: NID value for signature hash (0 = PQC/no signature)
@@ -105,6 +127,11 @@
* Request: sig_len = total - 36 - crt_len - kex_len - data_len
* Response: sig_len = total - 36 - crt_len - kex_len - data_len - hash_len
* where hash_len = md_len(req_md_nid / sha384)
+ *
+ * The signed plaintext inside the seal is prefix ‖ data_len ‖ crt_len ‖
+ * data ‖ crt ‖ sig; the cleartext prefix (fixed ‖ kex ‖ rsp_tag) is the
+ * AEAD AAD. Cleartext crt_len/data_len are 0 - the real lengths are sealed,
+ * hiding the cert and data sizes; oap_hdr_unseal reads them to split.
*/
/* Parsed OAP header - buffers pointing to a single memory region */
@@ -120,12 +147,15 @@ struct oap_hdr {
bool fmt; /* Format */
bool role; /* Role */
} kex_flags;
+
buffer_t id;
buffer_t crt;
buffer_t kex;
buffer_t data;
- buffer_t req_hash; /* H(request) - response only */
+ buffer_t rsp_tag; /* key-confirm tag / H(req), rsp only */
buffer_t sig;
+ buffer_t sealed; /* wire ciphertext ‖ tag (sealed rsp) */
+ buffer_t sealed_pt; /* prefix‖lens‖data‖crt‖sig, owned */
buffer_t hdr;
};
@@ -138,16 +168,26 @@ void oap_hdr_init(struct oap_hdr * hdr,
void oap_hdr_fini(struct oap_hdr * oap_hdr);
+/* oap_hdr_encode option flags */
+#define OAP_ENC_REKEY (1U << 0) /* signed, cert-less re-key packet */
+
int oap_hdr_encode(struct oap_hdr * hdr,
void * pkp,
void * crt,
- struct sec_config * kcfg,
- buffer_t req_hash,
- int req_md_nid);
+ struct sec_config * scfg,
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key,
+ int flags);
int oap_hdr_decode(struct oap_hdr * hdr,
buffer_t buf,
- int req_md_nid);
+ int req_md_nid,
+ bool rekey);
+
+/* Decrypt a sealed response identity block; fills data, crt and sig. */
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key);
void debug_oap_hdr_rcv(const struct oap_hdr * hdr);
diff --git a/src/irmd/oap/internal.h b/src/irmd/oap/internal.h
index 6dd44d56..4a156723 100644
--- a/src/irmd/oap/internal.h
+++ b/src/irmd/oap/internal.h
@@ -36,12 +36,13 @@
int oap_check_hdr(const struct oap_hdr * hdr);
-int oap_auth_peer(char * name,
- const struct oap_hdr * local_hdr,
- const struct oap_hdr * peer_hdr);
+int oap_auth_peer(char * name,
+ const struct sec_config * cfg,
+ const struct oap_hdr * local_hdr,
+ const struct oap_hdr * peer_hdr);
int oap_negotiate_cipher(const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg);
+ struct sec_config * scfg);
#ifndef OAP_TEST_MODE
int load_credentials(const char * name,
@@ -49,7 +50,7 @@ int load_credentials(const char * name,
void ** pkp,
void ** crt);
-int load_kex_config(const char * name,
+int load_sec_config(const char * name,
const char * path,
struct sec_config * cfg);
#endif
@@ -59,7 +60,7 @@ int load_srv_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-int load_srv_kex_config(const struct name_info * info,
+int load_srv_sec_config(const struct name_info * info,
struct sec_config * cfg);
int load_server_kem_keypair(const char * name,
@@ -69,7 +70,7 @@ int load_server_kem_keypair(const char * name,
extern int load_srv_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-extern int load_srv_kex_config(const struct name_info * info,
+extern int load_srv_sec_config(const struct name_info * info,
struct sec_config * cfg);
extern int load_server_kem_keypair(const char * name,
struct sec_config * cfg,
@@ -78,7 +79,7 @@ extern int load_server_kem_keypair(const char * name,
int do_server_kex(const struct name_info * info,
struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
buffer_t * kex,
struct crypt_sk * sk);
@@ -87,7 +88,7 @@ int load_cli_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-int load_cli_kex_config(const struct name_info * info,
+int load_cli_sec_config(const struct name_info * info,
struct sec_config * cfg);
int load_server_kem_pk(const char * name,
@@ -97,21 +98,21 @@ int load_server_kem_pk(const char * name,
extern int load_cli_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-extern int load_cli_kex_config(const struct name_info * info,
+extern int load_cli_sec_config(const struct name_info * info,
struct sec_config * cfg);
extern int load_server_kem_pk(const char * name,
struct sec_config * cfg,
buffer_t * pk);
#endif
-int oap_client_kex_prepare(struct sec_config * kcfg,
+int oap_client_kex_prepare(struct sec_config * scfg,
buffer_t server_pk,
buffer_t * kex,
uint8_t * key,
void ** ephemeral_pkp);
int oap_client_kex_complete(const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
void * pkp,
uint8_t * key);
diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c
index c2c91b91..845723fa 100644
--- a/src/irmd/oap/io.c
+++ b/src/irmd/oap/io.c
@@ -50,11 +50,17 @@ static bool file_exists(const char * path)
{
struct stat s;
- if (stat(path, &s) < 0 && errno == ENOENT) {
+ if (stat(path, &s) == 0)
+ return true;
+
+ if (errno == ENOENT) {
log_dbg("File %s does not exist.", path);
return false;
}
+ /* Can't stat for another reason; assume present, fail on load */
+ log_warn("Failed to stat %s: %s.", path, strerror(errno));
+
return true;
}
@@ -96,16 +102,16 @@ int load_credentials(const char * name,
return -EAUTH;
}
-int load_kex_config(const char * name,
+int load_sec_config(const char * name,
const char * path,
struct sec_config * cfg)
{
+ void * pin;
+
assert(name != NULL);
assert(cfg != NULL);
- memset(cfg, 0, sizeof(*cfg));
-
- /* Load encryption config */
+ /* Load security config */
if (!file_exists(path))
log_dbg("No encryption %s for %s.", path, name);
@@ -114,19 +120,33 @@ int load_kex_config(const char * name,
return -1;
}
+ if (cfg->a.cacert[0] != '\0') {
+ if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
+ log_err("Failed to load pinned CA %s for %s.",
+ cfg->a.cacert, name);
+ return -EAUTH;
+ }
+ crypt_free_crt(pin);
+ }
+
if (!IS_KEX_ALGO_SET(cfg)) {
log_info("Key exchange not configured for %s.", name);
return 0;
}
-#ifndef HAVE_OPENSSL_ML_KEM
+#ifndef HAVE_ML
if (IS_KEM_ALGORITHM(cfg->x.str)) {
log_err("PQC not available, can't use %s for %s.",
cfg->x.str, name);
return -ENOTSUP;
}
#endif
- if (cfg->c.nid == NID_undef) {
- log_err("Invalid cipher for %s.", name);
+ if (crypt_kex_rank(cfg->x.nid) < 1) {
+ log_err("Key exchange not supported for %s.", name);
+ return -ENOTSUP;
+ }
+
+ if (crypt_cipher_rank(cfg->c.nid) < 1) {
+ log_err("Cipher not supported for %s.", name);
return -ECRYPT;
}
diff --git a/src/irmd/oap/io.h b/src/irmd/oap/io.h
index 2d47c62f..953e3898 100644
--- a/src/irmd/oap/io.h
+++ b/src/irmd/oap/io.h
@@ -32,7 +32,7 @@ int load_credentials(const char * name,
void ** pkp,
void ** crt);
-int load_kex_config(const char * name,
+int load_sec_config(const char * name,
const char * path,
struct sec_config * cfg);
#endif
diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c
index 36391e50..cc3dec5b 100644
--- a/src/irmd/oap/srv.c
+++ b/src/irmd/oap/srv.c
@@ -49,7 +49,7 @@
extern int load_srv_credentials(const struct name_info * info,
void ** pkp,
void ** crt);
-extern int load_srv_kex_config(const struct name_info * info,
+extern int load_srv_sec_config(const struct name_info * info,
struct sec_config * cfg);
extern int load_server_kem_keypair(const char * name,
bool raw_fmt,
@@ -67,13 +67,16 @@ int load_srv_credentials(const struct name_info * info,
return load_credentials(info->name, &info->s, pkp, crt);
}
-int load_srv_kex_config(const struct name_info * info,
+int load_srv_sec_config(const struct name_info * info,
struct sec_config * cfg)
{
assert(info != NULL);
assert(cfg != NULL);
- return load_kex_config(info->name, info->s.enc, cfg);
+ memset(cfg, 0, sizeof(*cfg));
+
+ /* Client auth stays opt-in (mTLS); enable with auth=required */
+ return load_sec_config(info->name, info->s.sec, cfg);
}
int load_server_kem_keypair(const char * name,
@@ -135,7 +138,7 @@ static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr,
}
static int negotiate_cipher(const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg)
+ struct sec_config * scfg)
{
uint8_t * id = peer_hdr->id.data;
int cli_nid;
@@ -143,27 +146,25 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr,
int srv_rank;
/* Cipher: select the strongest of client and server */
- cli_nid = peer_hdr->cipher_str != NULL
- ? (int) crypt_str_to_nid(peer_hdr->cipher_str)
- : NID_undef;
+ if (peer_hdr->cipher_str != NULL)
+ cli_nid = (int) crypt_str_to_nid(peer_hdr->cipher_str);
+ else
+ cli_nid = NID_undef;
- if (cli_nid != NID_undef
- && crypt_cipher_rank(cli_nid) < 0) {
+ if (cli_nid != NID_undef && crypt_cipher_rank(cli_nid) < 0) {
log_err_id(id, "Unsupported cipher '%s'.",
peer_hdr->cipher_str);
return -ENOTSUP;
}
cli_rank = crypt_cipher_rank(cli_nid);
- srv_rank = crypt_cipher_rank(kcfg->c.nid);
+ srv_rank = crypt_cipher_rank(scfg->c.nid);
if (cli_rank > srv_rank) {
- SET_KEX_CIPHER_NID(kcfg, cli_nid);
- log_dbg_id(id, "Selected client cipher %s.",
- kcfg->c.str);
+ SET_KEX_CIPHER_NID(scfg, cli_nid);
+ log_dbg_id(id, "Selected client cipher %s.", scfg->c.str);
} else if (srv_rank > 0) {
- log_dbg_id(id, "Selected server cipher %s.",
- kcfg->c.str);
+ log_dbg_id(id, "Selected server cipher %s.", scfg->c.str);
} else {
log_err_id(id, "Encryption requested, no cipher.");
return -ECRYPT;
@@ -178,31 +179,27 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr,
}
cli_rank = crypt_kdf_rank(peer_hdr->kdf_nid);
- srv_rank = crypt_kdf_rank(kcfg->k.nid);
+ srv_rank = crypt_kdf_rank(scfg->k.nid);
- /*
- * For client-encap KEM, the KDF is baked into
- * the ciphertext. The server must use the client's
- * KDF and can only verify the minimum.
- */
+ /* Client-encap KEM bakes KDF into ciphertext; verify min. */
if (OAP_KEX_ROLE(peer_hdr) == KEM_MODE_CLIENT_ENCAP) {
if (srv_rank > cli_rank) {
log_err_id(id, "Client KDF too weak.");
return -ECRYPT;
}
- SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid);
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
} else if (cli_rank > srv_rank) {
- SET_KEX_KDF_NID(kcfg, peer_hdr->kdf_nid);
+ SET_KEX_KDF_NID(scfg, peer_hdr->kdf_nid);
log_dbg_id(id, "Selected client KDF %s.",
- md_nid_to_str(kcfg->k.nid));
+ md_nid_to_str(scfg->k.nid));
} else if (srv_rank > 0) {
log_dbg_id(id, "Selected server KDF %s.",
- md_nid_to_str(kcfg->k.nid));
+ md_nid_to_str(scfg->k.nid));
}
- if (IS_KEX_ALGO_SET(kcfg))
+ if (IS_KEX_ALGO_SET(scfg))
log_info_id(id, "Negotiated %s + %s.",
- kcfg->x.str, kcfg->c.str);
+ scfg->x.str, scfg->c.str);
else
log_info_id(id, "No key exchange.");
@@ -211,7 +208,7 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr,
static int do_server_kem_decap(const struct name_info * info,
const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
struct crypt_sk * sk)
{
buffer_t ct;
@@ -228,7 +225,7 @@ static int do_server_kem_decap(const struct name_info * info,
ct.data = peer_hdr->kex.data;
ct.len = peer_hdr->kex.len;
- ret = kex_kem_decap(server_pkp, ct, kcfg->k.nid, sk->key);
+ ret = kex_kem_decap(server_pkp, ct, scfg->k.nid, sk->key);
crypt_free_key(server_pkp);
@@ -243,7 +240,7 @@ static int do_server_kem_decap(const struct name_info * info,
}
static int do_server_kem_encap(const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
buffer_t * kex,
struct crypt_sk * sk)
{
@@ -254,12 +251,12 @@ static int do_server_kem_encap(const struct oap_hdr * peer_hdr,
client_pk.data = peer_hdr->kex.data;
client_pk.len = peer_hdr->kex.len;
- if (IS_HYBRID_KEM(kcfg->x.str))
+ if (IS_HYBRID_KEM(scfg->x.str))
ct_len = kex_kem_encap_raw(client_pk, kex->data,
- kcfg->k.nid, sk->key);
+ scfg->k.nid, sk->key);
else
ct_len = kex_kem_encap(client_pk, kex->data,
- kcfg->k.nid, sk->key);
+ scfg->k.nid, sk->key);
if (ct_len < 0) {
log_err_id(id, "Failed to encapsulate KEM.");
@@ -275,26 +272,26 @@ static int do_server_kem_encap(const struct oap_hdr * peer_hdr,
static int do_server_kex_kem(const struct name_info * info,
struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
buffer_t * kex,
struct crypt_sk * sk)
{
int ret;
- kcfg->x.mode = peer_hdr->kex_flags.role;
+ scfg->x.mode = peer_hdr->kex_flags.role;
- if (kcfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
- ret = do_server_kem_decap(info, peer_hdr, kcfg, sk);
+ if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) {
+ ret = do_server_kem_decap(info, peer_hdr, scfg, sk);
kex->len = 0;
} else {
- ret = do_server_kem_encap(peer_hdr, kcfg, kex, sk);
+ ret = do_server_kem_encap(peer_hdr, scfg, kex, sk);
}
return ret;
}
static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
buffer_t * kex,
struct crypt_sk * sk)
{
@@ -303,7 +300,7 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
int ret;
uint8_t * id = peer_hdr->id.data;
- key_len = kex_pkp_create(kcfg, &epkp, kex->data);
+ key_len = kex_pkp_create(scfg, &epkp, kex->data);
if (key_len < 0) {
log_err_id(id, "Failed to generate key pair.");
return -ECRYPT;
@@ -311,9 +308,9 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
kex->len = (size_t) key_len;
- log_dbg_id(id, "Generated %s ephemeral keys.", kcfg->x.str);
+ log_dbg_id(id, "Generated %s ephemeral keys.", scfg->x.str);
- ret = kex_dhe_derive(kcfg, epkp, peer_hdr->kex, sk->key);
+ ret = kex_dhe_derive(scfg, epkp, peer_hdr->kex, sk->key);
if (ret < 0) {
log_err_id(id, "Failed to derive secret.");
kex_pkp_destroy(epkp);
@@ -327,7 +324,7 @@ static int do_server_kex_dhe(const struct oap_hdr * peer_hdr,
int do_server_kex(const struct name_info * info,
struct oap_hdr * peer_hdr,
- struct sec_config * kcfg,
+ struct sec_config * scfg,
buffer_t * kex,
struct crypt_sk * sk)
{
@@ -339,60 +336,72 @@ int do_server_kex(const struct name_info * info,
/* No KEX data from client */
if (peer_hdr->kex.len == 0) {
- if (IS_KEX_ALGO_SET(kcfg)) {
+ if (IS_KEX_ALGO_SET(scfg)) {
log_warn_id(id, "KEX requested without info.");
return -ECRYPT;
}
return 0;
}
- if (negotiate_cipher(peer_hdr, kcfg) < 0)
+ if (negotiate_cipher(peer_hdr, scfg) < 0)
return -ECRYPT;
/* Save server's configured KEX before overwriting */
- srv_kex_nid = kcfg->x.nid;
+ srv_kex_nid = scfg->x.nid;
if (OAP_KEX_ROLE(peer_hdr) != KEM_MODE_CLIENT_ENCAP) {
/* Server encapsulation or DHE: extract algo from DER PK */
if (get_algo_from_peer_key(peer_hdr, algo_buf) < 0)
return -ECRYPT;
- SET_KEX_ALGO(kcfg, algo_buf);
+ SET_KEX_ALGO(scfg, algo_buf);
/* Reject if client KEX is weaker than server's */
- if (crypt_kex_rank(kcfg->x.nid)
+ if (crypt_kex_rank(scfg->x.nid)
< crypt_kex_rank(srv_kex_nid)) {
log_err_id(id, "Client KEX %s too weak.",
- kcfg->x.str);
+ scfg->x.str);
return -ECRYPT;
}
}
/* Dispatch based on algorithm type */
- if (IS_KEM_ALGORITHM(kcfg->x.str))
- return do_server_kex_kem(info, peer_hdr, kcfg, kex, sk);
+ if (IS_KEM_ALGORITHM(scfg->x.str))
+ return do_server_kex_kem(info, peer_hdr, scfg, kex, sk);
else
- return do_server_kex_dhe(peer_hdr, kcfg, kex, sk);
+ return do_server_kex_dhe(peer_hdr, scfg, kex, sk);
}
int oap_srv_process(const struct name_info * info,
buffer_t req_buf,
buffer_t * rsp_buf,
buffer_t * data,
- struct crypt_sk * sk)
+ struct crypt_sk * sk,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
{
struct oap_hdr peer_hdr;
struct oap_hdr local_hdr;
- struct sec_config kcfg;
- uint8_t kex_buf[MSGBUFSZ];
+ struct sec_config scfg;
+ uint8_t kex_buf[CRYPT_KEY_BUFSZ];
uint8_t hash_buf[MAX_HASH_SIZE];
- buffer_t req_hash = BUF_INIT;
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ const uint8_t * seal_key = NULL;
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
+ buffer_t crt_der = BUF_INIT;
+ buffer_t rsp_tag = BUF_INIT;
ssize_t hash_ret;
- char cli_name[NAME_SIZE + 1]; /* TODO */
+ char cli_name[NAME_SIZE + 1];
uint8_t * id;
void * pkp = NULL;
void * crt = NULL;
int req_md_nid;
+ int enc_flags = 0;
+ int ret;
assert(info != NULL);
assert(rsp_buf != NULL);
@@ -412,13 +421,22 @@ int oap_srv_process(const struct name_info * info,
goto fail_cred;
}
- if (load_srv_kex_config(info, &kcfg) < 0) {
- log_err("Failed to load KEX config for %s.", info->name);
+ /* Re-key omits the cert; the peer verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
+ if (rekey)
+ enc_flags = OAP_ENC_REKEY;
+
+ if (load_srv_sec_config(info, &scfg) < 0) {
+ log_err("Failed to load security config for %s.", info->name);
goto fail_kex;
}
/* Decode incoming header (NID_undef = request, no hash) */
- if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef) < 0) {
+ if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef, rekey) < 0) {
log_err("Failed to decode OAP header.");
goto fail_auth;
}
@@ -427,22 +445,38 @@ int oap_srv_process(const struct name_info * info,
id = peer_hdr.id.data; /* Logging */
- if (oap_check_hdr(&peer_hdr) < 0) {
- log_err_id(id, "OAP header failed replay check.");
+ ret = oap_check_hdr(&peer_hdr);
+ if (ret == -EREPLAY) {
+ log_warn_id(id, "OAP header failed replay check.");
+ goto fail_replay;
+ }
+ if (ret < 0) {
+ log_err_id(id, "OAP header check failed.");
goto fail_auth;
}
oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef);
- if (oap_auth_peer(cli_name, &local_hdr, &peer_hdr) < 0) {
+ if (oap_auth_peer(cli_name, &scfg, &local_hdr, &peer_hdr,
+ cached_crt) < 0) {
log_err_id(id, "Failed to authenticate client.");
goto fail_auth;
}
- if (do_server_kex(info, &peer_hdr, &kcfg, &local_hdr.kex, sk) < 0)
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_auth;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
+ if (do_server_kex(info, &peer_hdr, &scfg, &local_hdr.kex, sk) < 0)
goto fail_kex;
- sk->nid = kcfg.c.nid;
+ sk->nid = scfg.c.nid;
/* Build response header with hash of client request */
local_hdr.nid = sk->nid;
@@ -458,10 +492,58 @@ int oap_srv_process(const struct name_info * info,
goto fail_auth;
}
req_hash.data = hash_buf;
- req_hash.len = (size_t) hash_ret;
+ req_hash.len = (size_t) hash_ret;
+
+ rsp_tag = req_hash;
+
+ /* Bind the key to the transcript and confirm it to the client */
+ if (sk->nid != NID_undef) {
+ if (crt != NULL && crypt_crt_der(crt, &crt_der) < 0) {
+ log_err_id(id, "Failed to serialize cert.");
+ goto fail_auth;
+ }
+
+ resp_hash.data = resp_hash_buf;
+
+ ret = oap_resp_hash(req_md_nid, local_hdr.kex, *data,
+ crt_der, &resp_hash);
+
+ freebuf(crt_der);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_auth;
+ }
+
+ /* Derive the identity-seal key before bind mutates sk->key */
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_auth;
+ }
+
+ seal_key = hs_key;
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ scfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_auth;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ (size_t) hash_ret) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_auth;
+ }
- if (oap_hdr_encode(&local_hdr, pkp, crt, &kcfg,
- req_hash, req_md_nid) < 0) {
+ rsp_tag.data = kc_buf;
+ }
+
+ ret = oap_hdr_encode(&local_hdr, pkp, crt, &scfg,
+ rsp_tag, req_md_nid, seal_key, enc_flags);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (ret < 0) {
log_err_id(id, "Failed to create OAP response header.");
goto fail_auth;
}
@@ -486,11 +568,17 @@ int oap_srv_process(const struct name_info * info,
fail_data:
oap_hdr_fini(&local_hdr);
fail_auth:
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
crypt_free_crt(crt);
crypt_free_key(pkp);
fail_cred:
return -EAUTH;
+ fail_replay:
+ crypt_free_crt(crt);
+ crypt_free_key(pkp);
+ return -EREPLAY;
+
fail_kex:
crypt_free_crt(crt);
crypt_free_key(pkp);
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c
index 0a1af100..16d52c63 100644
--- a/src/irmd/oap/tests/common.c
+++ b/src/irmd/oap/tests/common.c
@@ -29,39 +29,47 @@
#include <string.h>
#include <stdio.h>
-int load_srv_kex_config(const struct name_info * info,
+int load_srv_sec_config(const struct name_info * info,
struct sec_config * cfg)
{
(void) info;
memset(cfg, 0, sizeof(*cfg));
+ cfg->a.req = test_cfg.srv.req_auth;
+
+ /* Digest is kept without kex, as in parse_sec_config */
+ SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md);
+
if (test_cfg.srv.kex == NID_undef)
return 0;
SET_KEX_ALGO_NID(cfg, test_cfg.srv.kex);
SET_KEX_CIPHER_NID(cfg, test_cfg.srv.cipher);
SET_KEX_KDF_NID(cfg, test_cfg.srv.kdf);
- SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md);
SET_KEX_KEM_MODE(cfg, test_cfg.srv.kem_mode);
return 0;
}
-int load_cli_kex_config(const struct name_info * info,
+int load_cli_sec_config(const struct name_info * info,
struct sec_config * cfg)
{
(void) info;
memset(cfg, 0, sizeof(*cfg));
+ cfg->a.req = test_cfg.cli.req_auth;
+
+ /* Digest is kept without kex, as in parse_sec_config */
+ SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md);
+
if (test_cfg.cli.kex == NID_undef)
return 0;
SET_KEX_ALGO_NID(cfg, test_cfg.cli.kex);
SET_KEX_CIPHER_NID(cfg, test_cfg.cli.cipher);
SET_KEX_KDF_NID(cfg, test_cfg.cli.kdf);
- SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md);
SET_KEX_KEM_MODE(cfg, test_cfg.cli.kem_mode);
return 0;
@@ -152,13 +160,15 @@ void oap_test_teardown(struct oap_test_ctx * ctx)
if (ctx->cli.state != NULL) {
res.key = ctx->cli.key;
oap_cli_complete(ctx->cli.state, &ctx->cli.info, dummy,
- &ctx->data, &res);
+ &ctx->data, &res, NULL, NULL);
ctx->cli.state = NULL;
}
freebuf(ctx->data);
freebuf(ctx->resp_hdr);
freebuf(ctx->req_hdr);
+ freebuf(ctx->srv_crt);
+ freebuf(ctx->cli_crt);
crypt_free_crt(ctx->im_ca);
crypt_free_crt(ctx->root_ca);
@@ -170,7 +180,7 @@ void oap_test_teardown(struct oap_test_ctx * ctx)
int oap_cli_prepare_ctx(struct oap_test_ctx * ctx)
{
return oap_cli_prepare(&ctx->cli.state, &ctx->cli.info, &ctx->req_hdr,
- ctx->data);
+ ctx->data, ctx->rekey);
}
int oap_srv_process_ctx(struct oap_test_ctx * ctx)
@@ -179,7 +189,9 @@ int oap_srv_process_ctx(struct oap_test_ctx * ctx)
int ret;
ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr,
- &ctx->resp_hdr, &ctx->data, &res);
+ &ctx->resp_hdr, &ctx->data, &res, ctx->rekey,
+ ctx->rekey ? &ctx->srv_crt : NULL,
+ ctx->rekey ? NULL : &ctx->srv_crt);
if (ret == 0)
ctx->srv.nid = res.nid;
@@ -192,7 +204,9 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx)
int ret;
ret = oap_cli_complete(ctx->cli.state, &ctx->cli.info, ctx->resp_hdr,
- &ctx->data, &res);
+ &ctx->data, &res,
+ ctx->rekey ? &ctx->cli_crt : NULL,
+ ctx->rekey ? NULL : &ctx->cli_crt);
ctx->cli.state = NULL;
if (ret == 0)
@@ -243,6 +257,249 @@ int roundtrip_auth_only(const char * root_ca,
return TEST_RC_FAIL;
}
+static const char * rekey_mode(bool srv_auth,
+ bool cli_auth)
+{
+ if (srv_auth && cli_auth)
+ return "mutual";
+
+ if (srv_auth)
+ return "srv-only";
+
+ if (cli_auth)
+ return "cli-only";
+
+ return "none";
+}
+
+int roundtrip_rekey(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth,
+ bool cli_auth)
+{
+ struct oap_test_ctx ctx;
+ uint8_t key0[SYMMKEYSZ];
+ const char * mode = rekey_mode(srv_auth, cli_auth);
+
+ TEST_START("(%s)", mode);
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Initial keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ /* The client caches the server cert only if the server authed. */
+ if (srv_auth && ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached for re-key.\n");
+ goto fail_cleanup;
+ }
+
+ /* The server caches the client cert only if the client authed. */
+ if (cli_auth && ctx.srv_crt.len == 0) {
+ printf("Client cert was not cached by the server.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(key0, ctx.cli.key, SYMMKEYSZ);
+
+ /* Re-key: cert dropped on the wire, verified against the cache. */
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Re-key keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) {
+ printf("Re-key did not produce a fresh key.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) {
+ printf("Cipher not set after re-key.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", mode);
+
+ return TEST_RC_SUCCESS;
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", mode);
+ return TEST_RC_FAIL;
+}
+
+int roundtrip_rekey_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool cli_auth)
+{
+ struct oap_test_ctx ctx;
+ const char * mode = rekey_mode(true, cli_auth);
+
+ TEST_START("(%s)", mode);
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the client's cached server cert: re-key must fail closed. */
+ ctx.cli_crt.data[ctx.cli_crt.len / 2] ^= 0xFF;
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Re-key accepted a corrupted cached cert.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", mode);
+
+ return TEST_RC_SUCCESS;
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", mode);
+ return TEST_RC_FAIL;
+}
+
+int roundtrip_rekey_srv_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth)
+{
+ struct oap_test_ctx ctx;
+ const char * mode = rekey_mode(srv_auth, true);
+
+ TEST_START("(%s)", mode);
+
+ if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.srv_crt.len == 0) {
+ printf("Client cert was not cached by the server.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the server's cached client cert: re-key must fail closed. */
+ ctx.srv_crt.data[ctx.srv_crt.len / 2] ^= 0xFF;
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server accepted a corrupted cached client cert.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", mode);
+
+ return TEST_RC_SUCCESS;
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", mode);
+ return TEST_RC_FAIL;
+}
+
int roundtrip_kex_only(void)
{
struct name_info cli_info;
@@ -271,14 +528,15 @@ int roundtrip_kex_only(void)
}
if (oap_cli_prepare(&cli_state, &cli_info, &req_hdr,
- data) < 0) {
+ data, false) < 0) {
printf("Client prepare failed.\n");
goto fail_cleanup;
}
res.key = srv_key;
- if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res) < 0) {
+ if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res,
+ false, NULL, NULL) < 0) {
printf("Server process failed.\n");
goto fail_cleanup;
}
@@ -287,7 +545,8 @@ int roundtrip_kex_only(void)
res.key = cli_key;
- if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res) < 0) {
+ if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res,
+ NULL, NULL) < 0) {
printf("Client complete failed.\n");
cli_state = NULL;
goto fail_cleanup;
@@ -316,7 +575,8 @@ int roundtrip_kex_only(void)
fail_cleanup:
if (cli_state != NULL) {
res.key = cli_key;
- oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res);
+ oap_cli_complete(cli_state, &cli_info, resp_hdr, &data,
+ &res, NULL, NULL);
}
freebuf(resp_hdr);
freebuf(req_hdr);
@@ -396,7 +656,7 @@ int corrupted_response(const char * root_ca,
res.key = ctx.cli.key;
if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr,
- &ctx.data, &res) == 0) {
+ &ctx.data, &res, NULL, NULL) == 0) {
printf("Client should reject corrupted response.\n");
ctx.cli.state = NULL;
goto fail_cleanup;
diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h
index d4b6733a..7aead07a 100644
--- a/src/irmd/oap/tests/common.h
+++ b/src/irmd/oap/tests/common.h
@@ -30,14 +30,18 @@
#include <stdbool.h>
+#define AUTH true
+#define NO_AUTH false
+
/* Per-side security configuration for tests */
struct test_sec_cfg {
- int kex; /* KEX algorithm NID */
- int cipher; /* Cipher NID for encryption */
- int kdf; /* KDF NID for key derivation */
- int md; /* Digest NID for signatures */
- int kem_mode; /* KEM encapsulation mode (0 for ECDH) */
- bool auth; /* Use authentication (certificates) */
+ int kex; /* KEX algorithm NID */
+ int cipher; /* Cipher NID for encryption */
+ int kdf; /* KDF NID for key derivation */
+ int md; /* Digest NID for signatures */
+ int kem_mode; /* KEM encapsulation mode (0 for ECDH) */
+ bool auth; /* Use authentication (certificates) */
+ bool req_auth; /* Require peer authentication */
};
/* Test configuration - set by each test before running roundtrip */
@@ -69,6 +73,11 @@ struct oap_test_ctx {
buffer_t data;
void * root_ca;
void * im_ca;
+
+ /* Re-key (tier iii): drop the cert, verify against the cache. */
+ bool rekey;
+ buffer_t srv_crt; /* client cert cached by server */
+ buffer_t cli_crt; /* server cert cached by client */
};
int oap_test_setup(struct oap_test_ctx * ctx,
@@ -86,6 +95,19 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx);
int roundtrip_auth_only(const char * root_ca,
const char * im_ca_str);
+int roundtrip_rekey(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth,
+ bool cli_auth);
+
+int roundtrip_rekey_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool cli_auth);
+
+int roundtrip_rekey_srv_badcache(const char * root_ca,
+ const char * im_ca_str,
+ bool srv_auth);
+
int roundtrip_kex_only(void);
int corrupted_request(const char * root_ca,
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
index 2f0f0b4d..145b68c7 100644
--- a/src/irmd/oap/tests/oap_test.c
+++ b/src/irmd/oap/tests/oap_test.c
@@ -32,6 +32,7 @@
#include <ouroboros/crypt.h>
#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
#include <ouroboros/flow.h>
#include <ouroboros/name.h>
#include <ouroboros/random.h>
@@ -41,18 +42,18 @@
#include <test/certs/ecdsa.h>
#include "oap.h"
+#include "oap/auth.h"
#include "common.h"
#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
#endif
-#define AUTH true
-#define NO_AUTH false
-
extern const uint16_t kex_supported_nids[];
extern const uint16_t md_supported_nids[];
@@ -113,6 +114,13 @@ static void test_default_cfg(void)
test_cfg.cli.auth = NO_AUTH;
}
+/* Encrypted, unauthenticated on both sides. */
+static void test_enc_noauth_cfg(void)
+{
+ test_default_cfg();
+ test_cfg.srv.auth = NO_AUTH;
+}
+
static int test_oap_auth_init_fini(void)
{
TEST_START();
@@ -174,6 +182,7 @@ static int test_oap_roundtrip(int kex)
oap_test_teardown(&ctx);
TEST_SUCCESS("(%s)", kex_str);
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -198,23 +207,71 @@ static int test_oap_roundtrip_auth_only(void)
return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec);
}
-static int test_oap_roundtrip_kex_only(void)
+static int test_oap_rekey(bool srv_auth,
+ bool cli_auth)
{
- memset(&test_cfg, 0, sizeof(test_cfg));
+ test_default_cfg();
+ test_cfg.srv.auth = srv_auth;
+ test_cfg.cli.auth = cli_auth;
- /* Server: KEX only, no auth */
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
+ return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec,
+ srv_auth, cli_auth);
+}
- /* Client: KEX only, no auth */
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+static int test_oap_rekey_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey(AUTH, NO_AUTH);
+ ret |= test_oap_rekey(AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, NO_AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_srv_badcache(bool srv_auth)
+{
+ test_default_cfg();
+ test_cfg.srv.auth = srv_auth;
+ test_cfg.cli.auth = AUTH;
+
+ return roundtrip_rekey_srv_badcache(root_ca_crt_ec, im_ca_crt_ec,
+ srv_auth);
+}
+
+static int test_oap_rekey_srv_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_srv_badcache(AUTH);
+ ret |= test_oap_rekey_srv_badcache(NO_AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_badcache(bool cli_auth)
+{
+ test_default_cfg();
+ test_cfg.cli.auth = cli_auth;
+
+ return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec,
+ cli_auth);
+}
+
+static int test_oap_rekey_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_badcache(NO_AUTH);
+ ret |= test_oap_rekey_badcache(AUTH);
+
+ return ret;
+}
+
+static int test_oap_roundtrip_kex_only(void)
+{
+ test_enc_noauth_cfg();
return roundtrip_kex_only();
}
@@ -238,6 +295,7 @@ static int test_oap_piggyback_data(void)
ctx.data.data = malloc(ctx.data.len);
if (ctx.data.data == NULL)
goto fail_cleanup;
+
memcpy(ctx.data.data, cli_data_str, ctx.data.len);
if (oap_cli_prepare_ctx(&ctx) < 0)
@@ -288,6 +346,7 @@ static int test_oap_piggyback_data(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -356,6 +415,7 @@ static int test_oap_inflated_length_field(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -400,6 +460,7 @@ static int test_oap_deflated_length_field(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -411,8 +472,13 @@ static int test_oap_deflated_length_field(void)
/* Header field offsets for byte manipulation */
#define OAP_CIPHER_NID_OFFSET 24
+#define OAP_KDF_NID_OFFSET 26
+#define OAP_MD_NID_OFFSET 28
#define OAP_KEX_LEN_OFFSET 32
+/* A NID the crypto backend does not recognise; guarded by a test below. */
+#define UNSUPPORTED_NID 9999
+
/* Server rejects request when cipher NID set but no KEX data provided */
static int test_oap_nid_without_kex(void)
{
@@ -420,20 +486,9 @@ static int test_oap_nid_without_kex(void)
uint16_t cipher_nid;
uint16_t zero = 0;
- TEST_START();
+ test_enc_noauth_cfg();
- /* Configure unsigned KEX-only mode */
- memset(&test_cfg, 0, sizeof(test_cfg));
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -458,6 +513,7 @@ static int test_oap_nid_without_kex(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -467,26 +523,44 @@ static int test_oap_nid_without_kex(void)
return TEST_RC_FAIL;
}
-/* Server rejects OAP request with unsupported cipher NID */
-static int test_oap_unsupported_nid(void)
+/* Guard: the tamper tests below rely on UNSUPPORTED_NID being invalid. */
+static int test_oap_unsupported_nid_undefined(void)
+{
+ TEST_START();
+
+ if (crypt_cipher_rank(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid cipher NID.\n");
+ goto fail;
+ }
+
+ if (crypt_kdf_rank(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid KDF NID.\n");
+ goto fail;
+ }
+
+ if (md_validate_nid(UNSUPPORTED_NID) >= 0) {
+ printf("UNSUPPORTED_NID is a valid digest NID.\n");
+ goto fail;
+ }
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server rejects a request whose cipher/kdf/digest NID is unsupported */
+static int test_oap_unsupported_nid(size_t offset,
+ const char * label)
{
struct oap_test_ctx ctx;
uint16_t bad_nid;
- TEST_START();
+ test_enc_noauth_cfg();
- /* Configure unsigned KEX-only mode */
- memset(&test_cfg, 0, sizeof(test_cfg));
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = NO_AUTH;
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START("(%s)", label);
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -496,19 +570,73 @@ static int test_oap_unsupported_nid(void)
goto fail_cleanup;
}
- /* Tamper: set cipher_nid to unsupported value */
- bad_nid = hton16(9999);
- memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &bad_nid,
- sizeof(bad_nid));
+ bad_nid = hton16(UNSUPPORTED_NID);
+ memcpy(ctx.req_hdr.data + offset, &bad_nid, sizeof(bad_nid));
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject unsupported cipher NID.\n");
+ printf("Server should reject unsupported %s NID.\n", label);
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", label);
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", label);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_unsupported_nid_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_unsupported_nid(OAP_CIPHER_NID_OFFSET, "cipher");
+ ret |= test_oap_unsupported_nid(OAP_KDF_NID_OFFSET, "kdf");
+ ret |= test_oap_unsupported_nid(OAP_MD_NID_OFFSET, "digest");
+
+ return ret;
+}
+
+/* Client rejects a response whose key-confirmation tag is tampered */
+static int test_oap_key_confirm_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ /* Unauthenticated + encrypted: response unsigned, KC is the gate */
+ test_enc_noauth_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* The key-confirm tag is the last field of an unsigned response */
+ ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a bad key-confirmation tag.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -609,6 +737,7 @@ static int test_oap_cipher_mismatch(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -655,6 +784,7 @@ static int test_oap_srv_enc_cli_none(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -724,6 +854,7 @@ static int test_oap_cli_enc_srv_none(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -733,27 +864,15 @@ static int test_oap_cli_enc_srv_none(void)
return TEST_RC_FAIL;
}
-/* Client rejects server response with downgraded cipher */
+/* Unauthenticated server: client floor-rejects a downgraded cipher */
static int test_oap_cli_rejects_downgrade(void)
{
struct oap_test_ctx ctx;
uint16_t weak;
- TEST_START();
-
- memset(&test_cfg, 0, sizeof(test_cfg));
-
- test_cfg.srv.kex = NID_X25519;
- test_cfg.srv.cipher = NID_aes_256_gcm;
- test_cfg.srv.kdf = NID_sha256;
- test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = AUTH;
+ test_enc_noauth_cfg();
- test_cfg.cli.kex = NID_X25519;
- test_cfg.cli.cipher = NID_aes_256_gcm;
- test_cfg.cli.kdf = NID_sha256;
- test_cfg.cli.md = NID_sha256;
- test_cfg.cli.auth = NO_AUTH;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -769,7 +888,7 @@ static int test_oap_cli_rejects_downgrade(void)
}
/* Tamper: replace cipher NID with weaker one */
- weak = hton16(NID_aes_128_ctr);
+ weak = hton16(NID_aes_128_gcm);
memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
&weak, sizeof(weak));
@@ -782,6 +901,60 @@ static int test_oap_cli_rejects_downgrade(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Suite binding: a cipher swapped to a higher rank clears the client floor
+ * check, but the bound key commits to the negotiated suite, so the swap must
+ * still fail key confirmation.
+ */
+static int test_oap_cli_rejects_suite_swap(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t swap;
+
+ /* Both AES-128-GCM: a swap to AES-256 outranks the client floor */
+ test_enc_noauth_cfg();
+ test_cfg.srv.cipher = NID_aes_128_gcm;
+ test_cfg.cli.cipher = NID_aes_128_gcm;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Swap the response cipher to a higher-ranked one */
+ swap = hton16(NID_aes_256_gcm);
+ memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
+ &swap, sizeof(swap));
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a swapped cipher suite.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -831,6 +1004,7 @@ static int test_oap_srv_rejects_weak_kex(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -890,6 +1064,7 @@ static int test_oap_roundtrip_md(int md)
oap_test_teardown(&ctx);
TEST_SUCCESS("(%s)", md_str ? md_str : "default");
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -917,15 +1092,17 @@ static int test_oap_roundtrip_md_all(void)
/* Timestamp is at offset 16 (after the 16-byte ID) */
#define OAP_TIMESTAMP_OFFSET 16
/* Test that packets with outdated timestamps are rejected */
-static int test_oap_outdated_packet(void)
+/* Server rejects a request whose timestamp is outside the replay window */
+static int test_oap_ts_reject(int delta_sec,
+ const char * label)
{
struct oap_test_ctx ctx;
- struct timespec old_ts;
- uint64_t old_stamp;
+ struct timespec ts;
+ uint64_t stamp;
test_default_cfg();
- TEST_START();
+ TEST_START("(%s)", label);
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
@@ -940,21 +1117,363 @@ static int test_oap_outdated_packet(void)
goto fail_cleanup;
}
- /* Set timestamp to 30 seconds in the past (> 20s replay timer) */
- clock_gettime(CLOCK_REALTIME, &old_ts);
- old_ts.tv_sec -= OAP_REPLAY_TIMER + 10;
- old_stamp = hton64(TS_TO_UINT64(old_ts));
- memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &old_stamp,
- sizeof(old_stamp));
+ clock_gettime(CLOCK_REALTIME, &ts);
+ ts.tv_sec += delta_sec;
+ stamp = hton64(TS_TO_UINT64(ts));
+ memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &stamp,
+ sizeof(stamp));
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject outdated packet.\n");
+ printf("Server should reject %s packet.\n", label);
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS("(%s)", label);
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL("(%s)", label);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_ts_reject_all(void)
+{
+ int ret = 0;
+
+ /* Past the 20s replay window, and past the 100ms future slack. */
+ ret |= test_oap_ts_reject(-(OAP_REPLAY_TIMER + 10), "outdated");
+ ret |= test_oap_ts_reject(1, "future");
+
+ return ret;
+}
+
+/* Test that replayed packets (same ID + timestamp) are rejected */
+static int test_oap_replay_packet(void)
+{
+ struct oap_test_ctx ctx;
+ buffer_t saved_req;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Save the request for replay */
+ saved_req.len = ctx.req_hdr.len;
+ saved_req.data = malloc(saved_req.len);
+ if (saved_req.data == NULL) {
+ printf("Failed to allocate saved request.\n");
+ goto fail_cleanup;
+ }
+ memcpy(saved_req.data, ctx.req_hdr.data, saved_req.len);
+
+ /* First request should succeed */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("First request should succeed.\n");
+ free(saved_req.data);
+ goto fail_cleanup;
+ }
+
+ /* Free response from first request before replay */
+ freebuf(ctx.resp_hdr);
+
+ /* Restore the saved request for replay */
+ freebuf(ctx.req_hdr);
+ ctx.req_hdr = saved_req;
+
+ /* Replay must return -EREPLAY so callers can drop silently. */
+ if (oap_srv_process_ctx(&ctx) != -EREPLAY) {
+ printf("Replayed packet rejection != -EREPLAY.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ 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;
+ struct timespec wait;
+ uint8_t id[OAP_ID_SIZE];
+ uint64_t cur;
+ uint64_t gen_ns;
+ uint64_t off;
+ uint64_t wait_ns;
+ uint64_t stamp_a;
+ uint64_t stamp_b;
+
+ TEST_START();
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ gen_ns = (uint64_t) OAP_REPLAY_TIMER * BILLION;
+
+ /* Prev-gen stamp flakes on staleness near a generation top. */
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ off = cur % gen_ns;
+ if (gen_ns - off < BILLION) {
+ wait_ns = gen_ns - off + MILLION;
+ wait.tv_sec = (time_t) (wait_ns / BILLION);
+ wait.tv_nsec = (long) (wait_ns % BILLION);
+ nanosleep(&wait, NULL);
+ clock_gettime(CLOCK_REALTIME, &now);
+ cur = TS_TO_UINT64(now);
+ }
+
+ /* 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)
+{
+ struct oap_test_ctx ctx;
+ void * im_ca = NULL;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ memset(&ctx, 0, sizeof(ctx));
+
+ strcpy(ctx.srv.info.name, "test-1.unittest.o7s");
+ strcpy(ctx.cli.info.name, "test-1.unittest.o7s");
+
+ if (oap_auth_init() < 0) {
+ printf("Failed to init OAP.\n");
+ goto fail;
+ }
+
+ /* Load intermediate CA but intentionally omit the root CA */
+ if (crypt_load_crt_str(im_ca_crt_ec, &im_ca) < 0) {
+ printf("Failed to load intermediate CA cert.\n");
+ goto fail_fini;
+ }
+
+ ctx.im_ca = im_ca;
+
+ if (oap_auth_add_ca_crt(im_ca) < 0) {
+ printf("Failed to add intermediate CA cert to store.\n");
+ goto fail_fini;
+ }
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_fini;
+ }
+
+ /* Server processes and signs response - succeeds without root CA */
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_teardown;
+ }
+
+ /* Client verifies server certificate against trust store:
+ * should reject because root CA is not in the store */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject without root CA.\n");
+ goto fail_teardown;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_teardown:
+ oap_test_teardown(&ctx);
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+ fail_fini:
+ crypt_free_crt(im_ca);
+ oap_auth_fini();
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Test that client rejects server with wrong certificate name */
+static int test_oap_server_name_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ /* Set client's expected name to something different from cert name */
+ strcpy(ctx.cli.info.name, "wrong.server.name");
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* Client should reject due to name mismatch */
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject server with wrong cert name.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -964,14 +1483,14 @@ static int test_oap_outdated_packet(void)
return TEST_RC_FAIL;
}
-/* Test that packets from the future are rejected */
-static int test_oap_future_packet(void)
+/* Client requiring auth rejects a response without certificate */
+static int test_oap_cli_requires_srv_auth(void)
{
struct oap_test_ctx ctx;
- struct timespec future_ts;
- uint64_t future_stamp;
test_default_cfg();
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.req_auth = true;
TEST_START();
@@ -983,26 +1502,56 @@ static int test_oap_future_packet(void)
goto fail_cleanup;
}
- if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) {
- printf("Request too short for test.\n");
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
goto fail_cleanup;
}
- /* Set timestamp to 1 second in the future (> 100ms slack) */
- clock_gettime(CLOCK_REALTIME, &future_ts);
- future_ts.tv_sec += 1;
- future_stamp = hton64(TS_TO_UINT64(future_ts));
- memcpy(ctx.req_hdr.data + OAP_TIMESTAMP_OFFSET, &future_stamp,
- sizeof(future_stamp));
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject unauthenticated server.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server requiring auth rejects a request without certificate */
+static int test_oap_srv_requires_cli_auth(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.req_auth = true;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject future packet.\n");
+ printf("Server should reject unauthenticated client.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1012,13 +1561,15 @@ static int test_oap_future_packet(void)
return TEST_RC_FAIL;
}
-/* Test that replayed packets (same ID + timestamp) are rejected */
-static int test_oap_replay_packet(void)
+/* Roundtrip succeeds when both sides require and provide auth */
+static int test_oap_mutual_req_auth(void)
{
struct oap_test_ctx ctx;
- buffer_t saved_req;
test_default_cfg();
+ test_cfg.srv.req_auth = true;
+ test_cfg.cli.auth = AUTH;
+ test_cfg.cli.req_auth = true;
TEST_START();
@@ -1030,38 +1581,103 @@ static int test_oap_replay_packet(void)
goto fail_cleanup;
}
- /* Save the request for replay */
- saved_req.len = ctx.req_hdr.len;
- saved_req.data = malloc(saved_req.len);
- if (saved_req.data == NULL) {
- printf("Failed to allocate saved request.\n");
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Client rejects a server signature with a different digest */
+static int test_oap_cli_rejects_md_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.srv.md = NID_sha384;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
goto fail_cleanup;
}
- memcpy(saved_req.data, ctx.req_hdr.data, saved_req.len);
- /* First request should succeed */
if (oap_srv_process_ctx(&ctx) < 0) {
- printf("First request should succeed.\n");
- free(saved_req.data);
+ printf("Server process failed.\n");
goto fail_cleanup;
}
- /* Free response from first request before replay */
- freebuf(ctx.resp_hdr);
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject digest mismatch.\n");
+ goto fail_cleanup;
+ }
- /* Restore the saved request for replay */
- freebuf(ctx.req_hdr);
- ctx.req_hdr = saved_req;
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Server rejects a client signature with a different digest */
+static int test_oap_srv_rejects_md_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ test_default_cfg();
+ test_cfg.cli.auth = AUTH;
+ test_cfg.cli.md = NID_sha384;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
- /* Replayed request should fail */
if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject replayed packet.\n");
+ printf("Server should reject digest mismatch.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1071,10 +1687,31 @@ static int test_oap_replay_packet(void)
return TEST_RC_FAIL;
}
-/* Test that client rejects server with wrong certificate name */
-static int test_oap_server_name_mismatch(void)
+/* Naive substring search over raw bytes (memmem is not portable here). */
+static bool buf_contains(const uint8_t * hay,
+ size_t hlen,
+ const uint8_t * needle,
+ size_t nlen)
+{
+ size_t i;
+
+ if (nlen == 0 || nlen > hlen)
+ return false;
+
+ for (i = 0; i + nlen <= hlen; i++) {
+ if (memcmp(hay + i, needle, nlen) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+/* The server certificate must not appear in cleartext on the wire */
+static int test_oap_server_cert_hidden(void)
{
struct oap_test_ctx ctx;
+ void * crt = NULL;
+ buffer_t der = BUF_INIT;
test_default_cfg();
@@ -1083,8 +1720,79 @@ static int test_oap_server_name_mismatch(void)
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
goto fail;
- /* Set client's expected name to something different from cert name */
- strcpy(ctx.cli.info.name, "wrong.server.name");
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
+ printf("Failed to load server crt.\n");
+ goto fail_cleanup;
+ }
+
+ if (crypt_crt_der(crt, &der) < 0) {
+ printf("Failed to DER-encode server crt.\n");
+ goto fail_crt;
+ }
+
+ if (der.len == 0 || der.len > ctx.resp_hdr.len) {
+ printf("Unexpected cert/response sizes.\n");
+ goto fail_der;
+ }
+
+ if (buf_contains(ctx.resp_hdr.data, ctx.resp_hdr.len,
+ der.data, der.len)) {
+ printf("Server certificate found in cleartext.\n");
+ goto fail_der;
+ }
+
+ /* The handshake must still complete and agree on a key */
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_der;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_der;
+ }
+
+ freebuf(der);
+ crypt_free_crt(crt);
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_der:
+ freebuf(der);
+ fail_crt:
+ crypt_free_crt(crt);
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Tampering the sealed identity block fails the handshake */
+static int test_oap_sealed_tamper(void)
+{
+ struct oap_test_ctx ctx;
+ size_t pos;
+
+ test_default_cfg();
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
if (oap_cli_prepare_ctx(&ctx) < 0) {
printf("Client prepare failed.\n");
@@ -1096,15 +1804,119 @@ static int test_oap_server_name_mismatch(void)
goto fail_cleanup;
}
- /* Client should reject due to name mismatch */
+ if (ctx.resp_hdr.len < 64) {
+ printf("Response too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Flip a byte inside the sealed ciphertext, before the AEAD tag */
+ pos = ctx.resp_hdr.len - 32;
+ ctx.resp_hdr.data[pos] ^= 0xFF;
+
if (oap_cli_complete_ctx(&ctx) == 0) {
- printf("Client should reject server with wrong cert name.\n");
+ printf("Client accepted a tampered identity block.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Cleartext md-only: rsp_tag echoes H(request) and is the sole gate */
+static int test_oap_cleartext_echo_tamper(void)
+{
+ struct oap_test_ctx ctx;
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* rsp_tag is the trailing field of an unsealed, unsigned response */
+ ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a tampered request echo.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Client rejects a response whose session ID does not match the request */
+static int test_oap_response_id_tamper(void)
+{
+ struct oap_test_ctx ctx;
+
+ /* Cleartext md-only: no seal, so the ID check itself must reject. */
+ memset(&test_cfg, 0, sizeof(test_cfg));
+ test_cfg.srv.md = NID_sha256;
+ test_cfg.srv.auth = NO_AUTH;
+ test_cfg.cli.md = NID_sha256;
+ test_cfg.cli.auth = NO_AUTH;
+
+ TEST_START();
+
+ if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* The session ID is the first field of the fixed header. */
+ ctx.resp_hdr.data[0] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a mismatched response ID.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1123,33 +1935,54 @@ 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();
ret |= test_oap_roundtrip_kex_only();
ret |= test_oap_piggyback_data();
+ ret |= test_oap_rekey_all();
+ ret |= test_oap_rekey_badcache_all();
+ ret |= test_oap_rekey_srv_badcache_all();
ret |= test_oap_roundtrip_all();
ret |= test_oap_roundtrip_md_all();
ret |= test_oap_corrupted_request();
ret |= test_oap_corrupted_response();
+ ret |= test_oap_key_confirm_mismatch();
ret |= test_oap_truncated_request();
ret |= test_oap_inflated_length_field();
ret |= test_oap_deflated_length_field();
ret |= test_oap_nid_without_kex();
- ret |= test_oap_unsupported_nid();
+ ret |= test_oap_unsupported_nid_undefined();
+ ret |= test_oap_unsupported_nid_all();
ret |= test_oap_cipher_mismatch();
ret |= test_oap_srv_enc_cli_none();
ret |= test_oap_cli_enc_srv_none();
ret |= test_oap_cli_rejects_downgrade();
+ ret |= test_oap_cli_rejects_suite_swap();
ret |= test_oap_srv_rejects_weak_kex();
- ret |= test_oap_outdated_packet();
- ret |= test_oap_future_packet();
+ ret |= test_oap_ts_reject_all();
ret |= test_oap_replay_packet();
+ ret |= test_oap_missing_root_ca();
ret |= test_oap_server_name_mismatch();
+
+ ret |= test_oap_cli_requires_srv_auth();
+ ret |= test_oap_srv_requires_cli_auth();
+ ret |= test_oap_mutual_req_auth();
+
+
+ ret |= test_oap_cli_rejects_md_mismatch();
+ ret |= test_oap_srv_rejects_md_mismatch();
+
+ ret |= test_oap_server_cert_hidden();
+ ret |= test_oap_sealed_tamper();
+ ret |= test_oap_cleartext_echo_tamper();
+ ret |= test_oap_response_id_tamper();
#else
(void) test_oap_roundtrip_auth_only;
(void) test_oap_roundtrip_kex_only;
@@ -1160,6 +1993,7 @@ int oap_test(int argc,
(void) test_oap_roundtrip_md_all;
(void) test_oap_corrupted_request;
(void) test_oap_corrupted_response;
+ (void) test_oap_key_confirm_mismatch;
(void) test_oap_truncated_request;
(void) test_oap_inflated_length_field;
(void) test_oap_deflated_length_field;
@@ -1169,13 +2003,27 @@ int oap_test(int argc,
(void) test_oap_srv_enc_cli_none;
(void) test_oap_cli_enc_srv_none;
(void) test_oap_cli_rejects_downgrade;
+ (void) test_oap_cli_rejects_suite_swap;
(void) test_oap_srv_rejects_weak_kex;
(void) test_oap_outdated_packet;
(void) test_oap_future_packet;
(void) test_oap_replay_packet;
+ (void) test_oap_replay_generations;
+ (void) test_oap_missing_root_ca;
(void) test_oap_server_name_mismatch;
+ (void) test_oap_cli_requires_srv_auth;
+ (void) test_oap_srv_requires_cli_auth;
+ (void) test_oap_mutual_req_auth;
+ (void) test_oap_cli_rejects_md_mismatch;
+ (void) test_oap_srv_rejects_md_mismatch;
+ (void) test_oap_server_cert_hidden;
+ (void) test_oap_sealed_tamper;
+ (void) test_oap_rekey;
+ (void) test_oap_rekey_badcache;
ret = TEST_RC_SKIP;
#endif
+ crypt_cleanup();
+
return ret;
}
diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c
index f9e6bdb2..36712830 100644
--- a/src/irmd/oap/tests/oap_test_ml_dsa.c
+++ b/src/irmd/oap/tests/oap_test_ml_dsa.c
@@ -29,6 +29,7 @@
#include "config.h"
#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
#include <ouroboros/flow.h>
#include <ouroboros/name.h>
#include <ouroboros/random.h>
@@ -46,10 +47,12 @@
#include <openssl/evp.h>
#endif
-#define CLI_AUTH 1
-#define NO_CLI_AUTH 0
-#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP
-#define SRV_ENCAP KEM_MODE_SERVER_ENCAP
+#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP
+#define SRV_ENCAP KEM_MODE_SERVER_ENCAP
+
+/* Wire constants for inspecting the encoded kex_len field. */
+#define OAP_KEX_LEN_OFFSET 32
+#define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 1 = client encaps */
extern const uint16_t kex_supported_nids[];
extern const uint16_t md_supported_nids[];
@@ -179,6 +182,7 @@ int load_server_kem_pk(const char * name,
pk->data = malloc(test_kem_pk_len);
if (pk->data == NULL)
return -1;
+
memcpy(pk->data, test_kem_pk, test_kem_pk_len);
pk->len = test_kem_pk_len;
@@ -237,10 +241,89 @@ static int test_oap_roundtrip_auth_only(void)
return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
}
+/* Digest pin does not apply to PQC: the digest is intrinsic */
+static int test_oap_cli_md_pin_exempts_pqc(void)
+{
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, NO_AUTH);
+ test_cfg.cli.md = NID_sha256;
+
+ return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_srv_md_pin_exempts_pqc(void)
+{
+ test_cfg_init(NID_undef, NID_undef, NID_undef, 0, AUTH);
+ test_cfg.srv.md = NID_sha256;
+
+ return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_rekey(bool srv_auth,
+ bool cli_auth)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, cli_auth);
+ test_cfg.srv.auth = srv_auth;
+
+ return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml,
+ srv_auth, cli_auth);
+}
+
+static int test_oap_rekey_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey(AUTH, NO_AUTH);
+ ret |= test_oap_rekey(AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, AUTH);
+ ret |= test_oap_rekey(NO_AUTH, NO_AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_badcache(bool cli_auth)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, cli_auth);
+
+ return roundtrip_rekey_badcache(root_ca_crt_ml, im_ca_crt_ml,
+ cli_auth);
+}
+
+static int test_oap_rekey_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_badcache(NO_AUTH);
+ ret |= test_oap_rekey_badcache(AUTH);
+
+ return ret;
+}
+
+static int test_oap_rekey_srv_badcache(bool srv_auth)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, AUTH);
+ test_cfg.srv.auth = srv_auth;
+
+ return roundtrip_rekey_srv_badcache(root_ca_crt_ml, im_ca_crt_ml,
+ srv_auth);
+}
+
+static int test_oap_rekey_srv_badcache_all(void)
+{
+ int ret = 0;
+
+ ret |= test_oap_rekey_srv_badcache(AUTH);
+ ret |= test_oap_rekey_srv_badcache(NO_AUTH);
+
+ return ret;
+}
+
static int test_oap_corrupted_request(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, CLI_AUTH);
+ SRV_ENCAP, AUTH);
return corrupted_request(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -248,7 +331,7 @@ static int test_oap_corrupted_request(void)
static int test_oap_corrupted_response(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, NO_CLI_AUTH);
+ SRV_ENCAP, NO_AUTH);
return corrupted_response(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -256,7 +339,7 @@ static int test_oap_corrupted_response(void)
static int test_oap_truncated_request(void)
{
test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(),
- SRV_ENCAP, NO_CLI_AUTH);
+ SRV_ENCAP, NO_AUTH);
return truncated_request(root_ca_crt_ml, im_ca_crt_ml);
}
@@ -269,7 +352,7 @@ static int test_oap_roundtrip_kem(int kex,
const char * mode_str = kem_mode == CLI_ENCAP ? "cli" : "srv";
test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(),
- kem_mode, NO_CLI_AUTH);
+ kem_mode, NO_AUTH);
TEST_START("(%s, %s encaps)", kex_str, mode_str);
@@ -332,6 +415,231 @@ static int test_oap_roundtrip_kem_all(void)
return ret;
}
+/* Re-key over a KEM KEX: forced ephemeral server-encap + cert-drop cache. */
+static int test_oap_rekey_kem(int kex,
+ int kem_mode)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+ const char * mode_str = "srv";
+ uint8_t key0[SYMMKEYSZ];
+
+ if (kem_mode == CLI_ENCAP)
+ mode_str = "cli";
+
+ test_cfg_init(kex, NID_aes_256_gcm, get_random_kdf(),
+ kem_mode, NO_AUTH);
+
+ TEST_START("(%s, %s encaps)", kex_str, mode_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Initial keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached for re-key.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(key0, ctx.cli.key, SYMMKEYSZ);
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Re-key keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, key0, SYMMKEYSZ) == 0) {
+ printf("Re-key did not produce a fresh key.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s, %s encaps)", kex_str, mode_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s, %s encaps)", kex_str, mode_str);
+ return TEST_RC_FAIL;
+}
+
+static int test_oap_rekey_kem_all(void)
+{
+ int ret = 0;
+ int i;
+
+ for (i = 0; kex_supported_nids[i] != NID_undef; i++) {
+ const char * algo = kex_nid_to_str(kex_supported_nids[i]);
+
+ if (!IS_KEM_ALGORITHM(algo))
+ continue;
+
+ ret |= test_oap_rekey_kem(kex_supported_nids[i], SRV_ENCAP);
+ ret |= test_oap_rekey_kem(kex_supported_nids[i], CLI_ENCAP);
+ }
+
+ return ret;
+}
+
+/*
+ * Client-encap bakes the KDF into the ciphertext, so the server cannot
+ * upgrade it: a client KDF weaker than the server floor must be rejected.
+ */
+static int test_oap_kem_kdf_floor(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+
+ test_cfg_init(kex, NID_aes_256_gcm, NID_sha256,
+ CLI_ENCAP, NO_AUTH);
+ test_cfg.srv.kdf = NID_sha512;
+ test_cfg.cli.kdf = NID_sha256;
+
+ TEST_START("(%s)", kex_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server accepted a client KDF below its floor.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
+/*
+ * A client-encap flow re-keys to ephemeral server-encap, so it keeps
+ * forward secrecy. The re-key request must advertise server-encap
+ * (kex_len Role bit clear) rather than re-using client encapsulation.
+ */
+static int test_oap_rekey_kem_forcing(int kex)
+{
+ struct oap_test_ctx ctx;
+ const char * kex_str = kex_nid_to_str(kex);
+ uint16_t kex_len;
+
+ test_cfg_init(kex, NID_aes_256_gcm, NID_sha256,
+ CLI_ENCAP, NO_AUTH);
+
+ TEST_START("(%s)", kex_str);
+
+ if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0)
+ goto fail;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ memcpy(&kex_len, ctx.req_hdr.data + OAP_KEX_LEN_OFFSET,
+ sizeof(kex_len));
+ kex_len = ntoh16(kex_len);
+
+ if (kex_len & OAP_KEX_ROLE_BIT) {
+ printf("Re-key did not force server-encap KEX.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown_kem(&ctx);
+
+ TEST_SUCCESS("(%s)", kex_str);
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown_kem(&ctx);
+ fail:
+ TEST_FAIL("(%s)", kex_str);
+ return TEST_RC_FAIL;
+}
+
static int test_oap_kem_srv_uncfg(int kex)
{
struct oap_test_ctx ctx;
@@ -420,8 +728,10 @@ int oap_test_ml_dsa(int argc,
(void) argc;
(void) argv;
-#ifdef HAVE_OPENSSL_ML_KEM
+#ifdef HAVE_ML
ret |= test_oap_roundtrip_auth_only();
+ ret |= test_oap_cli_md_pin_exempts_pqc();
+ ret |= test_oap_srv_md_pin_exempts_pqc();
ret |= test_oap_roundtrip_kem_all();
@@ -430,10 +740,29 @@ int oap_test_ml_dsa(int argc,
ret |= test_oap_corrupted_request();
ret |= test_oap_corrupted_response();
ret |= test_oap_truncated_request();
+
+ ret |= test_oap_rekey_all();
+ ret |= test_oap_rekey_badcache_all();
+ ret |= test_oap_rekey_srv_badcache_all();
+ ret |= test_oap_rekey_kem_all();
+ ret |= test_oap_kem_kdf_floor(NID_MLKEM768);
+ ret |= test_oap_rekey_kem_forcing(NID_MLKEM768);
#else
(void) test_oap_roundtrip_auth_only;
+ (void) test_oap_cli_md_pin_exempts_pqc;
+ (void) test_oap_srv_md_pin_exempts_pqc;
+ (void) test_oap_rekey;
+ (void) test_oap_rekey_all;
+ (void) test_oap_rekey_badcache;
+ (void) test_oap_rekey_badcache_all;
+ (void) test_oap_rekey_srv_badcache;
+ (void) test_oap_rekey_srv_badcache_all;
(void) test_oap_roundtrip_kem;
(void) test_oap_roundtrip_kem_all;
+ (void) test_oap_rekey_kem;
+ (void) test_oap_rekey_kem_all;
+ (void) test_oap_kem_kdf_floor;
+ (void) test_oap_rekey_kem_forcing;
(void) test_oap_kem_srv_uncfg;
(void) test_oap_kem_srv_uncfg_all;
(void) test_oap_corrupted_request;
@@ -442,6 +771,7 @@ int oap_test_ml_dsa(int argc,
ret = TEST_RC_SKIP;
#endif
+ crypt_cleanup();
return ret;
}
diff --git a/src/irmd/reg/flow.c b/src/irmd/reg/flow.c
index 93c3e128..8be2dfc7 100644
--- a/src/irmd/reg/flow.c
+++ b/src/irmd/reg/flow.c
@@ -24,6 +24,7 @@
#define OUROBOROS_PREFIX "reg/flow"
+#include <ouroboros/crypt.h>
#include <ouroboros/logs.h>
#include "flow.h"
@@ -32,6 +33,7 @@
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
+#include <string.h>
struct reg_flow * reg_flow_create(const struct flow_info * info)
{
@@ -42,6 +44,7 @@ struct reg_flow * reg_flow_create(const struct flow_info * info)
assert(info->n_pid != 0);
assert(info->n_1_pid == 0);
assert(info->mpl == 0);
+ assert(info->mtu == 0);
assert(info->state == FLOW_INIT);
flow = malloc(sizeof(*flow));
@@ -67,10 +70,12 @@ static void destroy_rbuffs(struct reg_flow * flow)
{
if (flow->n_rb != NULL)
ssm_rbuff_destroy(flow->n_rb);
+
flow->n_rb = NULL;
if (flow->n_1_rb != NULL)
ssm_rbuff_destroy(flow->n_1_rb);
+
flow->n_1_rb = NULL;
}
@@ -78,6 +83,11 @@ void reg_flow_destroy(struct reg_flow * flow)
{
assert(flow != NULL);
+ if (flow->rk.pending_seed != NULL)
+ crypt_secure_free(flow->rk.pending_seed, SYMMKEYSZ);
+
+ freebuf(flow->rk.peer_crt);
+
switch(flow->info.state) {
case FLOW_ACCEPT_PENDING:
clrbuf(flow->req_data);
@@ -160,6 +170,7 @@ int reg_flow_update(struct reg_flow * flow,
assert(info->mpl != 0);
flow->info.mpl = info->mpl;
+ flow->info.mtu = info->mtu;
if (flow->info.state == FLOW_ALLOC_PENDING)
break;
diff --git a/src/irmd/reg/flow.h b/src/irmd/reg/flow.h
index 9a4046d3..166bed61 100644
--- a/src/irmd/reg/flow.h
+++ b/src/irmd/reg/flow.h
@@ -49,6 +49,22 @@ struct reg_flow {
bool direct;
+ /* Tier-2 re-key state (encrypted flows only) */
+ struct {
+ bool encrypted; /* flow carries a cipher */
+ uint8_t epoch; /* last epoch installed by app */
+ bool initiator; /* OAP initiator (role 0) */
+ bool in_flight; /* a re-key is in progress */
+ bool req_queued; /* a peer REQ is in the inbox */
+ bool resp_queued; /* a peer RESP is in the inbox */
+ uint8_t * pending_seed; /* secure heap; NULL until set */
+ uint8_t pending_epoch;
+ bool pending_initiator; /* pending seed: oap_cli side */
+ bool has_pending; /* new seed awaits app pull */
+ uint8_t pulled; /* direct: per-app pull mask */
+ buffer_t peer_crt; /* peer cert DER, cached at HS */
+ } rk;
+
struct ssm_rbuff * n_rb;
struct ssm_rbuff * n_1_rb;
};
diff --git a/src/irmd/reg/reg.c b/src/irmd/reg/reg.c
index 9ee47221..ebf3959d 100644
--- a/src/irmd/reg/reg.c
+++ b/src/irmd/reg/reg.c
@@ -25,6 +25,7 @@ The IPC Resource Manager - Registry
#define OUROBOROS_PREFIX "reg"
#include <ouroboros/bitmap.h>
+#include <ouroboros/crypt.h>
#include <ouroboros/errno.h>
#include <ouroboros/list.h>
#include <ouroboros/logs.h>
@@ -871,6 +872,7 @@ int reg_list_ipcps(ipcp_list_msg_t *** ipcps)
fail:
while (i-- > 0)
ipcp_list_msg__free_unpacked((*ipcps)[i], NULL);
+
free(*ipcps);
fail_malloc:
pthread_mutex_unlock(&reg.mtx);
@@ -1032,6 +1034,20 @@ int reg_get_name_for_flow_id(char * buf,
return f == NULL ? -ENOENT : 0;
}
+void reg_set_name_for_flow_id(const char * name,
+ int flow_id)
+{
+ struct reg_flow * f;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ f = __reg_get_flow(flow_id);
+ if (f != NULL)
+ strcpy(f->name, name);
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
int reg_list_names(name_info_msg_t *** names)
{
struct list_head * p;
@@ -1076,6 +1092,7 @@ int reg_list_names(name_info_msg_t *** names)
fail:
while (i-- > 0)
name_info_msg__free_unpacked((*names)[i], NULL);
+
free(*names);
fail_malloc:
pthread_mutex_unlock(&reg.mtx);
@@ -1820,7 +1837,11 @@ int reg_respond_alloc(struct flow_info * info,
goto fail_flow;
}
- assert(flow->info.state == FLOW_ALLOC_PENDING);
+ if (flow->info.state != FLOW_ALLOC_PENDING) {
+ log_warn("Flow %d already responded.", info->id);
+ goto fail_flow;
+ }
+
assert(flow->rsp_data.len == 0);
assert(flow->rsp_data.data == NULL);
@@ -2008,11 +2029,7 @@ int reg_respond_accept(struct flow_info * info,
info->n_pid = flow->info.n_pid;
-<<<<<<< HEAD
flow->req_data = *pbuf;
-=======
- reg_flow_set_data(flow, pbuf);
->>>>>>> c82f0de4 (irmd: Fix memleak in reg tests)
clrbuf(*pbuf);
if (reg_flow_update(flow, info) < 0) {
@@ -2102,6 +2119,511 @@ bool reg_flow_is_direct(int flow_id)
return ret;
}
+void reg_flow_set_rekey(int flow_id,
+ bool initiator,
+ buffer_t peer_crt)
+{
+ struct reg_flow * flow;
+ uint8_t * crt = NULL;
+
+ /* Copy the cert outside the lock; publish it with rk.encrypted. */
+ if (peer_crt.len > 0) {
+ crt = malloc(peer_crt.len);
+ if (crt != NULL)
+ memcpy(crt, peer_crt.data, peer_crt.len);
+ else
+ log_warn("Failed to cache peer cert for re-key.");
+ }
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ flow->rk.encrypted = true;
+ flow->rk.initiator = initiator;
+ flow->rk.epoch = 0;
+ if (crt != NULL) {
+ freebuf(flow->rk.peer_crt);
+ flow->rk.peer_crt.data = crt;
+ flow->rk.peer_crt.len = peer_crt.len;
+ crt = NULL;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ free(crt);
+}
+
+int reg_flow_get_peer_crt(int flow_id,
+ buffer_t * crt)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ assert(crt != NULL);
+
+ clrbuf(*crt);
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.peer_crt.len > 0) {
+ crt->data = malloc(flow->rk.peer_crt.len);
+ if (crt->data == NULL) {
+ ret = -ENOMEM;
+ } else {
+ memcpy(crt->data, flow->rk.peer_crt.data,
+ flow->rk.peer_crt.len);
+ crt->len = flow->rk.peer_crt.len;
+ ret = 0;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_get_epoch(int flow_id)
+{
+ struct reg_flow * flow;
+ int epoch = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted)
+ epoch = flow->rk.epoch;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return epoch;
+}
+
+bool reg_flow_rekey_pending(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.has_pending;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+pid_t reg_flow_get_n_1_pid(int flow_id)
+{
+ struct reg_flow * flow;
+ pid_t pid = -1;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ pid = flow->info.n_1_pid;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return pid;
+}
+
+int reg_flow_snapshot_rekey_due(struct rekey_info * snap,
+ int max)
+{
+ struct list_head * p;
+ int n = 0;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ llist_for_each(p, &reg.flows) {
+ struct reg_flow * f;
+
+ if (n == max)
+ break;
+
+ f = list_entry(p, struct reg_flow, next);
+
+ if (f->info.state != FLOW_ALLOCATED)
+ continue;
+
+ if (!f->rk.encrypted)
+ continue;
+
+ /* Direct flows have no IPCP initiator; either side drives. */
+ if (!f->direct && !f->rk.initiator)
+ continue;
+
+ if (f->rk.in_flight || f->rk.has_pending)
+ continue;
+
+ f->rk.in_flight = true;
+
+ snap[n].flow_id = f->info.id;
+ snap[n].n_pid = f->info.n_pid;
+ snap[n].n_1_pid = f->info.n_1_pid;
+ snap[n].epoch = f->rk.epoch;
+ snap[n].direct = f->direct;
+ strcpy(snap[n].name, f->name);
+ ++n;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return n;
+}
+
+void reg_flow_clear_in_flight(int flow_id)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ flow->rk.in_flight = false;
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+/* Test-and-set the in-flight latch; refuse if a re-key is already active. */
+bool reg_flow_rekey_begin(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted) {
+ if (!flow->rk.in_flight && !flow->rk.has_pending) {
+ flow->rk.in_flight = true;
+ ret = true;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Initiator yields the responder role while driving its own exchange. */
+bool reg_flow_rekey_should_yield(int flow_id)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = flow->rk.initiator && flow->rk.in_flight;
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+int reg_flow_store_pending(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch,
+ bool initiator)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ /* Exchange done: release the latch regardless of parking. */
+ flow->rk.in_flight = false;
+
+ if (flow->rk.pending_seed == NULL)
+ flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ);
+
+ if (flow->rk.pending_seed != NULL) {
+ memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ);
+ flow->rk.pending_epoch = epoch;
+ flow->rk.pending_initiator = initiator;
+ flow->rk.has_pending = true;
+ /* Doorbell raised only after the seed is parked. */
+ if (flow->n_rb != NULL)
+ ssm_rbuff_set_bits(flow->n_rb, RB_REKEY);
+ ret = 0;
+ } else {
+ ret = -ENOMEM;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Direct re-key: which of the two local apps has pulled the seed. */
+#define RK_N_PID 0x1 /* acceptor (n_pid) pulled the seed */
+#define RK_N_1_PID 0x2 /* allocator (n_1_pid) pulled the seed */
+#define RK_PID_MASK (RK_N_PID | RK_N_1_PID)
+
+/*
+ * Park a single re-key seed for a direct flow and ring BOTH apps'
+ * doorbells. The seed is the one shared secret; each app pulls it once
+ * (reg_flow_take_pending), so it is held until both have taken it.
+ */
+int reg_flow_store_pending_direct(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL)
+ goto out;
+
+ /* Exchange done: release the latch regardless of parking. */
+ flow->rk.in_flight = false;
+
+ if (flow->rk.pending_seed == NULL)
+ flow->rk.pending_seed = crypt_secure_malloc(SYMMKEYSZ);
+
+ if (flow->rk.pending_seed == NULL) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ memcpy(flow->rk.pending_seed, seed, SYMMKEYSZ);
+ flow->rk.pending_epoch = epoch;
+ flow->rk.has_pending = true;
+ flow->rk.pulled = 0;
+
+ /* A departed peer never pulls; treat its side as already done. */
+ if (flow->info.n_pid <= 0)
+ flow->rk.pulled |= RK_N_PID;
+
+ if (flow->info.n_1_pid <= 0)
+ flow->rk.pulled |= RK_N_1_PID;
+
+ if (flow->n_rb != NULL && !(flow->rk.pulled & RK_N_PID))
+ ssm_rbuff_set_bits(flow->n_rb, RB_REKEY);
+
+ if (flow->n_1_rb != NULL && !(flow->rk.pulled & RK_N_1_PID))
+ ssm_rbuff_set_bits(flow->n_1_rb, RB_REKEY);
+
+ ret = 0;
+ out:
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* A caller may act on a flow if it is privileged or owns the flow. */
+static bool uid_may_access(uid_t caller,
+ uid_t owner)
+{
+ return is_ouroboros_member_uid(caller) || caller == owner;
+}
+
+/*
+ * Caller holds reg.mtx. The direct seed is shared by both apps, so the
+ * per-app initiator role is resolved from the verified caller pid (the
+ * allocator is n_1_pid), and the seed is held until both have pulled.
+ */
+static void __take_pending_direct(struct reg_flow * flow,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator)
+{
+ bool allocator;
+
+ allocator = cpid == flow->info.n_1_pid;
+
+ memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ);
+ *epoch = flow->rk.pending_epoch;
+ *initiator = allocator;
+ flow->rk.epoch = flow->rk.pending_epoch;
+
+ if (allocator) {
+ flow->rk.pulled |= RK_N_1_PID;
+ if (flow->n_1_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_1_rb, RB_REKEY);
+ } else {
+ flow->rk.pulled |= RK_N_PID;
+ if (flow->n_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_rb, RB_REKEY);
+ }
+
+ if ((flow->rk.pulled & RK_PID_MASK) != RK_PID_MASK)
+ return;
+
+ flow->rk.has_pending = false;
+ flow->rk.pulled = 0;
+ crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ);
+}
+
+int reg_flow_take_pending(int flow_id,
+ uid_t uid,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator)
+{
+ struct reg_flow * flow;
+ int ret = -ENOENT;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow == NULL || !flow->rk.has_pending)
+ goto out;
+
+ if (!uid_may_access(uid, flow->info.uid)) {
+ ret = -EPERM;
+ goto out;
+ }
+
+ if (flow->direct) {
+ __take_pending_direct(flow, cpid, seed, epoch, initiator);
+ ret = 0;
+ goto out;
+ }
+
+ memcpy(seed, flow->rk.pending_seed, SYMMKEYSZ);
+ *epoch = flow->rk.pending_epoch;
+ *initiator = flow->rk.pending_initiator;
+ flow->rk.epoch = flow->rk.pending_epoch;
+ flow->rk.has_pending = false;
+ crypt_secure_clear(flow->rk.pending_seed, SYMMKEYSZ);
+ if (flow->n_rb != NULL)
+ ssm_rbuff_clr_bits(flow->n_rb, RB_REKEY);
+
+ ret = 0;
+ out:
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/*
+ * Admit a peer-driven re-key arrival before a worker event is allocated:
+ * the flow must exist, carry a cipher, and the update must come from its
+ * own lower IPCP. Coalesces to one queued REQ and one queued RESP per flow
+ * so a flooding peer cannot grow the inbox without bound.
+ */
+bool reg_flow_rekey_arr_admit(int flow_id,
+ pid_t n_1_pid,
+ bool is_req)
+{
+ struct reg_flow * flow;
+ bool admit = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL && flow->rk.encrypted
+ && flow->info.n_1_pid == n_1_pid) {
+ if (is_req && !flow->rk.req_queued) {
+ flow->rk.req_queued = true;
+ admit = true;
+ } else if (!is_req && flow->rk.in_flight
+ && !flow->rk.resp_queued) {
+ flow->rk.resp_queued = true;
+ admit = true;
+ }
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return admit;
+}
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ if (is_req)
+ flow->rk.req_queued = false;
+ else
+ flow->rk.resp_queued = false;
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid)
+{
+ struct reg_flow * flow;
+ bool ret = false;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ ret = uid_may_access(uid, flow->info.uid);
+
+ pthread_mutex_unlock(&reg.mtx);
+
+ return ret;
+}
+
+/* Caller holds reg.mtx. */
+static void __notify_proc(pid_t pid,
+ int flow_id,
+ int event)
+{
+ struct reg_proc * proc;
+
+ proc = __reg_get_proc(pid);
+ if (proc != NULL)
+ ssm_flow_set_notify(proc->set, flow_id, event);
+}
+
+void reg_notify_flow(int flow_id,
+ int event)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL)
+ __notify_proc(flow->info.n_pid, flow_id, event);
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
+/* Wake both endpoints of a direct flow (acceptor and allocator). */
+void reg_notify_flow_peers(int flow_id,
+ int event)
+{
+ struct reg_flow * flow;
+
+ pthread_mutex_lock(&reg.mtx);
+
+ flow = __reg_get_flow(flow_id);
+ if (flow != NULL) {
+ __notify_proc(flow->info.n_pid, flow_id, event);
+ __notify_proc(flow->info.n_1_pid, flow_id, event);
+ }
+
+ pthread_mutex_unlock(&reg.mtx);
+}
+
int reg_respond_flow_direct(int flow_id,
buffer_t * pbuf)
{
diff --git a/src/irmd/reg/reg.h b/src/irmd/reg/reg.h
index 6b576471..8a313d46 100644
--- a/src/irmd/reg/reg.h
+++ b/src/irmd/reg/reg.h
@@ -109,6 +109,9 @@ int reg_get_name_for_hash(char * buf,
int reg_get_name_for_flow_id(char * buf,
int flow_id);
+void reg_set_name_for_flow_id(const char * name,
+ int flow_id);
+
/* TODO don't rely on protobuf here */
int reg_list_names(name_info_msg_t *** names);
@@ -163,6 +166,70 @@ int reg_wait_flow_direct(int flow_id,
bool reg_flow_is_direct(int flow_id);
+/* Per-flow snapshot for the re-key timer */
+struct rekey_info {
+ int flow_id;
+ pid_t n_pid;
+ pid_t n_1_pid;
+ char name[NAME_SIZE + 1];
+ uint8_t epoch;
+ bool direct;
+};
+
+void reg_flow_set_rekey(int flow_id,
+ bool initiator,
+ buffer_t peer_crt);
+
+int reg_flow_get_peer_crt(int flow_id,
+ buffer_t * crt);
+
+int reg_flow_get_epoch(int flow_id);
+
+bool reg_flow_rekey_pending(int flow_id);
+
+pid_t reg_flow_get_n_1_pid(int flow_id);
+
+int reg_flow_snapshot_rekey_due(struct rekey_info * snap,
+ int max);
+
+void reg_flow_clear_in_flight(int flow_id);
+
+bool reg_flow_rekey_begin(int flow_id);
+
+bool reg_flow_rekey_should_yield(int flow_id);
+
+int reg_flow_store_pending(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch,
+ bool initiator);
+
+int reg_flow_store_pending_direct(int flow_id,
+ const uint8_t * seed,
+ uint8_t epoch);
+
+int reg_flow_take_pending(int flow_id,
+ uid_t uid,
+ pid_t cpid,
+ uint8_t * seed,
+ uint8_t * epoch,
+ bool * initiator);
+
+bool reg_flow_rekey_arr_admit(int flow_id,
+ pid_t n_1_pid,
+ bool is_req);
+
+void reg_flow_rekey_arr_done(int flow_id,
+ bool is_req);
+
+bool reg_flow_owned_by(int flow_id,
+ uid_t uid);
+
+void reg_notify_flow(int flow_id,
+ int event);
+
+void reg_notify_flow_peers(int flow_id,
+ int event);
+
void reg_dealloc_flow(struct flow_info * info);
void reg_dealloc_flow_resp(struct flow_info * info);
diff --git a/src/irmd/reg/tests/flow_test.c b/src/irmd/reg/tests/flow_test.c
index 7e1c1360..18214078 100644
--- a/src/irmd/reg/tests/flow_test.c
+++ b/src/irmd/reg/tests/flow_test.c
@@ -122,6 +122,21 @@ static int test_reg_flow_create_has_mpl(void) {
return TEST_RC_SUCCESS;
}
+static int test_reg_flow_create_has_mtu(void) {
+ struct flow_info info = {
+ .id = 1,
+ .n_pid = 1,
+ .n_1_pid = 0,
+ .mtu = 1400,
+ .qs = qos_raw,
+ .state = FLOW_ALLOC_PENDING
+ };
+
+ reg_flow_create(&info); /* assert fail */
+
+ return TEST_RC_SUCCESS;
+}
+
static int test_reg_flow_update(void)
{
struct reg_flow * f;
@@ -136,7 +151,7 @@ static int test_reg_flow_update(void)
struct flow_info upd = {
.id = 1,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -179,7 +194,7 @@ static int test_reg_flow_update_wrong_id(void)
struct flow_info upd = {
.id = 2,
.n_pid = 1,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED
};
@@ -210,6 +225,7 @@ static int test_reg_flow_assert_fails(void)
ret |= test_assert_fail(test_reg_flow_create_has_n_1_pid);
ret |= test_assert_fail(test_reg_flow_create_wrong_state);
ret |= test_assert_fail(test_reg_flow_create_has_mpl);
+ ret |= test_assert_fail(test_reg_flow_create_has_mtu);
ret |= test_assert_fail(test_reg_flow_update_wrong_id);
return ret;
diff --git a/src/irmd/reg/tests/reg_test.c b/src/irmd/reg/tests/reg_test.c
index b426c0dd..a8c1b1fa 100644
--- a/src/irmd/reg/tests/reg_test.c
+++ b/src/irmd/reg/tests/reg_test.c
@@ -31,6 +31,7 @@
#define TEST_N_1_PID 3999
#define TEST_FAKE_ID 9128349
#define TEST_MPL 5
+#define TEST_MTU 1400
#define TEST_PROG "reg_test" /* own binary for binary check */
#define TEST_IPCP "testipcp"
#define TEST_NAME "testname"
@@ -239,7 +240,7 @@ static int test_reg_accept_flow_success(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -266,6 +267,7 @@ static int test_reg_accept_flow_success(void)
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
@@ -284,6 +286,11 @@ static int test_reg_accept_flow_success(void)
goto fail;
}
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
if (rbuf.data == NULL) {
printf("rbuf data not returned.\n");
goto fail;
@@ -336,7 +343,7 @@ static int test_reg_accept_flow_success_no_crypt(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
};
@@ -363,6 +370,7 @@ static int test_reg_accept_flow_success_no_crypt(void)
n_1_info.id = info.id;
n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
pthread_create(&thr, NULL, test_flow_respond_accept, &n_1_info);
@@ -381,6 +389,11 @@ static int test_reg_accept_flow_success_no_crypt(void)
goto fail;
}
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
if (rbuf.data == NULL) {
printf("rbuf data was not returned.\n");
goto fail;
@@ -431,7 +444,7 @@ static int test_reg_allocate_flow_fail(void)
struct flow_info n_1_info = {
.n_1_pid = TEST_N_1_PID,
- .qs = qos_data,
+ .qs = qos_msg,
.state = FLOW_DEALLOCATED /* RESPONSE FAIL */
};
@@ -489,6 +502,93 @@ static int test_reg_allocate_flow_fail(void)
return TEST_RC_FAIL;
}
+static int test_reg_respond_alloc_duplicate(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t empty = BUF_INIT;
+ struct flow_info dup_info;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ struct flow_info n_1_info = {
+ .n_1_pid = TEST_N_1_PID,
+ .qs = qos_msg,
+ .state = FLOW_ALLOCATED /* RESPONSE SUCCESS */
+ };
+
+ TEST_START();
+
+ clock_gettime(PTHREAD_COND_CLOCK, &abstime);
+ ts_add(&abstime, &timeo, &abstime);
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ if (reg_create_flow(&info) < 0) {
+ printf("Failed to add flow.\n");
+ goto fail;
+ }
+
+ info.n_1_pid = TEST_N_1_PID;
+
+ if (reg_prepare_flow_alloc(&info) < 0) {
+ printf("Failed to prepare flow for alloc.\n");
+ goto fail;
+ }
+
+ n_1_info.id = info.id;
+ n_1_info.mpl = 1;
+ n_1_info.mtu = TEST_MTU;
+
+ pthread_create(&thr, NULL, test_flow_respond_alloc, &n_1_info);
+
+ if (reg_wait_flow_allocated(&info, &rbuf, &abstime) < 0) {
+ printf("Flow allocation failed.\n");
+ pthread_join(thr, NULL);
+ reg_destroy_flow(info.id);
+ reg_fini();
+ goto fail;
+ }
+
+ pthread_join(thr, NULL);
+ freebuf(rbuf);
+
+ if (info.mtu != TEST_MTU) {
+ printf("MTU not propagated.\n");
+ goto fail;
+ }
+
+ /* Duplicate reply on an already-ALLOCATED flow must not assert. */
+ dup_info = n_1_info;
+ dup_info.state = FLOW_DEALLOCATED;
+
+ if (reg_respond_alloc(&dup_info, &empty, -EREPLAY) != -1) {
+ printf("Duplicate respond_alloc should return -1.\n");
+ goto fail;
+ }
+
+ reg_dealloc_flow(&info);
+ reg_dealloc_flow_resp(&info);
+ reg_destroy_flow(n_1_info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
struct direct_alloc_info {
struct flow_info info;
buffer_t rsp;
@@ -564,7 +664,7 @@ static int test_reg_direct_flow_success(void)
dai.info.id = info.id;
dai.info.n_1_pid = TEST_N_1_PID;
dai.info.mpl = TEST_MPL;
- dai.info.qs = qos_data;
+ dai.info.qs = qos_msg;
dai.info.state = FLOW_ALLOCATED;
dai.rsp.len = 0;
dai.rsp.data = NULL;
@@ -671,6 +771,167 @@ static int test_reg_direct_flow_success(void)
return TEST_RC_FAIL;
}
+/*
+ * Direct-flow re-key: one shared seed is parked for both local apps. The
+ * per-app initiator role is resolved from the verified caller pid (the
+ * allocator is n_1_pid), and the seed is held until both have pulled it.
+ */
+static int test_reg_direct_flow_rekey(void)
+{
+ pthread_t thr;
+ struct timespec abstime;
+ struct timespec timeo = TIMESPEC_INIT_S(1);
+ buffer_t rbuf = BUF_INIT;
+ buffer_t rsp;
+ buffer_t no_crt = BUF_INIT;
+ struct direct_alloc_info dai;
+ uint8_t seed[SYMMKEYSZ];
+ uint8_t out[SYMMKEYSZ];
+ uint8_t epoch;
+ bool initiator;
+ size_t i;
+
+ struct flow_info info = {
+ .n_pid = TEST_PID,
+ .qs = qos_raw
+ };
+
+ TEST_START();
+
+ for (i = 0; i < SYMMKEYSZ; ++i)
+ seed[i] = (uint8_t) i;
+
+ clock_gettime(PTHREAD_COND_CLOCK, &abstime);
+
+ ts_add(&abstime, &timeo, &abstime);
+
+ if (reg_init() < 0) {
+ printf("Failed to init registry.\n");
+ goto fail;
+ }
+
+ if (reg_create_flow(&info) < 0) {
+ printf("Failed to add flow.\n");
+ goto fail;
+ }
+
+ if (reg_prepare_flow_accept(&info) < 0) {
+ printf("Failed to prepare for accept.\n");
+ goto fail;
+ }
+
+ dai.info.id = info.id;
+ dai.info.n_1_pid = TEST_N_1_PID;
+ dai.info.mpl = TEST_MPL;
+ dai.info.qs = qos_msg;
+ dai.info.state = FLOW_ALLOCATED;
+ dai.rsp.len = 0;
+ dai.rsp.data = NULL;
+ dai.abstime = abstime;
+
+ pthread_create(&thr, NULL, test_flow_alloc_direct, &dai);
+
+ if (reg_wait_flow_accepted(&info, &rbuf, &abstime) < 0) {
+ printf("Flow accept failed.\n");
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+
+ freebuf(rbuf);
+
+ rsp.data = (uint8_t *) strdup(TEST_DATA2);
+ if (rsp.data == NULL) {
+ printf("Failed to strdup rsp data.\n");
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+ rsp.len = strlen(TEST_DATA2) + 1;
+
+ if (reg_respond_flow_direct(info.id, &rsp) < 0) {
+ printf("Failed to respond direct.\n");
+ freebuf(rsp);
+ pthread_join(thr, NULL);
+ goto fail;
+ }
+
+ pthread_join(thr, NULL);
+
+ freebuf(dai.rsp);
+
+ if (!reg_flow_is_direct(info.id)) {
+ printf("Flow not marked direct.\n");
+ goto fail;
+ }
+
+ reg_flow_set_rekey(info.id, false, no_crt);
+
+ if (reg_flow_store_pending_direct(info.id, seed, 5) < 0) {
+ printf("Failed to store pending direct seed.\n");
+ goto fail;
+ }
+
+ if (!reg_flow_rekey_pending(info.id)) {
+ printf("Seed not pending after store.\n");
+ goto fail;
+ }
+
+ /* Allocator (n_1_pid) pulls: initiator role, seed still held. */
+ if (reg_flow_take_pending(info.id, 0, TEST_N_1_PID, out,
+ &epoch, &initiator) != 0) {
+ printf("Allocator failed to take pending seed.\n");
+ goto fail;
+ }
+
+ if (!initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) {
+ printf("Allocator got wrong seed/role/epoch.\n");
+ goto fail;
+ }
+
+ if (!reg_flow_rekey_pending(info.id)) {
+ printf("Seed cleared before both apps pulled.\n");
+ goto fail;
+ }
+
+ /* Acceptor (n_pid) pulls: responder role, seed now released. */
+ if (reg_flow_take_pending(info.id, 0, TEST_PID, out,
+ &epoch, &initiator) != 0) {
+ printf("Acceptor failed to take pending seed.\n");
+ goto fail;
+ }
+
+ if (initiator || epoch != 5 || memcmp(out, seed, SYMMKEYSZ) != 0) {
+ printf("Acceptor got wrong seed/role/epoch.\n");
+ goto fail;
+ }
+
+ if (reg_flow_rekey_pending(info.id)) {
+ printf("Seed still pending after both pulled.\n");
+ goto fail;
+ }
+
+ if (reg_flow_get_epoch(info.id) != 5) {
+ printf("Flow epoch not advanced.\n");
+ goto fail;
+ }
+
+ info.n_pid = TEST_PID;
+ reg_dealloc_flow(&info);
+
+ info.n_pid = TEST_N_1_PID;
+ reg_dealloc_flow(&info);
+
+ reg_destroy_flow(info.id);
+
+ reg_fini();
+
+ TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+ fail:
+ REG_TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
static int test_reg_flow(void) {
int rc = 0;
@@ -679,7 +940,9 @@ static int test_reg_flow(void) {
rc |= test_reg_accept_flow_success();
rc |= test_reg_accept_flow_success_no_crypt();
rc |= test_reg_allocate_flow_fail();
+ rc |= test_reg_respond_alloc_duplicate();
rc |= test_reg_direct_flow_success();
+ rc |= test_reg_direct_flow_rekey();
return rc;
}
@@ -774,6 +1037,7 @@ static int test_reg_list_ipcps(void)
while (len-- > 0)
ipcp_list_msg__free_unpacked(ipcps[len], NULL);
+
free(ipcps);
for (i = 0; i < 10; i++)
@@ -840,6 +1104,7 @@ static int test_insert_ipcps(void)
while (len-- > 0)
ipcp_list_msg__free_unpacked(ipcps[len], NULL);
+
free(ipcps);
reg_clear();
@@ -1017,6 +1282,7 @@ static int test_reg_list_names(void)
for (i = 0; i < len; i++)
name_info_msg__free_unpacked(names[i], NULL);
+
free(names);
for (i = 0; i < 10; i++) {
@@ -1491,7 +1757,7 @@ static int test_wait_accepting_fail_name(void)
static void * test_call_flow_accept(void * o)
{
struct timespec abstime;
- struct timespec timeo = TIMESPEC_INIT_MS(10);
+ struct timespec timeo = TIMESPEC_INIT_MS(30);
buffer_t pbuf = BUF_INIT;
struct proc_info pinfo = TEST_PROC_INFO;