diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-08-29 12:08:29 +0200 |
|---|---|---|
| committer | Sander Vrijders <sander@ouroboros.rocks> | 2026-08-31 08:31:46 +0200 |
| commit | 0719ed46d29b0e57cb9128f5396ff894b7456766 (patch) | |
| tree | 1ecd3c8ea9a2367971434d76d7a9125cbdf2740d | |
| parent | fee337529da2d2b386b241812e176852bd5d4c4c (diff) | |
| download | ouroboros-0719ed46d29b0e57cb9128f5396ff894b7456766.tar.gz ouroboros-0719ed46d29b0e57cb9128f5396ff894b7456766.zip | |
lib: Improve hybrid KEX support
The algorithm was inferred from the key length. That will not always
work as SecP256r1MLKEM768 private key is 2432 bytes, exactly like an
X25519MLKEM768 one.
Raw OAP kex payloads now lead with the algorithm NID in network byte
order, so a peer reads the algorithm from the wire instead of guessing
it from the payload length.
Test if the KEX is hybrid KEM with kex_nid_is_hybrid() based on the
NID range.
The configured algorithm is passed to the raw key loaders. The public
key loader imports the key to validate it, so a corrupt or mismatched
file is reported at load time.
Signed-off-by: Dimitri Staessens <dimitri@ouroboros.rocks>
Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
| -rw-r--r-- | include/ouroboros/crypt.h | 35 | ||||
| -rw-r--r-- | sec.conf.in | 2 | ||||
| -rw-r--r-- | src/irmd/oap/cli.c | 35 | ||||
| -rw-r--r-- | src/irmd/oap/hdr.c | 52 | ||||
| -rw-r--r-- | src/irmd/oap/hdr.h | 68 | ||||
| -rw-r--r-- | src/irmd/oap/srv.c | 106 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test.c | 6 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test_ml_dsa.c | 52 | ||||
| -rw-r--r-- | src/lib/crypt.c | 38 | ||||
| -rw-r--r-- | src/lib/crypt/openssl.c | 113 | ||||
| -rw-r--r-- | src/lib/crypt/openssl.h | 265 | ||||
| -rw-r--r-- | src/lib/tests/kex_test.c | 19 | ||||
| -rw-r--r-- | src/lib/tests/kex_test_ml_kem.c | 8 |
13 files changed, 443 insertions, 356 deletions
diff --git a/include/ouroboros/crypt.h b/include/ouroboros/crypt.h index 9feaa610..dd0c2853 100644 --- a/include/ouroboros/crypt.h +++ b/include/ouroboros/crypt.h @@ -65,8 +65,12 @@ #define NID_MLKEM512 1454 #define NID_MLKEM768 1455 #define NID_MLKEM1024 1456 -#define NID_X25519MLKEM768 2053 /* !! not in OpenSSL */ -#define NID_X448MLKEM1024 2054 /* !! not in OpenSSL */ + +/* Hybrid KEM NIDs: project-assigned range, OpenSSL defines none */ +#define NID_X25519MLKEM768 2053 +#define NID_X448MLKEM1024 2054 +#define NID_HYBRID_KEM_MIN NID_X25519MLKEM768 +#define NID_HYBRID_KEM_MAX NID_X448MLKEM1024 /* KDF NIDs (match OpenSSL values) */ #define NID_hkdf 1036 @@ -85,15 +89,8 @@ #define IS_KEM_ALGORITHM(algo) \ (strstr(algo, "ML-KEM") != NULL || strstr(algo, "MLKEM") != NULL) -#define IS_HYBRID_KEM(algo) \ - ((strstr(algo, "X25519") != NULL || strstr(algo, "X448") != NULL) && \ - strstr(algo, "MLKEM") != NULL) - -#define X25519MLKEM768_PKSZ 1216 /* 32 + 1184 */ -#define X25519MLKEM768_CTSZ 1120 /* 32 + 1088 */ -#define X25519MLKEM768_SKSZ 2432 /* 32 + 2400 */ -#define X448MLKEM1024_PKSZ 1624 /* 56 + 1568 */ -#define X448MLKEM1024_SKSZ 3224 /* 56 + 3168 */ +#define IS_HYBRID_KEM_NID(nid) kex_nid_is_hybrid(nid) +#define IS_HYBRID_KEM(algo) kex_nid_is_hybrid(kex_str_to_nid(algo)) #define CRYPT_KEY_BUFSZ 4096 /* Safe buffer for key material */ @@ -268,10 +265,11 @@ ssize_t kex_kem_encap(buffer_t pk, int kdf_nid, uint8_t * s); -ssize_t kex_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf_nid, - uint8_t * s); +ssize_t kex_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf_nid, + uint8_t * s); int kex_kem_decap(void * pkp, buffer_t ct, @@ -281,13 +279,12 @@ int kex_kem_decap(void * pkp, int kex_get_algo_from_pk_der(buffer_t pk, char * algo); -int kex_get_algo_from_pk_raw(buffer_t pk, - char * algo); - int kex_validate_algo(const char * algo); int kex_validate_nid(int nid); +bool kex_nid_is_hybrid(uint16_t nid); + const char * kex_nid_to_str(uint16_t nid); uint16_t kex_str_to_nid(const char * algo); @@ -393,9 +390,11 @@ int crypt_load_pubkey_file_to_der(const char * path, buffer_t * buf); int crypt_load_pubkey_raw_file(const char * path, + const char * algo, buffer_t * buf); int crypt_load_privkey_raw_file(const char * path, + const char * algo, void ** key); int crypt_ct_cmp(const void * a, diff --git a/sec.conf.in b/sec.conf.in index 4796b72d..4115c850 100644 --- a/sec.conf.in +++ b/sec.conf.in @@ -137,7 +137,7 @@ # # File formats: # - Pure ML-KEM: PEM format (.pem extension) -# - Hybrid KEMs: Raw bytes (.raw extension) +# - Hybrid KEMs: Raw key bytes (.raw extension), for the kex= algorithm # # Examples: # --------- diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c index 2203596f..02ad2005 100644 --- a/src/irmd/oap/cli.c +++ b/src/irmd/oap/cli.c @@ -51,7 +51,7 @@ struct oap_cli_ctx { uint8_t __id[OAP_ID_SIZE]; buffer_t id; char peer[NAME_SIZE + 1]; /* expected server name */ - uint8_t kex_buf[CRYPT_KEY_BUFSZ]; + uint8_t kex_buf[OAP_KEX_NIDSZ + CRYPT_KEY_BUFSZ]; uint8_t req_hash[MAX_HASH_SIZE]; size_t req_hash_len; int req_md_nid; @@ -113,13 +113,13 @@ int load_server_kem_pk(const char * name, assert(cfg != NULL); assert(pk != NULL); - ext = IS_HYBRID_KEM(cfg->x.str) ? "raw" : "pem"; + ext = IS_HYBRID_KEM_NID(cfg->x.nid) ? "raw" : "pem"; snprintf(path, sizeof(path), OUROBOROS_CLI_CRT_DIR "/%s/kex.srv.pub.%s", name, ext); - if (IS_HYBRID_KEM(cfg->x.str)) { - if (crypt_load_pubkey_raw_file(path, pk) < 0) { + if (IS_HYBRID_KEM_NID(cfg->x.nid)) { + if (crypt_load_pubkey_raw_file(path, cfg->x.str, pk) < 0) { log_err("Failed to load %s pubkey from %s.", ext, path); return -1; } @@ -173,12 +173,14 @@ static int do_client_kex_prepare_kem_encap(const char * server_name, return -ECRYPT; } - if (IS_HYBRID_KEM(scfg->x.str)) - len = kex_kem_encap_raw(server_pk, kex->data, + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + len = kex_kem_encap_raw(scfg->x.str, server_pk, + kex->data + OAP_KEX_NIDSZ, scfg->k.nid, key_buf); - else - len = kex_kem_encap(server_pk, kex->data, - scfg->k.nid, key_buf); + len = oap_kex_tag_nid(kex->data, scfg->x.nid, len); + } else { + len = kex_kem_encap(server_pk, kex->data, scfg->k.nid, key_buf); + } freebuf(server_pk); @@ -210,7 +212,13 @@ static int do_client_kex_prepare_kem_decap(struct oap_cli_ctx * s) ssize_t len; /* Server encaps: generate keypair, send PK */ - len = kex_pkp_create(scfg, &s->pkp, kex->data); + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + len = kex_pkp_create(scfg, &s->pkp, kex->data + OAP_KEX_NIDSZ); + len = oap_kex_tag_nid(kex->data, scfg->x.nid, len); + } else { + len = kex_pkp_create(scfg, &s->pkp, kex->data); + } + if (len < 0) { log_err_id(id, "Failed to generate KEM keypair."); return -ECRYPT; @@ -421,6 +429,13 @@ static int do_client_kex_complete_kem(struct oap_cli_ctx * s, ct.data = peer_hdr->kex.data; ct.len = peer_hdr->kex.len; + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&ct, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in CT."); + return -ECRYPT; + } + } + if (kex_kem_decap(s->pkp, ct, scfg->k.nid, key_buf) < 0) { log_err_id(id, "Failed to decapsulate KEM."); return -ECRYPT; diff --git a/src/irmd/oap/hdr.c b/src/irmd/oap/hdr.c index 0cff345c..d037d7b8 100644 --- a/src/irmd/oap/hdr.c +++ b/src/irmd/oap/hdr.c @@ -211,6 +211,56 @@ void oap_hdr_fini(struct oap_hdr * oap_hdr) memset(oap_hdr, 0, sizeof(*oap_hdr)); } +uint16_t oap_kex_nid(buffer_t kex) +{ + uint16_t nid; + + if (kex.len <= OAP_KEX_NIDSZ) + return NID_undef; + + memcpy(&nid, kex.data, sizeof(nid)); + + return ntoh16(nid); +} + +void oap_kex_set_nid(uint8_t * buf, + uint16_t nid) +{ + uint16_t v; + + assert(buf != NULL); + + v = hton16(nid); + + memcpy(buf, &v, sizeof(v)); +} + +int oap_kex_strip_nid(buffer_t * kex, + uint16_t nid) +{ + assert(kex != NULL); + + if (oap_kex_nid(*kex) != nid) + return -1; + + kex->data += OAP_KEX_NIDSZ; + kex->len -= OAP_KEX_NIDSZ; + + return 0; +} + +ssize_t oap_kex_tag_nid(uint8_t * buf, + uint16_t nid, + ssize_t len) +{ + if (len < 0) + return len; + + oap_kex_set_nid(buf, nid); + + return len + OAP_KEX_NIDSZ; +} + int oap_hdr_copy_data(const struct oap_hdr * hdr, buffer_t * out) { @@ -286,7 +336,7 @@ static void write_oap_fixed(uint8_t * buf, kex_len = (uint16_t) hdr->kex.len; if (hdr->kex.len > 0 && IS_KEM_ALGORITHM(scfg->x.str)) { - if (IS_HYBRID_KEM(scfg->x.str)) + if (IS_HYBRID_KEM_NID(scfg->x.nid)) kex_len |= OAP_KEX_FMT_BIT; if (scfg->x.mode == KEM_MODE_CLIENT_ENCAP) kex_len |= OAP_KEX_ROLE_BIT; diff --git a/src/irmd/oap/hdr.h b/src/irmd/oap/hdr.h index 66fbac9a..4c5f885a 100644 --- a/src/irmd/oap/hdr.h +++ b/src/irmd/oap/hdr.h @@ -32,10 +32,13 @@ #define OAP_ID_SIZE (16) #define OAP_HDR_MIN_SIZE (OAP_ID_SIZE + sizeof(uint64_t) + 6 * sizeof(uint16_t)) -#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw */ +#define OAP_KEX_FMT_BIT 0x8000 /* bit 15: 0=X.509 DER, 1=Raw: NID + bytes */ #define OAP_KEX_ROLE_BIT 0x4000 /* bit 14: 0=Server encaps, 1=Client encaps */ #define OAP_KEX_LEN_MASK 0x3FFF /* bits 0-13: Length (0-16383 bytes) */ +/* Raw format kex payloads lead with the algorithm NID */ +#define OAP_KEX_NIDSZ sizeof(uint16_t) + #define OAP_KEX_ROLE(hdr) (hdr->kex_flags.role) #define OAP_KEX_FMT(hdr) (hdr->kex_flags.fmt) @@ -160,40 +163,55 @@ struct oap_hdr { }; -void oap_hdr_init(struct oap_hdr * hdr, - buffer_t id, - uint8_t * kex_buf, - buffer_t data, - uint16_t nid); +void oap_hdr_init(struct oap_hdr * hdr, + buffer_t id, + uint8_t * kex_buf, + buffer_t data, + uint16_t nid); + +void oap_hdr_fini(struct oap_hdr * oap_hdr); + +/* NID leading a raw format kex payload, NID_undef if too short */ +uint16_t oap_kex_nid(buffer_t kex); + +void oap_kex_set_nid(uint8_t * buf, + uint16_t nid); + +/* Verify the NID leading a raw kex payload, then strip it */ +int oap_kex_strip_nid(buffer_t * kex, + uint16_t nid); -void oap_hdr_fini(struct oap_hdr * oap_hdr); +/* Tag a raw payload written at buf + OAP_KEX_NIDSZ, len passthrough */ +ssize_t oap_kex_tag_nid(uint8_t * buf, + uint16_t nid, + ssize_t len); /* oap_hdr_encode option flags */ #define OAP_ENC_REKEY (1U << 0) /* signed, cert-less re-key packet */ -int oap_hdr_encode(struct oap_hdr * hdr, - void * pkp, - void * crt, - struct sec_config * scfg, - buffer_t rsp_tag, - int req_md_nid, - const uint8_t * seal_key, - int flags); +int oap_hdr_encode(struct oap_hdr * hdr, + void * pkp, + void * crt, + struct sec_config * scfg, + buffer_t rsp_tag, + int req_md_nid, + const uint8_t * seal_key, + int flags); -int oap_hdr_decode(struct oap_hdr * hdr, - buffer_t buf, - int req_md_nid, - bool rekey); +int oap_hdr_decode(struct oap_hdr * hdr, + buffer_t buf, + 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); +int oap_hdr_unseal(struct oap_hdr * hdr, + const uint8_t * key); -void debug_oap_hdr_rcv(const struct oap_hdr * hdr); +void debug_oap_hdr_rcv(const struct oap_hdr * hdr); -void debug_oap_hdr_snd(const struct oap_hdr * hdr); +void debug_oap_hdr_snd(const struct oap_hdr * hdr); -int oap_hdr_copy_data(const struct oap_hdr * hdr, - buffer_t * out); +int oap_hdr_copy_data(const struct oap_hdr * hdr, + buffer_t * out); #endif /* OUROBOROS_IRMD_OAP_HDR_H */ diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c index f78e0874..0ceba7b6 100644 --- a/src/irmd/oap/srv.c +++ b/src/irmd/oap/srv.c @@ -51,9 +51,9 @@ extern int load_srv_credentials(const struct name_info * info, void ** crt); extern int load_srv_sec_config(const struct name_info * info, struct sec_config * cfg); -extern int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp); +extern int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp); #else int load_srv_credentials(const struct name_info * info, @@ -79,23 +79,26 @@ int load_srv_sec_config(const struct name_info * info, return load_sec_config(info->name, info->s.sec, cfg); } -int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp) +int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp) { char path[PATH_MAX]; const char * ext; + bool raw_fmt; assert(name != NULL); + assert(cfg != NULL); assert(pkp != NULL); + raw_fmt = IS_HYBRID_KEM_NID(cfg->x.nid); ext = raw_fmt ? "raw" : "pem"; snprintf(path, sizeof(path), OUROBOROS_SRV_CRT_DIR "/%s/kex.key.%s", name, ext); if (raw_fmt) { - if (crypt_load_privkey_raw_file(path, pkp) < 0) { + if (crypt_load_privkey_raw_file(path, cfg->x.str, pkp) < 0) { log_err("Failed to load %s keypair from %s.", ext, path); return -ECRYPT; @@ -109,6 +112,7 @@ int load_server_kem_keypair(const char * name, } log_dbg("Loaded server KEM keypair from %s.", path); + return 0; } @@ -117,18 +121,19 @@ int load_server_kem_keypair(const char * name, static int get_algo_from_peer_key(const struct oap_hdr * peer_hdr, char * algo_buf) { - uint8_t * id = peer_hdr->id.data; - int ret; + uint8_t * id = peer_hdr->id.data; + const char * name; if (OAP_KEX_IS_RAW_FMT(peer_hdr)) { - ret = kex_get_algo_from_pk_raw(peer_hdr->kex, algo_buf); - if (ret < 0) { - log_err_id(id, "Failed to get algo from raw key."); + name = kex_nid_to_str(oap_kex_nid(peer_hdr->kex)); + if (name == NULL || !IS_HYBRID_KEM(name)) { + log_err_id(id, "Bad algo NID in raw key."); return -ECRYPT; } + + strcpy(algo_buf, name); } else { - ret = kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf); - if (ret < 0) { + if (kex_get_algo_from_pk_der(peer_hdr->kex, algo_buf) < 0) { log_err_id(id, "Failed to get algo from DER key."); return -ECRYPT; } @@ -216,15 +221,20 @@ static int do_server_kem_decap(const struct name_info * info, int ret; uint8_t * id = peer_hdr->id.data; - ret = load_server_kem_keypair(info->name, - peer_hdr->kex_flags.fmt, - &server_pkp); - if (ret < 0) - return ret; - ct.data = peer_hdr->kex.data; ct.len = peer_hdr->kex.len; + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&ct, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in CT."); + return -ECRYPT; + } + } + + ret = load_server_kem_keypair(info->name, scfg, &server_pkp); + if (ret < 0) + return ret; + ret = kex_kem_decap(server_pkp, ct, scfg->k.nid, sk->key); crypt_free_key(server_pkp); @@ -251,12 +261,20 @@ static int do_server_kem_encap(const struct oap_hdr * peer_hdr, client_pk.data = peer_hdr->kex.data; client_pk.len = peer_hdr->kex.len; - if (IS_HYBRID_KEM(scfg->x.str)) - ct_len = kex_kem_encap_raw(client_pk, kex->data, + if (IS_HYBRID_KEM_NID(scfg->x.nid)) { + if (oap_kex_strip_nid(&client_pk, scfg->x.nid) < 0) { + log_err_id(id, "KEX algo mismatch in PK."); + return -ECRYPT; + } + + ct_len = kex_kem_encap_raw(scfg->x.str, client_pk, + kex->data + OAP_KEX_NIDSZ, scfg->k.nid, sk->key); - else + ct_len = oap_kex_tag_nid(kex->data, scfg->x.nid, ct_len); + } else { ct_len = kex_kem_encap(client_pk, kex->data, scfg->k.nid, sk->key); + } if (ct_len < 0) { log_err_id(id, "Failed to encapsulate KEM."); @@ -385,27 +403,27 @@ int oap_srv_process(const struct name_info * info, const buffer_t * cached_crt, buffer_t * peer_crt) { - struct oap_hdr peer_hdr; - struct oap_hdr local_hdr; - struct sec_config scfg; - uint8_t kex_buf[CRYPT_KEY_BUFSZ]; - uint8_t hash_buf[MAX_HASH_SIZE]; - 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; - void * pkp = NULL; - void * crt = NULL; - int req_md_nid; - int enc_flags = 0; - int ret; + struct oap_hdr peer_hdr; + struct oap_hdr local_hdr; + struct sec_config scfg; + uint8_t kex_buf[OAP_KEX_NIDSZ + CRYPT_KEY_BUFSZ]; + uint8_t hash_buf[MAX_HASH_SIZE]; + 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; + void * pkp = NULL; + void * crt = NULL; + int req_md_nid; + int enc_flags = 0; + int ret; assert(info != NULL); assert(rsp_buf != NULL); diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c index b24bb786..3e2bae56 100644 --- a/src/irmd/oap/tests/oap_test.c +++ b/src/irmd/oap/tests/oap_test.c @@ -81,9 +81,9 @@ int mock_load_credentials(void ** pkp, } /* Stub KEM functions - ECDSA tests don't use KEM */ -int load_server_kem_keypair(__attribute__((unused)) const char * name, - __attribute__((unused)) bool raw_fmt, - __attribute__((unused)) void ** pkp) +int load_server_kem_keypair(__attribute__((unused)) const char * name, + __attribute__((unused)) struct sec_config * cfg, + __attribute__((unused)) void ** pkp) { return -1; } diff --git a/src/irmd/oap/tests/oap_test_ml_dsa.c b/src/irmd/oap/tests/oap_test_ml_dsa.c index 477b712b..b9132b13 100644 --- a/src/irmd/oap/tests/oap_test_ml_dsa.c +++ b/src/irmd/oap/tests/oap_test_ml_dsa.c @@ -37,6 +37,7 @@ #include <test/certs/ml_dsa.h> +#include "oap/hdr.h" #include "oap.h" #include "common.h" @@ -97,16 +98,16 @@ int mock_load_credentials(void ** pkp, return 0; } -int load_server_kem_keypair(const char * name, - bool raw_fmt, - void ** pkp) +int load_server_kem_keypair(const char * name, + struct sec_config * cfg, + void ** pkp) { #ifdef HAVE_OPENSSL struct sec_config local_cfg; ssize_t pk_len; (void) name; - (void) raw_fmt; + (void) cfg; /* * Uses reference counting. The caller will call @@ -150,7 +151,7 @@ int load_server_kem_keypair(const char * name, #else (void) name; - (void) raw_fmt; + (void) cfg; (void) pkp; return -1; #endif @@ -720,6 +721,44 @@ static int test_oap_kem_srv_uncfg_all(void) return ret; } +/* Server must reject a raw kex payload tagged with a bad NID */ +static int test_oap_kem_bad_nid_tag(uint16_t bad_nid) +{ + struct oap_test_ctx ctx; + + test_cfg_init(NID_X25519MLKEM768, NID_aes_256_gcm, + get_random_kdf(), SRV_ENCAP, NO_AUTH); + + TEST_START("(%u)", bad_nid); + + if (oap_test_setup_kem(&ctx, root_ca_crt_ml, im_ca_crt_ml) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* NO_AUTH request: raw kex starts after the fixed header */ + oap_kex_set_nid(ctx.req_hdr.data + OAP_HDR_MIN_SIZE, bad_nid); + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server accepted bad NID tag %u.\n", bad_nid); + goto fail_cleanup; + } + + oap_test_teardown_kem(&ctx); + + TEST_SUCCESS("(%u)", bad_nid); + + return TEST_RC_SUCCESS; + fail_cleanup: + oap_test_teardown_kem(&ctx); + fail: + TEST_FAIL("(%u)", bad_nid); + return TEST_RC_FAIL; +} + /* Client encap must be rejected when the server has no KEX config */ static int test_oap_kem_cli_encap_srv_uncfg(void) { @@ -782,6 +821,8 @@ int oap_test_ml_dsa(int argc, ret |= test_oap_kem_srv_uncfg_all(); ret |= test_oap_kem_cli_encap_srv_uncfg(); + ret |= test_oap_kem_bad_nid_tag(NID_X25519); /* known, not hybrid */ + ret |= test_oap_kem_bad_nid_tag(0xFFFF); /* unknown */ ret |= test_oap_corrupted_request(); ret |= test_oap_corrupted_response(); @@ -812,6 +853,7 @@ int oap_test_ml_dsa(int argc, (void) test_oap_kem_srv_uncfg; (void) test_oap_kem_srv_uncfg_all; (void) test_oap_kem_cli_encap_srv_uncfg; + (void) test_oap_kem_bad_nid_tag; (void) test_oap_corrupted_request; (void) test_oap_corrupted_response; (void) test_oap_truncated_request; diff --git a/src/lib/crypt.c b/src/lib/crypt.c index bc3dcd16..35007ef3 100644 --- a/src/lib/crypt.c +++ b/src/lib/crypt.c @@ -378,14 +378,16 @@ ssize_t kex_kem_encap(buffer_t pk, #endif } -ssize_t kex_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf, - uint8_t * s) +ssize_t kex_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf, + uint8_t * s) { #ifdef HAVE_OPENSSL - return openssl_kem_encap_raw(pk, ct, kdf, s); + return openssl_kem_encap_raw(algo, pk, ct, kdf, s); #else + (void) algo; (void) pk; (void) ct; (void) kdf; @@ -427,19 +429,6 @@ int kex_get_algo_from_pk_der(buffer_t pk, #endif } -int kex_get_algo_from_pk_raw(buffer_t pk, - char * algo) -{ -#ifdef HAVE_OPENSSL - return openssl_get_algo_from_pk_raw(pk, algo); -#else - (void) pk; - algo[0] = '\0'; - - return -ECRYPT; -#endif -} - int kex_validate_algo(const char * algo) { if (algo == NULL) @@ -547,6 +536,11 @@ int kex_validate_nid(int nid) return -ENOTSUP; } +bool kex_nid_is_hybrid(uint16_t nid) +{ + return nid >= NID_HYBRID_KEM_MIN && nid <= NID_HYBRID_KEM_MAX; +} + const char * md_nid_to_str(uint16_t nid) { const struct nid_map * p; @@ -1032,14 +1026,16 @@ int crypt_load_pubkey_file_to_der(const char * path, } int crypt_load_pubkey_raw_file(const char * path, + const char * algo, buffer_t * buf) { assert(buf != NULL); #ifdef HAVE_OPENSSL - return openssl_load_pubkey_raw_file(path, buf); + return openssl_load_pubkey_raw_file(path, algo, buf); #else (void) path; + (void) algo; buf->data = NULL; buf->len = 0; @@ -1048,14 +1044,16 @@ int crypt_load_pubkey_raw_file(const char * path, } int crypt_load_privkey_raw_file(const char * path, + const char * algo, void ** key) { *key = NULL; #ifdef HAVE_OPENSSL - return openssl_load_privkey_raw_file(path, key); + return openssl_load_privkey_raw_file(path, algo, key); #else (void) path; + (void) algo; return 0; #endif diff --git a/src/lib/crypt/openssl.c b/src/lib/crypt/openssl.c index 7716f27f..8ec3b40f 100644 --- a/src/lib/crypt/openssl.c +++ b/src/lib/crypt/openssl.c @@ -721,23 +721,6 @@ static int __openssl_kem_gen_key(const char * algo, return -ECRYPT; } -/* Determine hybrid KEM algorithm from raw key/ciphertext length */ -static const char * __openssl_hybrid_algo_from_len(size_t len) -{ - switch(len) { - case X25519MLKEM768_PKSZ: - return "X25519MLKEM768"; - case X25519MLKEM768_CTSZ: - return "X25519MLKEM768"; - case X448MLKEM1024_PKSZ: - return "X448MLKEM1024"; - default: - break; - } - - return NULL; -} - static int __openssl_kex_gen_key(const char * algo, EVP_PKEY ** kp) { @@ -900,18 +883,19 @@ ssize_t openssl_kem_encap(buffer_t pk, return -ECRYPT; } -/* Hybrid KEM encapsulation: raw-encoded public key */ -ssize_t openssl_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf, - uint8_t * s) +/* Hybrid KEM encapsulation: NID-tagged raw-encoded public key */ +ssize_t openssl_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf, + uint8_t * s) { - EVP_PKEY * pub; - const char * algo; - uint8_t salt[HKDF_SALT_LEN]; - buffer_t salt_b; - ssize_t ret; + EVP_PKEY * pub; + uint8_t salt[HKDF_SALT_LEN]; + buffer_t salt_b; + ssize_t ret; + assert(algo != NULL); assert(pk.data != NULL); assert(ct != NULL); assert(s != NULL); @@ -922,10 +906,6 @@ ssize_t openssl_kem_encap_raw(buffer_t pk, if (derive_salt_from_pk_bytes(pk, salt_b) < 0) goto fail_salt; - algo = __openssl_hybrid_algo_from_len(pk.len); - if (algo == NULL) - goto fail_salt; - pub = EVP_PKEY_new_raw_public_key_ex(NULL, algo, NULL, pk.data, pk.len); if (pub == NULL) @@ -1071,23 +1051,6 @@ int openssl_get_algo_from_pk_der(buffer_t pk, return -ECRYPT; } -int openssl_get_algo_from_pk_raw(buffer_t pk, - char * algo) -{ - const char * hybrid_algo; - - assert(pk.data != NULL); - assert(algo != NULL); - - hybrid_algo = __openssl_hybrid_algo_from_len(pk.len); - if (hybrid_algo == NULL) - return -ECRYPT; - - strcpy(algo, hybrid_algo); - - return 0; -} - int openssl_dhe_derive(EVP_PKEY * pkp, buffer_t pk, int kdf, @@ -1519,14 +1482,16 @@ int openssl_load_pubkey_str(const char * str, } int openssl_load_pubkey_raw_file(const char * path, + const char * algo, buffer_t * buf) { - FILE * fp; - uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; - size_t bytes_read; - const char * algo; + FILE * fp; + uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; + size_t bytes_read; + EVP_PKEY * chk; assert(path != NULL); + assert(algo != NULL); assert(buf != NULL); fp = fopen(path, "rb"); @@ -1539,14 +1504,19 @@ int openssl_load_pubkey_raw_file(const char * path, pthread_cleanup_pop(false); - if (bytes_read == 0) + /* A full buffer means the file was truncated */ + if (bytes_read == 0 || bytes_read == CRYPT_KEY_BUFSZ) goto fail_read; - /* Validate that this is a known hybrid KEM format */ - algo = __openssl_hybrid_algo_from_len(bytes_read); - if (algo == NULL) + /* Trial import: reject bad keys at load time */ + chk = EVP_PKEY_new_raw_public_key_ex(NULL, algo, NULL, + tmp_buf, bytes_read); + + if (chk == NULL) goto fail_read; + EVP_PKEY_free(chk); + buf->data = malloc(bytes_read); if (buf->data == NULL) goto fail_malloc; @@ -1565,21 +1535,6 @@ int openssl_load_pubkey_raw_file(const char * path, return -1; } -/* Determine hybrid KEM algorithm from raw private key length */ -static const char * __openssl_hybrid_algo_from_sk_len(size_t len) -{ - switch(len) { - case X25519MLKEM768_SKSZ: - return "X25519MLKEM768"; - case X448MLKEM1024_SKSZ: - return "X448MLKEM1024"; - default: - break; - } - - return NULL; -} - /* Wipe the raw-key staging buffer if a cancel aborts the read. */ static void __cleanse_key_buf(void * o) { @@ -1587,15 +1542,16 @@ static void __cleanse_key_buf(void * o) } int openssl_load_privkey_raw_file(const char * path, + const char * algo, void ** key) { - FILE * fp; - uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; - size_t bytes_read; - const char * algo; - EVP_PKEY * pkey; + FILE * fp; + uint8_t tmp_buf[CRYPT_KEY_BUFSZ]; + size_t bytes_read; + EVP_PKEY * pkey; assert(path != NULL); + assert(algo != NULL); assert(key != NULL); fp = fopen(path, "rb"); @@ -1613,11 +1569,6 @@ int openssl_load_privkey_raw_file(const char * path, if (bytes_read == 0) goto fail_read; - /* Determine algorithm from key size */ - algo = __openssl_hybrid_algo_from_sk_len(bytes_read); - if (algo == NULL) - goto fail_read; - pkey = EVP_PKEY_new_raw_private_key_ex(NULL, algo, NULL, tmp_buf, bytes_read); /* Clear sensitive data from stack */ diff --git a/src/lib/crypt/openssl.h b/src/lib/crypt/openssl.h index e5cc35f7..6857e39b 100644 --- a/src/lib/crypt/openssl.h +++ b/src/lib/crypt/openssl.h @@ -28,72 +28,70 @@ struct ossl_crypt_ctx; -ssize_t openssl_pkp_create(const char * algo, - EVP_PKEY ** pkp, - uint8_t * pk); +ssize_t openssl_pkp_create(const char * algo, + EVP_PKEY ** pkp, + uint8_t * pk); -void openssl_pkp_destroy(EVP_PKEY * pkp); +void openssl_pkp_destroy(EVP_PKEY * pkp); -int openssl_dhe_derive(EVP_PKEY * pkp, - buffer_t pk, - int kdf_nid, - uint8_t * s); +int openssl_dhe_derive(EVP_PKEY * pkp, + buffer_t pk, + int kdf_nid, + uint8_t * s); -ssize_t openssl_kem_encap(buffer_t pk, - uint8_t * ct, - int kdf_nid, - uint8_t * s); +ssize_t openssl_kem_encap(buffer_t pk, + uint8_t * ct, + int kdf_nid, + uint8_t * s); /* no X509 DER support yet for DHKEM public keys */ -ssize_t openssl_kem_encap_raw(buffer_t pk, - uint8_t * ct, - int kdf_nid, - uint8_t * s); - -int openssl_kem_decap(EVP_PKEY * priv, - buffer_t ct, - int kdf_nid, - uint8_t * s); - -int openssl_get_algo_from_pk_der(buffer_t pk, - char * algo); - -int openssl_get_algo_from_pk_raw(buffer_t pk, - char * algo); - -int openssl_seal(struct ossl_crypt_ctx * ctx, - const uint8_t * key, - const uint8_t * nonce, - buffer_t aad, - buffer_t in, - uint8_t * out, - uint8_t * tag); - -int openssl_open(struct ossl_crypt_ctx * ctx, - const uint8_t * key, - const uint8_t * nonce, - buffer_t aad, - buffer_t in, - const uint8_t * tag, - buffer_t * out); - -int openssl_oneshot_seal(int nid, - const uint8_t * key, - const uint8_t * nonce, - buffer_t aad, - buffer_t in, - buffer_t * out); - -int openssl_oneshot_open(int nid, - const uint8_t * key, - const uint8_t * nonce, - buffer_t aad, - buffer_t in, - buffer_t * out); - -int openssl_hkdf_expand(buffer_t key, - buffer_t info, - buffer_t out); +ssize_t openssl_kem_encap_raw(const char * algo, + buffer_t pk, + uint8_t * ct, + int kdf_nid, + uint8_t * s); + +int openssl_kem_decap(EVP_PKEY * priv, + buffer_t ct, + int kdf_nid, + uint8_t * s); + +int openssl_get_algo_from_pk_der(buffer_t pk, + char * algo); + +int openssl_seal(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + uint8_t * out, + uint8_t * tag); + +int openssl_open(struct ossl_crypt_ctx * ctx, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + const uint8_t * tag, + buffer_t * out); + +int openssl_oneshot_seal(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out); + +int openssl_oneshot_open(int nid, + const uint8_t * key, + const uint8_t * nonce, + buffer_t aad, + buffer_t in, + buffer_t * out); + +int openssl_hkdf_expand(buffer_t key, + buffer_t info, + buffer_t out); struct ossl_crypt_ctx * openssl_crypt_create_ctx(struct crypt_sk * sk); @@ -102,112 +100,113 @@ void openssl_crypt_destroy_ctx(struct ossl_crypt_ctx * ctx); int openssl_crypt_get_tagsz(struct ossl_crypt_ctx * ctx); /* AUTHENTICATION */ +int openssl_load_crt_file(const char * path, + void ** crt); -int openssl_load_crt_file(const char * path, - void ** crt); +int openssl_load_crt_str(const char * str, + void ** crt); -int openssl_load_crt_str(const char * str, - void ** crt); +int openssl_load_crt_der(buffer_t buf, + void ** crt); -int openssl_load_crt_der(buffer_t buf, - void ** crt); +int openssl_get_pubkey_crt(void * crt, + void ** pk); -int openssl_get_pubkey_crt(void * crt, - void ** pk); +void openssl_free_crt(void * crt); -void openssl_free_crt(void * crt); +int openssl_load_privkey_file(const char * path, + void ** key); -int openssl_load_privkey_file(const char * path, - void ** key); +int openssl_load_privkey_str(const char * str, + void ** key); -int openssl_load_privkey_str(const char * str, - void ** key); +int openssl_load_pubkey_file(const char * path, + void ** key); -int openssl_load_pubkey_file(const char * path, - void ** key); +int openssl_load_pubkey_str(const char * str, + void ** key); +int openssl_load_pubkey_file_to_der(const char * path, + buffer_t * buf); +int openssl_load_pubkey_raw_file(const char * path, + const char * algo, + buffer_t * buf); -int openssl_load_pubkey_str(const char * str, - void ** key); -int openssl_load_pubkey_file_to_der(const char * path, - buffer_t * buf); -int openssl_load_pubkey_raw_file(const char * path, - buffer_t * buf); - -int openssl_load_privkey_raw_file(const char * path, - void ** key); +int openssl_load_privkey_raw_file(const char * path, + const char * algo, + void ** key); -int openssl_cmp_key(const EVP_PKEY * key1, - const EVP_PKEY * key2); +int openssl_cmp_key(const EVP_PKEY * key1, + const EVP_PKEY * key2); -void openssl_free_key(EVP_PKEY * key); +void openssl_free_key(EVP_PKEY * key); -int openssl_check_crt_name(void * crt, - const char * name); +int openssl_check_crt_name(void * crt, + const char * name); -int openssl_get_crt_name(void * crt, - char * name); +int openssl_get_crt_name(void * crt, + char * name); -int openssl_crt_str(const void * crt, - char * str); +int openssl_crt_str(const void * crt, + char * str); -int openssl_crt_der(const void * crt, - buffer_t * buf); +int openssl_crt_der(const void * crt, + buffer_t * buf); -void * openssl_auth_create_store(void); +void * openssl_auth_create_store(void); -void openssl_auth_destroy_store(void * store); +void openssl_auth_destroy_store(void * store); -int openssl_auth_add_crt_to_store(void * store, - void * crt); +int openssl_auth_add_crt_to_store(void * store, + void * crt); -void * openssl_auth_create_chain(void); +void * openssl_auth_create_chain(void); -void openssl_auth_destroy_chain(void * chain); +void openssl_auth_destroy_chain(void * chain); -int openssl_auth_add_crt_to_chain(void * chain, - void * crt); +int openssl_auth_add_crt_to_chain(void * chain, + void * crt); -int openssl_verify_crt(void * store, - void * untrusted, - void * crt); +int openssl_verify_crt(void * store, + void * untrusted, + void * crt); -int openssl_verify_crt_pin(void * store, - void * untrusted, - void * crt, - void * pin); +int openssl_verify_crt_pin(void * store, + void * untrusted, + void * crt, + void * pin); -bool openssl_pk_requires_md(const EVP_PKEY * pk); +bool openssl_pk_requires_md(const EVP_PKEY * pk); -int openssl_sign(EVP_PKEY * pkp, - int md_nid, - buffer_t msg, - buffer_t * sig); +int openssl_sign(EVP_PKEY * pkp, + int md_nid, + buffer_t msg, + buffer_t * sig); -int openssl_verify_sig(EVP_PKEY * pk, - int md_nid, - buffer_t msg, - buffer_t sig); +int openssl_verify_sig(EVP_PKEY * pk, + int md_nid, + buffer_t msg, + buffer_t sig); -ssize_t openssl_md_digest(int md_nid, - buffer_t in, - uint8_t * out); +ssize_t openssl_md_digest(int md_nid, + buffer_t in, + uint8_t * out); -ssize_t openssl_md_len(int md_nid); +ssize_t openssl_md_len(int md_nid); /* Secure memory allocation */ -int openssl_secure_malloc_init(size_t max, - size_t guard); +int openssl_secure_malloc_init(size_t max, + size_t guard); -void openssl_secure_malloc_fini(void); +void openssl_secure_malloc_fini(void); -void * openssl_secure_malloc(size_t size); +void * openssl_secure_malloc(size_t size); -void openssl_secure_free(void * ptr, - size_t size); +void openssl_secure_free(void * ptr, + size_t size); -void openssl_secure_clear(void * ptr, - size_t size); +void openssl_secure_clear(void * ptr, + size_t size); -void openssl_cleanup(void); +void openssl_cleanup(void); #endif /* OUROBOROS_LIB_CRYPT_OPENSSL_H */ diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c index 38835265..d7629f95 100644 --- a/src/lib/tests/kex_test.c +++ b/src/lib/tests/kex_test.c @@ -182,17 +182,9 @@ static int test_kex_get_algo_from_pk(const char * algo) pk.len = (size_t) len; pk.data = buf; - /* Use raw decode for hybrid KEMs, DER for others */ - if (IS_HYBRID_KEM(algo)) { - if (kex_get_algo_from_pk_raw(pk, extracted_algo) < 0) { - printf("Failed to extract algo from pk.\n"); - goto fail_pkp; - } - } else { - if (kex_get_algo_from_pk_der(pk, extracted_algo) < 0) { - printf("Failed to extract algo from pk.\n"); - goto fail_pkp; - } + if (kex_get_algo_from_pk_der(pk, extracted_algo) < 0) { + printf("Failed to extract algo from pk.\n"); + goto fail_pkp; } /* All algorithms should now return the specific group name */ @@ -221,6 +213,11 @@ static int test_kex_get_algo_from_pk_all(void) for (i = 0; kex_supported_nids[i] != NID_undef; i++) { const char * algo = kex_nid_to_str(kex_supported_nids[i]); + + /* Raw hybrid PKs are opaque, OAP carries the NID */ + if (IS_HYBRID_KEM(algo)) + continue; + ret |= test_kex_get_algo_from_pk(algo); } diff --git a/src/lib/tests/kex_test_ml_kem.c b/src/lib/tests/kex_test_ml_kem.c index 1f7d7531..7521c5d5 100644 --- a/src/lib/tests/kex_test_ml_kem.c +++ b/src/lib/tests/kex_test_ml_kem.c @@ -220,7 +220,7 @@ static int test_kex_kem(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, kdf, s1); else ct_len = kex_kem_encap(pk, buf2, kdf, s1); @@ -285,7 +285,7 @@ static int test_kex_kem_corrupted_ciphertext(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, kdf, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, kdf, s1); else ct_len = kex_kem_encap(pk, buf2, kdf, s1); @@ -360,7 +360,7 @@ static int test_kex_kem_wrong_keypair(const char * algo) } if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk1, buf3, NID_sha256, s1); + ct_len = kex_kem_encap_raw(algo, pk1, buf3, NID_sha256, s1); else ct_len = kex_kem_encap(pk1, buf3, NID_sha256, s1); @@ -422,7 +422,7 @@ static int test_kex_kem_truncated_ciphertext(const char * algo) pk.data = buf1; if (IS_HYBRID_KEM(algo)) - ct_len = kex_kem_encap_raw(pk, buf2, NID_sha256, s1); + ct_len = kex_kem_encap_raw(algo, pk, buf2, NID_sha256, s1); else ct_len = kex_kem_encap(pk, buf2, NID_sha256, s1); |
