diff options
Diffstat (limited to 'src/irmd/oap')
| -rw-r--r-- | src/irmd/oap/auth.c | 52 | ||||
| -rw-r--r-- | src/irmd/oap/auth.h | 9 | ||||
| -rw-r--r-- | src/irmd/oap/cli.c | 8 | ||||
| -rw-r--r-- | src/irmd/oap/internal.h | 7 | ||||
| -rw-r--r-- | src/irmd/oap/io.c | 11 | ||||
| -rw-r--r-- | src/irmd/oap/srv.c | 7 | ||||
| -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 |
10 files changed, 330 insertions, 35 deletions
diff --git a/src/irmd/oap/auth.c b/src/irmd/oap/auth.c index d165de73..ebe1949b 100644 --- a/src/irmd/oap/auth.c +++ b/src/irmd/oap/auth.c @@ -106,6 +106,11 @@ int oap_auth_add_ca_crt(void * crt) return auth_add_crt_to_store(oap_auth.ca_ctx, crt); } +int oap_auth_add_chain_crt(void * crt) +{ + return auth_add_crt_to_chain(oap_auth.ca_ctx, crt); +} + #define TIMESYNC_SLACK 100 /* ms */ #define ID_IS_EQUAL(id1, id2) (memcmp(id1, id2, OAP_ID_SIZE) == 0) int oap_check_hdr(const struct oap_hdr * hdr) @@ -179,16 +184,20 @@ int oap_check_hdr(const struct oap_hdr * hdr) return -EAUTH; } -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr) +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr) { void * crt; void * pk = NULL; + void * pin = NULL; buffer_t sign; /* Signed region */ uint8_t * id = peer_hdr->id.data; + int ret; assert(name != NULL); + assert(cfg != NULL); assert(local_hdr != NULL); assert(peer_hdr != NULL); @@ -198,6 +207,10 @@ int oap_auth_peer(char * name, } if (peer_hdr->crt.len == 0) { + if (cfg->req_auth) { + log_err_id(id, "Peer did not provide a certificate."); + goto fail_check; + } log_dbg_id(id, "No crt provided."); name[0] = '\0'; return 0; @@ -217,19 +230,40 @@ int oap_auth_peer(char * name, log_dbg_id(id, "Got public key from crt."); - if (auth_verify_crt(oap_auth.ca_ctx, crt) < 0) { - log_err_id(id, "Failed to verify peer with CA store."); + if (cfg->cacert[0] != '\0' && + crypt_load_crt_file(cfg->cacert, &pin) < 0) { + log_err_id(id, "Failed to load pinned CA %s.", cfg->cacert); goto fail_crt; } + ret = auth_verify_crt_pin(oap_auth.ca_ctx, crt, pin); + if (ret == -ENOENT) { + log_err_id(id, "Peer crt not issued by pinned CA %s.", + cfg->cacert); + goto fail_pin; + } + + if (ret < 0) { + log_err_id(id, "Failed to verify peer with CA store."); + goto fail_pin; + } + log_dbg_id(id, "Successfully verified peer crt."); + /* Digest pin: peer must sign with the configured digest */ + if (crypt_pk_requires_md(pk) && + cfg->d.nid != NID_undef && peer_hdr->md_nid != cfg->d.nid) { + log_err_id(id, "Peer did not sign with %s.", + md_nid_to_str(cfg->d.nid)); + goto fail_pin; + } + sign = peer_hdr->hdr; sign.len -= peer_hdr->sig.len; if (auth_verify_sig(pk, peer_hdr->md_nid, sign, peer_hdr->sig) < 0) { log_err_id(id, "Failed to verify signature."); - goto fail_check_sig; + goto fail_pin; } if (crypt_get_crt_name(crt, name) < 0) { @@ -237,6 +271,8 @@ int oap_auth_peer(char * name, name[0] = '\0'; } + if (pin != NULL) + crypt_free_crt(pin); crypt_free_key(pk); crypt_free_crt(crt); @@ -244,7 +280,9 @@ int oap_auth_peer(char * name, return 0; - fail_check_sig: + fail_pin: + if (pin != NULL) + crypt_free_crt(pin); fail_crt: crypt_free_key(pk); crypt_free_crt(crt); diff --git a/src/irmd/oap/auth.h b/src/irmd/oap/auth.h index 4f748750..be8d2cae 100644 --- a/src/irmd/oap/auth.h +++ b/src/irmd/oap/auth.h @@ -23,13 +23,16 @@ #ifndef OUROBOROS_IRMD_OAP_AUTH_H #define OUROBOROS_IRMD_OAP_AUTH_H +#include <ouroboros/crypt.h> + #include "hdr.h" int oap_check_hdr(const struct oap_hdr * hdr); /* name is updated with the peer's certificate name if available */ -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr); +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr); #endif /* OUROBOROS_IRMD_OAP_AUTH_H */ diff --git a/src/irmd/oap/cli.c b/src/irmd/oap/cli.c index d38f38dd..113abc4c 100644 --- a/src/irmd/oap/cli.c +++ b/src/irmd/oap/cli.c @@ -534,17 +534,11 @@ int oap_cli_complete(void * ctx, } /* Authenticate server */ - if (oap_auth_peer(peer, &s->local_hdr, &peer_hdr) < 0) { + if (oap_auth_peer(peer, &s->kcfg, &s->local_hdr, &peer_hdr) < 0) { log_err_id(id, "Failed to authenticate server."); goto fail_oap; } - /* Required peer auth makes sig and name binding mandatory */ - if (s->kcfg.req_auth && peer_hdr.crt.len == 0) { - log_err_id(id, "Server did not provide a certificate."); - goto fail_oap; - } - /* Verify request hash in authenticated response */ if (peer_hdr.req_hash.len == 0) { log_err_id(id, "Response missing req_hash."); diff --git a/src/irmd/oap/internal.h b/src/irmd/oap/internal.h index 6dd44d56..12d93bcd 100644 --- a/src/irmd/oap/internal.h +++ b/src/irmd/oap/internal.h @@ -36,9 +36,10 @@ int oap_check_hdr(const struct oap_hdr * hdr); -int oap_auth_peer(char * name, - const struct oap_hdr * local_hdr, - const struct oap_hdr * peer_hdr); +int oap_auth_peer(char * name, + const struct sec_config * cfg, + const struct oap_hdr * local_hdr, + const struct oap_hdr * peer_hdr); int oap_negotiate_cipher(const struct oap_hdr * peer_hdr, struct sec_config * kcfg); diff --git a/src/irmd/oap/io.c b/src/irmd/oap/io.c index 24d33e60..5c560ea5 100644 --- a/src/irmd/oap/io.c +++ b/src/irmd/oap/io.c @@ -100,6 +100,8 @@ int load_kex_config(const char * name, const char * path, struct sec_config * cfg) { + void * pin; + assert(name != NULL); assert(cfg != NULL); @@ -112,6 +114,15 @@ int load_kex_config(const char * name, return -1; } + if (cfg->cacert[0] != '\0') { + if (crypt_load_crt_file(cfg->cacert, &pin) < 0) { + log_err("Failed to load pinned CA %s for %s.", + cfg->cacert, name); + return -EAUTH; + } + crypt_free_crt(pin); + } + if (!IS_KEX_ALGO_SET(cfg)) { log_info("Key exchange not configured for %s.", name); return 0; diff --git a/src/irmd/oap/srv.c b/src/irmd/oap/srv.c index 08b4d9d2..b92c1946 100644 --- a/src/irmd/oap/srv.c +++ b/src/irmd/oap/srv.c @@ -439,16 +439,11 @@ 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, &local_hdr, &peer_hdr) < 0) { + if (oap_auth_peer(cli_name, &kcfg, &local_hdr, &peer_hdr) < 0) { log_err_id(id, "Failed to authenticate client."); goto fail_auth; } - if (kcfg.req_auth && peer_hdr.crt.len == 0) { - log_err_id(id, "Client did not provide a certificate."); - goto fail_auth; - } - if (do_server_kex(info, &peer_hdr, &kcfg, &local_hdr.kex, sk) < 0) goto fail_kex; 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; |
