From 55a8136859d82d9bdb8f85abb25290177ca7e561 Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Sun, 21 Jun 2026 14:07:00 +0200 Subject: 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 Signed-off-by: Sander Vrijders --- src/irmd/oap.h | 25 +- src/irmd/oap/auth.c | 220 ++++++++++++++++- src/irmd/oap/auth.h | 34 ++- src/irmd/oap/cli.c | 123 ++++++++-- src/irmd/oap/hdr.c | 462 ++++++++++++++++++++++++++++------- src/irmd/oap/hdr.h | 48 +++- src/irmd/oap/srv.c | 106 ++++++-- src/irmd/oap/tests/common.c | 168 ++++++++++++- src/irmd/oap/tests/common.h | 11 + src/irmd/oap/tests/oap_test.c | 354 ++++++++++++++++++++------- src/irmd/oap/tests/oap_test_ml_dsa.c | 22 ++ 11 files changed, 1334 insertions(+), 239 deletions(-) (limited to 'src/irmd') diff --git a/src/irmd/oap.h b/src/irmd/oap.h index d6d8dfe2..86f11e21 100644 --- a/src/irmd/oap.h +++ b/src/irmd/oap.h @@ -28,6 +28,8 @@ #include #include +#include + /* OAP authentication state (in oap/auth.c) */ int oap_auth_init(void); @@ -35,31 +37,46 @@ void oap_auth_fini(void); int oap_auth_add_ca_crt(void * crt); +int oap_auth_add_chain_crt(void * crt); + /* * Prepare OAP request header for server, returns context * Passes client data for srv, returns srv data for client +* rekey forces ephemeral server-encap KEX (no client-encap; preserves FS/PCS) */ int oap_cli_prepare(void ** ctx, const struct name_info * info, buffer_t * req_buf, - buffer_t data); + buffer_t data, + bool rekey); /* * Server processes header, creates response header, returns secret key. * data is in/out: input=srv data to send, output=cli data received. + * rekey drops the cert and verifies against cached_crt; peer_crt (or NULL) + * receives a copy of the peer cert to cache at the initial handshake. */ int oap_srv_process(const struct name_info * info, buffer_t req_buf, buffer_t * rsp_buf, buffer_t * data, - struct crypt_sk * sk); + struct crypt_sk * sk, + bool rekey, + const buffer_t * cached_crt, + buffer_t * peer_crt); -/* Complete OAP, returns secret key and server data, frees ctx */ +/* + * Complete OAP, returns secret key and server data, frees ctx. + * cached_crt verifies a cert-less re-key; peer_crt (or NULL) receives a + * copy of the peer cert to cache at the initial handshake. + */ int oap_cli_complete(void * ctx, const struct name_info * info, buffer_t rsp_buf, buffer_t * data, - struct crypt_sk * sk); + struct crypt_sk * sk, + const buffer_t * cached_crt, + buffer_t * peer_crt); /* Free OAP state (on failure before complete) */ void oap_ctx_free(void * ctx); diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c index 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 +#include #include #include #include @@ -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 #include +#include #include #include #include @@ -45,9 +46,17 @@ #include #include +#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: "); else log_proto(" crt: "); 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: "); @@ -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: "); else log_proto(" Data: "); - 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: "); + log_proto(" Rsp Tag: "); if (hdr->sig.len > 0) log_proto(" Signature: [%zu bytes]", hdr->sig.len); + else if (hdr->sealed.len > 0) + log_proto(" Signature: "); else log_proto(" Signature: "); } @@ -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 #include #include -#include #ifdef HAVE_OPENSSL #include @@ -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; -- cgit v1.2.3