summaryrefslogtreecommitdiff
path: root/src/irmd/oap
diff options
context:
space:
mode:
authorDimitri Staessens <dimitri@ouroboros.rocks>2026-06-21 14:07:00 +0200
committerSander Vrijders <sander@ouroboros.rocks>2026-06-29 08:33:00 +0200
commit55a8136859d82d9bdb8f85abb25290177ca7e561 (patch)
treee3b87e09322867245a49fe11c51b621efcff2730 /src/irmd/oap
parent552a4c4469db1cedacc02eb4f9969afe73e0fb42 (diff)
downloadouroboros-55a8136859d82d9bdb8f85abb25290177ca7e561.tar.gz
ouroboros-55a8136859d82d9bdb8f85abb25290177ca7e561.zip
irmd: Harden OAP handshake and add cert-less re-key
Adds support for: Server key confirmation: the session key is bound to the negotiated algorithm via the HKDF info. The server returns a key-confirmation tag (rsp_tag, replacing the bare request-hash echo), so a cipher downgrade or key desync is detected. The cleartext path keeps a request echo, compared in constant time. Sealed server identity: AEAD-seal the certificate, signature and piggybacked data in the encrypted response (kex and rsp_tag move ahead as AAD), hiding the server identity and response sizes. Cert-less re-key: let the client omit its certificate, verifying the peer against the cached certificate. On PQC flows, ephemeral server-encap KEX (preserving forward secrecy) is used, even if the original flow allocation was client-encap. Signed-off-by: Dimitri Staessens <dimitri@ouroboros.rocks> Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
Diffstat (limited to 'src/irmd/oap')
-rw-r--r--src/irmd/oap/auth.c220
-rw-r--r--src/irmd/oap/auth.h34
-rw-r--r--src/irmd/oap/cli.c123
-rw-r--r--src/irmd/oap/hdr.c462
-rw-r--r--src/irmd/oap/hdr.h48
-rw-r--r--src/irmd/oap/srv.c106
-rw-r--r--src/irmd/oap/tests/common.c168
-rw-r--r--src/irmd/oap/tests/common.h11
-rw-r--r--src/irmd/oap/tests/oap_test.c354
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c22
10 files changed, 1313 insertions, 235 deletions
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c
index 29e8b4d6..f70f9df1 100644
--- a/src/irmd/oap/auth.c
+++ b/src/irmd/oap/auth.c
@@ -29,6 +29,7 @@
#define OUROBOROS_PREFIX "irmd/oap"
#include <ouroboros/crypt.h>
+#include <ouroboros/endian.h>
#include <ouroboros/errno.h>
#include <ouroboros/logs.h>
#include <ouroboros/pthread.h>
@@ -168,6 +169,195 @@ int oap_auth_add_chain_crt(void * crt)
return auth_add_crt_to_chain(oap_auth.ca_ctx, crt);
}
+/* HKDF info = LABEL (incl. NUL separator) || request-hash [|| response-hash] */
+#define OAP_BIND_LABEL "o7s-oap-bind"
+#define OAP_KC_LABEL "o7s-oap-kc"
+#define OAP_HS_LABEL "o7s-oap-hs"
+
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t cat = BUF_INIT;
+ uint8_t * p;
+ ssize_t len;
+
+ assert(out != NULL);
+ assert(out->data != NULL);
+
+ cat.len = kex.len + data.len + crt.len;
+ if (cat.len == 0)
+ return -EINVAL;
+
+ cat.data = malloc(cat.len);
+ if (cat.data == NULL)
+ return -ENOMEM;
+
+ p = cat.data;
+ if (kex.len > 0) {
+ memcpy(p, kex.data, kex.len);
+ p += kex.len;
+ }
+
+ if (data.len > 0) {
+ memcpy(p, data.data, data.len);
+ p += data.len;
+ }
+
+ if (crt.len > 0)
+ memcpy(p, crt.data, crt.len);
+
+ len = md_digest(md_nid, cat, out->data);
+
+ freebuf(cat);
+
+ if (len < 0)
+ return -ECRYPT;
+
+ out->len = (size_t) len;
+
+ return 0;
+}
+
+/* HKDF-expand sk->key with info into out; -ECRYPT on failure. */
+static int oap_hkdf_expand(const struct crypt_sk * sk,
+ buffer_t info,
+ uint8_t * out,
+ size_t outlen)
+{
+ buffer_t prk;
+ buffer_t okm;
+
+ prk.len = SYMMKEYSZ;
+ prk.data = sk->key;
+ okm.len = outlen;
+ okm.data = out;
+
+ if (crypt_hkdf_expand(prk, info, okm) < 0)
+ return -ECRYPT;
+
+ return 0;
+}
+
+/* info = label || H(req) */
+#define OAP_HS_INFO_SZ (sizeof(OAP_HS_LABEL) + MAX_HASH_SIZE)
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out)
+{
+ uint8_t info_buf[OAP_HS_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_HS_LABEL);
+ memcpy(info_buf, OAP_HS_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, SYMMKEYSZ);
+}
+
+/* info = label || H(req) || H(resp) || cipher_nid || kdf_nid */
+#define OAP_BIND_INFO_SZ \
+ (sizeof(OAP_BIND_LABEL) + 2 * MAX_HASH_SIZE + 2 * sizeof(uint16_t))
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid)
+{
+ uint8_t info_buf[OAP_BIND_INFO_SZ];
+ uint8_t tmp[SYMMKEYSZ];
+ uint16_t suite[2];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_BIND_LABEL);
+ memcpy(info_buf, OAP_BIND_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ suite[0] = hton16((uint16_t) sk->nid);
+ suite[1] = hton16((uint16_t) kdf_nid);
+ memcpy(info_buf + len, suite, sizeof(suite));
+ len += sizeof(suite);
+
+ info.len = len;
+ info.data = info_buf;
+
+ if (oap_hkdf_expand(sk, info, tmp, SYMMKEYSZ) < 0)
+ return -ECRYPT;
+
+ memcpy(sk->key, tmp, SYMMKEYSZ);
+ crypt_secure_clear(tmp, SYMMKEYSZ);
+
+ return 0;
+}
+
+/* info = label || H(req) || H(resp) */
+#define OAP_KC_INFO_SZ (sizeof(OAP_KC_LABEL) + 2 * MAX_HASH_SIZE)
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen)
+{
+ uint8_t info_buf[OAP_KC_INFO_SZ];
+ buffer_t info;
+ size_t len;
+
+ assert(sk != NULL);
+ assert(req_hash.data != NULL);
+ assert(resp_hash.data != NULL);
+ assert(out != NULL);
+
+ if (req_hash.len == 0 || req_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (resp_hash.len == 0 || resp_hash.len > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ if (outlen > MAX_HASH_SIZE)
+ return -EINVAL;
+
+ len = sizeof(OAP_KC_LABEL);
+ memcpy(info_buf, OAP_KC_LABEL, len);
+ memcpy(info_buf + len, req_hash.data, req_hash.len);
+ len += req_hash.len;
+
+ memcpy(info_buf + len, resp_hash.data, resp_hash.len);
+ len += resp_hash.len;
+
+ info.len = len;
+ info.data = info_buf;
+
+ return oap_hkdf_expand(sk, info, out, outlen);
+}
+
#define TIMESYNC_SLACK 100 /* ms */
#define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0)
int oap_check_hdr(const struct oap_hdr * hdr)
@@ -248,12 +438,14 @@ int oap_check_hdr(const struct oap_hdr * hdr)
int oap_auth_peer(char * name,
const struct sec_config * cfg,
const struct oap_hdr * local_hdr,
- const struct oap_hdr * peer_hdr)
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt)
{
void * crt;
void * pk = NULL;
void * pin = NULL;
- buffer_t sign; /* Signed region */
+ buffer_t crt_der; /* cert source: wire, else cached (re-key) */
+ buffer_t sign; /* Signed region */
uint8_t * id = peer_hdr->id.data;
int ret;
@@ -267,7 +459,12 @@ int oap_auth_peer(char * name,
goto fail_check;
}
- if (peer_hdr->crt.len == 0) {
+ /* Re-key drops the wire cert; fall back to the cached peer cert. */
+ crt_der = peer_hdr->crt;
+ if (crt_der.len == 0 && cached_crt != NULL)
+ crt_der = *cached_crt;
+
+ if (crt_der.len == 0) {
if (cfg->a.req) {
log_err_id(id, "Peer did not provide a certificate.");
goto fail_check;
@@ -277,7 +474,7 @@ int oap_auth_peer(char * name,
return 0;
}
- if (crypt_load_crt_der(peer_hdr->crt, &crt) < 0) {
+ if (crypt_load_crt_der(crt_der, &crt) < 0) {
log_err_id(id, "Failed to load crt.");
goto fail_check;
}
@@ -291,10 +488,12 @@ int oap_auth_peer(char * name,
log_dbg_id(id, "Got public key from crt.");
- if (cfg->a.cacert[0] != '\0' &&
- crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
- log_err_id(id, "Failed to load pinned CA %s.", cfg->a.cacert);
- goto fail_crt;
+ if (cfg->a.cacert[0] != '\0') {
+ if (crypt_load_crt_file(cfg->a.cacert, &pin) < 0) {
+ log_err_id(id, "Failed to load pinned CA %s.",
+ cfg->a.cacert);
+ goto fail_crt;
+ }
}
ret = auth_verify_crt_pin(oap_auth.ca_ctx, crt, pin);
@@ -319,7 +518,9 @@ int oap_auth_peer(char * name,
goto fail_pin;
}
- sign = peer_hdr->hdr;
+ /* Sealed responses verify over the reconstructed plaintext. */
+ sign = peer_hdr->sealed_pt.data != NULL ?
+ peer_hdr->sealed_pt : peer_hdr->hdr;
sign.len -= peer_hdr->sig.len;
if (auth_verify_sig(pk, peer_hdr->md_nid, sign, peer_hdr->sig) < 0) {
@@ -338,6 +539,7 @@ int oap_auth_peer(char * name,
if (pin != NULL)
crypt_free_crt(pin);
+
crypt_free_key(pk);
crypt_free_crt(crt);
diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h
index 4a350120..72938b53 100644
--- a/src/irmd/oap/auth.h
+++ b/src/irmd/oap/auth.h
@@ -29,10 +29,40 @@
int oap_check_hdr(const struct oap_hdr * hdr);
-/* name is set to the peer crt CN, "" if no crt was presented */
+/*
+ * name is set to the peer crt CN, "" if no crt was presented.
+ * cached_crt (or NULL) is the peer cert from the initial handshake, used
+ * to verify a cert-less re-key.
+ */
int oap_auth_peer(char * name,
const struct sec_config * cfg,
const struct oap_hdr * local_hdr,
- const struct oap_hdr * peer_hdr);
+ const struct oap_hdr * peer_hdr,
+ const buffer_t * cached_crt);
+
+/* Derive the handshake key that seals the response identity block. */
+int oap_derive_hs_key(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ uint8_t * out);
+
+/* resp_hash = H(kex || data || crt): binds the server response transcript. */
+int oap_resp_hash(int md_nid,
+ buffer_t kex,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out);
+
+/* Fold request + response transcript + negotiated suite into the key. */
+int oap_bind_session_key(struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ int kdf_nid);
+
+/* Server->client key-confirmation tag derived from the bound key. */
+int oap_key_confirm_tag(const struct crypt_sk * sk,
+ buffer_t req_hash,
+ buffer_t resp_hash,
+ uint8_t * out,
+ size_t outlen);
#endif /* OUROBOROS_IRMD_OAP_AUTH_H */
diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c
index 2a57d12e..2db56792 100644
--- a/src/irmd/oap/cli.c
+++ b/src/irmd/oap/cli.c
@@ -242,11 +242,13 @@ static int do_client_kex_prepare(const char * server_name,
int oap_cli_prepare(void ** ctx,
const struct name_info * info,
buffer_t * req_buf,
- buffer_t data)
+ buffer_t data,
+ bool rekey)
{
struct oap_cli_ctx * s;
void * pkp = NULL;
void * crt = NULL;
+ buffer_t no_tag = BUF_INIT;
ssize_t ret;
assert(ctx != NULL);
@@ -288,6 +290,18 @@ int oap_cli_prepare(void ** ctx,
goto fail_kex;
}
+ /* Re-key forces server-encap: client-encap forfeits FS/PCS. */
+ if (rekey && s->kcfg.x.mode == KEM_MODE_CLIENT_ENCAP) {
+ s->kcfg.x.mode = KEM_MODE_SERVER_ENCAP;
+ log_dbg_id(s->id.data, "Re-key forcing ephemeral server KEX.");
+ }
+
+ /* Re-key omits the cert; the server verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
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) {
@@ -296,7 +310,7 @@ int oap_cli_prepare(void ** ctx,
}
if (oap_hdr_encode(&s->local_hdr, pkp, crt, &s->kcfg,
- (buffer_t) BUF_INIT, NID_undef)) {
+ no_tag, NID_undef, NULL)) {
log_err_id(s->id.data, "Failed to create OAP request header.");
goto fail_hdr;
}
@@ -329,6 +343,7 @@ int oap_cli_prepare(void ** ctx,
return 0;
fail_hash:
+ oap_hdr_fini(&s->local_hdr);
fail_hdr:
crypt_secure_free(s->key, SYMMKEYSZ);
crypt_free_key(s->pkp);
@@ -497,12 +512,20 @@ int oap_cli_complete(void * ctx,
const struct name_info * info,
buffer_t rsp_buf,
buffer_t * data,
- struct crypt_sk * sk)
+ struct crypt_sk * sk,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
{
struct oap_cli_ctx * s = ctx;
struct oap_hdr peer_hdr;
char peer[NAME_SIZE + 1];
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
uint8_t * id;
+ int rc;
assert(ctx != NULL);
assert(info != NULL);
@@ -520,7 +543,7 @@ int oap_cli_complete(void * ctx,
log_dbg_id(id, "Completing OAP for %s.", info->name);
/* Decode response header using client's md_nid for hash length */
- if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid) < 0) {
+ if (oap_hdr_decode(&peer_hdr, rsp_buf, s->req_md_nid, false) < 0) {
log_err_id(id, "Failed to decode OAP response header.");
goto fail_oap;
}
@@ -533,20 +556,52 @@ int oap_cli_complete(void * ctx,
goto fail_oap;
}
- /* Authenticate server */
- if (oap_auth_peer(peer, &s->kcfg, &s->local_hdr, &peer_hdr) < 0) {
- log_err_id(id, "Failed to authenticate server.");
+ /* Complete key exchange first; the sealed identity needs the secret */
+ if (do_client_kex_complete(s, &peer_hdr, sk) < 0) {
+ log_err_id(id, "Failed to complete key exchange.");
goto fail_oap;
}
- /* Verify request hash in authenticated response */
- if (peer_hdr.req_hash.len == 0) {
- log_err_id(id, "Response missing req_hash.");
+ req_hash.data = s->req_hash;
+ req_hash.len = s->req_hash_len;
+
+ /* Decrypt the sealed server identity (data+cert+sig) before auth */
+ if (sk->nid != NID_undef && peer_hdr.sealed.data != NULL) {
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_oap;
+ }
+
+ rc = oap_hdr_unseal(&peer_hdr, hs_key);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (rc < 0) {
+ log_err_id(id, "Failed to unseal server identity.");
+ goto fail_oap;
+ }
+ }
+
+ /* Authenticate server (cert + signature now in cleartext) */
+ if (oap_auth_peer(peer, &s->kcfg, &s->local_hdr, &peer_hdr,
+ cached_crt) < 0) {
+ log_err_id(id, "Failed to authenticate server.");
goto fail_oap;
}
- if (memcmp(peer_hdr.req_hash.data, s->req_hash, s->req_hash_len) != 0) {
- log_err_id(id, "Response req_hash mismatch.");
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_oap;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
+ /* Response must carry a transcript tag of the expected length */
+ if (peer_hdr.rsp_tag.len != s->req_hash_len) {
+ log_err_id(id, "Response transcript tag mismatch.");
goto fail_oap;
}
@@ -557,10 +612,43 @@ int oap_cli_complete(void * ctx,
goto fail_oap;
}
- /* Complete key exchange */
- if (do_client_kex_complete(s, &peer_hdr, sk) < 0) {
- log_err_id(id, "Failed to complete key exchange.");
- goto fail_oap;
+ if (sk->nid != NID_undef) {
+ /* Encrypted: bind the key and verify key confirmation */
+ resp_hash.data = resp_hash_buf;
+
+ if (oap_resp_hash(s->req_md_nid, peer_hdr.kex,
+ peer_hdr.data, peer_hdr.crt,
+ &resp_hash) < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_oap;
+ }
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ s->kcfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_oap;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ s->req_hash_len) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_oap;
+ }
+
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, kc_buf,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Key confirmation mismatch.");
+ goto fail_oap;
+ }
+ } else {
+ /* Cleartext path is config-driven, never a wire downgrade */
+ assert(!IS_KEX_ALGO_SET(&s->kcfg));
+ /* Unencrypted: verify request-echo integrity */
+ if (crypt_ct_cmp(peer_hdr.rsp_tag.data, s->req_hash,
+ s->req_hash_len) != 0) {
+ log_err_id(id, "Response tag mismatch.");
+ goto fail_oap;
+ }
}
/* Copy piggybacked data from server response */
@@ -571,11 +659,14 @@ int oap_cli_complete(void * ctx,
log_info_id(id, "OAP completed for %s.", info->name);
+ freebuf(peer_hdr.sealed_pt);
+
oap_ctx_free(s);
return 0;
fail_oap:
+ freebuf(peer_hdr.sealed_pt);
oap_ctx_free(s);
return -ECRYPT;
}
diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c
index 5465dd2a..f0411f64 100644
--- a/src/irmd/oap/hdr.c
+++ b/src/irmd/oap/hdr.c
@@ -30,6 +30,7 @@
#include <ouroboros/crypt.h>
#include <ouroboros/endian.h>
+#include <ouroboros/errno.h>
#include <ouroboros/hash.h>
#include <ouroboros/logs.h>
#include <ouroboros/rib.h>
@@ -45,9 +46,17 @@
#include <string.h>
#include <time.h>
+#define OAP_SEAL_TAGSZ 16 /* AEAD tag on the sealed identity block */
+/* Sealed length prefix: data_len ‖ crt_len. */
+#define OAP_SEAL_LENSZ (sizeof(uint16_t) + sizeof(uint16_t))
+
+/* hs_key is single-use per handshake, so a fixed nonce is reuse-safe. */
+static const uint8_t oap_seal_nonce[12];
+
int oap_hdr_decode(struct oap_hdr * oap_hdr,
buffer_t hdr,
- int req_md_nid)
+ int req_md_nid,
+ bool rekey)
{
off_t offset;
uint16_t kex_len;
@@ -88,11 +97,13 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
oap_hdr->md_str = md_nid_to_str(oap_hdr->md_nid);
offset += sizeof(uint16_t);
- /* Validate NIDs: NID_undef is valid at parse time, else must be known.
+ /*
+ * Validate NIDs: NID_undef is valid at parse time, else must be known.
* Note: md_nid=NID_undef only valid for PQC; enforced at sign/verify.
*/
if (ciph_nid != NID_undef && crypt_validate_nid(ciph_nid) < 0)
goto fail_decode;
+
if (oap_hdr->kdf_nid != NID_undef &&
md_validate_nid(oap_hdr->kdf_nid) < 0)
goto fail_decode;
@@ -115,10 +126,37 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
data_len = (size_t) ntoh16(*(uint16_t *)(hdr.data + offset));
offset += sizeof(uint16_t);
- /* Response includes req_hash when md_nid is set */
+ assert((size_t) offset == OAP_HDR_MIN_SIZE);
+
+ /* Response includes rsp_tag when md_nid is set */
hash_len = (req_md_nid != NID_undef) ?
(size_t) md_len(req_md_nid) : 0;
+ /* Encrypted response: sealed block is data_len‖crt_len‖data‖crt‖sig. */
+ if (req_md_nid != NID_undef && ciph_nid != NID_undef) {
+ if (hdr.len < (size_t) offset + oap_hdr->kex.len + hash_len +
+ OAP_SEAL_TAGSZ + OAP_SEAL_LENSZ)
+ goto fail_decode;
+
+ oap_hdr->kex.data = hdr.data + offset;
+ offset += oap_hdr->kex.len;
+
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
+ offset += hash_len;
+
+ oap_hdr->sealed.data = hdr.data + offset;
+ oap_hdr->sealed.len = hdr.len - offset;
+
+ /* crt/data/sig lengths are sealed; set by oap_hdr_unseal. */
+ oap_hdr->crt.len = crt_len;
+ oap_hdr->data.len = data_len;
+
+ oap_hdr->hdr = hdr;
+
+ return 0;
+ }
+
/* Validate total length */
if (hdr.len < (size_t) offset + crt_len + oap_hdr->kex.len +
data_len + hash_len)
@@ -128,8 +166,12 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
sig_len = hdr.len - offset - crt_len - oap_hdr->kex.len -
data_len - hash_len;
- /* Unsigned packets must not have trailing bytes */
- if (crt_len == 0 && sig_len != 0)
+ /*
+ * Unsigned packets must not have trailing bytes. A re-key request
+ * is signed but cert-less (verified against the cached peer cert),
+ * so the rekey caller permits crt_len==0 with a signature.
+ */
+ if (crt_len == 0 && sig_len != 0 && !rekey)
goto fail_decode;
/* Parse variable fields */
@@ -144,8 +186,8 @@ int oap_hdr_decode(struct oap_hdr * oap_hdr,
oap_hdr->data.len = data_len;
offset += data_len;
- oap_hdr->req_hash.data = hdr.data + offset;
- oap_hdr->req_hash.len = hash_len;
+ oap_hdr->rsp_tag.data = hdr.data + offset;
+ oap_hdr->rsp_tag.len = hash_len;
offset += hash_len;
oap_hdr->sig.data = hdr.data + offset;
@@ -164,6 +206,7 @@ void oap_hdr_fini(struct oap_hdr * oap_hdr)
{
assert(oap_hdr != NULL);
+ freebuf(oap_hdr->sealed_pt);
freebuf(oap_hdr->hdr);
memset(oap_hdr, 0, sizeof(*oap_hdr));
}
@@ -207,12 +250,229 @@ void oap_hdr_init(struct oap_hdr * hdr,
hdr->nid = nid;
}
+/* Write the 36-byte fixed header; stamp is already in network order. */
+static void write_oap_fixed(uint8_t * buf,
+ const struct oap_hdr * hdr,
+ const struct sec_config * kcfg,
+ size_t crt_len,
+ size_t data_len,
+ uint64_t stamp)
+{
+ uint16_t v;
+ uint16_t kex_len;
+ off_t offset = 0;
+
+ memcpy(buf + offset, hdr->id.data, hdr->id.len);
+ offset += hdr->id.len;
+
+ memcpy(buf + offset, &stamp, sizeof(stamp));
+ offset += sizeof(stamp);
+
+ v = hton16(hdr->nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(kcfg->k.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16(kcfg->d.nid);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ v = hton16((uint16_t) crt_len);
+ memcpy(buf + offset, &v, sizeof(v));
+ offset += sizeof(v);
+
+ kex_len = (uint16_t) hdr->kex.len;
+ if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(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);
+ memcpy(buf + offset, &kex_len, sizeof(kex_len));
+ offset += sizeof(kex_len);
+
+ v = hton16((uint16_t) data_len);
+ memcpy(buf + offset, &v, sizeof(v));
+}
+
+/*
+ * Pack lens ‖ data ‖ crt, sign prefix ‖ body, append the signature, then
+ * AEAD-seal lens ‖ data ‖ crt ‖ sig under prefix as AAD. The cert, app data
+ * and their sizes stay confidential; *out is the opaque sealed block. The
+ * signature rides inside the seal so it can't deanonymise the server.
+ */
+static int oap_seal_body(int nid,
+ const uint8_t * seal_key,
+ void * pkp,
+ int md_nid,
+ buffer_t prefix,
+ buffer_t data,
+ buffer_t crt,
+ buffer_t * out)
+{
+ buffer_t sig = BUF_INIT;
+ buffer_t sign;
+ buffer_t aad;
+ buffer_t plain;
+ uint8_t * buf;
+ uint8_t * tmp;
+ uint16_t datalen;
+ uint16_t crtlen;
+ size_t body_len;
+ off_t offset;
+
+ datalen = hton16((uint16_t) data.len);
+ crtlen = hton16((uint16_t) crt.len);
+
+ body_len = OAP_SEAL_LENSZ + data.len + crt.len;
+
+ buf = malloc(prefix.len + body_len);
+ if (buf == NULL)
+ return -1;
+
+ memcpy(buf, prefix.data, prefix.len);
+ offset = (off_t) prefix.len;
+
+ memcpy(buf + offset, &datalen, sizeof(datalen));
+ offset += sizeof(datalen);
+
+ memcpy(buf + offset, &crtlen, sizeof(crtlen));
+ offset += sizeof(crtlen);
+
+ if (data.len != 0)
+ memcpy(buf + offset, data.data, data.len);
+
+ offset += data.len;
+
+ if (crt.len != 0)
+ memcpy(buf + offset, crt.data, crt.len);
+
+ /* Sign prefix ‖ lens ‖ data ‖ crt (plaintext, before sealing). */
+ sign.data = buf;
+ sign.len = prefix.len + body_len;
+
+ if (pkp != NULL && auth_sign(pkp, md_nid, sign, &sig) < 0)
+ goto fail_buf;
+
+ /* Append the signature so the seal covers lens ‖ data ‖ crt ‖ sig. */
+ if (sig.len != 0) {
+ tmp = realloc(buf, prefix.len + body_len + sig.len);
+ if (tmp == NULL)
+ goto fail_sig;
+
+ buf = tmp;
+ memcpy(buf + prefix.len + body_len, sig.data, sig.len);
+ }
+
+ aad.data = buf;
+ aad.len = prefix.len;
+ plain.data = buf + prefix.len;
+ plain.len = body_len + sig.len;
+
+ if (crypt_oneshot_seal(nid, seal_key, oap_seal_nonce,
+ aad, plain, out) < 0)
+ goto fail_sig;
+
+ free(buf);
+ freebuf(sig);
+
+ return 0;
+
+ fail_sig:
+ freebuf(sig);
+ fail_buf:
+ free(buf);
+ return -1;
+}
+
+/* Encode an identity-hidden response: wire = prefix ‖ oap_seal_body(...). */
+static int oap_hdr_encode_sealed(struct oap_hdr * hdr,
+ void * pkp,
+ void * crt,
+ struct sec_config * kcfg,
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key)
+{
+ struct timespec now;
+ uint64_t stamp;
+ buffer_t der = BUF_INIT;
+ buffer_t sealed = BUF_INIT;
+ buffer_t prefix;
+ off_t offset;
+
+ clock_gettime(CLOCK_REALTIME, &now);
+ stamp = hton64(TS_TO_UINT64(now));
+
+ if (crt != NULL && crypt_crt_der(crt, &der) < 0)
+ goto fail_der;
+
+ prefix.len = OAP_HDR_MIN_SIZE + hdr->kex.len + rsp_tag.len;
+ prefix.data = malloc(prefix.len);
+ if (prefix.data == NULL)
+ goto fail_der;
+
+ /* Cleartext crt_len/data_len are 0; real lengths prefix the seal. */
+ write_oap_fixed(prefix.data, hdr, kcfg, 0, 0, stamp);
+ offset = OAP_HDR_MIN_SIZE;
+
+ if (hdr->kex.len != 0)
+ memcpy(prefix.data + offset, hdr->kex.data, hdr->kex.len);
+
+ offset += hdr->kex.len;
+
+ if (rsp_tag.len != 0)
+ memcpy(prefix.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
+
+ assert((size_t) offset == prefix.len);
+
+ if (oap_seal_body(hdr->nid, seal_key, pkp, kcfg->d.nid,
+ prefix, hdr->data, der, &sealed) < 0)
+ goto fail_prefix;
+
+ hdr->hdr.len = prefix.len + sealed.len;
+ hdr->hdr.data = malloc(hdr->hdr.len);
+ if (hdr->hdr.data == NULL)
+ goto fail_sealed;
+
+ memcpy(hdr->hdr.data, prefix.data, prefix.len);
+ memcpy(hdr->hdr.data + prefix.len, sealed.data, sealed.len);
+
+ freebuf(sealed);
+ free(prefix.data);
+ freebuf(der);
+
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, false) < 0)
+ goto fail_decode;
+
+ return 0;
+
+ fail_decode:
+ oap_hdr_fini(hdr);
+ return -1;
+ fail_sealed:
+ freebuf(sealed);
+ fail_prefix:
+ free(prefix.data);
+ fail_der:
+ freebuf(der);
+ return -1;
+}
+
int oap_hdr_encode(struct oap_hdr * hdr,
void * pkp,
void * crt,
struct sec_config * kcfg,
- buffer_t req_hash,
- int req_md_nid)
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key)
{
struct timespec now;
uint64_t stamp;
@@ -220,103 +480,56 @@ int oap_hdr_encode(struct oap_hdr * hdr,
buffer_t der = BUF_INIT;
buffer_t sig = BUF_INIT;
buffer_t sign;
- uint16_t len;
- uint16_t ciph_nid;
- uint16_t kdf_nid;
- uint16_t md_nid;
- uint16_t kex_len;
off_t offset;
assert(hdr != NULL);
assert(hdr->id.data != NULL && hdr->id.len == OAP_ID_SIZE);
assert(kcfg != NULL);
+ if (seal_key != NULL)
+ return oap_hdr_encode_sealed(hdr, pkp, crt, kcfg, rsp_tag,
+ req_md_nid, seal_key);
+
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) */
+ /* Fixed header (36 bytes) + variable fields + rsp_tag (rsp only) */
out.len = OAP_HDR_MIN_SIZE + der.len + hdr->kex.len + hdr->data.len +
- req_hash.len;
+ rsp_tag.len;
out.data = malloc(out.len);
if (out.data == NULL)
goto fail_out;
- offset = 0;
-
- /* id (16 bytes) */
- memcpy(out.data + offset, hdr->id.data, hdr->id.len);
- offset += hdr->id.len;
-
- /* timestamp (8 bytes) */
- memcpy(out.data + offset, &stamp, sizeof(stamp));
- offset += sizeof(stamp);
-
- /* cipher_nid (2 bytes) */
- memcpy(out.data + offset, &ciph_nid, sizeof(ciph_nid));
- offset += sizeof(ciph_nid);
-
- /* kdf_nid (2 bytes) */
- memcpy(out.data + offset, &kdf_nid, sizeof(kdf_nid));
- offset += sizeof(kdf_nid);
-
- /* md_nid (2 bytes) */
- memcpy(out.data + offset, &md_nid, sizeof(md_nid));
- offset += sizeof(md_nid);
-
- /* crt_len (2 bytes) */
- len = hton16((uint16_t) der.len);
- memcpy(out.data + offset, &len, sizeof(len));
- offset += sizeof(len);
-
- /* kex_len + flags (2 bytes) */
- memcpy(out.data + offset, &kex_len, sizeof(kex_len));
- offset += sizeof(kex_len);
-
- /* data_len (2 bytes) */
- len = hton16((uint16_t) hdr->data.len);
- memcpy(out.data + offset, &len, sizeof(len));
- offset += sizeof(len);
-
- /* Fixed header complete (36 bytes) */
- assert((size_t) offset == OAP_HDR_MIN_SIZE);
+ write_oap_fixed(out.data, hdr, kcfg, der.len, hdr->data.len, stamp);
+ offset = OAP_HDR_MIN_SIZE;
/* certificate (variable) */
if (der.len != 0)
memcpy(out.data + offset, der.data, der.len);
+
offset += der.len;
/* kex data (variable) */
if (hdr->kex.len != 0)
memcpy(out.data + offset, hdr->kex.data, hdr->kex.len);
+
offset += hdr->kex.len;
/* data (variable) */
if (hdr->data.len != 0)
memcpy(out.data + offset, hdr->data.data, hdr->data.len);
+
offset += hdr->data.len;
- /* req_hash (variable, only for authenticated responses) */
- if (req_hash.len != 0)
- memcpy(out.data + offset, req_hash.data, req_hash.len);
- offset += req_hash.len;
+ /* rsp_tag (variable, response only) */
+ if (rsp_tag.len != 0)
+ memcpy(out.data + offset, rsp_tag.data, rsp_tag.len);
+
+ offset += rsp_tag.len;
assert((size_t) offset == out.len);
@@ -340,7 +553,7 @@ int oap_hdr_encode(struct oap_hdr * hdr,
clrbuf(out);
}
- if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid) < 0)
+ if (oap_hdr_decode(hdr, hdr->hdr, req_md_nid, false) < 0)
goto fail_decode;
freebuf(der);
@@ -360,28 +573,99 @@ int oap_hdr_encode(struct oap_hdr * hdr,
return -1;
}
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key)
+{
+ buffer_t pt = BUF_INIT;
+ buffer_t prefix;
+ uint8_t * recon;
+ size_t body_len;
+ size_t pt_len;
+ size_t data_len;
+ size_t crt_len;
+
+ assert(hdr != NULL);
+ assert(key != NULL);
+
+ if (hdr->sealed.data == NULL || hdr->sealed.len == 0)
+ return -EINVAL;
+
+ /* AAD prefix is fixed‖kex‖rsp_tag; sealed starts right after. */
+ prefix.data = hdr->hdr.data;
+ prefix.len = (size_t) (hdr->sealed.data - hdr->hdr.data);
+
+ if (crypt_oneshot_open(hdr->nid, key, oap_seal_nonce, prefix,
+ hdr->sealed, &pt) < 0)
+ return -ECRYPT;
+
+ pt_len = pt.len;
+
+ /* Plaintext = data_len ‖ crt_len ‖ data ‖ crt ‖ sig. */
+ if (pt_len < OAP_SEAL_LENSZ)
+ goto fail_auth;
+
+ data_len = (size_t) ntoh16(*(uint16_t *) pt.data);
+ crt_len = (size_t) ntoh16(*(uint16_t *)(pt.data + sizeof(uint16_t)));
+
+ body_len = OAP_SEAL_LENSZ + data_len + crt_len;
+ if (pt_len < body_len)
+ goto fail_auth;
+
+ /* Rebuild prefix ‖ lens ‖ data ‖ crt ‖ sig (whole signed region). */
+ recon = malloc(prefix.len + pt_len);
+ if (recon == NULL)
+ goto fail_mem;
+
+ memcpy(recon, prefix.data, prefix.len);
+ memcpy(recon + prefix.len, pt.data, pt_len);
+
+ freebuf(pt);
+
+ hdr->sealed_pt.data = recon;
+ hdr->sealed_pt.len = prefix.len + pt_len;
+
+ hdr->data.data = recon + prefix.len + OAP_SEAL_LENSZ;
+ hdr->data.len = data_len;
+ hdr->crt.data = recon + prefix.len + OAP_SEAL_LENSZ + data_len;
+ hdr->crt.len = crt_len;
+ hdr->sig.data = recon + prefix.len + body_len;
+ hdr->sig.len = pt_len - body_len;
+
+ return 0;
+
+ fail_mem:
+ freebuf(pt);
+ return -ENOMEM;
+ fail_auth:
+ freebuf(pt);
+ return -EAUTH;
+}
+
#ifdef DEBUG_PROTO_OAP
#define OAP_KEX_IS_KEM(hdr) ((hdr)->kex_flags.role | (hdr)->kex_flags.fmt)
static void debug_oap_hdr(const struct oap_hdr * hdr)
{
assert(hdr);
+ if (hdr->sealed.len > 0)
+ log_proto(" Sealed block: [%zu bytes] on wire",
+ hdr->sealed.len);
+
if (hdr->crt.len > 0)
log_proto(" crt: [%zu bytes]", hdr->crt.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" crt: <sealed>");
else
log_proto(" crt: <none>");
if (hdr->kex.len > 0) {
if (OAP_KEX_IS_KEM(hdr))
- log_proto(" Key Exchange Data:"
- " [%zu bytes] [%s]",
+ log_proto(" Key Exchange Data: [%zu bytes] [%s]",
hdr->kex.len,
hdr->kex_flags.role ?
- "Client encaps" :
- "Server encaps");
+ "Client encaps" : "Server encaps");
else
- log_proto(" Key Exchange Data:"
- " [%zu bytes]",
+ log_proto(" Key Exchange Data: [%zu bytes]",
hdr->kex.len);
} else
log_proto(" Key Exchange Data: <none>");
@@ -403,16 +687,20 @@ static void debug_oap_hdr(const struct oap_hdr * hdr)
if (hdr->data.len > 0)
log_proto(" Data: [%zu bytes]", hdr->data.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Data: <sealed>");
else
log_proto(" Data: <none>");
- if (hdr->req_hash.len > 0)
- log_proto(" Req Hash: [%zu bytes]", hdr->req_hash.len);
+ if (hdr->rsp_tag.len > 0)
+ log_proto(" Rsp Tag: [%zu bytes]", hdr->rsp_tag.len);
else
- log_proto(" Req Hash: <none>");
+ log_proto(" Rsp Tag: <none>");
if (hdr->sig.len > 0)
log_proto(" Signature: [%zu bytes]", hdr->sig.len);
+ else if (hdr->sealed.len > 0)
+ log_proto(" Signature: <sealed>");
else
log_proto(" Signature: <none>");
}
@@ -432,8 +720,9 @@ void debug_oap_hdr_rcv(const struct oap_hdr * hdr)
tm = gmtime(&stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] <--",
- HASH_VAL64(hdr->id.data), tmstr);
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s <--",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
debug_oap_hdr(hdr);
#else
@@ -455,8 +744,9 @@ void debug_oap_hdr_snd(const struct oap_hdr * hdr)
tm = gmtime(&stamp);
strftime(tmstr, sizeof(tmstr), RIB_TM_FORMAT, tm);
- log_proto("OAP_HDR [" HASH_FMT64 " @ %s ] -->",
- HASH_VAL64(hdr->id.data), tmstr);
+ log_proto("OAP_HDR [" HASH_FMT64 " @ %s ]%s -->",
+ HASH_VAL64(hdr->id.data), tmstr,
+ hdr->sealed.len > 0 ? " [sealed]" : "");
debug_oap_hdr(hdr);
#else
diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h
index 6016452c..e6c5fffc 100644
--- a/src/irmd/oap/hdr.h
+++ b/src/irmd/oap/hdr.h
@@ -43,6 +43,9 @@
#define OAP_KEX_IS_RAW_FMT(hdr) (((hdr)->kex_flags.fmt) == 1)
/*
+ * Plaintext layout (request, and unencrypted/signed response). The
+ * signature covers the whole packet except itself.
+ *
* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
@@ -83,8 +86,8 @@
* | | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
* | | |
- * + req_hash (variable, response only) + |
- * | H(request) using req md_nid / sha384 | |
+ * + rsp_tag (variable, response only) + |
+ * | key-confirm tag (enc), else H(request) | |
* | | |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
* | |
@@ -92,6 +95,25 @@
* | DSA signature over signed region |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
+ * Encrypted response - wire layout. The certificate, application data and
+ * signature are AEAD-sealed - hiding the server identity and the cert/data
+ * sizes; kex and rsp_tag move ahead of the sealed block as cleartext AAD.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | fixed header (36 bytes, see above) | |
+ * + id, timestamp, NIDs, crt_len=0, kex_len, data_len=0 + | AAD
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | kex_data (variable) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
+ * | rsp_tag (variable, response only) | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ * | SEAL( data_len ‖ crt_len ‖ data ‖ crt ‖ sig ) | |
+ * + encrypted cert, app data and signature + | Sealed
+ * | + AEAD tag (128 bits) | | area
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ---+
+ *
* cipher_nid: NID value for symmetric cipher (0 = none)
* kdf_nid: NID value for KDF function (0 = none)
* md_nid: NID value for signature hash (0 = PQC/no signature)
@@ -105,6 +127,11 @@
* Request: sig_len = total - 36 - crt_len - kex_len - data_len
* Response: sig_len = total - 36 - crt_len - kex_len - data_len - hash_len
* where hash_len = md_len(req_md_nid / sha384)
+ *
+ * The signed plaintext inside the seal is prefix ‖ data_len ‖ crt_len ‖
+ * data ‖ crt ‖ sig; the cleartext prefix (fixed ‖ kex ‖ rsp_tag) is the
+ * AEAD AAD. Cleartext crt_len/data_len are 0 - the real lengths are sealed,
+ * hiding the cert and data sizes; oap_hdr_unseal reads them to split.
*/
/* Parsed OAP header - buffers pointing to a single memory region */
@@ -120,12 +147,15 @@ struct oap_hdr {
bool fmt; /* Format */
bool role; /* Role */
} kex_flags;
+
buffer_t id;
buffer_t crt;
buffer_t kex;
buffer_t data;
- buffer_t req_hash; /* H(request) - response only */
+ buffer_t rsp_tag; /* key-confirm tag / H(req), rsp only */
buffer_t sig;
+ buffer_t sealed; /* wire ciphertext ‖ tag (sealed rsp) */
+ buffer_t sealed_pt; /* prefix‖lens‖data‖crt‖sig, owned */
buffer_t hdr;
};
@@ -142,12 +172,18 @@ int oap_hdr_encode(struct oap_hdr * hdr,
void * pkp,
void * crt,
struct sec_config * kcfg,
- buffer_t req_hash,
- int req_md_nid);
+ buffer_t rsp_tag,
+ int req_md_nid,
+ const uint8_t * seal_key);
int oap_hdr_decode(struct oap_hdr * hdr,
buffer_t buf,
- int req_md_nid);
+ int req_md_nid,
+ bool rekey);
+
+/* Decrypt a sealed response identity block; fills data, crt and sig. */
+int oap_hdr_unseal(struct oap_hdr * hdr,
+ const uint8_t * key);
void debug_oap_hdr_rcv(const struct oap_hdr * hdr);
diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c
index 9ace8ed1..aef987eb 100644
--- a/src/irmd/oap/srv.c
+++ b/src/irmd/oap/srv.c
@@ -146,12 +146,12 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr,
int srv_rank;
/* Cipher: select the strongest of client and server */
- cli_nid = peer_hdr->cipher_str != NULL
- ? (int) crypt_str_to_nid(peer_hdr->cipher_str)
- : NID_undef;
+ if (peer_hdr->cipher_str != NULL)
+ cli_nid = (int) crypt_str_to_nid(peer_hdr->cipher_str);
+ else
+ cli_nid = NID_undef;
- if (cli_nid != NID_undef
- && crypt_cipher_rank(cli_nid) < 0) {
+ if (cli_nid != NID_undef && crypt_cipher_rank(cli_nid) < 0) {
log_err_id(id, "Unsupported cipher '%s'.",
peer_hdr->cipher_str);
return -ENOTSUP;
@@ -162,11 +162,9 @@ static int negotiate_cipher(const struct oap_hdr * peer_hdr,
if (cli_rank > srv_rank) {
SET_KEX_CIPHER_NID(kcfg, cli_nid);
- log_dbg_id(id, "Selected client cipher %s.",
- kcfg->c.str);
+ log_dbg_id(id, "Selected client cipher %s.", kcfg->c.str);
} else if (srv_rank > 0) {
- log_dbg_id(id, "Selected server cipher %s.",
- kcfg->c.str);
+ log_dbg_id(id, "Selected server cipher %s.", kcfg->c.str);
} else {
log_err_id(id, "Encryption requested, no cipher.");
return -ECRYPT;
@@ -378,14 +376,24 @@ int oap_srv_process(const struct name_info * info,
buffer_t req_buf,
buffer_t * rsp_buf,
buffer_t * data,
- struct crypt_sk * sk)
+ struct crypt_sk * sk,
+ bool rekey,
+ const buffer_t * cached_crt,
+ buffer_t * peer_crt)
{
struct oap_hdr peer_hdr;
struct oap_hdr local_hdr;
struct sec_config kcfg;
uint8_t kex_buf[CRYPT_KEY_BUFSZ];
uint8_t hash_buf[MAX_HASH_SIZE];
- buffer_t req_hash = BUF_INIT;
+ uint8_t kc_buf[MAX_HASH_SIZE];
+ uint8_t resp_hash_buf[MAX_HASH_SIZE];
+ uint8_t hs_key[SYMMKEYSZ];
+ const uint8_t * seal_key = NULL;
+ buffer_t req_hash = BUF_INIT;
+ buffer_t resp_hash = BUF_INIT;
+ buffer_t crt_der = BUF_INIT;
+ buffer_t rsp_tag = BUF_INIT;
ssize_t hash_ret;
char cli_name[NAME_SIZE + 1];
uint8_t * id;
@@ -412,13 +420,19 @@ int oap_srv_process(const struct name_info * info,
goto fail_cred;
}
+ /* Re-key omits the cert; the peer verifies against its cache. */
+ if (rekey && crt != NULL) {
+ crypt_free_crt(crt);
+ crt = NULL;
+ }
+
if (load_srv_kex_config(info, &kcfg) < 0) {
log_err("Failed to load KEX config for %s.", info->name);
goto fail_kex;
}
/* Decode incoming header (NID_undef = request, no hash) */
- if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef) < 0) {
+ if (oap_hdr_decode(&peer_hdr, req_buf, NID_undef, rekey) < 0) {
log_err("Failed to decode OAP header.");
goto fail_auth;
}
@@ -439,11 +453,22 @@ int oap_srv_process(const struct name_info * info,
oap_hdr_init(&local_hdr, peer_hdr.id, kex_buf, *data, NID_undef);
- if (oap_auth_peer(cli_name, &kcfg, &local_hdr, &peer_hdr) < 0) {
+ if (oap_auth_peer(cli_name, &kcfg, &local_hdr, &peer_hdr,
+ cached_crt) < 0) {
log_err_id(id, "Failed to authenticate client.");
goto fail_auth;
}
+ /* Surface the peer cert so the caller can cache it for re-key. */
+ if (peer_crt != NULL && peer_hdr.crt.len > 0) {
+ peer_crt->data = malloc(peer_hdr.crt.len);
+ if (peer_crt->data == NULL)
+ goto fail_auth;
+
+ memcpy(peer_crt->data, peer_hdr.crt.data, peer_hdr.crt.len);
+ peer_crt->len = peer_hdr.crt.len;
+ }
+
if (do_server_kex(info, &peer_hdr, &kcfg, &local_hdr.kex, sk) < 0)
goto fail_kex;
@@ -463,10 +488,58 @@ int oap_srv_process(const struct name_info * info,
goto fail_auth;
}
req_hash.data = hash_buf;
- req_hash.len = (size_t) hash_ret;
+ req_hash.len = (size_t) hash_ret;
+
+ rsp_tag = req_hash;
- if (oap_hdr_encode(&local_hdr, pkp, crt, &kcfg,
- req_hash, req_md_nid) < 0) {
+ /* Bind the key to the transcript and confirm it to the client */
+ if (sk->nid != NID_undef) {
+ if (crt != NULL && crypt_crt_der(crt, &crt_der) < 0) {
+ log_err_id(id, "Failed to serialize cert.");
+ goto fail_auth;
+ }
+
+ resp_hash.data = resp_hash_buf;
+
+ ret = oap_resp_hash(req_md_nid, local_hdr.kex, *data,
+ crt_der, &resp_hash);
+
+ freebuf(crt_der);
+
+ if (ret < 0) {
+ log_err_id(id, "Failed to hash response.");
+ goto fail_auth;
+ }
+
+ /* Derive the identity-seal key before bind mutates sk->key */
+ if (oap_derive_hs_key(sk, req_hash, hs_key) < 0) {
+ log_err_id(id, "Failed to derive handshake key.");
+ goto fail_auth;
+ }
+
+ seal_key = hs_key;
+
+ if (oap_bind_session_key(sk, req_hash, resp_hash,
+ kcfg.k.nid) < 0) {
+ log_err_id(id, "Failed to bind session key.");
+ goto fail_auth;
+ }
+
+ if (oap_key_confirm_tag(sk, req_hash, resp_hash, kc_buf,
+ (size_t) hash_ret) < 0) {
+ log_err_id(id, "Failed to confirm session key.");
+ goto fail_auth;
+ }
+
+ rsp_tag.data = kc_buf;
+ }
+
+ ret = oap_hdr_encode(&local_hdr, pkp, crt, &kcfg,
+ rsp_tag, req_md_nid, seal_key);
+
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
+
+ if (ret < 0) {
log_err_id(id, "Failed to create OAP response header.");
goto fail_auth;
}
@@ -491,6 +564,7 @@ int oap_srv_process(const struct name_info * info,
fail_data:
oap_hdr_fini(&local_hdr);
fail_auth:
+ crypt_secure_clear(hs_key, SYMMKEYSZ);
crypt_free_crt(crt);
crypt_free_key(pkp);
fail_cred:
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c
index 8c271b2e..e9ac82ed 100644
--- a/src/irmd/oap/tests/common.c
+++ b/src/irmd/oap/tests/common.c
@@ -164,13 +164,15 @@ void oap_test_teardown(struct oap_test_ctx * ctx)
if (ctx->cli.state != NULL) {
res.key = ctx->cli.key;
oap_cli_complete(ctx->cli.state, &ctx->cli.info, dummy,
- &ctx->data, &res);
+ &ctx->data, &res, NULL, NULL);
ctx->cli.state = NULL;
}
freebuf(ctx->data);
freebuf(ctx->resp_hdr);
freebuf(ctx->req_hdr);
+ freebuf(ctx->srv_crt);
+ freebuf(ctx->cli_crt);
crypt_free_crt(ctx->im_ca);
crypt_free_crt(ctx->root_ca);
@@ -182,7 +184,7 @@ void oap_test_teardown(struct oap_test_ctx * ctx)
int oap_cli_prepare_ctx(struct oap_test_ctx * ctx)
{
return oap_cli_prepare(&ctx->cli.state, &ctx->cli.info, &ctx->req_hdr,
- ctx->data);
+ ctx->data, ctx->rekey);
}
int oap_srv_process_ctx(struct oap_test_ctx * ctx)
@@ -191,7 +193,9 @@ int oap_srv_process_ctx(struct oap_test_ctx * ctx)
int ret;
ret = oap_srv_process(&ctx->srv.info, ctx->req_hdr,
- &ctx->resp_hdr, &ctx->data, &res);
+ &ctx->resp_hdr, &ctx->data, &res, ctx->rekey,
+ ctx->rekey ? &ctx->srv_crt : NULL,
+ ctx->rekey ? NULL : &ctx->srv_crt);
if (ret == 0)
ctx->srv.nid = res.nid;
@@ -204,7 +208,9 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx)
int ret;
ret = oap_cli_complete(ctx->cli.state, &ctx->cli.info, ctx->resp_hdr,
- &ctx->data, &res);
+ &ctx->data, &res,
+ ctx->rekey ? &ctx->cli_crt : NULL,
+ ctx->rekey ? NULL : &ctx->cli_crt);
ctx->cli.state = NULL;
if (ret == 0)
@@ -255,6 +261,147 @@ int roundtrip_auth_only(const char * root_ca,
return TEST_RC_FAIL;
}
+int roundtrip_rekey(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;
+
+ /* Initial handshake: the client caches the server cert. */
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Initial keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached for re-key.\n");
+ goto fail_cleanup;
+ }
+
+ /* Re-key: cert dropped on the wire, verified against the cache. */
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Re-key client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Re-key keys do not match.\n");
+ goto fail_cleanup;
+ }
+
+ 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_rekey_badcache(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("Initial client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Initial server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Initial client complete failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.cli_crt.len == 0) {
+ printf("Server cert was not cached.\n");
+ goto fail_cleanup;
+ }
+
+ /* Corrupt the cached cert: the re-key must fail closed. */
+ ctx.cli_crt.data[ctx.cli_crt.len / 2] ^= 0xFF;
+
+ freebuf(ctx.req_hdr);
+ freebuf(ctx.resp_hdr);
+ freebuf(ctx.data);
+
+ ctx.rekey = true;
+
+ if (oap_cli_prepare_ctx(&ctx) < 0) {
+ printf("Re-key client prepare failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Re-key server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Re-key accepted a corrupted cached cert.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
+ 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;
@@ -283,14 +430,15 @@ int roundtrip_kex_only(void)
}
if (oap_cli_prepare(&cli_state, &cli_info, &req_hdr,
- data) < 0) {
+ data, false) < 0) {
printf("Client prepare failed.\n");
goto fail_cleanup;
}
res.key = srv_key;
- if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res) < 0) {
+ if (oap_srv_process(&srv_info, req_hdr, &resp_hdr, &data, &res,
+ false, NULL, NULL) < 0) {
printf("Server process failed.\n");
goto fail_cleanup;
}
@@ -299,7 +447,8 @@ int roundtrip_kex_only(void)
res.key = cli_key;
- if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res) < 0) {
+ if (oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res,
+ NULL, NULL) < 0) {
printf("Client complete failed.\n");
cli_state = NULL;
goto fail_cleanup;
@@ -328,7 +477,8 @@ int roundtrip_kex_only(void)
fail_cleanup:
if (cli_state != NULL) {
res.key = cli_key;
- oap_cli_complete(cli_state, &cli_info, resp_hdr, &data, &res);
+ oap_cli_complete(cli_state, &cli_info, resp_hdr, &data,
+ &res, NULL, NULL);
}
freebuf(resp_hdr);
freebuf(req_hdr);
@@ -408,7 +558,7 @@ int corrupted_response(const char * root_ca,
res.key = ctx.cli.key;
if (oap_cli_complete(ctx.cli.state, &ctx.cli.info, ctx.resp_hdr,
- &ctx.data, &res) == 0) {
+ &ctx.data, &res, NULL, NULL) == 0) {
printf("Client should reject corrupted response.\n");
ctx.cli.state = NULL;
goto fail_cleanup;
diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h
index 4fe2f779..c47096fb 100644
--- a/src/irmd/oap/tests/common.h
+++ b/src/irmd/oap/tests/common.h
@@ -71,6 +71,11 @@ struct oap_test_ctx {
buffer_t data;
void * root_ca;
void * im_ca;
+
+ /* Re-key (tier iii): drop the cert, verify against the cache. */
+ bool rekey;
+ buffer_t srv_crt; /* client cert cached by server */
+ buffer_t cli_crt; /* server cert cached by client */
};
int oap_test_setup(struct oap_test_ctx * ctx,
@@ -88,6 +93,12 @@ int oap_cli_complete_ctx(struct oap_test_ctx * ctx);
int roundtrip_auth_only(const char * root_ca,
const char * im_ca_str);
+int roundtrip_rekey(const char * root_ca,
+ const char * im_ca_str);
+
+int roundtrip_rekey_badcache(const char * root_ca,
+ const char * im_ca_str);
+
int roundtrip_kex_only(void);
int corrupted_request(const char * root_ca,
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c
index 53b525a7..fc10150b 100644
--- a/src/irmd/oap/tests/oap_test.c
+++ b/src/irmd/oap/tests/oap_test.c
@@ -49,7 +49,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <unistd.h>
#ifdef HAVE_OPENSSL
#include <openssl/evp.h>
@@ -179,6 +178,7 @@ static int test_oap_roundtrip(int kex)
oap_test_teardown(&ctx);
TEST_SUCCESS("(%s)", kex_str);
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -203,6 +203,20 @@ static int test_oap_roundtrip_auth_only(void)
return roundtrip_auth_only(root_ca_crt_ec, im_ca_crt_ec);
}
+static int test_oap_rekey(void)
+{
+ test_default_cfg();
+
+ return roundtrip_rekey(root_ca_crt_ec, im_ca_crt_ec);
+}
+
+static int test_oap_rekey_badcache(void)
+{
+ test_default_cfg();
+
+ return roundtrip_rekey_badcache(root_ca_crt_ec, im_ca_crt_ec);
+}
+
static int test_oap_roundtrip_kex_only(void)
{
memset(&test_cfg, 0, sizeof(test_cfg));
@@ -243,6 +257,7 @@ static int test_oap_piggyback_data(void)
ctx.data.data = malloc(ctx.data.len);
if (ctx.data.data == NULL)
goto fail_cleanup;
+
memcpy(ctx.data.data, cli_data_str, ctx.data.len);
if (oap_cli_prepare_ctx(&ctx) < 0)
@@ -293,6 +308,7 @@ static int test_oap_piggyback_data(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -361,6 +377,7 @@ static int test_oap_inflated_length_field(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -405,6 +422,7 @@ static int test_oap_deflated_length_field(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -463,6 +481,7 @@ static int test_oap_nid_without_kex(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -514,6 +533,61 @@ static int test_oap_unsupported_nid(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/* Client rejects a response whose key-confirmation tag is tampered */
+static int test_oap_key_confirm_mismatch(void)
+{
+ struct oap_test_ctx ctx;
+
+ TEST_START();
+
+ /* Unauthenticated + encrypted: response unsigned, KC is the gate */
+ 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;
+ }
+
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ /* The key-confirm tag is the last field of an unsigned response */
+ ctx.resp_hdr.data[ctx.resp_hdr.len - 1] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a bad key-confirmation tag.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -614,6 +688,7 @@ static int test_oap_cipher_mismatch(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -660,6 +735,7 @@ static int test_oap_srv_enc_cli_none(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -729,6 +805,7 @@ static int test_oap_cli_enc_srv_none(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -738,7 +815,7 @@ static int test_oap_cli_enc_srv_none(void)
return TEST_RC_FAIL;
}
-/* Client rejects server response with downgraded cipher */
+/* Unauthenticated server: client floor-rejects a downgraded cipher */
static int test_oap_cli_rejects_downgrade(void)
{
struct oap_test_ctx ctx;
@@ -752,7 +829,7 @@ static int test_oap_cli_rejects_downgrade(void)
test_cfg.srv.cipher = NID_aes_256_gcm;
test_cfg.srv.kdf = NID_sha256;
test_cfg.srv.md = NID_sha256;
- test_cfg.srv.auth = AUTH;
+ test_cfg.srv.auth = NO_AUTH;
test_cfg.cli.kex = NID_X25519;
test_cfg.cli.cipher = NID_aes_256_gcm;
@@ -774,7 +851,7 @@ static int test_oap_cli_rejects_downgrade(void)
}
/* Tamper: replace cipher NID with weaker one */
- weak = hton16(NID_aes_128_ctr);
+ weak = hton16(NID_aes_128_gcm);
memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
&weak, sizeof(weak));
@@ -787,6 +864,69 @@ static int test_oap_cli_rejects_downgrade(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
+ return TEST_RC_SUCCESS;
+
+ fail_cleanup:
+ oap_test_teardown(&ctx);
+ fail:
+ TEST_FAIL();
+ return TEST_RC_FAIL;
+}
+
+/*
+ * Suite binding: a cipher swapped to a higher rank clears the client floor
+ * check, but the bound key commits to the negotiated suite, so the swap must
+ * still fail key confirmation.
+ */
+static int test_oap_cli_rejects_suite_swap(void)
+{
+ struct oap_test_ctx ctx;
+ uint16_t swap;
+
+ TEST_START();
+
+ memset(&test_cfg, 0, sizeof(test_cfg));
+
+ /* Both AES-128-GCM: a swap to AES-256 outranks the client floor */
+ test_cfg.srv.kex = NID_X25519;
+ test_cfg.srv.cipher = NID_aes_128_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_128_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;
+ }
+
+ /* Swap the response cipher to a higher-ranked one */
+ swap = hton16(NID_aes_256_gcm);
+ memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET,
+ &swap, sizeof(swap));
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a swapped cipher suite.\n");
+ goto fail_cleanup;
+ }
+
+ oap_test_teardown(&ctx);
+
+ TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -836,6 +976,7 @@ static int test_oap_srv_rejects_weak_kex(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -895,6 +1036,7 @@ static int test_oap_roundtrip_md(int md)
oap_test_teardown(&ctx);
TEST_SUCCESS("(%s)", md_str ? md_str : "default");
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -960,6 +1102,7 @@ static int test_oap_outdated_packet(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1008,6 +1151,7 @@ static int test_oap_future_packet(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1067,6 +1211,7 @@ static int test_oap_replay_packet(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1274,6 +1419,7 @@ static int test_oap_missing_root_ca(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_teardown:
@@ -1322,6 +1468,7 @@ static int test_oap_server_name_mismatch(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1363,6 +1510,7 @@ static int test_oap_cli_requires_srv_auth(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1398,6 +1546,7 @@ static int test_oap_srv_requires_cli_auth(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1445,6 +1594,7 @@ static int test_oap_mutual_req_auth(void)
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1454,59 +1604,18 @@ static int test_oap_mutual_req_auth(void)
return TEST_RC_FAIL;
}
-/* Write a PEM cert to a temp file for cacert= pinning */
-static int write_tmp_crt(const char * pem,
- char * path)
-{
- FILE * fp;
- int fd;
-
- strcpy(path, "/tmp/oap_test_pin_XXXXXX");
-
- fd = mkstemp(path);
- if (fd < 0)
- return -1;
-
- fp = fdopen(fd, "w");
- if (fp == NULL) {
- close(fd);
- goto fail_file;
- }
-
- if (fputs(pem, fp) == EOF) {
- fclose(fp);
- goto fail_file;
- }
-
- fclose(fp);
-
- return 0;
-
- fail_file:
- unlink(path);
- return -1;
-}
-
-/* Client pins the server CA: in-chain accepted, out-of-chain rejected */
-static int test_oap_cli_pin_ca(const char * pem,
- bool expected)
+/* Client rejects a server signature with a different digest */
+static int test_oap_cli_rejects_md_mismatch(void)
{
struct oap_test_ctx ctx;
- char path[32];
test_default_cfg();
+ test_cfg.srv.md = NID_sha384;
- TEST_START("(%s)", expected ? "match" : "mismatch");
-
- if (write_tmp_crt(pem, path) < 0) {
- printf("Failed to write pinned CA file.\n");
- goto fail;
- }
-
- test_cfg.cli.cacert = path;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
- goto fail_unlink;
+ goto fail;
if (oap_cli_prepare_ctx(&ctx) < 0) {
printf("Client prepare failed.\n");
@@ -1518,80 +1627,88 @@ static int test_oap_cli_pin_ca(const char * pem,
goto fail_cleanup;
}
- if ((oap_cli_complete_ctx(&ctx) == 0) != expected) {
- printf("Pinned CA gave wrong verdict.\n");
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client should reject digest mismatch.\n");
goto fail_cleanup;
}
- unlink(path);
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
- fail_unlink:
- unlink(path);
fail:
TEST_FAIL();
return TEST_RC_FAIL;
}
-/* Server pins the client CA: in-chain accepted, out-of-chain rejected */
-static int test_oap_srv_pin_ca(const char * pem,
- bool expected)
+/* Server rejects a client signature with a different digest */
+static int test_oap_srv_rejects_md_mismatch(void)
{
struct oap_test_ctx ctx;
- char path[32];
test_default_cfg();
test_cfg.cli.auth = AUTH;
+ test_cfg.cli.md = NID_sha384;
- TEST_START("(%s)", expected ? "match" : "mismatch");
-
- if (write_tmp_crt(pem, path) < 0) {
- printf("Failed to write pinned CA file.\n");
- goto fail;
- }
-
- test_cfg.srv.cacert = path;
+ TEST_START();
if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0)
- goto fail_unlink;
+ goto fail;
if (oap_cli_prepare_ctx(&ctx) < 0) {
printf("Client prepare failed.\n");
goto fail_cleanup;
}
- if ((oap_srv_process_ctx(&ctx) == 0) != expected) {
- printf("Pinned CA gave wrong verdict.\n");
+ if (oap_srv_process_ctx(&ctx) == 0) {
+ printf("Server should reject digest mismatch.\n");
goto fail_cleanup;
}
- unlink(path);
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
oap_test_teardown(&ctx);
- fail_unlink:
- unlink(path);
fail:
TEST_FAIL();
return TEST_RC_FAIL;
}
-/* Client rejects a server signature with a different digest */
-static int test_oap_cli_rejects_md_mismatch(void)
+/* Naive substring search over raw bytes (memmem is not portable here). */
+static bool buf_contains(const uint8_t * hay,
+ size_t hlen,
+ const uint8_t * needle,
+ size_t nlen)
+{
+ size_t i;
+
+ if (nlen == 0 || nlen > hlen)
+ return false;
+
+ for (i = 0; i + nlen <= hlen; i++) {
+ if (memcmp(hay + i, needle, nlen) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+/* The server certificate must not appear in cleartext on the wire */
+static int test_oap_server_cert_hidden(void)
{
struct oap_test_ctx ctx;
+ void * crt = NULL;
+ buffer_t der = BUF_INIT;
test_default_cfg();
- test_cfg.srv.md = NID_sha384;
TEST_START();
@@ -1608,16 +1725,50 @@ static int test_oap_cli_rejects_md_mismatch(void)
goto fail_cleanup;
}
- if (oap_cli_complete_ctx(&ctx) == 0) {
- printf("Client should reject digest mismatch.\n");
+ if (crypt_load_crt_str(signed_server_crt_ec, &crt) < 0) {
+ printf("Failed to load server crt.\n");
goto fail_cleanup;
}
+ if (crypt_crt_der(crt, &der) < 0) {
+ printf("Failed to DER-encode server crt.\n");
+ goto fail_crt;
+ }
+
+ if (der.len == 0 || der.len > ctx.resp_hdr.len) {
+ printf("Unexpected cert/response sizes.\n");
+ goto fail_der;
+ }
+
+ if (buf_contains(ctx.resp_hdr.data, ctx.resp_hdr.len,
+ der.data, der.len)) {
+ printf("Server certificate found in cleartext.\n");
+ goto fail_der;
+ }
+
+ /* The handshake must still complete and agree on a key */
+ if (oap_cli_complete_ctx(&ctx) < 0) {
+ printf("Client complete failed.\n");
+ goto fail_der;
+ }
+
+ if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) {
+ printf("Client and server keys do not match!\n");
+ goto fail_der;
+ }
+
+ freebuf(der);
+ crypt_free_crt(crt);
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
+ fail_der:
+ freebuf(der);
+ fail_crt:
+ crypt_free_crt(crt);
fail_cleanup:
oap_test_teardown(&ctx);
fail:
@@ -1625,14 +1776,13 @@ static int test_oap_cli_rejects_md_mismatch(void)
return TEST_RC_FAIL;
}
-/* Server rejects a client signature with a different digest */
-static int test_oap_srv_rejects_md_mismatch(void)
+/* Tampering the sealed identity block fails the handshake */
+static int test_oap_sealed_tamper(void)
{
struct oap_test_ctx ctx;
+ size_t pos;
test_default_cfg();
- test_cfg.cli.auth = AUTH;
- test_cfg.cli.md = NID_sha384;
TEST_START();
@@ -1644,14 +1794,29 @@ static int test_oap_srv_rejects_md_mismatch(void)
goto fail_cleanup;
}
- if (oap_srv_process_ctx(&ctx) == 0) {
- printf("Server should reject digest mismatch.\n");
+ if (oap_srv_process_ctx(&ctx) < 0) {
+ printf("Server process failed.\n");
+ goto fail_cleanup;
+ }
+
+ if (ctx.resp_hdr.len < 64) {
+ printf("Response too short for test.\n");
+ goto fail_cleanup;
+ }
+
+ /* Flip a byte inside the sealed ciphertext, before the AEAD tag */
+ pos = ctx.resp_hdr.len - 32;
+ ctx.resp_hdr.data[pos] ^= 0xFF;
+
+ if (oap_cli_complete_ctx(&ctx) == 0) {
+ printf("Client accepted a tampered identity block.\n");
goto fail_cleanup;
}
oap_test_teardown(&ctx);
TEST_SUCCESS();
+
return TEST_RC_SUCCESS;
fail_cleanup:
@@ -1677,12 +1842,15 @@ int oap_test(int argc,
ret |= test_oap_roundtrip_auth_only();
ret |= test_oap_roundtrip_kex_only();
ret |= test_oap_piggyback_data();
+ ret |= test_oap_rekey();
+ ret |= test_oap_rekey_badcache();
ret |= test_oap_roundtrip_all();
ret |= test_oap_roundtrip_md_all();
ret |= test_oap_corrupted_request();
ret |= test_oap_corrupted_response();
+ ret |= test_oap_key_confirm_mismatch();
ret |= test_oap_truncated_request();
ret |= test_oap_inflated_length_field();
ret |= test_oap_deflated_length_field();
@@ -1693,6 +1861,7 @@ int oap_test(int argc,
ret |= test_oap_srv_enc_cli_none();
ret |= test_oap_cli_enc_srv_none();
ret |= test_oap_cli_rejects_downgrade();
+ ret |= test_oap_cli_rejects_suite_swap();
ret |= test_oap_srv_rejects_weak_kex();
ret |= test_oap_outdated_packet();
@@ -1705,14 +1874,12 @@ int oap_test(int argc,
ret |= test_oap_srv_requires_cli_auth();
ret |= test_oap_mutual_req_auth();
- ret |= test_oap_cli_pin_ca(im_ca_crt_ec, true);
- ret |= test_oap_cli_pin_ca(root_ca_crt_ec, true);
- ret |= test_oap_cli_pin_ca(other_ca_crt_ec, false);
- ret |= test_oap_srv_pin_ca(im_ca_crt_ec, true);
- ret |= test_oap_srv_pin_ca(other_ca_crt_ec, false);
ret |= test_oap_cli_rejects_md_mismatch();
ret |= test_oap_srv_rejects_md_mismatch();
+
+ ret |= test_oap_server_cert_hidden();
+ ret |= test_oap_sealed_tamper();
#else
(void) test_oap_roundtrip_auth_only;
(void) test_oap_roundtrip_kex_only;
@@ -1723,6 +1890,7 @@ int oap_test(int argc,
(void) test_oap_roundtrip_md_all;
(void) test_oap_corrupted_request;
(void) test_oap_corrupted_response;
+ (void) test_oap_key_confirm_mismatch;
(void) test_oap_truncated_request;
(void) test_oap_inflated_length_field;
(void) test_oap_deflated_length_field;
@@ -1732,19 +1900,23 @@ int oap_test(int argc,
(void) test_oap_srv_enc_cli_none;
(void) test_oap_cli_enc_srv_none;
(void) test_oap_cli_rejects_downgrade;
+ (void) test_oap_cli_rejects_suite_swap;
(void) test_oap_srv_rejects_weak_kex;
(void) test_oap_outdated_packet;
(void) test_oap_future_packet;
(void) test_oap_replay_packet;
+ (void) test_oap_replay_generations;
(void) test_oap_missing_root_ca;
(void) test_oap_server_name_mismatch;
(void) test_oap_cli_requires_srv_auth;
(void) test_oap_srv_requires_cli_auth;
(void) test_oap_mutual_req_auth;
- (void) test_oap_cli_pin_ca;
- (void) test_oap_srv_pin_ca;
(void) test_oap_cli_rejects_md_mismatch;
(void) test_oap_srv_rejects_md_mismatch;
+ (void) test_oap_server_cert_hidden;
+ (void) test_oap_sealed_tamper;
+ (void) test_oap_rekey;
+ (void) test_oap_rekey_badcache;
ret = TEST_RC_SKIP;
#endif
diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c
index 19be7400..8691aa00 100644
--- a/src/irmd/oap/tests/oap_test_ml_dsa.c
+++ b/src/irmd/oap/tests/oap_test_ml_dsa.c
@@ -179,6 +179,7 @@ int load_server_kem_pk(const char * name,
pk->data = malloc(test_kem_pk_len);
if (pk->data == NULL)
return -1;
+
memcpy(pk->data, test_kem_pk, test_kem_pk_len);
pk->len = test_kem_pk_len;
@@ -254,6 +255,22 @@ static int test_oap_srv_md_pin_exempts_pqc(void)
return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml);
}
+static int test_oap_rekey(void)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, NO_CLI_AUTH);
+
+ return roundtrip_rekey(root_ca_crt_ml, im_ca_crt_ml);
+}
+
+static int test_oap_rekey_badcache(void)
+{
+ test_cfg_init(NID_X25519, NID_aes_256_gcm, NID_sha256,
+ 0, NO_CLI_AUTH);
+
+ return roundtrip_rekey_badcache(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(),
@@ -449,10 +466,15 @@ int oap_test_ml_dsa(int argc,
ret |= test_oap_corrupted_request();
ret |= test_oap_corrupted_response();
ret |= test_oap_truncated_request();
+
+ ret |= test_oap_rekey();
+ ret |= test_oap_rekey_badcache();
#else
(void) test_oap_roundtrip_auth_only;
(void) test_oap_cli_md_pin_exempts_pqc;
(void) test_oap_srv_md_pin_exempts_pqc;
+ (void) test_oap_rekey;
+ (void) test_oap_rekey_badcache;
(void) test_oap_roundtrip_kem;
(void) test_oap_roundtrip_kem_all;
(void) test_oap_kem_srv_uncfg;