From 60b04305d70614580b4f883c0a147507edef3779 Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Wed, 7 Jan 2026 16:44:34 +0100 Subject: lib: Add post-quantum cryptography support This adds initial support for runtime-configurable encryption and post-quantum Key Encapsulation Mechanisms (KEMs) and authentication (ML-DSA). Supported key exchange algorithms: ECDH: prime256v1, secp384r1, secp521r1, X25519, X448 Finite Field DH: ffdhe2048, ffdhe3072, ffdhe4096 ML-KEM (FIPS 203): ML-KEM-512, ML-KEM-768, ML-KEM-1024 Hybrid KEMs: X25519MLKEM768, X448MLKEM1024 Supported ciphers: AEAD: aes-128-gcm, aes-192-gcm, aes-256-gcm, chacha20-poly1305 CTR: aes-128-ctr, aes-192-ctr, aes-256-ctr Supported HKDFs: sha256, sha384, sha512, sha3-256, sha3-384, sha3-512, blake2b512, blake2s256 Supported Digests for DSA: sha256, sha384, sha512, sha3-256, sha3-384, sha3-512, blake2b512, blake2s256 PQC support requires OpenSSL 3.4.0+ and is detected automatically via CMake. A DISABLE_PQC option allows building without PQC even when available. KEMs differ from traditional DH in that they require asymmetric roles: one party encapsulates to the other's public key. This creates a coordination problem during simultaneous reconnection attempts. The kem_mode configuration parameter resolves this by pre-assigning roles: kem_mode=server # Server encapsulates (1-RTT, full forward secrecy) kem_mode=client # Client encapsulates (0-RTT, cached server key) The enc.conf file format supports: kex= # Key exchange algorithm cipher= # Symmetric cipher kdf= # Key derivation function digest= # Digest for DSA kem_mode= # Server (default) or client none # Disable encryption The OAP protocol is extended to negotiate algorithms and exchange KEX data. All KEX messages are signed using existing authentication infrastructure for integrity and replay protection. Tests are split into base and _pqc variants to handle conditional PQC compilation (kex_test.c/kex_test_pqc.c, oap_test.c/oap_test_pqc.c). Bumped minimum required OpenSSL version for encryption to 3.0 (required for HKDF API). 1.1.1 is long time EOL. Signed-off-by: Dimitri Staessens Signed-off-by: Sander Vrijders --- src/irmd/config.h.in | 6 + src/irmd/main.c | 746 +++++++----------------------- src/irmd/oap.c | 290 +++--------- src/irmd/oap.h | 93 ++-- src/irmd/oap/auth.c | 252 ++++++++++ src/irmd/oap/auth.h | 35 ++ src/irmd/oap/cli.c | 553 ++++++++++++++++++++++ src/irmd/oap/hdr.c | 456 ++++++++++++++++++ src/irmd/oap/hdr.h | 159 +++++++ src/irmd/oap/internal.h | 133 ++++++ src/irmd/oap/io.c | 132 ++++++ src/irmd/oap/io.h | 40 ++ src/irmd/oap/srv.c | 462 +++++++++++++++++++ src/irmd/oap/tests/CMakeLists.txt | 78 ++++ src/irmd/oap/tests/common.c | 457 ++++++++++++++++++ src/irmd/oap/tests/common.h | 100 ++++ src/irmd/oap/tests/oap_test.c | 947 +++++++++++++++++++++++++++++++++++++ src/irmd/oap/tests/oap_test_pqc.c | 363 +++++++++++++++ src/irmd/reg/ipcp.c | 1 - src/irmd/reg/tests/CMakeLists.txt | 4 +- src/irmd/reg/tests/flow_test.c | 2 +- src/irmd/reg/tests/ipcp_test.c | 2 +- src/irmd/reg/tests/name_test.c | 2 +- src/irmd/reg/tests/proc_test.c | 2 +- src/irmd/reg/tests/prog_test.c | 2 +- src/irmd/reg/tests/reg_test.c | 4 +- src/irmd/tests/CMakeLists.txt | 28 -- src/irmd/tests/irm_test.c | 33 -- src/irmd/tests/oap_test.c | 948 +++++++++++++++++++++++++++++++------- 29 files changed, 5239 insertions(+), 1091 deletions(-) create mode 100644 src/irmd/oap/auth.c create mode 100644 src/irmd/oap/auth.h create mode 100644 src/irmd/oap/cli.c create mode 100644 src/irmd/oap/hdr.c create mode 100644 src/irmd/oap/hdr.h create mode 100644 src/irmd/oap/internal.h create mode 100644 src/irmd/oap/io.c create mode 100644 src/irmd/oap/io.h create mode 100644 src/irmd/oap/srv.c create mode 100644 src/irmd/oap/tests/CMakeLists.txt create mode 100644 src/irmd/oap/tests/common.c create mode 100644 src/irmd/oap/tests/common.h create mode 100644 src/irmd/oap/tests/oap_test.c create mode 100644 src/irmd/oap/tests/oap_test_pqc.c delete mode 100644 src/irmd/tests/CMakeLists.txt delete mode 100644 src/irmd/tests/irm_test.c (limited to 'src/irmd') diff --git a/src/irmd/config.h.in b/src/irmd/config.h.in index 527694c0..06d51ccd 100644 --- a/src/irmd/config.h.in +++ b/src/irmd/config.h.in @@ -41,6 +41,8 @@ #define FLOW_ALLOC_TIMEOUT @FLOW_ALLOC_TIMEOUT@ #define FLOW_DEALLOC_TIMEOUT @FLOW_DEALLOC_TIMEOUT@ +#define OAP_REPLAY_TIMER @OAP_REPLAY_TIMER@ + #define BOOTSTRAP_TIMEOUT @BOOTSTRAP_TIMEOUT@ #define ENROLL_TIMEOUT @ENROLL_TIMEOUT@ #define REG_TIMEOUT @REG_TIMEOUT@ @@ -73,6 +75,10 @@ #cmakedefine IRMD_KILL_ALL_PROCESSES #cmakedefine HAVE_LIBGCRYPT #cmakedefine HAVE_OPENSSL +#ifdef HAVE_OPENSSL +#define IRMD_SECMEM_MAX @IRMD_SECMEM_MAX@ +#cmakedefine HAVE_OPENSSL_PQC +#endif #ifdef CONFIG_OUROBOROS_DEBUG #cmakedefine DEBUG_PROTO_OAP #endif diff --git a/src/irmd/main.c b/src/irmd/main.c index daaf4129..57703254 100644 --- a/src/irmd/main.c +++ b/src/irmd/main.c @@ -85,13 +85,6 @@ enum irm_state { IRMD_SHUTDOWN }; -struct oaph { - struct list_head next; - - uint64_t stamp; - uint8_t id[OAP_ID_SIZE]; -}; - struct cmd { struct list_head next; @@ -105,12 +98,6 @@ struct { #ifdef HAVE_TOML char * cfg_file; /* configuration file path */ #endif - struct { - struct auth_ctx * ctx; /* default authentication ctx */ - struct list_head list; /* OAP headers seen before */ - pthread_mutex_t mtx; /* mutex for OAP headers */ - } auth; - struct lockfile * lf; /* single irmd per system */ struct shm_rdrbuff * rdrb; /* rdrbuff for packets */ @@ -456,7 +443,7 @@ 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.cfg", srv_dir, info->name); + sprintf(info->s.enc, "%s/%s/enc.conf", srv_dir, info->name); if (strlen(info->s.crt) == 0) sprintf(info->s.crt, "%s/%s/crt.pem", srv_dir, info->name); @@ -465,7 +452,7 @@ static void name_update_sec_paths(struct name_info * info) 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.cfg", cli_dir, info->name); + sprintf(info->c.enc, "%s/%s/enc.conf", cli_dir, info->name); if (strlen(info->c.crt) == 0) sprintf(info->c.crt, "%s/%s/crt.pem", cli_dir, info->name); @@ -613,18 +600,21 @@ static int unbind_program(const char * prog, if (name == NULL) { if (reg_destroy_prog(prog) < 0) { log_err("Failed to unbind %s.", prog); - return -1; + goto fail; } log_info("Program %s unbound.", prog); } else { if (reg_unbind_prog(name, prog) < 0) { log_err("Failed to unbind %s from %s", prog, name); - return -1; + goto fail; } log_info("Name %s unbound for %s.", name, prog); } return 0; + + fail: + return -1; } static int unbind_process(pid_t pid, @@ -633,18 +623,21 @@ static int unbind_process(pid_t pid, if (name == NULL) { if (reg_destroy_proc(pid) < 0) { log_err("Failed to unbind %d.", pid); - return -1; + goto fail; } log_info("Process %d unbound.", pid); } else { if (reg_unbind_proc(name, pid) < 0) { log_err("Failed to unbind %d from %s", pid, name); - return -1; + goto fail; } log_info("Name %s unbound for process %d.", name, pid); } return 0; + + fail: + return -1; } static int list_ipcps(ipcp_list_msg_t *** ipcps, @@ -716,8 +709,7 @@ int name_reg(const char * name, if (ipcp_reg(pid, hash)) { log_err("Could not register " HASH_FMT32 " with IPCP %d.", HASH_VAL32(hash.data), pid); - freebuf(hash); - return -1; + goto fail_hash; } log_info("Registered %s with IPCP %d as " HASH_FMT32 ".", @@ -726,6 +718,10 @@ int name_reg(const char * name, freebuf(hash); return 0; + + fail_hash: + freebuf(hash); + return -1; } static int name_unreg(const char * name, @@ -760,8 +756,7 @@ static int name_unreg(const char * name, if (ipcp_unreg(pid, hash)) { log_err("Could not unregister %s with IPCP %d.", name, pid); - freebuf(hash); - return -1; + goto fail_hash; } log_info("Unregistered %s from %d.", name, pid); @@ -769,6 +764,10 @@ static int name_unreg(const char * name, freebuf(hash); return 0; + + fail_hash: + freebuf(hash); + return -1; } static int proc_announce(const struct proc_info * info) @@ -796,266 +795,26 @@ static int proc_exit(pid_t pid) return 0; } -static void __cleanup_pkp(void * pkp) -{ - if (pkp != NULL) - crypt_dh_pkp_destroy(pkp); -} - static void __cleanup_flow(void * flow) { reg_destroy_flow(((struct flow_info *) flow)->id); } -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; -} - -static int load_credentials(const char * name, - const struct name_sec_paths * paths, - void ** pkp, - void ** crt, - bool * crypt) -{ - assert(paths != NULL); - assert(pkp != NULL); - assert(crt != NULL); - - *pkp = NULL; - *crt = NULL; - - /* TODO: Allow configuration. For now, encrypt if path exists */ - *crypt = file_exists(paths->enc); - if (*crypt) - log_info("Encryption enabled for %s.", name); - - if (!file_exists(paths->crt) || !file_exists(paths->key)) { - log_info("No security info 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 security keys for %s.", name); - - return 0; - - fail_key: - crypt_free_crt(*crt); - *crt = NULL; - fail_crt: - return -EAUTH; -} - -static int load_srv_credentials(const char * name, - void ** pkp, - void ** crt, - bool * crypt) -{ - struct name_info info; - - assert(name != NULL); - assert(pkp != NULL); - assert(crt != NULL); - - if (reg_get_name_info(name, &info) < 0) { - log_err("Failed to get name info for %s.", name); - return -ENAME; - } - - return load_credentials(name, &info.s, pkp, crt, crypt); -} - -static int load_cli_credentials(const char * name, - void ** pkp, - void ** crt, - bool * crypt) -{ - struct name_info info; - - assert(name != NULL); - assert(pkp != NULL); - assert(crt != NULL); - - if (reg_get_name_info(name, &info) < 0) { - log_err("Failed to get name info for %s.", name); - return -ENAME; - } - - return load_credentials(name, &info.c, pkp, crt, crypt); -} - -#define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0) -static int irm_check_oap_hdr(const struct oap_hdr * oap_hdr, - time_t mpl) -{ - struct list_head * p; - struct list_head * h; - struct timespec now; - struct oaph * new; - uint64_t stamp; - uint64_t cur; - uint8_t * id; - ssize_t delta; - - assert(oap_hdr != NULL); - - stamp = oap_hdr->timestamp; - id = oap_hdr->id.data; - - clock_gettime(CLOCK_REALTIME, &now); - - cur = TS_TO_UINT64(now); - - delta = (ssize_t)(cur - stamp) / MILLION; - if (delta > mpl) - log_warn("Transit time exceeds MPL by %zd ms.", delta); - if (delta < -TIMESYNC_SLACK) - log_warn("OAP header sent %zd ms from the future.", -delta); - - new = malloc(sizeof(*new)); - if (new == NULL) { - log_err("Failed to allocate memory for OAP element."); - return -ENOMEM; - } - - pthread_mutex_lock(&irmd.auth.mtx); - - list_for_each_safe(p, h, &irmd.auth.list) { - struct oaph * oaph = list_entry(p, struct oaph, next); - if (cur > oaph->stamp + OAP_SEEN_TIMER * BILLION) { - list_del(&oaph->next); - free(oaph); - continue; - } - - if (oaph->stamp == stamp && ID_IS_EQUAL(oaph->id, id)) { - log_warn("OAP header already known: " HASH_FMT64 ".", - HASH_VAL64(id)); - goto fail_replay; - } - } - - memcpy(new->id, id, OAP_ID_SIZE); - new->stamp = stamp; - - list_add_tail(&new->next, &irmd.auth.list); - - pthread_mutex_unlock(&irmd.auth.mtx); - - return 0; - - fail_replay: - pthread_mutex_unlock(&irmd.auth.mtx); - free(new); - return -EAUTH; -} - -static int irm_auth_peer(const char * name, - const struct oap_hdr * oap_hdr, - const struct oap_hdr * r_oap_hdr) -{ - void * crt; - void * pk; - buffer_t sign; - const char * n = name == NULL ? "" : name; - - if (memcmp(r_oap_hdr->id.data, oap_hdr->id.data, OAP_ID_SIZE) != 0) { - log_err("OAP ID mismatch in flow allocation."); - goto fail_check; - } - - if (r_oap_hdr->crt.len == 0) { - log_info("No certificate provided by %s.", n); - return 0; - } - - if (crypt_load_crt_der(r_oap_hdr->crt, &crt) < 0) { - log_err("Failed to load certificate from %s.", n); - goto fail_check; - } - - log_dbg("Loaded peer certificate for %s.", n); - - if (name != NULL) { - if (crypt_check_crt_name(crt, n) < 0) { - log_err("Certificate does not match %s.", n); - goto fail_crt; - } - log_dbg("Certificate matches name %s.", n); - } - - if (crypt_get_pubkey_crt(crt, &pk) < 0) { - log_err("Failed to get pubkey from certificate for %s.", n); - goto fail_crt; - } - - log_dbg("Got public key from certificate for %s.", n); - - if (auth_verify_crt(irmd.auth.ctx, crt) < 0) { - log_err("Failed to verify peer %s with CA store.", n); - goto fail_crt; - } - - log_info("Successfully verified peer certificate for %s.", n); - - sign = r_oap_hdr->hdr; - sign.len -= (r_oap_hdr->sig.len + sizeof(uint16_t)); - - if (auth_verify_sig(pk, sign, r_oap_hdr->sig) < 0) { - log_err("Failed to verify signature for peer %s.", n); - goto fail_check_sig; - } - - crypt_free_key(pk); - crypt_free_crt(crt); - - log_info("Successfully authenticated %s.", n); - - return 0; - - fail_check_sig: - crypt_free_key(pk); - fail_crt: - crypt_free_crt(crt); - fail_check: - return -1; -} - static int flow_accept(struct flow_info * flow, - buffer_t * symmkey, buffer_t * data, - struct timespec * abstime) + struct timespec * abstime, + struct crypt_sk * sk) { - struct oap_hdr oap_hdr; /* incoming request */ - struct oap_hdr r_oap_hdr; /* outgoing response */ - uint8_t buf[MSGBUFSZ]; /* buffer for local ephkey */ - buffer_t lpk = BUF_INIT; /* local ephemeral pubkey */ - char name[NAME_SIZE + 1]; /* name for flow */ - void * pkp = NULL; /* signing private key */ - void * crt = NULL; /* signing certificate */ - int err; - bool crypt; + buffer_t req_hdr; + buffer_t resp_hdr; + char name[NAME_SIZE + 1]; + struct name_info info; + int err; - /* piggyback of user data not yet implemented */ assert(data != NULL && BUF_IS_EMPTY(data)); - assert(symmkey != NULL && BUF_IS_EMPTY(symmkey)); + + clrbuf(req_hdr); + clrbuf(resp_hdr); if (!reg_has_proc(flow->n_pid)) { log_err("Unknown process %d calling accept.", flow->n_pid); @@ -1077,7 +836,7 @@ static int flow_accept(struct flow_info * flow, pthread_cleanup_push(__cleanup_flow, flow); - err = reg_wait_flow_accepted(flow, &oap_hdr.hdr, abstime); + err = reg_wait_flow_accepted(flow, &req_hdr, abstime); pthread_cleanup_pop(false); @@ -1097,119 +856,38 @@ static int flow_accept(struct flow_info * flow, if (reg_get_name_for_flow_id(name, flow->id) < 0) { log_err("Failed to get name for flow %d.", flow->id); err = -EIPCP; - goto fail_cred; - } - - log_dbg("IPCP %d accepting flow %d for %s.", - flow->n_pid, flow->id, name); - - if (load_srv_credentials(name, &pkp, &crt, &crypt) < 0) { - log_err("Failed to load security keys for %s.", name); - err = -EAUTH; - goto fail_cred; - } - - if (oap_hdr_decode(oap_hdr.hdr, &oap_hdr) < 0) { - log_err("Failed to decode OAP header from %s.", name); - err = -EIPCP; - goto fail_oap_hdr; - } -#ifdef DEBUG_PROTO_OAP - debug_oap_hdr_rcv(&oap_hdr); -#endif - if (irm_check_oap_hdr(&oap_hdr, flow->mpl) < 0) { - log_err("OAP header failed replay check."); - goto fail_oap_hdr; - } - - if (crypt && oap_hdr.eph.len == 0) { - log_warn("Encryption required but no key provided."); - err = -ECRYPT; - goto fail_oap_hdr; + goto fail_oap; } - if (oap_hdr.eph.len > 0) { /* crypto requested */ - uint8_t * s; /* symmetric encryption key */ - ssize_t key_len; /* length of local pubkey */ - void * pkp = NULL; /* ephemeral private key pair */ - - s = malloc(SYMMKEYSZ); - if (s == NULL) { - log_err("Failed to malloc symmkey."); - err = -ENOMEM; - goto fail_keys; - } - - key_len = crypt_dh_pkp_create(&pkp, buf); - if (key_len < 0) { - free(s); - log_err("Failed to generate key pair."); - err = -ECRYPT; - goto fail_keys; - } - - lpk.data = buf; - lpk.len = (size_t) key_len; - - log_dbg("Generated ephemeral keys for %d.", flow->n_pid); - - if (crypt_dh_derive(pkp, oap_hdr.eph, s) < 0) { - log_err("Failed to derive secret for %d.", flow->id); - crypt_dh_pkp_destroy(pkp); - free(s); - err = -ECRYPT; - goto fail_derive; - } - - symmkey->data = s; - symmkey->len = SYMMKEYSZ; - - crypt_dh_pkp_destroy(pkp); + if (reg_get_name_info(name, &info) < 0) { + log_err("Failed to get name info for %s.", name); + err = -ENAME; + goto fail_oap; } - if (oap_hdr_init(oap_hdr.id, pkp, crt, lpk, *data, &r_oap_hdr) < 0) { - log_err("Failed to create OAP header."); - err = -ENOMEM; - goto fail_r_oap_hdr; - } + log_dbg("IPCP %d accepting flow %d for %s.", + flow->n_pid, flow->id, name); - if (irm_auth_peer(NULL, &r_oap_hdr, &oap_hdr) < 0) { - log_err("Failed to auth %s client, flow %d.", name, flow->id); - err = -EAUTH; - goto fail_r_oap_hdr; + err = oap_srv_process(&info, req_hdr, &resp_hdr, data, sk); + if (err < 0) { + log_err("OAP processing failed for %s.", name); + goto fail_oap; } - crypt_free_crt(crt); - crypt_free_key(pkp); - -#ifdef DEBUG_PROTO_OAP - debug_oap_hdr_snd(&r_oap_hdr); -#endif - if (ipcp_flow_alloc_resp(flow, 0, r_oap_hdr.hdr) < 0) { + if (ipcp_flow_alloc_resp(flow, 0, resp_hdr) < 0) { log_err("Failed to respond to flow allocation."); goto fail_resp; } - log_info("Flow %d accepted by %d for %s.", - flow->id, flow->n_pid, name); + log_info("Flow %d accepted by %d for %s.", flow->id, flow->n_pid, name); - oap_hdr_fini(&oap_hdr); - oap_hdr_fini(&r_oap_hdr); + freebuf(req_hdr); + freebuf(resp_hdr); return 0; - fail_r_oap_hdr: - freebuf(*symmkey); - fail_derive: - clrbuf(lpk); - fail_keys: - oap_hdr_fini(&oap_hdr); - fail_oap_hdr: - crypt_free_crt(crt); - crypt_free_key(pkp); - fail_cred: - assert(lpk.data == NULL && lpk.len == 0); - ipcp_flow_alloc_resp(flow, err, lpk); + fail_oap: + ipcp_flow_alloc_resp(flow, err, resp_hdr); fail_wait: reg_destroy_flow(flow->id); fail_flow: @@ -1217,10 +895,8 @@ static int flow_accept(struct flow_info * flow, fail_resp: flow->state = FLOW_NULL; - oap_hdr_fini(&r_oap_hdr); - freebuf(*symmkey); - clrbuf(lpk); - oap_hdr_fini(&oap_hdr); + freebuf(req_hdr); + freebuf(resp_hdr); reg_destroy_flow(flow->id); return -EIPCP; } @@ -1361,78 +1037,31 @@ static int get_ipcp_by_dst(const char * dst, return err; } -static int flow_alloc(struct flow_info * flow, - const char * dst, - buffer_t * symmkey, +static int flow_alloc(const char * dst, + struct flow_info * flow, buffer_t * data, - struct timespec * abstime) + struct timespec * abstime, + struct crypt_sk * sk) { - struct oap_hdr oap_hdr; /* outgoing request */ - struct oap_hdr r_oap_hdr; /* incoming response */ - uint8_t buf[MSGBUFSZ]; /* buffer for local ephkey */ - buffer_t lpk = BUF_INIT; /* local ephemeral pubkey */ - void * pkp = NULL; /* ephemeral private key pair */ - uint8_t * s = NULL; /* symmetric key */ - void * cpkp = NULL; /* signing private key */ - void * ccrt = NULL; /* signing certificate */ - buffer_t hash; - uint8_t idbuf[OAP_ID_SIZE]; - buffer_t id; - int err; - bool crypt; + buffer_t req_hdr = BUF_INIT; + buffer_t resp_hdr = BUF_INIT; + buffer_t hash = BUF_INIT; + struct name_info info; + void * ctx; + int err; /* piggyback of user data not yet implemented */ assert(data != NULL && BUF_IS_EMPTY(data)); - assert(symmkey != NULL && BUF_IS_EMPTY(symmkey)); log_info("Allocating flow for %d to %s.", flow->n_pid, dst); - if (random_buffer(idbuf, OAP_ID_SIZE) < 0) { - log_err("Failed to generate ID."); - err = -EIRMD; - goto fail_id; - } - - id.data = idbuf; - id.len = OAP_ID_SIZE; - - if (load_cli_credentials(dst, &cpkp, &ccrt, &crypt) < 0) { - log_err("Failed to load security keys for %s.", dst); - err = -EAUTH; - goto fail_cred; - } - - if (crypt > 0) { - ssize_t key_len; - - s = malloc(SYMMKEYSZ); - if (s == NULL) { - log_err("Failed to malloc symmetric key"); - err = -ENOMEM; - goto fail_malloc; - } - - key_len = crypt_dh_pkp_create(&pkp, buf); - if (key_len < 0) { - log_err("Failed to generate key pair."); - err = -ECRYPT; - goto fail_pkp; - } - - lpk.data = buf; - lpk.len = (size_t) key_len; - - log_dbg("Generated ephemeral keys for %d.", flow->n_pid); + /* Look up name_info for dst */ + if (reg_get_name_info(dst, &info) < 0) { + log_err("Failed to get name info for %s.", dst); + err = -ENAME; + goto fail_flow; } - if (oap_hdr_init(id, cpkp, ccrt, lpk, *data, &oap_hdr) < 0) { - log_err("Failed to create OAP header."); - err = -ENOMEM; - goto fail_oap_hdr; - } -#ifdef DEBUG_PROTO_OAP - debug_oap_hdr_snd(&oap_hdr); -#endif if (reg_create_flow(flow) < 0) { log_err("Failed to create flow."); err = -EBADF; @@ -1445,109 +1074,70 @@ static int flow_alloc(struct flow_info * flow, goto fail_ipcp; } - reg_prepare_flow_alloc(flow); + if (reg_prepare_flow_alloc(flow) < 0) { + log_err("Failed to prepare flow allocation."); + err = -EBADF; + goto fail_prepare; + } - if (ipcp_flow_alloc(flow, hash, oap_hdr.hdr)) { + if (oap_cli_prepare(&ctx, &info, &req_hdr, *data) < 0) { + log_err("Failed to prepare OAP request for %s.", dst); + err = -EBADF; + goto fail_prepare; + } + + if (ipcp_flow_alloc(flow, hash, req_hdr)) { log_err("Flow allocation %d failed.", flow->id); - err = -ENOTALLOC; + err = -EIPCP; goto fail_alloc; } pthread_cleanup_push(__cleanup_flow, flow); - pthread_cleanup_push(__cleanup_pkp, pkp); pthread_cleanup_push(free, hash.data); - pthread_cleanup_push(free, s); - err = reg_wait_flow_allocated(flow, &r_oap_hdr.hdr, abstime); + err = reg_wait_flow_allocated(flow, &resp_hdr, abstime); - pthread_cleanup_pop(false); - pthread_cleanup_pop(false); pthread_cleanup_pop(false); pthread_cleanup_pop(false); if (err == -ETIMEDOUT) { log_err("Flow allocation timed out."); - goto fail_alloc; - } - - if (err == -1) { - log_dbg("Flow allocation terminated."); - err = -EIPCP; - goto fail_alloc; + goto fail_wait; } - log_dbg("Response received for flow %d to %s.", flow->id, dst); + log_dbg("Response for flow %d to %s.", flow->id, dst); if (err < 0) { - log_warn("Flow allocation rejected for %s: %d.", dst, err); - goto fail_alloc; + log_warn("Allocation rejected: %s (%d).", dst, err); + goto fail_peer; } - if (oap_hdr_decode(r_oap_hdr.hdr, &r_oap_hdr) < 0) { - log_err("Failed to decode OAP header."); - err = -EIPCP; - goto fail_r_oap_hdr; - } -#ifdef DEBUG_PROTO_OAP - debug_oap_hdr_rcv(&r_oap_hdr); -#endif - if (irm_check_oap_hdr(&r_oap_hdr, flow->mpl) < 0) { - log_err("OAP header failed replay check."); - err = -EAUTH; - goto fail_r_oap_hdr; - } - - if (irm_auth_peer(dst, &oap_hdr, &r_oap_hdr) < 0) { - log_err("Failed to authenticate %s (flow %d).", dst, flow->id); - err = -EAUTH; - goto fail_r_oap_hdr; - } - - if (lpk.len > 0) { /* crypto requested */ - if (crypt_dh_derive(pkp, r_oap_hdr.eph, s) < 0) { - log_err("Failed to derive secret for %d.", flow->id); - err = -ECRYPT; - goto fail_r_oap_hdr; - } - crypt_dh_pkp_destroy(pkp); - - symmkey->data = s; - symmkey->len = SYMMKEYSZ; - s = NULL; + err = oap_cli_complete(ctx, &info, resp_hdr, data, sk); + if (err < 0) { + log_err("OAP completion failed for %s.", dst); + goto fail_complete; } - oap_hdr_fini(&r_oap_hdr); - oap_hdr_fini(&oap_hdr); - - crypt_free_crt(ccrt); - crypt_free_key(cpkp); - - /* TODO: piggyback user data if needed */ - + freebuf(req_hdr); + freebuf(resp_hdr); freebuf(hash); - free(s); return 0; - fail_r_oap_hdr: + fail_complete: + ctx = NULL; /* freee'd on complete */ + fail_peer: flow->state = FLOW_DEALLOCATED; - oap_hdr_fini(&r_oap_hdr); + fail_wait: + freebuf(resp_hdr); fail_alloc: + freebuf(req_hdr); + oap_ctx_free(ctx); + fail_prepare: freebuf(hash); fail_ipcp: reg_destroy_flow(flow->id); fail_flow: - oap_hdr_fini(&oap_hdr); - fail_oap_hdr: - crypt_dh_pkp_destroy(pkp); - fail_pkp: - free(s); - fail_malloc: - crypt_free_crt(ccrt); - crypt_free_key(cpkp); - fail_cred: - clrbuf(id); - fail_id: return err; } @@ -1744,19 +1334,21 @@ static void __cleanup_irm_msg(void * o) static irm_msg_t * do_command_msg(irm_msg_t * msg) { - struct ipcp_config conf; - struct ipcp_info ipcp; - struct flow_info flow; - struct proc_info proc; - struct name_info name; - struct timespec * abstime; - struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT); - struct timespec now; - struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */ - int res; - irm_msg_t * ret_msg; - buffer_t data; - buffer_t symmkey = BUF_INIT;; + struct ipcp_config conf; + struct ipcp_info ipcp; + struct flow_info flow; + struct proc_info proc; + struct name_info name; + struct crypt_sk sk; + uint8_t kbuf[SYMMKEYSZ]; /* stack buffer for OAP */ + uint8_t * hbuf = NULL; /* heap copy for response */ + struct timespec * abstime; + struct timespec max = TIMESPEC_INIT_MS(FLOW_ALLOC_TIMEOUT); + struct timespec now; + struct timespec ts = TIMESPEC_INIT_S(0); /* static analysis */ + int res; + irm_msg_t * ret_msg; + buffer_t data; memset(&flow, 0, sizeof(flow)); @@ -1858,15 +1450,27 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg) msg->has_pk = false; assert(data.len > 0 ? data.data != NULL : data.data == NULL); flow = flow_info_msg_to_s(msg->flow_info); - res = flow_accept(&flow, &symmkey, &data, abstime); + 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_symmkey = symmkey.len != 0; - ret_msg->symmkey.data = symmkey.data; - ret_msg->symmkey.len = symmkey.len; - ret_msg->has_pk = data.len != 0; - ret_msg->pk.data = data.data; - ret_msg->pk.len = data.len; + 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; + } } break; case IRM_MSG_CODE__IRM_FLOW_ALLOC: @@ -1876,15 +1480,26 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg) assert(data.len > 0 ? data.data != NULL : data.data == NULL); flow = flow_info_msg_to_s(msg->flow_info); abstime = abstime == NULL ? &max : abstime; - res = flow_alloc(&flow, msg->dst, &symmkey, &data, 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_symmkey = symmkey.len != 0; - ret_msg->symmkey.data = symmkey.data; - ret_msg->symmkey.len = symmkey.len; - ret_msg->has_pk = data.len != 0; - ret_msg->pk.data = data.data; - ret_msg->pk.len = data.len; + 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; + } } break; case IRM_MSG_CODE__IRM_FLOW_JOIN: @@ -1938,14 +1553,16 @@ static irm_msg_t * do_command_msg(irm_msg_t * msg) else ret_msg->result = res; + explicit_bzero(kbuf, SYMMKEYSZ); + return ret_msg; } static void * mainloop(void * o) { - int sfd; - irm_msg_t * msg; - buffer_t buffer; + int sfd; + irm_msg_t * msg; + buffer_t buffer; (void) o; @@ -2170,7 +1787,7 @@ static int irm_load_store(char * dpath) goto fail_file; } - if (auth_add_crt_to_store(irmd.auth.ctx, crt) < 0) { + if (oap_auth_add_ca_crt(crt) < 0) { log_err("Failed to add certificate from %s to store.", path); goto fail_crt_add; @@ -2295,27 +1912,19 @@ static int irm_init(void) goto fail_tpm_create; } - if (pthread_mutex_init(&irmd.auth.mtx, NULL) < 0) { - log_err("Failed to initialize auth mutex."); - goto fail_auth_mtx; + if (oap_auth_init() < 0) { + log_err("Failed to initialize OAP module."); + goto fail_oap; } - irmd.auth.ctx = auth_create_ctx(); - if (irmd.auth.ctx == NULL) { - log_err("Failed to create auth store context."); - goto fail_auth_ctx; - } - - list_head_init(&irmd.auth.list); - if (irm_load_store(OUROBOROS_CA_CRT_DIR) < 0) { log_err("Failed to load CA certificates."); - goto fail_auth_ctx; + goto fail_load_store; } if (irm_load_store(OUROBOROS_CHAIN_DIR) < 0) { log_err("Failed to load intermediate certificates."); - goto fail_auth_ctx; + goto fail_load_store; } #ifdef HAVE_FUSE @@ -2352,11 +1961,10 @@ static int irm_init(void) #ifdef HAVE_FUSE rmdir(FUSE_PREFIX); #endif - auth_destroy_ctx(irmd.auth.ctx); #endif - fail_auth_ctx: - pthread_mutex_destroy(&irmd.auth.mtx); - fail_auth_mtx: + fail_load_store: + oap_auth_fini(); + fail_oap: tpm_destroy(irmd.tpm); fail_tpm_create: shm_rdrbuff_destroy(irmd.rdrb); @@ -2388,18 +1996,7 @@ static void irm_fini(void) if (irmd_get_state() != IRMD_INIT) log_warn("Unsafe destroy."); - pthread_mutex_lock(&irmd.auth.mtx); - - list_for_each_safe(p, h, &irmd.auth.list) { - struct oaph * oaph = list_entry(p, struct oaph, next); - list_del(&oaph->next); - free(oaph); - } - - pthread_mutex_unlock(&irmd.auth.mtx); - pthread_mutex_destroy(&irmd.auth.mtx); - - auth_destroy_ctx(irmd.auth.ctx); + oap_auth_fini(); tpm_destroy(irmd.tpm); @@ -2637,6 +2234,11 @@ int main(int argc, 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) @@ -2656,6 +2258,8 @@ int main(int argc, pthread_sigmask(SIG_UNBLOCK, &sigset, NULL); + crypt_secure_malloc_fini(); + reg_clear(); reg_fini(); diff --git a/src/irmd/oap.c b/src/irmd/oap.c index 500da6f1..085e06a3 100644 --- a/src/irmd/oap.c +++ b/src/irmd/oap.c @@ -1,7 +1,7 @@ /* * Ouroboros - Copyright (C) 2016 - 2024 * - * Ouroboros flow allocation protocol header + * OAP - Shared credential and configuration loading * * Dimitri Staessens * Sander Vrijders @@ -29,260 +29,102 @@ #define OUROBOROS_PREFIX "irmd/oap" #include -#include +#include #include -#include -#include #include "config.h" -#include "oap.h" - #include +#include +#include -int oap_hdr_init(buffer_t id, - void * pkp, - void * pubcrt, - buffer_t ephkey, - buffer_t data, - struct oap_hdr * oap_hdr) -{ - struct timespec now; - uint64_t stamp; - buffer_t hdr; - buffer_t der = BUF_INIT; - buffer_t sig = BUF_INIT; - buffer_t sign; - uint16_t len; - off_t offset; - - assert(id.data != NULL && id.len == OAP_ID_SIZE); - assert(oap_hdr != NULL); - memset(oap_hdr, 0, sizeof(*oap_hdr)); - - clock_gettime(CLOCK_REALTIME, &now); - stamp = hton64(TS_TO_UINT64(now)); - - if (pubcrt != NULL && crypt_crt_der(pubcrt, &der) < 0) - goto fail_der; - - hdr.len = id.len + - sizeof(stamp) + - sizeof(len) + der.len + - sizeof(len) + ephkey.len + - sizeof(len) + data.len + - sizeof(len); /* sig len */ - - hdr.data = malloc(hdr.len); - if (hdr.data == NULL) - goto fail_hdr; - - offset = 0; - - memcpy(hdr.data, id.data, id.len); - offset += id.len; - - memcpy(hdr.data + offset, &stamp, sizeof(stamp)); - offset += sizeof(stamp); - - /* pubcrt */ - len = hton16((uint16_t) der.len); - memcpy(hdr.data + offset, &len, sizeof(len)); - offset += sizeof(len); - if (der.len != 0) - memcpy(hdr.data + offset, der.data, der.len); - offset += der.len; - - /* ephkey */ - len = hton16((uint16_t) ephkey.len); - memcpy(hdr.data + offset, &len, sizeof(len)); - offset += sizeof(len); - if (ephkey.len != 0) - memcpy(hdr.data + offset, ephkey.data, ephkey.len); - offset += ephkey.len; - - /* data */ - len = hton16((uint16_t) data.len); - memcpy(hdr.data + offset, &len, sizeof(len)); - offset += sizeof(len); - if (data.len != 0) - memcpy(hdr.data + offset, data.data, data.len); - offset += data.len; - - sign.data = hdr.data; - sign.len = hdr.len - sizeof(len); - - if (pkp != NULL && auth_sign(pkp, sign, &sig) < 0) - goto fail_sig; - - len = hton16((uint16_t) sig.len); - memcpy(hdr.data + offset, &len, sizeof(len)); - offset += sizeof(len); - - oap_hdr->hdr = hdr; +/* + * Shared credential and configuration loading helpers + */ - assert((size_t) offset == hdr.len); +#ifndef OAP_TEST_MODE - if (sig.len > 0) { - oap_hdr->hdr.len += sig.len; - oap_hdr->hdr.data = realloc(hdr.data, oap_hdr->hdr.len); - if (oap_hdr->hdr.data == NULL) - goto fail_oap_hdr; +static bool file_exists(const char * path) +{ + struct stat s; - memcpy(oap_hdr->hdr.data + offset, sig.data, sig.len); - clrbuf(hdr); + if (stat(path, &s) < 0 && errno == ENOENT) { + log_dbg("File %s does not exist.", path); + return false; } - if (oap_hdr_decode(oap_hdr->hdr, oap_hdr) < 0) - goto fail_decode; - - freebuf(der); - freebuf(sig); - - return 0; - - fail_decode: - oap_hdr_fini(oap_hdr); - fail_oap_hdr: - freebuf(sig); - fail_sig: - freebuf(hdr); - fail_hdr: - freebuf(der); - fail_der: - memset(oap_hdr, 0, sizeof(*oap_hdr)); - return -1; + return true; } -void oap_hdr_fini(struct oap_hdr * oap_hdr) +int load_credentials(const char * name, + const struct name_sec_paths * paths, + void ** pkp, + void ** crt) { - assert(oap_hdr != NULL); - - freebuf(oap_hdr->hdr); - memset(oap_hdr, 0, sizeof(*oap_hdr)); -} - -int oap_hdr_decode(buffer_t hdr, - struct oap_hdr * oap_hdr) -{ - off_t offset; - - assert(oap_hdr != NULL); - memset(oap_hdr, 0, sizeof(*oap_hdr)); - - if (hdr.len < OAP_HDR_MIN_SIZE) - goto fail_decode; - - oap_hdr->id.data = hdr.data; - oap_hdr->id.len = OAP_ID_SIZE; + assert(paths != NULL); + assert(pkp != NULL); + assert(crt != NULL); - offset = OAP_ID_SIZE; + *pkp = NULL; + *crt = NULL; - oap_hdr->timestamp = ntoh64(*(uint64_t *)(hdr.data + offset)); - - offset += sizeof(uint64_t); - - oap_hdr->crt.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); - oap_hdr->crt.data = hdr.data + offset + sizeof(uint16_t); - - offset += sizeof(uint16_t) + oap_hdr->crt.len; - - if ((size_t) offset + sizeof(uint16_t) >= hdr.len) - goto fail_decode; - - oap_hdr->eph.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); - oap_hdr->eph.data = hdr.data + offset + sizeof(uint16_t); - - offset += sizeof(uint16_t) + oap_hdr->eph.len; - - if ((size_t) offset + sizeof(uint16_t) >= hdr.len) - goto fail_decode; - - oap_hdr->data.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); - oap_hdr->data.data = hdr.data + offset + sizeof(uint16_t); - - offset += sizeof(uint16_t) + oap_hdr->data.len; - - if ((size_t) offset + sizeof(uint16_t) > hdr.len) - goto fail_decode; - - oap_hdr->sig.len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); - oap_hdr->sig.data = hdr.data + offset + sizeof(uint16_t); + if (!file_exists(paths->crt) || !file_exists(paths->key)) { + log_info("No authentication certificates for %s.", name); + return 0; + } - offset += sizeof(uint16_t) + oap_hdr->sig.len; + if (crypt_load_crt_file(paths->crt, crt) < 0) { + log_err("Failed to load %s for %s.", paths->crt, name); + goto fail_crt; + } - if ((size_t) offset != hdr.len) - goto fail_decode; + if (crypt_load_privkey_file(paths->key, pkp) < 0) { + log_err("Failed to load %s for %s.", paths->key, name); + goto fail_key; + } - oap_hdr->hdr = hdr; + log_info("Loaded authentication certificates for %s.", name); return 0; - fail_decode: - memset(oap_hdr, 0, sizeof(*oap_hdr)); - return -1; -} - -#ifdef DEBUG_PROTO_OAP -static void debug_oap_hdr(const struct oap_hdr * hdr) -{ - assert(hdr); - - if (hdr->crt.len > 0) - log_proto(" Certificate: [%zu bytes]", hdr->crt.len); - else - log_proto(" Certificate: "); - - if (hdr->eph.len > 0) - log_proto(" Ephemeral Public Key: [%zu bytes]", hdr->eph.len); - else - log_proto(" Ephemeral Public Key: "); - if (hdr->data.len > 0) - log_proto(" Data: [%zu bytes]", hdr->data.len); - else - log_proto(" Data: "); - if (hdr->sig.len > 0) - log_proto(" Signature: [%zu bytes]", hdr->sig.len); - else - log_proto(" Signature: "); + fail_key: + crypt_free_crt(*crt); + *crt = NULL; + fail_crt: + return -EAUTH; } -void debug_oap_hdr_rcv(const struct oap_hdr * hdr) +int load_kex_config(const char * name, + const char * path, + struct sec_config * cfg) { - struct tm * tm; - char tmstr[RIB_TM_STRLEN]; - time_t stamp; + assert(name != NULL); + assert(cfg != NULL); - assert(hdr); + memset(cfg, 0, sizeof(*cfg)); - stamp = (time_t) hdr->timestamp / BILLION; - - tm = gmtime(&stamp); - strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); - - log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--", - HASH_VAL64(hdr->id.data), tmstr); - - debug_oap_hdr(hdr); -} + /* Load encryption config */ + if (!file_exists(path)) + log_dbg("No encryption %s for %s.", path, name); -void debug_oap_hdr_snd(const struct oap_hdr * hdr) -{ - struct tm * tm; - char tmstr[RIB_TM_STRLEN]; - time_t stamp; - - assert(hdr); + if (load_sec_config_file(cfg, path) < 0) { + log_warn("Failed to load %s for %s.", path, name); + return -1; + } - stamp = (time_t) hdr->timestamp / BILLION; + if (!IS_KEX_ALGO_SET(cfg)) { + log_info("Key exchange not configured for %s.", name); + return 0; + } - tm = gmtime(&stamp); - strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); + 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_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->", - HASH_VAL64(hdr->id.data), tmstr); + log_info("Encryption enabled for %s.", name); - debug_oap_hdr(hdr); + return 0; } -#endif +#endif /* OAP_TEST_MODE */ diff --git a/src/irmd/oap.h b/src/irmd/oap.h index ccdfa804..25c07408 100644 --- a/src/irmd/oap.h +++ b/src/irmd/oap.h @@ -1,7 +1,7 @@ /* * Ouroboros - Copyright (C) 2016 - 2024 * - * Ouroboros flow allocation protocol header + * Ouroboros Allocation Protocol (OAP) Component * * Dimitri Staessens * Sander Vrijders @@ -23,72 +23,45 @@ #ifndef OUROBOROS_IRMD_OAP_H #define OUROBOROS_IRMD_OAP_H +#include +#include +#include #include -#define OAP_ID_SIZE (16) -#define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 4 * sizeof(uint16_t)) +/* OAP authentication state (in oap/auth.c) */ +int oap_auth_init(void); +void oap_auth_fini(void); -/* - * 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 - * +---------------------------------------------------------------+ - * | | - * | id (128 bits) | - * | | - * | | - * +---------------------------------------------------------------+ - * | timestamp (64 bits) | - * | | - * +---------------------------------------------------------------+ - * | crt_len (16 bits) | | - * +-----------+-----------------+ | - * | certificate | - * | | - * +---------------------------------------------------------------+ - * | eph_len (16 bits) | | - * +-----------+-----------------+ | - * | public key for ECDHE | - * | | - * +---------------------------------------------------------------+ - * | data_len (16 bits) | | - * +-----------+-----------------+ | - * | piggy backed application data | - * | | - * +---------------------------------------------------------------+ - * | sig_len (16 bits) | | - * +-----------+-----------------+ | - * | signature | - * | | - * +---------------------------------------------------------------+ - */ - -struct oap_hdr { - uint64_t timestamp; - buffer_t id; - buffer_t crt; - buffer_t eph; - buffer_t data; - buffer_t sig; - buffer_t hdr; -}; - -int oap_hdr_init(buffer_t id, - void * pkp, - void * pubcrt, - buffer_t ephkey, - buffer_t data, - struct oap_hdr * oap_hdr); +int oap_auth_add_ca_crt(void * crt); -void oap_hdr_fini(struct oap_hdr * oap_hdr); +/* +* Prepare OAP request header for server, returns context +* Passes client data for srv, returns srv data for client +*/ +int oap_cli_prepare(void ** ctx, + const struct name_info * info, + buffer_t * req_buf, + buffer_t data); -int oap_hdr_decode(buffer_t hdr, - struct oap_hdr * oap_hdr); +/* + * Server processes header, creates response header, returns secret key. + * data is in/out: input=srv data to send, output=cli data received. + */ +int oap_srv_process(const struct name_info * info, + buffer_t req_buf, + buffer_t * rsp_buf, + buffer_t * data, + struct crypt_sk * sk); -#ifdef DEBUG_PROTO_OAP -void debug_oap_hdr_snd(const struct oap_hdr * hdr); +/* Complete OAP, returns secret key and server data, frees ctx */ +int oap_cli_complete(void * ctx, + const struct name_info * info, + buffer_t rsp_buf, + buffer_t * data, + struct crypt_sk * sk); -void debug_oap_hdr_rcv(const struct oap_hdr * hdr); -#endif /* DEBUG_PROTO_OAP */ +/* Free OAP state (on failure before complete) */ +void oap_ctx_free(void * ctx); #endif /* OUROBOROS_IRMD_OAP_H */ diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c new file mode 100644 index 00000000..cea7b7a0 --- /dev/null +++ b/src/irmd/oap/auth.c @@ -0,0 +1,252 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Authentication, replay detection, and validation + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 +#include +#include +#include +#include +#include + +#include "config.h" + +#include "auth.h" +#include "hdr.h" + +#include +#include +#include + +struct oap_replay_entry { + struct list_head next; + uint64_t timestamp; + uint8_t id[OAP_ID_SIZE]; +}; + +static struct { + struct auth_ctx * ca_ctx; + struct { + struct list_head list; + pthread_mutex_t mtx; + } replay; +} oap_auth; + +int oap_auth_init(void) +{ + 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); + + if (pthread_mutex_init(&oap_auth.replay.mtx, NULL)) { + log_err("Failed to init OAP replay mutex."); + goto fail_mtx; + } + + return 0; + + fail_mtx: + auth_destroy_ctx(oap_auth.ca_ctx); + fail_ctx: + return -1; +} + +void oap_auth_fini(void) +{ + struct list_head * p; + struct list_head * h; + + 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); + } + + pthread_mutex_unlock(&oap_auth.replay.mtx); + pthread_mutex_destroy(&oap_auth.replay.mtx); + + auth_destroy_ctx(oap_auth.ca_ctx); +} + +int oap_auth_add_ca_crt(void * crt) +{ + return auth_add_crt_to_store(oap_auth.ca_ctx, crt); +} + +#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; + + assert(hdr != NULL); + + stamp = hdr->timestamp; + id = hdr->id.data; + + clock_gettime(CLOCK_REALTIME, &now); + + cur = TS_TO_UINT64(now); + + 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; + } + + if (delta > OAP_REPLAY_TIMER * 1000) { + log_err_id(id, "OAP header too old (%zd ms).", delta); + goto fail_stamp; + } + + new = malloc(sizeof(*new)); + if (new == NULL) { + log_err_id(id, "Failed to allocate memory for OAP element."); + goto fail_stamp; + } + + 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; + } + + if (e->timestamp == stamp && ID_IS_EQUAL(e->id, id)) { + log_warn_id(id, "OAP header already known."); + goto fail_replay; + } + } + + memcpy(new->id, id, OAP_ID_SIZE); + new->timestamp = stamp; + + list_add_tail(&new->next, &oap_auth.replay.list); + + pthread_mutex_unlock(&oap_auth.replay.mtx); + + return 0; + + fail_replay: + pthread_mutex_unlock(&oap_auth.replay.mtx); + free(new); + fail_stamp: + return -EAUTH; +} + +int oap_auth_peer(char * name, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr) +{ + void * crt; + void * pk; + buffer_t sign; /* Signed region */ + uint8_t * id = peer_hdr->id.data; + + assert(name != NULL); + assert(local_hdr != NULL); + assert(peer_hdr != NULL); + + if (memcmp(peer_hdr->id.data, local_hdr->id.data, OAP_ID_SIZE) != 0) { + log_err_id(id, "OAP ID mismatch in flow allocation."); + goto fail_check; + } + + if (peer_hdr->crt.len == 0) { + log_dbg_id(id, "No crt provided."); + name[0] = '\0'; + return 0; + } + + if (crypt_load_crt_der(peer_hdr->crt, &crt) < 0) { + log_err_id(id, "Failed to load crt."); + goto fail_check; + } + + log_dbg_id(id, "Loaded peer crt."); + + if (crypt_get_pubkey_crt(crt, &pk) < 0) { + log_err_id(id, "Failed to get pubkey from crt."); + goto fail_crt; + } + + log_dbg_id(id, "Got public key from crt."); + + if (auth_verify_crt(oap_auth.ca_ctx, crt) < 0) { + log_err_id(id, "Failed to verify peer with CA store."); + goto fail_crt; + } + + log_dbg_id(id, "Successfully verified peer crt."); + + sign = 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; + } + + if (crypt_get_crt_name(crt, name) < 0) { + log_warn_id(id, "Failed to extract name from certificate."); + name[0] = '\0'; + } + + crypt_free_key(pk); + crypt_free_crt(crt); + + log_dbg_id(id, "Successfully authenticated peer."); + + return 0; + + fail_check_sig: + crypt_free_key(pk); + fail_crt: + crypt_free_crt(crt); + fail_check: + return -EAUTH; +} diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h new file mode 100644 index 00000000..07c33a23 --- /dev/null +++ b/src/irmd/oap/auth.h @@ -0,0 +1,35 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Authentication functions + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#ifndef OUROBOROS_IRMD_OAP_AUTH_H +#define OUROBOROS_IRMD_OAP_AUTH_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); + +#endif /* OUROBOROS_IRMD_OAP_AUTH_H */ diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c new file mode 100644 index 00000000..12660d7f --- /dev/null +++ b/src/irmd/oap/cli.c @@ -0,0 +1,553 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Client-side processing + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 +#include +#include +#include + +#include "config.h" + +#include "auth.h" +#include "hdr.h" +#include "io.h" +#include "../oap.h" + +#include +#include +#include +#include +#include + +/* Client context between oap_cli_prepare and oap_cli_complete */ +struct oap_cli_ctx { + uint8_t __id[OAP_ID_SIZE]; + buffer_t id; + uint8_t kex_buf[MSGBUFSZ]; + uint8_t req_hash[MAX_HASH_SIZE]; + size_t req_hash_len; + int req_md_nid; + struct sec_config kcfg; + struct oap_hdr local_hdr; + void * pkp; /* Ephemeral keypair */ + uint8_t * key; /* For client-encap KEM */ +}; + +#define OAP_CLI_CTX_INIT(s) \ + do { s->id.len = OAP_ID_SIZE; s->id.data = s->__id; } while (0) + +/* Client-side credential loading, mocked in tests */ + +#ifdef OAP_TEST_MODE +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, + struct sec_config * cfg); +extern int load_server_kem_pk(const char * name, + struct sec_config * cfg, + buffer_t * buf); +#else + +int load_cli_credentials(const struct name_info * info, + void ** pkp, + void ** crt) +{ + assert(info != NULL); + assert(pkp != NULL); + assert(crt != NULL); + + return load_credentials(info->name, &info->c, pkp, crt); +} + +int load_cli_kex_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); +} + +int load_server_kem_pk(const char * name, + struct sec_config * cfg, + buffer_t * pk) +{ + char path[PATH_MAX]; + const char * ext; + + assert(name != NULL); + assert(cfg != NULL); + assert(pk != NULL); + + ext = IS_HYBRID_KEM(cfg->x.str) ? "raw" : "pem"; + + snprintf(path, sizeof(path), + OUROBOROS_CLI_CRT_DIR "/%s/kex.srv.pub.%s", name, ext); + + if (IS_HYBRID_KEM(cfg->x.str)) { + if (crypt_load_pubkey_raw_file(path, pk) < 0) { + log_err("Failed to load %s pubkey from %s.", ext, path); + return -1; + } + } else { + if (crypt_load_pubkey_file_to_der(path, pk) < 0) { + log_err("Failed to load %s pubkey from %s.", ext, path); + return -1; + } + } + + log_dbg("Loaded %s pubkey from %s (%zu bytes).", ext, path, pk->len); + + return 0; +} + +#endif /* OAP_TEST_MODE */ + +static int do_client_kex_prepare_dhe(struct oap_cli_ctx * s) +{ + struct sec_config * kcfg = &s->kcfg; + 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); + if (len < 0) { + log_err_id(id, "Failed to generate DHE keypair."); + return -ECRYPT; + } + + kex->len = (size_t) len; + log_dbg_id(id, "Generated ephemeral %s keys (%zd bytes).", + kcfg->x.str, len); + + return 0; +} + +static int do_client_kex_prepare_kem_encap(const char * server_name, + struct oap_cli_ctx * s) +{ + struct sec_config * kcfg = &s->kcfg; + 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) { + log_err_id(id, "Failed to load server KEM pk."); + return -ECRYPT; + } + + if (IS_HYBRID_KEM(kcfg->x.str)) + len = kex_kem_encap_raw(server_pk, kex->data, + kcfg->k.nid, key_buf); + else + len = kex_kem_encap(server_pk, kex->data, + kcfg->k.nid, key_buf); + + freebuf(server_pk); + + if (len < 0) { + log_err_id(id, "Failed to encapsulate KEM."); + return -ECRYPT; + } + + kex->len = (size_t) len; + log_dbg_id(id, "Client encaps: CT len=%zd.", len); + + /* Store derived key */ + s->key = crypt_secure_malloc(SYMMKEYSZ); + if (s->key == NULL) { + log_err_id(id, "Failed to allocate secure key."); + return -ENOMEM; + } + memcpy(s->key, key_buf, SYMMKEYSZ); + explicit_bzero(key_buf, SYMMKEYSZ); + + return 0; +} + +static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s) +{ + struct sec_config * kcfg = &s->kcfg; + 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); + if (len < 0) { + log_err_id(id, "Failed to generate KEM keypair."); + return -ECRYPT; + } + + kex->len = (size_t) len; + log_dbg_id(id, "Client PK for server encaps (%zd bytes).", len); + + return 0; +} + +static int do_client_kex_prepare(const char * server_name, + struct oap_cli_ctx * s) +{ + struct sec_config * kcfg = &s->kcfg; + + if (!IS_KEX_ALGO_SET(kcfg)) + return 0; + + if (IS_KEM_ALGORITHM(kcfg->x.str)) { + if (kcfg->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); + } + + return do_client_kex_prepare_dhe(s); +} + +int oap_cli_prepare(void ** ctx, + const struct name_info * info, + buffer_t * req_buf, + buffer_t data) +{ + struct oap_cli_ctx * s; + void * pkp = NULL; + void * crt = NULL; + ssize_t ret; + + assert(ctx != NULL); + assert(info != NULL); + assert(req_buf != NULL); + + clrbuf(*req_buf); + *ctx = NULL; + + /* Allocate ctx to carry between prepare and complete */ + s = malloc(sizeof(*s)); + if (s == NULL) { + log_err("Failed to allocate OAP client ctx."); + return -ENOMEM; + } + + memset(s, 0, sizeof(*s)); + OAP_CLI_CTX_INIT(s); + + /* Generate session ID */ + if (random_buffer(s->__id, OAP_ID_SIZE) < 0) { + log_err("Failed to generate OAP session ID."); + goto fail_id; + } + + log_dbg_id(s->id.data, "Preparing OAP request for %s.", info->name); + + /* Load client credentials */ + if (load_cli_credentials(info, &pkp, &crt) < 0) { + log_err_id(s->id.data, "Failed to load credentials for %s.", + info->name); + 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.", + info->name); + goto fail_kex; + } + + log_dbg_id(s->id.data, "KEX config: algo=%s, mode=%s, cipher=%s.", + s->kcfg.x.str != NULL ? s->kcfg.x.str : "none", + s->kcfg.x.mode == KEM_MODE_CLIENT_ENCAP ? "client-encap" : + s->kcfg.x.mode == KEM_MODE_SERVER_ENCAP ? "server-encap" : + "none", + s->kcfg.c.str != NULL ? s->kcfg.c.str : "none"); + + oap_hdr_init(&s->local_hdr, s->id, s->kex_buf, data, s->kcfg.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)) { + log_err_id(s->id.data, "Failed to create OAP request header."); + goto fail_hdr; + } + + 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; + 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."); + goto fail_hash; + } + s->req_hash_len = (size_t) ret; + + /* Transfer ownership of request buffer */ + *req_buf = s->local_hdr.hdr; + clrbuf(s->local_hdr.hdr); + + crypt_free_crt(crt); + crypt_free_key(pkp); + + *ctx = s; + + log_dbg_id(s->id.data, "OAP request prepared for %s.", info->name); + + return 0; + + fail_hash: + fail_hdr: + crypt_secure_free(s->key, SYMMKEYSZ); + crypt_free_key(s->pkp); + fail_kex: + crypt_free_crt(crt); + crypt_free_key(pkp); + fail_id: + free(s); + return -ECRYPT; +} + +void oap_ctx_free(void * ctx) +{ + struct oap_cli_ctx * s = ctx; + + if (s == NULL) + return; + + oap_hdr_fini(&s->local_hdr); + + if (s->pkp != NULL) + crypt_free_key(s->pkp); + + if (s->key != NULL) + crypt_secure_free(s->key, SYMMKEYSZ); + + memset(s, 0, sizeof(*s)); + free(s); +} + +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; + uint8_t * id = s->id.data; + uint8_t key_buf[SYMMKEYSZ]; + + if (kcfg->x.mode == KEM_MODE_SERVER_ENCAP) { + buffer_t ct; + + if (peer_hdr->kex.len == 0) { + log_err_id(id, "Server did not send KEM CT."); + return -ECRYPT; + } + + 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) { + 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) { + /* 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); + return 0; + } + + memcpy(sk->key, key_buf, SYMMKEYSZ); + sk->nid = kcfg->c.nid; + explicit_bzero(key_buf, SYMMKEYSZ); + + log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str); + + return 0; +} + +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; + uint8_t * id = s->id.data; + uint8_t key_buf[SYMMKEYSZ]; + + /* DHE: derive from server's public key */ + if (peer_hdr->kex.len == 0) { + log_err_id(id, "Server did not send DHE public key."); + return -ECRYPT; + } + + if (kex_dhe_derive(kcfg, s->pkp, peer_hdr->kex, key_buf) < 0) { + log_err_id(id, "Failed to derive DHE secret."); + return -ECRYPT; + } + + log_dbg_id(id, "DHE: derived shared secret."); + + memcpy(sk->key, key_buf, SYMMKEYSZ); + sk->nid = kcfg->c.nid; + explicit_bzero(key_buf, SYMMKEYSZ); + + log_info_id(id, "Negotiated %s + %s.", kcfg->x.str, kcfg->c.str); + + return 0; +} + + +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; + uint8_t * id = s->id.data; + + if (!IS_KEX_ALGO_SET(kcfg)) + return 0; + + /* Accept server's cipher choice */ + if (peer_hdr->cipher_str == NULL) { + log_err_id(id, "Server did not provide cipher."); + return -ECRYPT; + } + + SET_KEX_CIPHER(kcfg, peer_hdr->cipher_str); + if (crypt_validate_nid(kcfg->c.nid) < 0) { + log_err_id(id, "Server cipher '%s' not supported.", + peer_hdr->cipher_str); + return -ENOTSUP; + } + + log_dbg_id(id, "Accepted server cipher %s.", peer_hdr->cipher_str); + + /* Derive shared secret */ + if (IS_KEM_ALGORITHM(kcfg->x.str)) + return do_client_kex_complete_kem(s, peer_hdr, sk); + + return do_client_kex_complete_dhe(s, peer_hdr, sk); +} + +int oap_cli_complete(void * ctx, + const struct name_info * info, + buffer_t rsp_buf, + buffer_t * data, + struct crypt_sk * sk) +{ + struct oap_cli_ctx * s = ctx; + struct oap_hdr peer_hdr; + char peer[NAME_SIZE + 1]; + uint8_t * id; + + assert(ctx != NULL); + assert(info != NULL); + assert(data != NULL); + assert(sk != NULL); + + sk->nid = NID_undef; + + clrbuf(*data); + + memset(&peer_hdr, 0, sizeof(peer_hdr)); + + id = s->id.data; + + 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) { + log_err_id(id, "Failed to decode OAP response header."); + goto fail_oap; + } + + debug_oap_hdr_rcv(&peer_hdr); + + /* Verify response ID matches request */ + if (memcmp(peer_hdr.id.data, id, OAP_ID_SIZE) != 0) { + log_err_id(id, "OAP response ID mismatch."); + goto fail_oap; + } + + /* Authenticate server */ + if (oap_auth_peer(peer, &s->local_hdr, &peer_hdr) < 0) { + log_err_id(id, "Failed to authenticate server."); + goto fail_oap; + } + + /* Verify request hash in authenticated response */ + if (peer_hdr.req_hash.len == 0) { + log_err_id(id, "Response missing req_hash."); + 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."); + goto fail_oap; + } + + /* Verify peer certificate name matches expected destination */ + if (peer_hdr.crt.len > 0 && strcmp(peer, info->name) != 0) { + log_err_id(id, "Peer crt for '%s' does not match '%s'.", + peer, info->name); + 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; + } + + /* Copy piggybacked data from server response */ + if (oap_hdr_copy_data(&peer_hdr, data) < 0) { + log_err_id(id, "Failed to copy server data."); + goto fail_oap; + } + + log_info_id(id, "OAP completed for %s.", info->name); + + oap_ctx_free(s); + + return 0; + + fail_oap: + oap_ctx_free(s); + return -ECRYPT; +} diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c new file mode 100644 index 00000000..cdff7ab6 --- /dev/null +++ b/src/irmd/oap/hdr.c @@ -0,0 +1,456 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Header encoding, decoding, and debugging + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 +#include +#include +#include +#include +#include + +#include "config.h" + +#include "hdr.h" + +#include +#include +#include +#include +#include + +int oap_hdr_decode(struct oap_hdr * oap_hdr, + buffer_t hdr, + int req_md_nid) +{ + off_t offset; + uint16_t kex_len; + uint16_t ciph_nid; + size_t crt_len; + size_t data_len; + size_t hash_len; + size_t sig_len; + + assert(oap_hdr != NULL); + memset(oap_hdr, 0, sizeof(*oap_hdr)); + + if (hdr.len < OAP_HDR_MIN_SIZE) + goto fail_decode; + + /* Parse fixed header (36 bytes) */ + oap_hdr->id.data = hdr.data; + oap_hdr->id.len = OAP_ID_SIZE; + + offset = OAP_ID_SIZE; + + oap_hdr->timestamp = ntoh64(*(uint64_t *)(hdr.data + offset)); + offset += sizeof(uint64_t); + + /* cipher NID */ + ciph_nid = ntoh16(*(uint16_t *)(hdr.data + offset)); + oap_hdr->nid = ciph_nid; + oap_hdr->cipher_str = crypt_nid_to_str(ciph_nid); + offset += sizeof(uint16_t); + + /* kdf NID */ + oap_hdr->kdf_nid = ntoh16(*(uint16_t *)(hdr.data + offset)); + oap_hdr->kdf_str = md_nid_to_str(oap_hdr->kdf_nid); + offset += sizeof(uint16_t); + + /* md NID (signature hash) */ + oap_hdr->md_nid = ntoh16(*(uint16_t *)(hdr.data + offset)); + 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. + * 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; + if (oap_hdr->md_nid != NID_undef && + md_validate_nid(oap_hdr->md_nid) < 0) + goto fail_decode; + + /* crt_len */ + crt_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); + offset += sizeof(uint16_t); + + /* kex_len + flags */ + kex_len = ntoh16(*(uint16_t *)(hdr.data + offset)); + oap_hdr->kex.len = (size_t) (kex_len & OAP_KEX_LEN_MASK); + oap_hdr->kex_flags.fmt = (kex_len & OAP_KEX_FMT_BIT) ? 1 : 0; + oap_hdr->kex_flags.role = (kex_len & OAP_KEX_ROLE_BIT) ? 1 : 0; + offset += sizeof(uint16_t); + + /* data_len */ + data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset)); + offset += sizeof(uint16_t); + + /* Response includes req_hash when md_nid is set */ + hash_len = (req_md_nid != NID_undef) ? + (size_t) md_len(req_md_nid) : 0; + + /* Validate total length */ + if (hdr.len < (size_t) offset + crt_len + oap_hdr->kex.len + + data_len + hash_len) + goto fail_decode; + + /* Derive sig_len from remaining bytes */ + 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) + goto fail_decode; + + /* Parse variable fields */ + oap_hdr->crt.data = hdr.data + offset; + oap_hdr->crt.len = crt_len; + offset += crt_len; + + oap_hdr->kex.data = hdr.data + offset; + offset += oap_hdr->kex.len; + + oap_hdr->data.data = hdr.data + offset; + oap_hdr->data.len = data_len; + offset += data_len; + + oap_hdr->req_hash.data = hdr.data + offset; + oap_hdr->req_hash.len = hash_len; + offset += hash_len; + + oap_hdr->sig.data = hdr.data + offset; + oap_hdr->sig.len = sig_len; + + oap_hdr->hdr = hdr; + + return 0; + + fail_decode: + memset(oap_hdr, 0, sizeof(*oap_hdr)); + return -1; +} + +void oap_hdr_fini(struct oap_hdr * oap_hdr) +{ + assert(oap_hdr != NULL); + + freebuf(oap_hdr->hdr); + memset(oap_hdr, 0, sizeof(*oap_hdr)); +} + +int oap_hdr_copy_data(const struct oap_hdr * hdr, + buffer_t * out) +{ + assert(hdr != NULL); + assert(out != NULL); + + if (hdr->data.len == 0) { + clrbuf(*out); + return 0; + } + + out->data = malloc(hdr->data.len); + if (out->data == NULL) + return -ENOMEM; + + memcpy(out->data, hdr->data.data, hdr->data.len); + out->len = hdr->data.len; + + return 0; +} + +void oap_hdr_init(struct oap_hdr * hdr, + buffer_t id, + uint8_t * kex_buf, + buffer_t data, + uint16_t nid) +{ + assert(hdr != NULL); + assert(id.data != NULL && id.len == OAP_ID_SIZE); + + memset(hdr, 0, sizeof(*hdr)); + + hdr->id = id; + hdr->kex.data = kex_buf; + hdr->kex.len = 0; + hdr->data = data; + hdr->nid = nid; +} + +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 timespec now; + uint64_t stamp; + buffer_t out; + 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); + + clock_gettime(CLOCK_REALTIME, &now); + stamp = hton64(TS_TO_UINT64(now)); + + 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) */ + out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len + + req_hash.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); + + /* 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; + + assert((size_t) offset == out.len); + + /* Sign the entire header (fixed + variable, excluding signature) */ + sign.data = out.data; + sign.len = out.len; + + if (pkp != NULL && auth_sign(pkp, kcfg->d.nid, sign, &sig) < 0) + goto fail_sig; + + hdr->hdr = out; + + /* Append signature */ + if (sig.len > 0) { + hdr->hdr.len += sig.len; + hdr->hdr.data = realloc(out.data, hdr->hdr.len); + if (hdr->hdr.data == NULL) + 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) + goto fail_decode; + + freebuf(der); + freebuf(sig); + + return 0; + + fail_decode: + oap_hdr_fini(hdr); + fail_realloc: + freebuf(sig); + fail_sig: + freebuf(out); + fail_out: + freebuf(der); + fail_der: + return -1; +} + +#ifdef DEBUG_PROTO_OAP +static void debug_oap_hdr(const struct oap_hdr * hdr) +{ + assert(hdr); + + if (hdr->crt.len > 0) + log_proto(" crt: [%zu bytes]", hdr->crt.len); + else + log_proto(" crt: "); + + if (hdr->kex.len > 0) + log_proto(" Key Exchange Data: [%zu bytes] [%s]", + hdr->kex.len, hdr->kex_flags.role ? + "Client encaps" : "Server encaps"); + else + log_proto(" Ephemeral Public Key: "); + + if (hdr->cipher_str != NULL) + log_proto(" Cipher: %s", hdr->cipher_str); + else + log_proto(" Cipher: "); + + if (hdr->kdf_str != NULL) + log_proto(" KDF: HKDF-%s", hdr->kdf_str); + else + log_proto(" KDF: "); + + if (hdr->md_str != NULL) + log_proto(" Digest: %s", hdr->md_str); + else + log_proto(" Digest: "); + + if (hdr->data.len > 0) + log_proto(" Data: [%zu bytes]", hdr->data.len); + else + log_proto(" Data: "); + + if (hdr->req_hash.len > 0) + log_proto(" Req Hash: [%zu bytes]", hdr->req_hash.len); + else + log_proto(" Req Hash: "); + + if (hdr->sig.len > 0) + log_proto(" Signature: [%zu bytes]", hdr->sig.len); + else + log_proto(" Signature: "); +} +#endif + +void debug_oap_hdr_rcv(const struct oap_hdr * hdr) +{ +#ifdef DEBUG_PROTO_OAP + struct tm * tm; + char tmstr[RIB_TM_STRLEN]; + time_t stamp; + + assert(hdr); + + stamp = (time_t) hdr->timestamp / BILLION; + + tm = gmtime(&stamp); + strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); + + log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--", + HASH_VAL64(hdr->id.data), tmstr); + + debug_oap_hdr(hdr); +#else + (void) hdr; +#endif +} + +void debug_oap_hdr_snd(const struct oap_hdr * hdr) +{ +#ifdef DEBUG_PROTO_OAP + struct tm * tm; + char tmstr[RIB_TM_STRLEN]; + time_t stamp; + + assert(hdr); + + stamp = (time_t) hdr->timestamp / BILLION; + + tm = gmtime(&stamp); + strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm); + + log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->", + HASH_VAL64(hdr->id.data), tmstr); + + debug_oap_hdr(hdr); +#else + (void) hdr; +#endif +} diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h new file mode 100644 index 00000000..f603b169 --- /dev/null +++ b/src/irmd/oap/hdr.h @@ -0,0 +1,159 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Header definitions and functions + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#ifndef OUROBOROS_IRMD_OAP_HDR_H +#define OUROBOROS_IRMD_OAP_HDR_H + +#include +#include + +#include +#include + +#define OAP_ID_SIZE (16) +#define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 6 * sizeof(uint16_t)) + +#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw */ +#define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 0=Server encaps, 1=Client encaps */ +#define OAP_KEX_LEN_MASK 0x3FFF /* bits 0-13: Length (0-16383 bytes) */ + +#define OAP_KEX_ROLE(hdr) (hdr->kex_flags.role) +#define OAP_KEX_FMT(hdr) (hdr->kex_flags.fmt) + +#define OAP_KEX_IS_X509_FMT(hdr) (((hdr)->kex_flags.fmt) == 0) +#define OAP_KEX_IS_RAW_FMT(hdr) (((hdr)->kex_flags.fmt) == 1) + +/* + * 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 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ + * | | | + * + + | + * | | | + * + id (128 bits) + | + * | Unique flow allocation ID | | + * + + | + * | | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | | | + * + timestamp (64 bits) + | + * | UTC nanoseconds since epoch | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | cipher_nid (16 bits) | kdf_nid (16 bits) | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | md_nid (16 bits) | crt_len (16 bits) | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * |F|R| kex_len (14 bits) | data_len (16 bits) | | Signed + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Region + * | | | + * + certificate (variable) + | + * | X.509 certificate, DER encoded | | + * + + | + * | | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | | | + * + kex_data (variable) + | + * | public key (DER/raw) or ciphertext (KEM) | | + * + + | + * | | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | | | + * + data (variable) + | + * | Piggybacked application data | | + * + + | + * | | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | + * | | | + * + req_hash (variable, response only) + | + * | H(request) using req md_nid / sha384 | | + * | | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+ + * | | + * + signature (variable) + + * | DSA signature over signed region | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * 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) + * + * kex_len field bit layout: + * F (bit 15): Format - 0 = X.509 DER, 1 = Raw/Hybrid + * R (bit 14): Role - 0 = Server encaps, 1 = Client encaps + * (R is ignored for non-KEM algorithms) + * Bits 0-13: Length (0-16383 bytes) + * + * 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) + */ + +/* Parsed OAP header - buffers pointing to a single memory region */ +struct oap_hdr { + const char * cipher_str; + const char * kdf_str; + const char * md_str; + uint64_t timestamp; + uint16_t nid; + uint16_t kdf_nid; + uint16_t md_nid; + struct { + 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 sig; + buffer_t hdr; +}; + + +void oap_hdr_init(struct oap_hdr * hdr, + buffer_t id, + uint8_t * kex_buf, + buffer_t data, + uint16_t nid); + +void oap_hdr_fini(struct oap_hdr * oap_hdr); + +int oap_hdr_encode(struct oap_hdr * hdr, + void * pkp, + void * crt, + struct sec_config * kcfg, + buffer_t req_hash, + int req_md_nid); + +int oap_hdr_decode(struct oap_hdr * hdr, + buffer_t buf, + int req_md_nid); + +void debug_oap_hdr_rcv(const struct oap_hdr * hdr); + +void debug_oap_hdr_snd(const struct oap_hdr * hdr); + +int oap_hdr_copy_data(const struct oap_hdr * hdr, + buffer_t * out); + +#endif /* OUROBOROS_IRMD_OAP_HDR_H */ diff --git a/src/irmd/oap/internal.h b/src/irmd/oap/internal.h new file mode 100644 index 00000000..8363e3a2 --- /dev/null +++ b/src/irmd/oap/internal.h @@ -0,0 +1,133 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP internal definitions + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#ifndef OUROBOROS_IRMD_OAP_INTERNAL_H +#define OUROBOROS_IRMD_OAP_INTERNAL_H + +#include +#include +#include +#include +#include + +#include "hdr.h" + +#include +#include + +/* + * Authentication functions (auth.c) + */ +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); + +/* + * Key exchange functions (kex.c) + */ +int oap_negotiate_cipher(const struct oap_hdr * peer_hdr, + struct sec_config * kcfg); + +/* + * Credential loading (oap.c) - shared between client and server + */ +#ifndef OAP_TEST_MODE +int load_credentials(const char * name, + const struct name_sec_paths * paths, + void ** pkp, + void ** crt); + +int load_kex_config(const char * name, + const char * path, + struct sec_config * cfg); +#endif + +/* + * Server functions (srv.c) + */ +#ifndef OAP_TEST_MODE +int load_srv_credentials(const struct name_info * info, + void ** pkp, + void ** crt); + +int load_srv_kex_config(const struct name_info * info, + struct sec_config * cfg); + +int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp); +#else +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, + struct sec_config * cfg); +extern int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp); +#endif + +int do_server_kex(const struct name_info * info, + struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + buffer_t * kex, + struct crypt_sk * sk); + +/* + * Client functions (cli.c) + */ +#ifndef OAP_TEST_MODE +int load_cli_credentials(const struct name_info * info, + void ** pkp, + void ** crt); + +int load_cli_kex_config(const struct name_info * info, + struct sec_config * cfg); + +int load_server_kem_pk(const char * name, + struct sec_config * cfg, + buffer_t * pk); +#else +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, + 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, + 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, + void * pkp, + uint8_t * key); + +#endif /* OUROBOROS_IRMD_OAP_INTERNAL_H */ diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c new file mode 100644 index 00000000..e4189d4d --- /dev/null +++ b/src/irmd/oap/io.c @@ -0,0 +1,132 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - File I/O for credentials and configuration + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 +#include +#include + +#include "config.h" + +#include "io.h" + +#include +#include +#include + +/* + * 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/io.h b/src/irmd/oap/io.h new file mode 100644 index 00000000..a31ddf85 --- /dev/null +++ b/src/irmd/oap/io.h @@ -0,0 +1,40 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Credential and configuration file I/O + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#ifndef OUROBOROS_IRMD_OAP_IO_H +#define OUROBOROS_IRMD_OAP_IO_H + +#include +#include + +#ifndef OAP_TEST_MODE +int load_credentials(const char * name, + const struct name_sec_paths * paths, + void ** pkp, + void ** crt); + +int load_kex_config(const char * name, + const char * path, + struct sec_config * cfg); +#endif + +#endif /* OUROBOROS_IRMD_OAP_IO_H */ diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c new file mode 100644 index 00000000..c5a4453f --- /dev/null +++ b/src/irmd/oap/srv.c @@ -0,0 +1,462 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * OAP - Server-side processing + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 +#include +#include + +#include "config.h" + +#include "auth.h" +#include "hdr.h" +#include "io.h" +#include "oap.h" + +#include +#include +#include +#include +#include + +#ifdef OAP_TEST_MODE +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, + struct sec_config * cfg); +extern int load_server_kem_keypair(const char * name, + bool raw_fmt, + void ** pkp); +#else + +int load_srv_credentials(const struct name_info * info, + void ** pkp, + void ** crt) +{ + assert(info != NULL); + assert(pkp != NULL); + assert(crt != NULL); + + return load_credentials(info->name, &info->s, pkp, crt); +} + +int load_srv_kex_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); +} + +int load_server_kem_keypair(const char * name, + bool raw_fmt, + void ** pkp) +{ + char path[PATH_MAX]; + const char * ext; + + assert(name != NULL); + assert(pkp != NULL); + + ext = raw_fmt ? "raw" : "pem"; + + snprintf(path, sizeof(path), + OUROBOROS_SRV_CRT_DIR "/%s/kex.key.%s", name, ext); + + if (raw_fmt) { + if (crypt_load_privkey_raw_file(path, pkp) < 0) { + log_err("Failed to load %s keypair from %s.", + ext, path); + return -ECRYPT; + } + } else { + if (crypt_load_privkey_file(path, pkp) < 0) { + log_err("Failed to load %s keypair from %s.", + ext, path); + return -ECRYPT; + } + } + + log_dbg("Loaded server KEM keypair from %s.", path); + return 0; +} + +#endif /* OAP_TEST_MODE */ + +static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr, + char * algo_buf) +{ + uint8_t * id = peer_hdr->id.data; + int ret; + + if (OAP_KEX_IS_RAW_FMT(peer_hdr)) { + ret = kex_get_algo_from_pk_raw(peer_hdr->kex, algo_buf); + if (ret < 0) { + log_err_id(id, "Failed to get algo from raw key."); + return -ECRYPT; + } + } else { + ret = kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf); + if (ret < 0) { + log_err_id(id, "Failed to get algo from DER key."); + return -ECRYPT; + } + } + + return 0; +} + +static int negotiate_kex(const struct oap_hdr * peer_hdr, + struct sec_config * kcfg) +{ + uint8_t * id = peer_hdr->id.data; + + if (kcfg->c.nid == NID_undef) { + if (peer_hdr->cipher_str != NULL) { + SET_KEX_CIPHER(kcfg, peer_hdr->cipher_str); + if (kcfg->c.nid == NID_undef) { + log_err_id(id, "Unsupported cipher '%s'.", + peer_hdr->cipher_str); + return -ENOTSUP; + } + log_dbg_id(id, "Peer requested cipher %s.", + peer_hdr->cipher_str); + } else { + log_err_id(id, "Encryption requested, no cipher."); + return -ECRYPT; + } + } else { + log_dbg_id(id, "Using local cipher %s.", kcfg->c.str); + } + + /* Negotiate KDF - server overrides client if configured */ + if (kcfg->k.nid != NID_undef) { + log_dbg_id(id, "Using local KDF %s.", + md_nid_to_str(kcfg->k.nid)); + } else if (peer_hdr->kdf_nid != NID_undef) { + if (md_validate_nid(peer_hdr->kdf_nid) == 0) { + kcfg->k.nid = peer_hdr->kdf_nid; + log_dbg_id(id, "Using peer KDF %s.", + md_nid_to_str(peer_hdr->kdf_nid)); + } else { + log_err_id(id, "Unsupported KDF NID %d.", + peer_hdr->kdf_nid); + return -ENOTSUP; + } + } + + if (IS_KEX_ALGO_SET(kcfg)) + log_info_id(id, "Negotiated %s + %s.", + kcfg->x.str, kcfg->c.str); + else + log_info_id(id, "No key exchange."); + + return 0; +} + +static int do_server_kem_decap(const struct name_info * info, + const struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + struct crypt_sk * sk) +{ + buffer_t ct; + void * server_pkp = NULL; + int ret; + uint8_t * id = peer_hdr->id.data; + + ret = load_server_kem_keypair(info->name, + peer_hdr->kex_flags.fmt, + &server_pkp); + if (ret < 0) + return ret; + + ct.data = peer_hdr->kex.data; + ct.len = peer_hdr->kex.len; + + ret = kex_kem_decap(server_pkp, ct, kcfg->k.nid, sk->key); + + crypt_free_key(server_pkp); + + if (ret < 0) { + log_err_id(id, "Failed to decapsulate KEM."); + return -ECRYPT; + } + + log_dbg_id(id, "Client encaps: decapsulated CT."); + + return 0; +} + +static int do_server_kem_encap(const struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + buffer_t * kex, + struct crypt_sk * sk) +{ + buffer_t client_pk; + ssize_t ct_len; + uint8_t * id = peer_hdr->id.data; + + client_pk.data = peer_hdr->kex.data; + client_pk.len = peer_hdr->kex.len; + + if (IS_HYBRID_KEM(kcfg->x.str)) + ct_len = kex_kem_encap_raw(client_pk, kex->data, + kcfg->k.nid, sk->key); + else + ct_len = kex_kem_encap(client_pk, kex->data, + kcfg->k.nid, sk->key); + + if (ct_len < 0) { + log_err_id(id, "Failed to encapsulate KEM."); + return -ECRYPT; + } + + kex->len = (size_t) ct_len; + + log_dbg_id(id, "Server encaps: generated CT, len=%zd.", ct_len); + + return 0; +} + +static int do_server_kex_kem(const struct name_info * info, + struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + buffer_t * kex, + struct crypt_sk * sk) +{ + int ret; + + kcfg->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); + kex->len = 0; + } else { + ret = do_server_kem_encap(peer_hdr, kcfg, kex, sk); + } + + return ret; +} + +static int do_server_kex_dhe(const struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + buffer_t * kex, + struct crypt_sk * sk) +{ + ssize_t key_len; + void * epkp; + int ret; + uint8_t * id = peer_hdr->id.data; + + key_len = kex_pkp_create(kcfg, &epkp, kex->data); + if (key_len < 0) { + log_err_id(id, "Failed to generate key pair."); + return -ECRYPT; + } + + kex->len = (size_t) key_len; + + log_dbg_id(id, "Generated %s ephemeral keys.", kcfg->x.str); + + ret = kex_dhe_derive(kcfg, epkp, peer_hdr->kex, sk->key); + if (ret < 0) { + log_err_id(id, "Failed to derive secret."); + kex_pkp_destroy(epkp); + return -ECRYPT; + } + + kex_pkp_destroy(epkp); + + return 0; +} + +int do_server_kex(const struct name_info * info, + struct oap_hdr * peer_hdr, + struct sec_config * kcfg, + buffer_t * kex, + struct crypt_sk * sk) +{ + char algo_buf[KEX_ALGO_BUFSZ]; + uint8_t * id; + + id = peer_hdr->id.data; + + /* No KEX data from client */ + if (peer_hdr->kex.len == 0) { + if (IS_KEX_ALGO_SET(kcfg)) { + log_warn_id(id, "KEX requested without info."); + return -ECRYPT; + } + return 0; + } + + if (negotiate_kex(peer_hdr, kcfg) < 0) + return -ECRYPT; + + 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); + } + + /* Dispatch based on algorithm type */ + if (IS_KEM_ALGORITHM(kcfg->x.str)) + return do_server_kex_kem(info, peer_hdr, kcfg, kex, sk); + else + return do_server_kex_dhe(peer_hdr, kcfg, 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 oap_hdr peer_hdr; + struct oap_hdr local_hdr; + struct sec_config kcfg; + uint8_t kex_buf[MSGBUFSZ]; + uint8_t hash_buf[MAX_HASH_SIZE]; + buffer_t req_hash = BUF_INIT; + ssize_t hash_ret; + char cli_name[NAME_SIZE + 1]; /* TODO */ + uint8_t * id; + void * pkp = NULL; + void * crt = NULL; + int req_md_nid; + + assert(info != NULL); + assert(rsp_buf != NULL); + assert(data != NULL); + assert(sk != NULL); + + sk->nid = NID_undef; + + memset(&peer_hdr, 0, sizeof(peer_hdr)); + memset(&local_hdr, 0, sizeof(local_hdr)); + clrbuf(*rsp_buf); + + log_dbg("Processing OAP request for %s.", info->name); + + /* Load server credentials */ + if (load_srv_credentials(info, &pkp, &crt) < 0) { + log_err("Failed to load security keys for %s.", info->name); + goto fail_cred; + } + + /* Load KEX config */ + if (load_srv_kex_config(info, &kcfg) < 0) { + log_err("Failed to load KEX config for %s.", info->name); + goto fail_kex; + } + + sk->nid = kcfg.c.nid; + + /* Decode incoming header (NID_undef = request, no hash) */ + if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef) < 0) { + log_err("Failed to decode OAP header."); + goto fail_auth; + } + + debug_oap_hdr_rcv(&peer_hdr); + + id = peer_hdr.id.data; /* Logging */ + + /* Check for replay */ + if (oap_check_hdr(&peer_hdr) < 0) { + log_err_id(id, "OAP header failed replay check."); + goto fail_auth; + } + + /* Authenticate client before processing KEX data */ + oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef); + + if (oap_auth_peer(cli_name, &local_hdr, &peer_hdr) < 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) + goto fail_kex; + + /* Build response header with hash of client request */ + local_hdr.nid = sk->nid; + + /* Use client's md_nid, defaulting to SHA-384 for PQC */ + req_md_nid = peer_hdr.md_nid != NID_undef ? peer_hdr.md_nid : NID_sha384; + + /* Compute request hash using client's md_nid */ + hash_ret = md_digest(req_md_nid, req_buf, hash_buf); + if (hash_ret < 0) { + log_err_id(id, "Failed to hash request."); + goto fail_auth; + } + req_hash.data = hash_buf; + req_hash.len = (size_t) hash_ret; + + if (oap_hdr_encode(&local_hdr, pkp, crt, &kcfg, + req_hash, req_md_nid) < 0) { + log_err_id(id, "Failed to create OAP response header."); + goto fail_auth; + } + + debug_oap_hdr_snd(&local_hdr); + + if (oap_hdr_copy_data(&peer_hdr, data) < 0) { + log_err_id(id, "Failed to copy client data."); + goto fail_data; + } + + /* Transfer ownership of response buffer */ + *rsp_buf = local_hdr.hdr; + + log_info_id(id, "OAP request processed for %s.", info->name); + + crypt_free_crt(crt); + crypt_free_key(pkp); + + return 0; + + fail_data: + oap_hdr_fini(&local_hdr); + fail_auth: + crypt_free_crt(crt); + crypt_free_key(pkp); + fail_cred: + return -EAUTH; + + fail_kex: + crypt_free_crt(crt); + crypt_free_key(pkp); + return -ECRYPT; +} diff --git a/src/irmd/oap/tests/CMakeLists.txt b/src/irmd/oap/tests/CMakeLists.txt new file mode 100644 index 00000000..861b3590 --- /dev/null +++ b/src/irmd/oap/tests/CMakeLists.txt @@ -0,0 +1,78 @@ +get_filename_component(tmp ".." ABSOLUTE) +get_filename_component(src_folder "${tmp}" NAME) + +compute_test_prefix() + +create_test_sourcelist(${src_folder}_tests test_suite.c + # Add new tests here + oap_test.c +) + +create_test_sourcelist(${src_folder}_pqc_tests test_suite_pqc.c + # PQC-specific tests + oap_test_pqc.c +) + +# OAP test needs io.c compiled with OAP_TEST_MODE +set(OAP_TEST_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/../io.c + ${CMAKE_CURRENT_SOURCE_DIR}/../hdr.c + ${CMAKE_CURRENT_SOURCE_DIR}/../auth.c + ${CMAKE_CURRENT_SOURCE_DIR}/../srv.c + ${CMAKE_CURRENT_SOURCE_DIR}/../cli.c + ${CMAKE_CURRENT_SOURCE_DIR}/common.c +) + +# Regular test executable (ECDSA) +add_executable(${src_folder}_test ${${src_folder}_tests} ${OAP_TEST_SOURCES}) +set_source_files_properties(${OAP_TEST_SOURCES} + PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE" +) +target_link_libraries(${src_folder}_test ouroboros-irm) +target_include_directories(${src_folder}_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_BINARY_DIR}/../.. +) + +# PQC test executable (ML-DSA) +add_executable(${src_folder}_pqc_test ${${src_folder}_pqc_tests} ${OAP_TEST_SOURCES}) +set_source_files_properties(${OAP_TEST_SOURCES} + TARGET_DIRECTORY ${src_folder}_pqc_test + PROPERTIES COMPILE_DEFINITIONS "OAP_TEST_MODE" +) +target_link_libraries(${src_folder}_pqc_test ouroboros-irm) +target_include_directories(${src_folder}_pqc_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_BINARY_DIR}/../.. +) + +add_dependencies(build_tests ${src_folder}_test ${src_folder}_pqc_test) + +# Regular tests +set(tests_to_run ${${src_folder}_tests}) +if(CMAKE_VERSION VERSION_LESS "3.29.0") + remove(tests_to_run test_suite.c) +else () + list(POP_FRONT tests_to_run) +endif() + +foreach(test ${tests_to_run}) + get_filename_component(test_name ${test} NAME_WE) + add_test(${TEST_PREFIX}/${test_name} ${CMAKE_CURRENT_BINARY_DIR}/${src_folder}_test ${test_name}) +endforeach(test) + +# PQC tests +set(pqc_tests_to_run ${${src_folder}_pqc_tests}) +if(CMAKE_VERSION VERSION_LESS "3.29.0") + remove(pqc_tests_to_run test_suite_pqc.c) +else () + list(POP_FRONT pqc_tests_to_run) +endif() + +foreach(test ${pqc_tests_to_run}) + get_filename_component(test_name ${test} NAME_WE) + add_test(${TEST_PREFIX}/${test_name} ${CMAKE_CURRENT_BINARY_DIR}/${src_folder}_pqc_test ${test_name}) +endforeach(test) + +set_property(TEST ${TEST_PREFIX}/oap_test PROPERTY SKIP_RETURN_CODE 1) +set_property(TEST ${TEST_PREFIX}/oap_test_pqc PROPERTY SKIP_RETURN_CODE 1) diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c new file mode 100644 index 00000000..0a1af100 --- /dev/null +++ b/src/irmd/oap/tests/common.c @@ -0,0 +1,457 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Common test helper functions for OAP tests + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#include "common.h" + +#include + +#include "oap.h" + +#include +#include + +int load_srv_kex_config(const struct name_info * info, + struct sec_config * cfg) +{ + (void) info; + + memset(cfg, 0, sizeof(*cfg)); + + 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, + struct sec_config * cfg) +{ + (void) info; + + memset(cfg, 0, sizeof(*cfg)); + + 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; +} + +int load_srv_credentials(const struct name_info * info, + void ** pkp, + void ** crt) +{ + (void) info; + + *pkp = NULL; + *crt = NULL; + + if (!test_cfg.srv.auth) + return 0; + + return mock_load_credentials(pkp, crt); +} + +int load_cli_credentials(const struct name_info * info, + void ** pkp, + void ** crt) +{ + (void) info; + + *pkp = NULL; + *crt = NULL; + + if (!test_cfg.cli.auth) + return 0; + + return mock_load_credentials(pkp, crt); +} + +int oap_test_setup(struct oap_test_ctx * ctx, + const char * root_ca_str, + const char * im_ca_str) +{ + 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_init; + } + + if (crypt_load_crt_str(root_ca_str, &ctx->root_ca) < 0) { + printf("Failed to load root CA cert.\n"); + goto fail_root_ca; + } + + if (crypt_load_crt_str(im_ca_str, &ctx->im_ca) < 0) { + printf("Failed to load intermediate CA cert.\n"); + goto fail_im_ca; + } + + if (oap_auth_add_ca_crt(ctx->root_ca) < 0) { + printf("Failed to add root CA cert to store.\n"); + goto fail_add_ca; + } + + if (oap_auth_add_ca_crt(ctx->im_ca) < 0) { + printf("Failed to add intermediate CA cert to store.\n"); + goto fail_add_ca; + } + + return 0; + + fail_add_ca: + crypt_free_crt(ctx->im_ca); + fail_im_ca: + crypt_free_crt(ctx->root_ca); + fail_root_ca: + oap_auth_fini(); + fail_init: + memset(ctx, 0, sizeof(*ctx)); + return -1; +} + +void oap_test_teardown(struct oap_test_ctx * ctx) +{ + struct crypt_sk res; + buffer_t dummy = BUF_INIT; + + if (ctx->cli.state != NULL) { + res.key = ctx->cli.key; + oap_cli_complete(ctx->cli.state, &ctx->cli.info, dummy, + &ctx->data, &res); + ctx->cli.state = NULL; + } + + freebuf(ctx->data); + freebuf(ctx->resp_hdr); + freebuf(ctx->req_hdr); + + crypt_free_crt(ctx->im_ca); + crypt_free_crt(ctx->root_ca); + + oap_auth_fini(); + memset(ctx, 0, sizeof(*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); +} + +int oap_srv_process_ctx(struct oap_test_ctx * ctx) +{ + struct crypt_sk res = { .nid = NID_undef, .key = ctx->srv.key }; + int ret; + + ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr, + &ctx->resp_hdr, &ctx->data, &res); + if (ret == 0) + ctx->srv.nid = res.nid; + + return ret; +} + +int oap_cli_complete_ctx(struct oap_test_ctx * ctx) +{ + struct crypt_sk res = { .nid = NID_undef, .key = ctx->cli.key }; + int ret; + + ret = oap_cli_complete(ctx->cli.state, &ctx->cli.info, ctx->resp_hdr, + &ctx->data, &res); + ctx->cli.state = NULL; + + if (ret == 0) + ctx->cli.nid = res.nid; + + return ret; +} + +int roundtrip_auth_only(const char * root_ca, + const char * im_ca_str) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; + } + + if (ctx.cli.nid != NID_undef || ctx.srv.nid != NID_undef) { + printf("Cipher should not be set for auth-only.\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; +} + +int roundtrip_kex_only(void) +{ + struct name_info cli_info; + struct name_info srv_info; + struct crypt_sk res; + uint8_t cli_key[SYMMKEYSZ]; + uint8_t srv_key[SYMMKEYSZ]; + int cli_nid; + int srv_nid; + buffer_t req_hdr = BUF_INIT; + buffer_t resp_hdr = BUF_INIT; + buffer_t data = BUF_INIT; + void * cli_state = NULL; + + TEST_START(); + + memset(&cli_info, 0, sizeof(cli_info)); + memset(&srv_info, 0, sizeof(srv_info)); + + strcpy(cli_info.name, "test-1.unittest.o7s"); + strcpy(srv_info.name, "test-1.unittest.o7s"); + + if (oap_auth_init() < 0) { + printf("Failed to init OAP.\n"); + goto fail; + } + + if (oap_cli_prepare(&cli_state, &cli_info, &req_hdr, + data) < 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) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + srv_nid = res.nid; + + res.key = cli_key; + + if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res) < 0) { + printf("Client complete failed.\n"); + cli_state = NULL; + goto fail_cleanup; + } + + cli_nid = res.nid; + cli_state = NULL; + + if (memcmp(cli_key, srv_key, SYMMKEYSZ) != 0) { + printf("Client and server keys do not match!\n"); + goto fail_cleanup; + } + + if (cli_nid == NID_undef || srv_nid == NID_undef) { + printf("Cipher should be set for kex-only.\n"); + goto fail_cleanup; + } + + freebuf(resp_hdr); + freebuf(req_hdr); + oap_auth_fini(); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + if (cli_state != NULL) { + res.key = cli_key; + oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res); + } + freebuf(resp_hdr); + freebuf(req_hdr); + oap_auth_fini(); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int corrupted_request(const char * root_ca, + const char * im_ca_str) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* Corrupt the request */ + if (ctx.req_hdr.len > 100) { + ctx.req_hdr.data[50] ^= 0xFF; + ctx.req_hdr.data[51] ^= 0xAA; + ctx.req_hdr.data[52] ^= 0x55; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject corrupted request.\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; +} + +int corrupted_response(const char * root_ca, + const char * im_ca_str) +{ + struct oap_test_ctx ctx; + struct crypt_sk res; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 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; + } + + /* Corrupt the response */ + if (ctx.resp_hdr.len > 100) { + ctx.resp_hdr.data[50] ^= 0xFF; + ctx.resp_hdr.data[51] ^= 0xAA; + ctx.resp_hdr.data[52] ^= 0x55; + } + + res.key = ctx.cli.key; + + if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr, + &ctx.data, &res) == 0) { + printf("Client should reject corrupted response.\n"); + ctx.cli.state = NULL; + goto fail_cleanup; + } + + ctx.cli.state = NULL; + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int truncated_request(const char * root_ca, + const char * im_ca_str) +{ + struct oap_test_ctx ctx; + size_t orig_len; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca, im_ca_str) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* Truncate the request buffer */ + orig_len = ctx.req_hdr.len; + ctx.req_hdr.len = orig_len / 2; + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject truncated request.\n"); + ctx.req_hdr.len = orig_len; + goto fail_cleanup; + } + + ctx.req_hdr.len = orig_len; + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h new file mode 100644 index 00000000..d4b6733a --- /dev/null +++ b/src/irmd/oap/tests/common.h @@ -0,0 +1,100 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Common test helper functions for OAP tests + * + * Dimitri Staessens + * Sander Vrijders + * + * 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/. + */ + +#ifndef IRMD_TESTS_COMMON_H +#define IRMD_TESTS_COMMON_H + +#include +#include +#include +#include + +#include + +/* 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) */ +}; + +/* Test configuration - set by each test before running roundtrip */ +extern struct test_cfg { + struct test_sec_cfg srv; + struct test_sec_cfg cli; +} test_cfg; + +/* Each test file defines this with its own certificates */ +extern int mock_load_credentials(void ** pkp, + void ** crt); + +/* Per-side test context */ +struct oap_test_side { + struct name_info info; + struct flow_info flow; + uint8_t key[SYMMKEYSZ]; + int nid; + void * state; +}; + +/* Test context - holds all common state for OAP tests */ +struct oap_test_ctx { + struct oap_test_side srv; + struct oap_test_side cli; + + buffer_t req_hdr; + buffer_t resp_hdr; + buffer_t data; + void * root_ca; + void * im_ca; +}; + +int oap_test_setup(struct oap_test_ctx * ctx, + const char * root_ca_str, + const char * im_ca_str); + +void oap_test_teardown(struct oap_test_ctx * ctx); + +int oap_cli_prepare_ctx(struct oap_test_ctx * ctx); + +int oap_srv_process_ctx(struct oap_test_ctx * ctx); + +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_kex_only(void); + +int corrupted_request(const char * root_ca, + const char * im_ca_str); + +int corrupted_response(const char * root_ca, + const char * im_ca_str); + +int truncated_request(const char * root_ca, + const char * im_ca_str); + +#endif /* IRMD_TESTS_COMMON_H */ diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c new file mode 100644 index 00000000..70943d7c --- /dev/null +++ b/src/irmd/oap/tests/oap_test.c @@ -0,0 +1,947 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2024 + * + * Unit tests of Ouroboros Allocation Protocol (OAP) + * + * Dimitri Staessens + * Sander Vrijders + * + * 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__) + #ifndef _DEFAULT_SOURCE + #define _DEFAULT_SOURCE + #endif +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include "config.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "oap.h" +#include "common.h" + +#include +#include + +#ifdef HAVE_OPENSSL +#include +#endif + +#define AUTH true +#define NO_AUTH false + +extern const uint16_t kex_supported_nids[]; +extern const uint16_t md_supported_nids[]; + +struct test_cfg test_cfg; + +/* Mock load - called by load_*_credentials in common.c */ +int mock_load_credentials(void ** pkp, + void ** crt) +{ + *crt = NULL; + + if (crypt_load_privkey_str(server_pkp_ec, pkp) < 0) + goto fail_privkey; + + if (crypt_load_crt_str(signed_server_crt_ec, crt) < 0) + goto fail_crt; + + return 0; + + fail_crt: + crypt_free_key(*pkp); + fail_privkey: + *pkp = NULL; + return -1; +} + +/* Stub KEM functions - ECDSA tests don't use KEM */ +int load_server_kem_keypair(__attribute__((unused)) const char * name, + __attribute__((unused)) bool raw_fmt, + __attribute__((unused)) void ** pkp) +{ + return -1; +} + +int load_server_kem_pk(__attribute__((unused)) const char * name, + __attribute__((unused)) struct sec_config * cfg, + __attribute__((unused)) buffer_t * pk) +{ + return -1; +} + +static void test_default_cfg(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: X25519, AES-256-GCM, SHA-256, with 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 = AUTH; + + /* Client: same KEX/cipher/kdf/md, 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_auth_init_fini(void) +{ + TEST_START(); + + if (oap_auth_init() < 0) { + printf("Failed to init OAP.\n"); + goto fail; + } + + oap_auth_fini(); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip(int kex) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(kex); + + TEST_START("(%s)", kex_str); + + test_default_cfg(); + test_cfg.srv.kex = kex; + test_cfg.cli.kex = kex; + + 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; + } + + 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; + } + + if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) { + printf("Cipher not set in flow.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", kex_str); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", kex_str); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_auth_only(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: auth only, no encryption */ + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + /* Client: no auth, no encryption */ + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_roundtrip_kex_only(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* 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; + + /* 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; + + return roundtrip_kex_only(); +} + +static int test_oap_piggyback_data(void) +{ + struct oap_test_ctx ctx; + const char * cli_data_str = "client_data"; + const char * srv_data_str = "server_data"; + buffer_t srv_data = BUF_INIT; + + TEST_START(); + + test_default_cfg(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + /* Client prepares request with piggybacked data */ + ctx.data.len = strlen(cli_data_str); + 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) + goto fail_cleanup; + + /* Set server's response data (ctx.data will take cli data) */ + srv_data.len = strlen(srv_data_str); + srv_data.data = malloc(srv_data.len); + if (srv_data.data == NULL) + goto fail_cleanup; + memcpy(srv_data.data, srv_data_str, srv_data.len); + + freebuf(ctx.data); + ctx.data = srv_data; + clrbuf(srv_data); + + if (oap_srv_process_ctx(&ctx) < 0) + goto fail_cleanup; + + /* Verify server received client's piggybacked data */ + if (ctx.data.len != strlen(cli_data_str) || + memcmp(ctx.data.data, cli_data_str, ctx.data.len) != 0) { + printf("Server did not receive correct client data.\n"); + goto fail_cleanup; + } + + freebuf(ctx.data); + + if (oap_cli_complete_ctx(&ctx) < 0) + goto fail_cleanup; + + /* Verify client received server's piggybacked data */ + if (ctx.data.len != strlen(srv_data_str) || + memcmp(ctx.data.data, srv_data_str, ctx.data.len) != 0) { + printf("Client did not receive correct server data.\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: + freebuf(srv_data); + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_corrupted_request(void) +{ + test_default_cfg(); + test_cfg.cli.auth = AUTH; + + return corrupted_request(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_corrupted_response(void) +{ + test_default_cfg(); + + return corrupted_response(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_truncated_request(void) +{ + test_default_cfg(); + + return truncated_request(root_ca_crt_ec, im_ca_crt_ec); +} + +/* After ID (16), timestamp (8), cipher_nid (2), kdf_nid (2), md (2) */ +#define OAP_CERT_LEN_OFFSET 30 +static int test_oap_inflated_length_field(void) +{ + struct oap_test_ctx ctx; + uint16_t fake; + + 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; + } + + if (ctx.req_hdr.len < OAP_CERT_LEN_OFFSET + 2) { + printf("Request too short for test.\n"); + goto fail_cleanup; + } + + /* Set cert length to claim more bytes than packet contains */ + fake = hton16(60000); + memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject inflated length field.\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; +} + +/* Attacker claims cert is smaller - causes misparse of subsequent fields */ +static int test_oap_deflated_length_field(void) +{ + struct oap_test_ctx ctx; + uint16_t fake; + + 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; + } + + if (ctx.req_hdr.len < OAP_CERT_LEN_OFFSET + 2) { + printf("Request too short for test.\n"); + goto fail_cleanup; + } + + /* Set cert length to claim fewer bytes - will misparse rest */ + fake = hton16(1); + memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject deflated length field.\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; +} + +/* Header field offsets for byte manipulation */ +#define OAP_CIPHER_NID_OFFSET 24 +#define OAP_KEX_LEN_OFFSET 32 + +/* Server rejects request when cipher NID set but no KEX data provided */ +static int test_oap_nid_without_kex(void) +{ + struct oap_test_ctx ctx; + uint16_t cipher_nid; + uint16_t zero = 0; + + TEST_START(); + + /* 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; + + 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; + } + + /* Tamper: keep cipher_nid but set kex_len=0, truncate KEX data */ + cipher_nid = hton16(NID_aes_256_gcm); + memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &cipher_nid, + sizeof(cipher_nid)); + memcpy(ctx.req_hdr.data + OAP_KEX_LEN_OFFSET, &zero, sizeof(zero)); + ctx.req_hdr.len = 36; /* Fixed header only, no KEX data */ + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject cipher NID without KEX data.\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 rejects OAP request with unsupported cipher NID */ +static int test_oap_unsupported_nid(void) +{ + struct oap_test_ctx ctx; + uint16_t bad_nid; + + TEST_START(); + + /* 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; + + 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; + } + + /* 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)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject unsupported cipher NID.\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; +} + +static int test_oap_roundtrip_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]); + + /* Skip KEM algorithms - they're tested in oap_test_pqc */ + if (IS_KEM_ALGORITHM(algo)) + continue; + + ret |= test_oap_roundtrip(kex_supported_nids[i]); + } + + return ret; +} + +/* Cipher negotiation - client should accept server's chosen cipher */ +static int test_oap_cipher_mismatch(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: ChaCha20-Poly1305, SHA3-256, SHA-384 */ + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_chacha20_poly1305; + test_cfg.srv.kdf = NID_sha3_256; + test_cfg.srv.md = NID_sha384; + test_cfg.srv.auth = AUTH; + + /* Client: AES-256-GCM, SHA-256, SHA-256 */ + 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; + + 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; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; + } + + /* Verify: both should have the server's chosen cipher and KDF */ + if (ctx.srv.nid != test_cfg.srv.cipher) { + printf("Server cipher mismatch: expected %s, got %s\n", + crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(ctx.srv.nid)); + goto fail_cleanup; + } + + if (ctx.cli.nid != test_cfg.srv.cipher) { + printf("Client cipher mismatch: expected %s, got %s\n", + crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(ctx.cli.nid)); + goto fail_cleanup; + } + + /* Parse response header to check negotiated KDF */ + if (ctx.resp_hdr.len > 26) { + uint16_t resp_kdf_nid; + /* KDF NID at offset 26: ID(16) + ts(8) + cipher(2) */ + resp_kdf_nid = ntoh16(*(uint16_t *)(ctx.resp_hdr.data + 26)); + + if (resp_kdf_nid != test_cfg.srv.kdf) { + printf("Response KDF mismatch: expected %s, got %s\n", + md_nid_to_str(test_cfg.srv.kdf), + md_nid_to_str(resp_kdf_nid)); + 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; +} + +/* Test roundtrip with different signature digest algorithms */ +static int test_oap_roundtrip_md(int md) +{ + struct oap_test_ctx ctx; + const char * md_str = md_nid_to_str(md); + + TEST_START("(%s)", md_str ? md_str : "default"); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: auth + KEX with specified md */ + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_aes_256_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = md; + test_cfg.srv.auth = AUTH; + + /* Client: 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 = md; + test_cfg.cli.auth = NO_AUTH; + + 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; + } + + 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("(%s)", md_str ? md_str : "default"); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", md_str ? md_str : "default"); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_md_all(void) +{ + int ret = 0; + int i; + + /* Test with default (0) */ + ret |= test_oap_roundtrip_md(0); + + /* Test with all supported digest NIDs */ + for (i = 0; md_supported_nids[i] != NID_undef; i++) + ret |= test_oap_roundtrip_md(md_supported_nids[i]); + + return ret; +} + +/* 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) +{ + struct oap_test_ctx ctx; + struct timespec old_ts; + uint64_t old_stamp; + + 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; + } + + if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) { + printf("Request too short for test.\n"); + 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)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject outdated packet.\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; +} + +/* Test that packets from the future are rejected */ +static int test_oap_future_packet(void) +{ + struct oap_test_ctx ctx; + struct timespec future_ts; + uint64_t future_stamp; + + 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; + } + + if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) { + printf("Request too short for test.\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_srv_process_ctx(&ctx) == 0) { + printf("Server should reject future packet.\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; +} + +/* 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; + + /* Replayed request should fail */ + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject replayed packet.\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; +} + +/* 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: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +int oap_test(int argc, + char **argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + + ret |= test_oap_auth_init_fini(); + +#ifdef HAVE_OPENSSL + ret |= test_oap_roundtrip_auth_only(); + ret |= test_oap_roundtrip_kex_only(); + ret |= test_oap_piggyback_data(); + + ret |= test_oap_roundtrip_all(); + ret |= test_oap_roundtrip_md_all(); + + ret |= test_oap_corrupted_request(); + ret |= test_oap_corrupted_response(); + 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_cipher_mismatch(); + + ret |= test_oap_outdated_packet(); + ret |= test_oap_future_packet(); + ret |= test_oap_replay_packet(); + ret |= test_oap_server_name_mismatch(); +#else + (void) test_oap_roundtrip_auth_only; + (void) test_oap_roundtrip_kex_only; + (void) test_oap_piggyback_data; + (void) test_oap_roundtrip; + (void) test_oap_roundtrip_all; + (void) test_oap_roundtrip_md; + (void) test_oap_roundtrip_md_all; + (void) test_oap_corrupted_request; + (void) test_oap_corrupted_response; + (void) test_oap_truncated_request; + (void) test_oap_inflated_length_field; + (void) test_oap_deflated_length_field; + (void) test_oap_nid_without_kex; + (void) test_oap_unsupported_nid; + (void) test_oap_cipher_mismatch; + (void) test_oap_outdated_packet; + (void) test_oap_future_packet; + (void) test_oap_replay_packet; + (void) test_oap_server_name_mismatch; + + ret = TEST_RC_SKIP; +#endif + return ret; +} diff --git a/src/irmd/oap/tests/oap_test_pqc.c b/src/irmd/oap/tests/oap_test_pqc.c new file mode 100644 index 00000000..ed51a6b4 --- /dev/null +++ b/src/irmd/oap/tests/oap_test_pqc.c @@ -0,0 +1,363 @@ +/* + * Ouroboros - Copyright (C) 2016 - 2026 + * + * Unit tests of OAP post-quantum key exchange + * + * Dimitri Staessens + * Sander Vrijders + * + * 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 + +#include "config.h" + +#include +#include +#include +#include +#include + +#include + +#include "oap.h" +#include "common.h" + +#include +#include + +#ifdef HAVE_OPENSSL +#include +#endif + +#define CLI_AUTH 1 +#define NO_CLI_AUTH 0 +#define CLI_ENCAP KEM_MODE_CLIENT_ENCAP +#define SRV_ENCAP KEM_MODE_SERVER_ENCAP + +extern const uint16_t kex_supported_nids[]; +extern const uint16_t md_supported_nids[]; + +static int get_random_kdf(void) +{ + static int idx = 0; + int count; + + if (md_supported_nids[0] == NID_undef) + return NID_undef; + + for (count = 0; md_supported_nids[count] != NID_undef; count++) + ; + + return md_supported_nids[(idx++) % count]; +} + +struct test_cfg test_cfg; + +/* KEM keypair storage for tests (server-side keypair for KEM modes) */ +static void * test_kem_pkp = NULL; /* Private key pair */ +static uint8_t test_kem_pk[4096]; /* Public key buffer */ +static size_t test_kem_pk_len = 0; + +/* Mock load - called by load_*_credentials in common.c */ +int mock_load_credentials(void ** pkp, + void ** crt) +{ + *pkp = NULL; + *crt = NULL; + + if (crypt_load_privkey_str(server_pkp_ml, pkp) < 0) + return -1; + + if (crypt_load_crt_str(signed_server_crt_ml, crt) < 0) { + crypt_free_key(*pkp); + *pkp = NULL; + return -1; + } + + return 0; +} + +int load_server_kem_keypair(const char * name, + bool raw_fmt, + void ** pkp) +{ +#ifdef HAVE_OPENSSL + struct sec_config local_cfg; + ssize_t pk_len; + + (void) name; + (void) raw_fmt; + + /* + * Uses reference counting. The caller will call + * EVP_PKEY_free which decrements the count. + */ + if (test_kem_pkp != NULL) { + if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1) + return -1; + + *pkp = test_kem_pkp; + return 0; + } + + /* + * Generate a new KEM keypair from test_cfg.srv.kex. + */ + memset(&local_cfg, 0, sizeof(local_cfg)); + if (test_cfg.srv.kex == NID_undef) + goto fail; + + SET_KEX_ALGO_NID(&local_cfg, test_cfg.srv.kex); + + pk_len = kex_pkp_create(&local_cfg, &test_kem_pkp, test_kem_pk); + if (pk_len < 0) + goto fail; + + test_kem_pk_len = (size_t) pk_len; + + if (EVP_PKEY_up_ref((EVP_PKEY *)test_kem_pkp) != 1) + goto fail_ref; + + *pkp = test_kem_pkp; + + return 0; + fail_ref: + kex_pkp_destroy(test_kem_pkp); + test_kem_pkp = NULL; + test_kem_pk_len = 0; + fail: + return -1; + +#else + (void) name; + (void) raw_fmt; + (void) pkp; + return -1; +#endif +} + +int load_server_kem_pk(const char * name, + struct sec_config * cfg, + buffer_t * pk) +{ + ssize_t len; + + (void) name; + + if (test_kem_pk_len > 0) { + 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; + return 0; + } + + /* Generate keypair on demand if not already done */ + len = kex_pkp_create(cfg, &test_kem_pkp, test_kem_pk); + if (len < 0) + return -1; + + test_kem_pk_len = (size_t) len; + 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; + + return 0; +} + +static void reset_kem_state(void) +{ + if (test_kem_pkp != NULL) { + kex_pkp_destroy(test_kem_pkp); + test_kem_pkp = NULL; + } + test_kem_pk_len = 0; +} + +static void test_cfg_init(int kex, + int cipher, + int kdf, + int kem_mode, + bool cli_auth) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server config */ + test_cfg.srv.kex = kex; + test_cfg.srv.cipher = cipher; + test_cfg.srv.kdf = kdf; + test_cfg.srv.kem_mode = kem_mode; + test_cfg.srv.auth = true; + + /* Client config */ + test_cfg.cli.kex = kex; + test_cfg.cli.cipher = cipher; + test_cfg.cli.kdf = kdf; + test_cfg.cli.kem_mode = kem_mode; + test_cfg.cli.auth = cli_auth; +} + +static int oap_test_setup_kem(struct oap_test_ctx * ctx, + const char * root_ca, + const char * im_ca) +{ + reset_kem_state(); + return oap_test_setup(ctx, root_ca, im_ca); +} + +static void oap_test_teardown_kem(struct oap_test_ctx * ctx) +{ + oap_test_teardown(ctx); +} + +static int test_oap_roundtrip_auth_only(void) +{ + test_cfg_init(NID_undef, NID_undef, NID_undef, 0, false); + + return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_corrupted_request(void) +{ + test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), + SRV_ENCAP, CLI_AUTH); + + return corrupted_request(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_corrupted_response(void) +{ + test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), + SRV_ENCAP, NO_CLI_AUTH); + + return corrupted_response(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_truncated_request(void) +{ + test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), + SRV_ENCAP, NO_CLI_AUTH); + + return truncated_request(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_roundtrip_kem(int kex, + int kem_mode) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(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); + + 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("Client prepare failed.\n"); + goto fail_cleanup; + } + + 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; + } + + if (ctx.cli.nid == NID_undef || + ctx.srv.nid == NID_undef) { + printf("Cipher not set in flow.\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_roundtrip_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_roundtrip_kem(kex_supported_nids[i], SRV_ENCAP); + ret |= test_oap_roundtrip_kem(kex_supported_nids[i], CLI_ENCAP); + } + + return ret; +} + +int oap_test_pqc(int argc, + char **argv) +{ + int ret = 0; + + (void) argc; + (void) argv; + +#ifdef HAVE_OPENSSL_PQC + ret |= test_oap_roundtrip_auth_only(); + + ret |= test_oap_roundtrip_kem_all(); + + ret |= test_oap_corrupted_request(); + ret |= test_oap_corrupted_response(); + ret |= test_oap_truncated_request(); +#else + (void) test_oap_roundtrip_auth_only; + (void) test_oap_roundtrip_kem; + (void) test_oap_roundtrip_kem_all; + (void) test_oap_corrupted_request; + (void) test_oap_corrupted_response; + (void) test_oap_truncated_request; + + ret = TEST_RC_SKIP; +#endif + + return ret; +} diff --git a/src/irmd/reg/ipcp.c b/src/irmd/reg/ipcp.c index 474527a4..74ec4939 100644 --- a/src/irmd/reg/ipcp.c +++ b/src/irmd/reg/ipcp.c @@ -77,7 +77,6 @@ void reg_ipcp_update(struct reg_ipcp * ipcp, const struct ipcp_info * info) { assert(ipcp != NULL); - assert(info->state != IPCP_NULL); ipcp->info = *info; } diff --git a/src/irmd/reg/tests/CMakeLists.txt b/src/irmd/reg/tests/CMakeLists.txt index e33bd056..759d29b4 100644 --- a/src/irmd/reg/tests/CMakeLists.txt +++ b/src/irmd/reg/tests/CMakeLists.txt @@ -1,6 +1,8 @@ get_filename_component(tmp ".." ABSOLUTE) get_filename_component(src_folder "${tmp}" NAME) +compute_test_prefix() + # Set include directories for tests include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${CMAKE_CURRENT_BINARY_DIR}) @@ -39,5 +41,5 @@ endif() foreach(test ${tests_to_run}) get_filename_component(test_name ${test} NAME_WE) - add_test(irmd/reg/${test_name} ${CMAKE_CURRENT_BINARY_DIR}/${src_folder}_test ${test_name}) + add_test(${TEST_PREFIX}/${test_name} ${CMAKE_CURRENT_BINARY_DIR}/${src_folder}_test ${test_name}) endforeach(test) diff --git a/src/irmd/reg/tests/flow_test.c b/src/irmd/reg/tests/flow_test.c index 27fd61b0..2066c811 100644 --- a/src/irmd/reg/tests/flow_test.c +++ b/src/irmd/reg/tests/flow_test.c @@ -22,7 +22,7 @@ #include "../flow.c" -#include +#include #include diff --git a/src/irmd/reg/tests/ipcp_test.c b/src/irmd/reg/tests/ipcp_test.c index d7d8e524..6ab6443d 100644 --- a/src/irmd/reg/tests/ipcp_test.c +++ b/src/irmd/reg/tests/ipcp_test.c @@ -20,7 +20,7 @@ * Foundation, Inc., http://www.fsf.org/about/contact/. */ -#include +#include #include "../ipcp.c" diff --git a/src/irmd/reg/tests/name_test.c b/src/irmd/reg/tests/name_test.c index 9071364b..5b42875e 100644 --- a/src/irmd/reg/tests/name_test.c +++ b/src/irmd/reg/tests/name_test.c @@ -23,7 +23,7 @@ #include "../name.c" -#include +#include #define TEST_PID 65534 #define TEST_PROG "/usr/bin/testprog" diff --git a/src/irmd/reg/tests/proc_test.c b/src/irmd/reg/tests/proc_test.c index df0527fb..d53f18ec 100644 --- a/src/irmd/reg/tests/proc_test.c +++ b/src/irmd/reg/tests/proc_test.c @@ -22,7 +22,7 @@ #include "../proc.c" -#include +#include #define TEST_PID 65534 #define TEST_PROG "usr/bin/testprog" diff --git a/src/irmd/reg/tests/prog_test.c b/src/irmd/reg/tests/prog_test.c index c394c222..3900e7d7 100644 --- a/src/irmd/reg/tests/prog_test.c +++ b/src/irmd/reg/tests/prog_test.c @@ -22,7 +22,7 @@ #include "../prog.c" -#include +#include #define TEST_PROG "usr/bin/testprog" diff --git a/src/irmd/reg/tests/reg_test.c b/src/irmd/reg/tests/reg_test.c index 4699beab..e7ae1a97 100644 --- a/src/irmd/reg/tests/reg_test.c +++ b/src/irmd/reg/tests/reg_test.c @@ -23,7 +23,7 @@ #include "../reg.c" -#include +#include #define TEST_PID 3666 #define TEST_N_1_PID 3999 @@ -1471,7 +1471,7 @@ static int test_wait_ipcp_boot_fail(void) struct ipcp_info resp_info = { .name = TEST_IPCP, .pid = TEST_PID, - .state = IPCP_INIT + .state = IPCP_NULL }; TEST_START(); diff --git a/src/irmd/tests/CMakeLists.txt b/src/irmd/tests/CMakeLists.txt deleted file mode 100644 index a255ca00..00000000 --- a/src/irmd/tests/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -get_filename_component(tmp ".." ABSOLUTE) -get_filename_component(src_folder "${tmp}" NAME) - -create_test_sourcelist(${src_folder}_tests test_suite.c - # Add new tests here - irm_test.c - oap_test.c -) - -add_executable(${src_folder}_test ${${src_folder}_tests}) -target_link_libraries(${src_folder}_test ouroboros-irm) -target_include_directories(${src_folder}_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) - -add_dependencies(build_tests ${src_folder}_test) - -set(tests_to_run ${${src_folder}_tests}) -if(CMAKE_VERSION VERSION_LESS "3.29.0") - remove(tests_to_run test_suite.c) -else () - list(POP_FRONT tests_to_run) -endif() - -foreach(test ${tests_to_run}) - get_filename_component(test_name ${test} NAME_WE) - add_test(irmd/${test_name} ${CMAKE_CURRENT_BINARY_DIR}/${src_folder}_test ${test_name}) -endforeach(test) - -set_property(TEST irmd/oap_test PROPERTY SKIP_RETURN_CODE 1) diff --git a/src/irmd/tests/irm_test.c b/src/irmd/tests/irm_test.c deleted file mode 100644 index d440289c..00000000 --- a/src/irmd/tests/irm_test.c +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Ouroboros - Copyright (C) 2016 - 2024 - * - * Unit tests of IRMd functions - * Dimitri Staessens - * Sander Vrijders - * - * 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/. - */ - - - -int irm_test(int argc, - char **argv) -{ - int ret = 0; - - (void) argc; - (void) argv; - - return ret; -} diff --git a/src/irmd/tests/oap_test.c b/src/irmd/tests/oap_test.c index a5f9c706..0e7c11fc 100644 --- a/src/irmd/tests/oap_test.c +++ b/src/irmd/tests/oap_test.c @@ -1,7 +1,8 @@ /* * Ouroboros - Copyright (C) 2016 - 2024 * - * Unit tests of Ouroboros flow allocation protocol + * Unit tests of Ouroboros Allocation Protocol (OAP) + * * Dimitri Staessens * Sander Vrijders * @@ -19,245 +20,824 @@ * Foundation, Inc., http://www.fsf.org/about/contact/. */ -#include "oap.c" +#if defined(__linux__) || defined(__CYGWIN__) + #ifndef _DEFAULT_SOURCE + #define _DEFAULT_SOURCE + #endif +#else +#define _POSIX_C_SOURCE 200809L +#endif + +#include "config.h" +#include +#include +#include +#include #include -#include - -static const char * pkp_str = \ -"-----BEGIN EC PRIVATE KEY-----\n" -"MHcCAQEEIC13y+5jdKe80HBJD7WITpQamcn3rrkTX1r0v+JwSk4NoAoGCCqGSM49\n" -"AwEHoUQDQgAEcC0yLAfUtufH8cdLybrdWPc6U+xRuhDhqqrEcBO5+eob2xyqEaNk\n" -"nIV/86724zPptGRahWz0rzW2PvNppJdNBg==\n" -"-----END EC PRIVATE KEY-----\n"; - -/* Valid signed server certificate for server-2.unittest.o7s */ -static const char * crt_str = \ -"-----BEGIN CERTIFICATE-----\n" -"MIIDgjCCAyigAwIBAgICEAIwCgYIKoZIzj0EAwIwWzELMAkGA1UEBhMCQkUxDDAK\n" -"BgNVBAgMA09WTDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3Mx\n" -"GTAXBgNVBAMMEGltMi51bml0dGVzdC5vN3MwHhcNMjUwNzA0MTMxODI5WhcNMzUw\n" -"NzAyMTMxODI5WjBwMQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYDVQQH\n" -"DAVHaGVudDEMMAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3MxHjAc\n" -"BgNVBAMMFXNlcnZlci0yLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49\n" -"AwEHA0IABHAtMiwH1Lbnx/HHS8m63Vj3OlPsUboQ4aqqxHATufnqG9scqhGjZJyF\n" -"f/Ou9uMz6bRkWoVs9K81tj7zaaSXTQajggHFMIIBwTAJBgNVHRMEAjAAMBEGCWCG\n" -"SAGG+EIBAQQEAwIGQDA6BglghkgBhvhCAQ0ELRYrR3JpbGxlZCBDaGVlc2UgR2Vu\n" -"ZXJhdGVkIFNlcnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUTt3xHTwE9amoglxh\n" -"cEMqWv+PpDMwgb8GA1UdIwSBtzCBtIAUFfeZRx8QWWKQr7Aw8zjDu2shvcShgZek\n" -"gZQwgZExCzAJBgNVBAYTAkJFMQwwCgYDVQQIDANPVkwxDjAMBgNVBAcMBUdoZW50\n" -"MQwwCgYDVQQKDANvN3MxFTATBgNVBAsMDHVuaXR0ZXN0Lm83czEZMBcGA1UEAwwQ\n" -"Y2EyLnVuaXR0ZXN0Lm83czEkMCIGCSqGSIb3DQEJARYVZHVtbXlAb3Vyb2Jvcm9z\n" -"LnJvY2tzggIQAjAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw\n" -"EQYDVR0fBAowCDAGoASgAoYAMCoGCCsGAQUFBwEBBB4wHDAMBggrBgEFBQcwAoYA\n" -"MAwGCCsGAQUFBzABhgAwIAYDVR0RBBkwF4IVc2VydmVyLTEudW5pdHRlc3Qubzdz\n" -"MAoGCCqGSM49BAMCA0gAMEUCIQDHuDb62w/Uah4nKwUFoJVkr4rgdNGh2Rn3SWaK\n" -"0FV/gAIgOLKorTwSgrTFdyOUkuPOhRs8BEMpah+dp8UTO8AnLvY=\n" -"-----END CERTIFICATE-----\n"; - -static int test_oap_hdr_init_fini(void) +#include + +#include +#include + +#include "oap.h" +#include "common.h" + +#include +#include + +#ifdef HAVE_OPENSSL +#include +#endif + +#define AUTH true +#define NO_AUTH false + +extern const uint16_t kex_supported_nids[]; +extern const uint16_t md_supported_nids[]; + +struct test_cfg test_cfg; + +/* Mock load - called by load_*_credentials in common.c */ +int mock_load_credentials(void ** pkp, + void ** crt) { - struct oap_hdr oap_hdr; - struct timespec now; - uint64_t stamp; - buffer_t ephkey = BUF_INIT; - buffer_t data = BUF_INIT; - uint8_t buf[OAP_ID_SIZE]; - buffer_t id; - void * pkp = NULL; - void * pubcrt = NULL; + *crt = NULL; + + if (crypt_load_privkey_str(server_pkp_ec, pkp) < 0) + goto fail_privkey; + + if (crypt_load_crt_str(signed_server_crt_ec, crt) < 0) + goto fail_crt; + return 0; + + fail_crt: + crypt_free_key(*pkp); + fail_privkey: + *pkp = NULL; + return -1; +} + +/* Stub KEM functions - ECDSA tests don't use KEM */ +int load_server_kem_keypair(__attribute__((unused)) const char * name, + __attribute__((unused)) struct sec_config * cfg, + __attribute__((unused)) void ** pkp) +{ + return -1; +} + +int load_server_kem_pk(__attribute__((unused)) const char * name, + __attribute__((unused)) struct sec_config * cfg, + __attribute__((unused)) buffer_t * pk) +{ + return -1; +} + +static void test_default_cfg(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: X25519, AES-256-GCM, SHA-256, with 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 = AUTH; + + /* Client: same KEX/cipher/kdf/md, 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_init_fini(void) +{ TEST_START(); - random_buffer(buf, OAP_ID_SIZE); - id.data = buf; - id.len = OAP_ID_SIZE; + if (oap_init() < 0) { + printf("Failed to init OAP.\n"); + goto fail; + } - clock_gettime(CLOCK_REALTIME, &now); - stamp = TS_TO_UINT64(now); + oap_fini(); - if (oap_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) { - printf("Failed to init OAP request header.\n"); - goto fail_req_hdr; + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip(int kex) +{ + struct oap_test_ctx ctx; + const char * kex_str = kex_nid_to_str(kex); + + TEST_START("(%s)", kex_str); + + test_default_cfg(); + test_cfg.srv.kex = kex; + test_cfg.cli.kex = kex; + + 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; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; } - if (oap_hdr.hdr.len != OAP_HDR_MIN_SIZE) { - printf("OAP request header wrong: %zu < %zu.\n", - oap_hdr.hdr.len, OAP_HDR_MIN_SIZE); - goto fail_req_hdr_chk; + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Client and server keys do not match!\n"); + goto fail_cleanup; } - if (oap_hdr.id.len != OAP_ID_SIZE) { - printf("OAP request header ID wrong size: %zu != %zu.\n", - oap_hdr.id.len, (size_t) OAP_ID_SIZE); - goto fail_req_hdr_chk; + if (ctx.cli.nid == NID_undef || ctx.srv.nid == NID_undef) { + printf("Cipher not set in flow.\n"); + goto fail_cleanup; } - if (memcmp(oap_hdr.id.data, id.data, OAP_ID_SIZE) != 0) { - printf("OAP request header ID mismatch.\n"); - goto fail_req_hdr_chk; + oap_test_teardown(&ctx); + + TEST_SUCCESS("(%s)", kex_str); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", kex_str); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_auth_only(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: auth only, no encryption */ + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + /* Client: no auth, no encryption */ + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_roundtrip_kex_only(void) +{ + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* 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; + + /* 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; + + return roundtrip_kex_only(); +} + +static int test_oap_piggyback_data(void) +{ + struct oap_test_ctx ctx; + const char * cli_data_str = "client_data"; + const char * srv_data_str = "server_data"; + buffer_t srv_data = BUF_INIT; + + TEST_START(); + + test_default_cfg(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + /* Client prepares request with piggybacked data */ + ctx.data.len = strlen(cli_data_str); + 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) + goto fail_cleanup; + + /* Set server's response data (ctx.data replaced with cli data) */ + srv_data.len = strlen(srv_data_str); + srv_data.data = malloc(srv_data.len); + if (srv_data.data == NULL) + goto fail_cleanup; + memcpy(srv_data.data, srv_data_str, srv_data.len); + + freebuf(ctx.data); + ctx.data = srv_data; + clrbuf(srv_data); + + if (oap_srv_process_ctx(&ctx) < 0) + goto fail_cleanup; + + /* Verify server received client's piggybacked data */ + if (ctx.data.len != strlen(cli_data_str) || + memcmp(ctx.data.data, cli_data_str, ctx.data.len) != 0) { + printf("Server did not receive correct client data.\n"); + goto fail_cleanup; } - if (oap_hdr.timestamp < stamp) { - printf("OAP request header timestamp is too old.\n"); - goto fail_req_hdr_chk; + freebuf(ctx.data); + + if (oap_cli_complete_ctx(&ctx) < 0) + goto fail_cleanup; + + /* Verify client received server's piggybacked data */ + if (ctx.data.len != strlen(srv_data_str) || + memcmp(ctx.data.data, srv_data_str, ctx.data.len) != 0) { + printf("Client did not receive correct server data.\n"); + goto fail_cleanup; } - if (oap_hdr.timestamp > stamp + 1 * BILLION) { - printf("OAP request header timestamp is too new.\n"); - goto fail_req_hdr_chk; + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Client and server keys do not match!\n"); + goto fail_cleanup; } - oap_hdr_fini(&oap_hdr); + oap_test_teardown(&ctx); TEST_SUCCESS(); - return TEST_RC_SUCCESS; - fail_req_hdr_chk: - oap_hdr_fini(&oap_hdr); - fail_req_hdr: + fail_cleanup: + freebuf(srv_data); + oap_test_teardown(&ctx); + fail: TEST_FAIL(); return TEST_RC_FAIL; } -static int test_oap_hdr_init_fini_data(void) +static int test_oap_corrupted_request(void) +{ + test_default_cfg(); + test_cfg.cli.auth = AUTH; + return corrupted_request(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_corrupted_response(void) +{ + test_default_cfg(); + + return corrupted_response(root_ca_crt_ec, im_ca_crt_ec); +} + +static int test_oap_truncated_request(void) { - struct oap_hdr oap_hdr; - buffer_t data; - buffer_t ephkey = BUF_INIT; - uint8_t buf[OAP_ID_SIZE]; - buffer_t id; - void * pkp = NULL; - void * pubcrt = NULL; + test_default_cfg(); + + return truncated_request(root_ca_crt_ec, im_ca_crt_ec); +} + +/* After ID (16), timestamp (8), cipher_nid (2), kdf_nid (2), md (2) */ +#define OAP_CERT_LEN_OFFSET 30 +static int test_oap_inflated_length_field(void) +{ + struct oap_test_ctx ctx; + uint16_t fake; + + test_default_cfg(); TEST_START(); - random_buffer(buf, OAP_ID_SIZE); - id.data = buf; - id.len = OAP_ID_SIZE; + 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; + } - data.len = 100; - data.data = malloc(data.len); - if (data.data == NULL) { - printf("Failed to allocate data buffer.\n"); - goto fail_data; + if (ctx.req_hdr.len < OAP_CERT_LEN_OFFSET + 2) { + printf("Request too short for test.\n"); + goto fail_cleanup; } - random_buffer(data.data, data.len); + /* Set cert length to claim more bytes than packet contains */ + fake = hton16(60000); + memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake)); - if (oap_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) { - printf("Failed to create OAP request header.\n"); - goto fail_req_hdr; + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject inflated length field.\n"); + goto fail_cleanup; } - if (oap_hdr.hdr.len != OAP_HDR_MIN_SIZE + data.len) { - printf("OAP request header wrong: %zu < %zu.\n", - oap_hdr.hdr.len, OAP_HDR_MIN_SIZE + data.len); - goto fail_req_hdr_sz; + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Attacker claims cert is smaller - causes misparse of subsequent fields */ +static int test_oap_deflated_length_field(void) +{ + struct oap_test_ctx ctx; + uint16_t fake; + + 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; + } + + if (ctx.req_hdr.len < OAP_CERT_LEN_OFFSET + 2) { + printf("Request too short for test.\n"); + goto fail_cleanup; + } + + /* Set cert length to claim fewer bytes - will misparse rest */ + fake = hton16(1); + memcpy(ctx.req_hdr.data + OAP_CERT_LEN_OFFSET, &fake, sizeof(fake)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject deflated length field.\n"); + goto fail_cleanup; } - freebuf(data); - oap_hdr_fini(&oap_hdr); + oap_test_teardown(&ctx); TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} +/* Header field offsets for byte manipulation */ +#define OAP_CIPHER_NID_OFFSET 24 +#define OAP_KEX_LEN_OFFSET 32 + +/* Server rejects request when cipher NID set but no KEX data provided */ +static int test_oap_nid_without_kex(void) +{ + struct oap_test_ctx ctx; + uint16_t cipher_nid; + uint16_t zero = 0; + + TEST_START(); + + /* 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; + + 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; + } + + /* Tamper: keep cipher_nid but set kex_len=0, truncate KEX data */ + cipher_nid = hton16(NID_aes_256_gcm); + memcpy(ctx.req_hdr.data + OAP_CIPHER_NID_OFFSET, &cipher_nid, + sizeof(cipher_nid)); + memcpy(ctx.req_hdr.data + OAP_KEX_LEN_OFFSET, &zero, sizeof(zero)); + ctx.req_hdr.len = 36; /* Fixed header only, no KEX data */ + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject cipher NID without KEX data.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); return TEST_RC_SUCCESS; - fail_req_hdr_sz: - oap_hdr_fini(&oap_hdr); - fail_req_hdr: - freebuf(data); - fail_data: + fail_cleanup: + oap_test_teardown(&ctx); + fail: TEST_FAIL(); return TEST_RC_FAIL; } -static int test_oap_hdr_init_fini_signed(void) +/* Server rejects OAP request with unsupported cipher NID */ +static int test_oap_unsupported_nid(void) { - struct oap_hdr oap_hdr; - buffer_t ephkey = BUF_INIT; - buffer_t data = BUF_INIT; - buffer_t sign; - buffer_t id; - uint8_t buf[OAP_ID_SIZE]; - void * pkp; - void * pk; - void * pubcrt; - void * pubcrt2; + struct oap_test_ctx ctx; + uint16_t bad_nid; TEST_START(); - random_buffer(buf, OAP_ID_SIZE); - id.data = buf; - id.len = OAP_ID_SIZE; + /* 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; + + 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; + } + + /* 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)); - if (crypt_load_privkey_str(pkp_str, &pkp) < 0) { - printf("Failed to load private key.\n"); - goto fail_pkp; + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject unsupported cipher NID.\n"); + goto fail_cleanup; } - if (crypt_load_crt_str(crt_str, &pubcrt) < 0) { - printf("Failed to load public certificate.\n"); - goto fail_pubcrt; + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_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]); + + /* Skip KEM algorithms - they're tested in oap_test_pqc */ + if (IS_KEM_ALGORITHM(algo)) + continue; + + ret |= test_oap_roundtrip(kex_supported_nids[i]); + } + + return ret; +} + +/* Cipher negotiation - client should accept server's chosen cipher */ +static int test_oap_cipher_mismatch(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: ChaCha20-Poly1305, SHA3-256, SHA-384 */ + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_chacha20_poly1305; + test_cfg.srv.kdf = NID_sha3_256; + test_cfg.srv.md = NID_sha384; + test_cfg.srv.auth = AUTH; + + /* Client: AES-256-GCM, SHA-256, SHA-256 */ + 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; + + 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_hdr_init(id, pkp, pubcrt, ephkey, data, &oap_hdr) < 0) { - printf("Failed to create OAP request header.\n"); - goto fail_req_hdr; + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; } - if (oap_hdr.crt.len == 0) { - printf("OAP request header has no public certificate.\n"); - goto fail_req_hdr; + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; } - if (oap_hdr.sig.len == 0) { - printf("OAP request header no signature.\n"); - goto fail_req_hdr; + /* Verify: both should have the server's chosen cipher and KDF */ + if (ctx.srv.nid != test_cfg.srv.cipher) { + printf("Server cipher mismatch: expected %s, got %s\n", + crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(ctx.srv.nid)); + goto fail_cleanup; } - if (crypt_load_crt_der(oap_hdr.crt, &pubcrt2) < 0) { - printf("Failed to load public certificate from DER.\n"); - goto fail_crt_der; + if (ctx.cli.nid != test_cfg.srv.cipher) { + printf("Client cipher mismatch: expected %s, got %s\n", + crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(ctx.cli.nid)); + goto fail_cleanup; } - if (crypt_get_pubkey_crt(pubcrt2, &pk) < 0) { - printf("Failed to get public key from certificate.\n"); - goto fail_crt_pk; + /* Parse response header to check negotiated KDF */ + if (ctx.resp_hdr.len > 26) { + uint16_t resp_kdf_nid; + /* KDF NID at offset 26: ID(16) + ts(8) + cipher(2) */ + resp_kdf_nid = ntoh16(*(uint16_t *)(ctx.resp_hdr.data + 26)); + + if (resp_kdf_nid != test_cfg.srv.kdf) { + printf("Response KDF mismatch: expected %s, got %s\n", + md_nid_to_str(test_cfg.srv.kdf), + md_nid_to_str(resp_kdf_nid)); + goto fail_cleanup; + } } - sign = oap_hdr.hdr; - sign.len -= (oap_hdr.sig.len + sizeof(uint16_t)); + oap_test_teardown(&ctx); - if (auth_verify_sig(pk, sign, oap_hdr.sig) < 0) { - printf("Failed to verify OAP request header signature.\n"); - goto fail_check_sig; + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Test roundtrip with different signature digest algorithms */ +static int test_oap_roundtrip_md(int md) +{ + struct oap_test_ctx ctx; + const char * md_str = md_nid_to_str(md); + + TEST_START("(%s)", md_str ? md_str : "default"); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: auth + KEX with specified md */ + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_aes_256_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = md; + test_cfg.srv.auth = AUTH; + + /* Client: 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 = md; + test_cfg.cli.auth = NO_AUTH; + + 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; + } + + 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_hdr_fini(&oap_hdr); + oap_test_teardown(&ctx); - crypt_free_crt(pubcrt2); - crypt_free_crt(pubcrt); - crypt_free_key(pk); - crypt_free_key(pkp); + TEST_SUCCESS("(%s)", md_str ? md_str : "default"); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL("(%s)", md_str ? md_str : "default"); + return TEST_RC_FAIL; +} + +static int test_oap_roundtrip_md_all(void) +{ + int ret = 0; + int i; + + /* Test with default (0) */ + ret |= test_oap_roundtrip_md(0); + + /* Test with all supported digest NIDs */ + for (i = 0; md_supported_nids[i] != NID_undef; i++) + ret |= test_oap_roundtrip_md(md_supported_nids[i]); + + return ret; +} + +/* 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) +{ + struct oap_test_ctx ctx; + struct timespec old_ts; + uint64_t old_stamp; + + 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; + } + + if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) { + printf("Request too short for test.\n"); + 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)); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject outdated packet.\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; +} + +/* Test that packets from the future are rejected */ +static int test_oap_future_packet(void) +{ + struct oap_test_ctx ctx; + struct timespec future_ts; + uint64_t future_stamp; + + 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; + } + + if (ctx.req_hdr.len < OAP_TIMESTAMP_OFFSET + sizeof(uint64_t)) { + printf("Request too short for test.\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_srv_process_ctx(&ctx) == 0) { + printf("Server should reject future packet.\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; +} + +/* 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; + } + + /* Restore the saved request for replay */ + freebuf(ctx.req_hdr); + ctx.req_hdr = saved_req; + + /* Replayed request should fail */ + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject replayed packet.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); return TEST_RC_SUCCESS; - fail_check_sig: - crypt_free_key(pk); - fail_crt_pk: - crypt_free_crt(pubcrt2); - fail_crt_der: - oap_hdr_fini(&oap_hdr); - fail_req_hdr: - crypt_free_crt(pubcrt); - fail_pubcrt: - crypt_free_key(pkp); - fail_pkp: + fail_cleanup: + oap_test_teardown(&ctx); + fail: TEST_FAIL(); return TEST_RC_FAIL; } @@ -270,12 +850,48 @@ int oap_test(int argc, (void) argc; (void) argv; - ret |= test_oap_hdr_init_fini(); - ret |= test_oap_hdr_init_fini_data(); + ret |= test_oap_init_fini(); + #ifdef HAVE_OPENSSL - ret |= test_oap_hdr_init_fini_signed(); + ret |= test_oap_roundtrip_auth_only(); + ret |= test_oap_roundtrip_kex_only(); + ret |= test_oap_piggyback_data(); + + ret |= test_oap_roundtrip_all(); + ret |= test_oap_roundtrip_md_all(); + + ret |= test_oap_corrupted_request(); + ret |= test_oap_corrupted_response(); + 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_cipher_mismatch(); + + ret |= test_oap_outdated_packet(); + ret |= test_oap_future_packet(); + ret |= test_oap_replay_packet(); #else - (void) test_oap_hdr_init_fini_signed; + (void) test_oap_roundtrip_auth_only; + (void) test_oap_roundtrip_kex_only; + (void) test_oap_piggyback_data; + (void) test_oap_roundtrip; + (void) test_oap_roundtrip_all; + (void) test_oap_roundtrip_md; + (void) test_oap_roundtrip_md_all; + (void) test_oap_corrupted_request; + (void) test_oap_corrupted_response; + (void) test_oap_truncated_request; + (void) test_oap_inflated_length_field; + (void) test_oap_deflated_length_field; + (void) test_oap_nid_without_kex; + (void) test_oap_unsupported_nid; + (void) test_oap_cipher_mismatch; + (void) test_oap_outdated_packet; + (void) test_oap_future_packet; + (void) test_oap_replay_packet; ret = TEST_RC_SKIP; #endif -- cgit v1.2.3