diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-06-12 19:34:27 +0200 |
|---|---|---|
| committer | Sander Vrijders <sander@ouroboros.rocks> | 2026-06-29 08:32:58 +0200 |
| commit | 977bcac2d56a8793ed93b4aac7016ef36b51a07f (patch) | |
| tree | 7e26553a57cbdc75d9c33b25fe228631dea36142 /src/irmd/oap/tests | |
| parent | 67c55d5869d5473e5139614637f31ea37746181d (diff) | |
| download | ouroboros-977bcac2d56a8793ed93b4aac7016ef36b51a07f.tar.gz ouroboros-977bcac2d56a8793ed93b4aac7016ef36b51a07f.zip | |
irmd: Add issuer and digest pinning to OAP
A peer certificate that verifies against the CA store could have
been issued by any trusted CA, and a peer could pick any supported
digest for its signature. Tighten the authentication contract with
two local policies.
cacert= pins the issuing CA: a peer certificate, if presented, must
chain through the pinned CA. Whether a certificate is mandatory at
all remains controlled by auth= alone.
digest= now also pins the signature digest: a classical peer must sign
with the locally configured digest, and may not omit the digest NID to
fall back to the key's default digest. PQC signatures (ML-DSA,
SLH-DSA) have an intrinsic digest and may be NID_undef.
Signed-off-by: Dimitri Staessens <dimitri@ouroboros.rocks>
Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
Diffstat (limited to 'src/irmd/oap/tests')
| -rw-r--r-- | src/irmd/oap/tests/common.c | 12 | ||||
| -rw-r--r-- | src/irmd/oap/tests/common.h | 15 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test.c | 223 | ||||
| -rw-r--r-- | src/irmd/oap/tests/oap_test_ml_dsa.c | 21 |
4 files changed, 262 insertions, 9 deletions
diff --git a/src/irmd/oap/tests/common.c b/src/irmd/oap/tests/common.c index c5000e48..af815fd4 100644 --- a/src/irmd/oap/tests/common.c +++ b/src/irmd/oap/tests/common.c @@ -37,6 +37,11 @@ int load_srv_kex_config(const struct name_info * info, memset(cfg, 0, sizeof(*cfg)); cfg->req_auth = test_cfg.srv.req_auth; + if (test_cfg.srv.cacert != NULL) + strcpy(cfg->cacert, test_cfg.srv.cacert); + + /* Digest is kept without kex, as in parse_sec_config */ + SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md); if (test_cfg.srv.kex == NID_undef) return 0; @@ -44,7 +49,6 @@ int load_srv_kex_config(const struct name_info * info, SET_KEX_ALGO_NID(cfg, test_cfg.srv.kex); SET_KEX_CIPHER_NID(cfg, test_cfg.srv.cipher); SET_KEX_KDF_NID(cfg, test_cfg.srv.kdf); - SET_KEX_DIGEST_NID(cfg, test_cfg.srv.md); SET_KEX_KEM_MODE(cfg, test_cfg.srv.kem_mode); return 0; @@ -58,6 +62,11 @@ int load_cli_kex_config(const struct name_info * info, memset(cfg, 0, sizeof(*cfg)); cfg->req_auth = test_cfg.cli.req_auth; + if (test_cfg.cli.cacert != NULL) + strcpy(cfg->cacert, test_cfg.cli.cacert); + + /* Digest is kept without kex, as in parse_sec_config */ + SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md); if (test_cfg.cli.kex == NID_undef) return 0; @@ -65,7 +74,6 @@ int load_cli_kex_config(const struct name_info * info, SET_KEX_ALGO_NID(cfg, test_cfg.cli.kex); SET_KEX_CIPHER_NID(cfg, test_cfg.cli.cipher); SET_KEX_KDF_NID(cfg, test_cfg.cli.kdf); - SET_KEX_DIGEST_NID(cfg, test_cfg.cli.md); SET_KEX_KEM_MODE(cfg, test_cfg.cli.kem_mode); return 0; diff --git a/src/irmd/oap/tests/common.h b/src/irmd/oap/tests/common.h index fa500ffe..4fe2f779 100644 --- a/src/irmd/oap/tests/common.h +++ b/src/irmd/oap/tests/common.h @@ -32,13 +32,14 @@ /* Per-side security configuration for tests */ struct test_sec_cfg { - int kex; /* KEX algorithm NID */ - int cipher; /* Cipher NID for encryption */ - int kdf; /* KDF NID for key derivation */ - int md; /* Digest NID for signatures */ - int kem_mode; /* KEM encapsulation mode (0 for ECDH) */ - bool auth; /* Use authentication (certificates) */ - bool req_auth; /* Require peer authentication */ + int kex; /* KEX algorithm NID */ + int cipher; /* Cipher NID for encryption */ + int kdf; /* KDF NID for key derivation */ + int md; /* Digest NID for signatures */ + int kem_mode; /* KEM encapsulation mode (0 for ECDH) */ + bool auth; /* Use authentication (certificates) */ + bool req_auth; /* Require peer authentication */ + const char * cacert; /* Pinned issuing CA path */ }; /* Test configuration - set by each test before running roundtrip */ diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c index fd2c5629..311177b7 100644 --- a/src/irmd/oap/tests/oap_test.c +++ b/src/irmd/oap/tests/oap_test.c @@ -45,7 +45,10 @@ #include "common.h" #include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> #include <string.h> +#include <unistd.h> #ifdef HAVE_OPENSSL #include <openssl/evp.h> @@ -1306,6 +1309,213 @@ 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) +{ + struct oap_test_ctx ctx; + char path[32]; + + test_default_cfg(); + + 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; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail_unlink; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if ((oap_cli_complete_ctx(&ctx) == 0) != expected) { + printf("Pinned CA gave wrong verdict.\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) +{ + struct oap_test_ctx ctx; + char path[32]; + + test_default_cfg(); + test_cfg.cli.auth = AUTH; + + 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; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail_unlink; + + 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"); + 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) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.srv.md = NID_sha384; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client should reject digest mismatch.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Server rejects a client signature with a different digest */ +static int test_oap_srv_rejects_md_mismatch(void) +{ + struct oap_test_ctx ctx; + + test_default_cfg(); + test_cfg.cli.auth = AUTH; + test_cfg.cli.md = NID_sha384; + + TEST_START(); + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject digest mismatch.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int oap_test(int argc, char **argv) { @@ -1347,6 +1557,15 @@ int oap_test(int argc, ret |= test_oap_cli_requires_srv_auth(); 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(); #else (void) test_oap_roundtrip_auth_only; (void) test_oap_roundtrip_kex_only; @@ -1375,6 +1594,10 @@ int oap_test(int argc, (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; 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 81b307ab..19be7400 100644 --- a/src/irmd/oap/tests/oap_test_ml_dsa.c +++ b/src/irmd/oap/tests/oap_test_ml_dsa.c @@ -237,6 +237,23 @@ static int test_oap_roundtrip_auth_only(void) return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); } +/* Digest pin does not apply to PQC: the digest is intrinsic */ +static int test_oap_cli_md_pin_exempts_pqc(void) +{ + test_cfg_init(NID_undef, NID_undef, NID_undef, 0, NO_CLI_AUTH); + test_cfg.cli.md = NID_sha256; + + return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); +} + +static int test_oap_srv_md_pin_exempts_pqc(void) +{ + test_cfg_init(NID_undef, NID_undef, NID_undef, 0, CLI_AUTH); + test_cfg.srv.md = NID_sha256; + + return roundtrip_auth_only(root_ca_crt_ml, im_ca_crt_ml); +} + static int test_oap_corrupted_request(void) { test_cfg_init(NID_MLKEM768, NID_aes_256_gcm, get_random_kdf(), @@ -422,6 +439,8 @@ int oap_test_ml_dsa(int argc, #ifdef HAVE_OPENSSL_ML_KEM ret |= test_oap_roundtrip_auth_only(); + ret |= test_oap_cli_md_pin_exempts_pqc(); + ret |= test_oap_srv_md_pin_exempts_pqc(); ret |= test_oap_roundtrip_kem_all(); @@ -432,6 +451,8 @@ int oap_test_ml_dsa(int argc, ret |= test_oap_truncated_request(); #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_roundtrip_kem; (void) test_oap_roundtrip_kem_all; (void) test_oap_kem_srv_uncfg; |
