From 977bcac2d56a8793ed93b4aac7016ef36b51a07f Mon Sep 17 00:00:00 2001 From: Dimitri Staessens Date: Fri, 12 Jun 2026 19:34:27 +0200 Subject: 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 Signed-off-by: Sander Vrijders --- enc.conf.in | 13 ++ include/ouroboros/crypt.h | 16 ++- include/test/certs/ecdsa.h | 17 +++ src/irmd/main.c | 12 +- src/irmd/oap/auth.c | 52 ++++++-- src/irmd/oap/auth.h | 9 +- src/irmd/oap/cli.c | 8 +- src/irmd/oap/internal.h | 7 +- src/irmd/oap/io.c | 11 ++ src/irmd/oap/srv.c | 7 +- src/irmd/oap/tests/common.c | 12 +- src/irmd/oap/tests/common.h | 15 +-- src/irmd/oap/tests/oap_test.c | 223 +++++++++++++++++++++++++++++++++++ src/irmd/oap/tests/oap_test_ml_dsa.c | 21 ++++ src/lib/crypt.c | 58 ++++++++- src/lib/crypt/openssl.c | 72 ++++++++++- src/lib/crypt/openssl.h | 15 +++ src/lib/tests/auth_test.c | 94 +++++++++++++++ src/lib/tests/kex_test.c | 83 +++++++++++++ 19 files changed, 699 insertions(+), 46 deletions(-) diff --git a/enc.conf.in b/enc.conf.in index 17b480c1..980cfb2e 100644 --- a/enc.conf.in +++ b/enc.conf.in @@ -91,6 +91,19 @@ # the server too for mutual authentication. Combine encryption=none # with auth=required for authenticated but unencrypted flows. # +# Issuer Pinning (cacert=): +# ------------------------- +# +# cacert= Path to a CA certificate that must be part of the +# peer certificate's verified chain +# +# The peer certificate is always validated against the trusted CA +# store; cacert= further restricts which CA must have issued it: a +# certificate, if presented, must chain through the pinned CA. Whether +# a certificate is mandatory is controlled by auth= alone: under +# auth=optional a peer may still connect without one. The pinned CA +# must load when the config is read, otherwise flow allocation fails. +# # KEM Mode (kem_mode=): # --------------------- # diff --git a/include/ouroboros/crypt.h b/include/ouroboros/crypt.h index 255369e6..543facaa 100644 --- a/include/ouroboros/crypt.h +++ b/include/ouroboros/crypt.h @@ -33,6 +33,7 @@ #define MAX_HASH_SIZE 64 /* SHA-512/BLAKE2b max */ #define KEX_ALGO_BUFSZ 32 #define KEX_CIPHER_BUFSZ 32 +#define CACERT_PATH_BUFSZ 256 /* * On OSX the OpenSSL NIDs are automatically loaded with evp.h. @@ -130,7 +131,8 @@ struct sec_config { int nid; } d; /* digest */ - bool req_auth; /* require peer authentication */ + bool req_auth; /* require peer authentication */ + char cacert[CACERT_PATH_BUFSZ]; /* pinned issuing CA, "" = any */ }; /* Helper macros to set sec_config fields consistently */ @@ -216,9 +218,21 @@ void auth_destroy_ctx(struct auth_ctx * ctx); int auth_add_crt_to_store(struct auth_ctx * ctx, void * crt); +/* Untrusted intermediates: used to build a path, never as trust anchors */ +int auth_add_crt_to_chain(struct auth_ctx * ctx, + void * crt); + int auth_verify_crt(struct auth_ctx * ctx, void * crt); +/* As auth_verify_crt, pin must be in the verified chain (NULL: any) */ +int auth_verify_crt_pin(struct auth_ctx * ctx, + void * crt, + void * pin); + +/* False for PQC keys: their signature digest is intrinsic */ +bool crypt_pk_requires_md(const void * pk); + int auth_sign(void * pkp, int md_nid, buffer_t msg, diff --git a/include/test/certs/ecdsa.h b/include/test/certs/ecdsa.h index 1d61a3f8..989b5bc6 100644 --- a/include/test/certs/ecdsa.h +++ b/include/test/certs/ecdsa.h @@ -107,6 +107,23 @@ static const char * signed_server_crt_ec = \ "ktkxoHAFbjQEPQIhAMInHI7lvRmS0IMw1wBF/WlUZWKvhyU/TeMIZfk/JGCS\n" "-----END CERTIFICATE-----\n"; +/* Valid CA outside the test chain, for cacert= pin mismatch */ +static __attribute__((unused)) const char * other_ca_crt_ec = \ +"-----BEGIN CERTIFICATE-----\n" +"MIICNjCCAdugAwIBAgIUTZcZ9hKXyCT/VgTw8TD1TB2mzrgwCgYIKoZIzj0EAwIw\n" +"cDELMAkGA1UEBhMCQkUxDDAKBgNVBAgMA09WTDEOMAwGA1UEBwwFR2hlbnQxDDAK\n" +"BgNVBAoMA283czEVMBMGA1UECwwMdW5pdHRlc3QubzdzMR4wHAYDVQQDDBVvdGhl\n" +"ci1jYS51bml0dGVzdC5vN3MwHhcNMjYwNjEyMTU1MjAzWhcNNDYwNjA3MTU1MjAz\n" +"WjBwMQswCQYDVQQGEwJCRTEMMAoGA1UECAwDT1ZMMQ4wDAYDVQQHDAVHaGVudDEM\n" +"MAoGA1UECgwDbzdzMRUwEwYDVQQLDAx1bml0dGVzdC5vN3MxHjAcBgNVBAMMFW90\n" +"aGVyLWNhLnVuaXR0ZXN0Lm83czBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNtu\n" +"FghMww2kQ6a+Coe6VPzfBRUZlm7y6/RfbRFPvErowOqKLQP+wCs8Rq46VmHCYTbB\n" +"OlRwzJKcNoSeJ4MNWUqjUzBRMB0GA1UdDgQWBBTmEP8W6fgViKIjw8CpTuQwyuOi\n" +"kTAfBgNVHSMEGDAWgBTmEP8W6fgViKIjw8CpTuQwyuOikTAPBgNVHRMBAf8EBTAD\n" +"AQH/MAoGCCqGSM49BAMCA0kAMEYCIQDQOCfFcOJm49R975RBPfVMy0pXGx/YeQcy\n" +"6WKAeLuTowIhAISdVZ6KxsgkwuswMtDWAkCBujep0XSBGXtXmi4959DH\n" +"-----END CERTIFICATE-----\n"; + /* Self-signed by server test-1.unittest.o7s using its key */ static __attribute__((unused)) const char * server_crt_ec = \ "-----BEGIN CERTIFICATE-----\n" diff --git a/src/irmd/main.c b/src/irmd/main.c index f91e23fc..66f341eb 100644 --- a/src/irmd/main.c +++ b/src/irmd/main.c @@ -1895,12 +1895,14 @@ void * irm_sanitize(void * o) return (void *) 0; } -static int irm_load_store(char * dpath) +static int irm_load_store(char * dpath, + bool anchor) { struct stat st; struct dirent * dent; DIR * dir; void * crt; + int ret; if (stat(dpath, &st) == -1) { log_dbg("Store directory %s not found.", dpath); @@ -1944,7 +1946,9 @@ static int irm_load_store(char * dpath) goto fail_file; } - if (oap_auth_add_ca_crt(crt) < 0) { + ret = anchor ? oap_auth_add_ca_crt(crt) + : oap_auth_add_chain_crt(crt); + if (ret < 0) { log_err("Failed to add certificate from %s to store.", path); goto fail_crt_add; @@ -2088,12 +2092,12 @@ static int irm_init(void) goto fail_oap; } - if (irm_load_store(OUROBOROS_CA_CRT_DIR) < 0) { + if (irm_load_store(OUROBOROS_CA_CRT_DIR, true) < 0) { log_err("Failed to load CA certificates."); goto fail_load_store; } - if (irm_load_store(OUROBOROS_CHAIN_DIR) < 0) { + if (irm_load_store(OUROBOROS_CHAIN_DIR, false) < 0) { log_err("Failed to load intermediate certificates."); goto fail_load_store; } 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 + #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 +#include +#include #include +#include #ifdef HAVE_OPENSSL #include @@ -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; diff --git a/src/lib/crypt.c b/src/lib/crypt.c index 66d07131..73cb0b51 100644 --- a/src/lib/crypt.c +++ b/src/lib/crypt.c @@ -141,7 +141,8 @@ struct crypt_ctx { }; struct auth_ctx { - void * store; + void * store; /* trusted anchors */ + void * chain; /* untrusted build-only interm */ }; static int parse_kex_value(const char * value, @@ -229,6 +230,10 @@ int parse_sec_config(struct sec_config * cfg, } else { return -EINVAL; } + } else if (strcmp(key, "cacert") == 0) { + if (strlen(value) >= sizeof(cfg->cacert)) + return -EINVAL; + strcpy(cfg->cacert, value); } else if (strcmp(key, "encryption") == 0) { if (strcmp(value, "none") != 0) return -EINVAL; @@ -988,9 +993,15 @@ struct auth_ctx * auth_create_ctx(void) ctx->store = openssl_auth_create_store(); if (ctx->store == NULL) goto fail_store; + + ctx->chain = openssl_auth_create_chain(); + if (ctx->chain == NULL) + goto fail_chain; #endif return ctx; #ifdef HAVE_OPENSSL + fail_chain: + openssl_auth_destroy_store(ctx->store); fail_store: free(ctx); #endif @@ -1003,6 +1014,7 @@ void auth_destroy_ctx(struct auth_ctx * ctx) if (ctx == NULL) return; #ifdef HAVE_OPENSSL + openssl_auth_destroy_chain(ctx->chain); openssl_auth_destroy_store(ctx->store); #endif free(ctx); @@ -1024,11 +1036,27 @@ int auth_add_crt_to_store(struct auth_ctx * ctx, #endif } +int auth_add_crt_to_chain(struct auth_ctx * ctx, + void * crt) +{ + assert(ctx != NULL); + assert(crt != NULL); + +#ifdef HAVE_OPENSSL + return openssl_auth_add_crt_to_chain(ctx->chain, crt); +#else + (void) ctx; + (void) crt; + + return 0; +#endif +} + int auth_verify_crt(struct auth_ctx * ctx, void * crt) { #ifdef HAVE_OPENSSL - return openssl_verify_crt(ctx->store, crt); + return openssl_verify_crt(ctx->store, ctx->chain, crt); #else (void) ctx; (void) crt; @@ -1037,6 +1065,32 @@ int auth_verify_crt(struct auth_ctx * ctx, #endif } +int auth_verify_crt_pin(struct auth_ctx * ctx, + void * crt, + void * pin) +{ +#ifdef HAVE_OPENSSL + return openssl_verify_crt_pin(ctx->store, ctx->chain, crt, pin); +#else + (void) ctx; + (void) crt; + (void) pin; + + return 0; +#endif +} + +bool crypt_pk_requires_md(const void * pk) +{ +#ifdef HAVE_OPENSSL + return openssl_pk_requires_md((const EVP_PKEY *) pk); +#else + (void) pk; + + return false; +#endif +} + int auth_sign(void * pkp, int md_nid, buffer_t msg, diff --git a/src/lib/crypt/openssl.c b/src/lib/crypt/openssl.c index 5916e3cb..2ea35a17 100644 --- a/src/lib/crypt/openssl.c +++ b/src/lib/crypt/openssl.c @@ -1695,12 +1695,43 @@ int openssl_auth_add_crt_to_store(void * store, return ret == 1 ? 0 : -1; } -int openssl_verify_crt(void * store, - void * crt) +void * openssl_auth_create_chain(void) +{ + return sk_X509_new_null(); +} + +void openssl_auth_destroy_chain(void * chain) +{ + sk_X509_pop_free((STACK_OF(X509) *) chain, X509_free); +} + +int openssl_auth_add_crt_to_chain(void * chain, + void * crt) +{ + if (X509_up_ref((X509 *) crt) != 1) + goto fail_ref; + + if (sk_X509_push((STACK_OF(X509) *) chain, (X509 *) crt) == 0) + goto fail_push; + + return 0; + fail_push: + X509_free((X509 *) crt); + fail_ref: + return -1; +} + +int openssl_verify_crt_pin(void * store, + void * untrusted, + void * crt, + void * pin) { X509_STORE_CTX * ctx; X509_STORE * _store; X509* _crt; + STACK_OF(X509) * chain; + int i; + int n; int ret; _store = (X509_STORE *) store; @@ -1710,7 +1741,8 @@ int openssl_verify_crt(void * store, if (ctx == NULL) goto fail_store_ctx; - ret = X509_STORE_CTX_init(ctx, _store, _crt, NULL); + ret = X509_STORE_CTX_init(ctx, _store, _crt, + (STACK_OF(X509) *) untrusted); if (ret != 1) goto fail_ca; @@ -1718,13 +1750,39 @@ int openssl_verify_crt(void * store, if (ret != 1) goto fail_ca; + /* Peer cert only verifies a signature; gate on sig KU, not role. */ + if ((X509_get_key_usage(_crt) & KU_DIGITAL_SIGNATURE) == 0) + goto fail_ca; + + if (pin != NULL) { + chain = X509_STORE_CTX_get0_chain(ctx); + if (chain == NULL) + goto fail_ca; + n = sk_X509_num(chain); + for (i = 1; i < n; i++) /* Skip the leaf */ + if (X509_cmp(sk_X509_value(chain, i), pin) == 0) + break; + if (i == n) + goto fail_pin; + } + X509_STORE_CTX_free(ctx); return 0; + fail_pin: + X509_STORE_CTX_free(ctx); + return -ENOENT; fail_ca: X509_STORE_CTX_free(ctx); fail_store_ctx: - return -1; + return -EAUTH; +} + +int openssl_verify_crt(void * store, + void * untrusted, + void * crt) +{ + return openssl_verify_crt_pin(store, untrusted, crt, NULL); } static const EVP_MD * select_md(EVP_PKEY * pkey, @@ -1739,6 +1797,12 @@ static const EVP_MD * select_md(EVP_PKEY * pkey, return EVP_get_digestbynid(nid); } +bool openssl_pk_requires_md(const EVP_PKEY * pk) +{ + /* Provider-based (PQC) signatures have an intrinsic digest */ + return EVP_PKEY_get_id(pk) >= 0; +} + int openssl_sign(EVP_PKEY * pkp, int nid, buffer_t msg, diff --git a/src/lib/crypt/openssl.h b/src/lib/crypt/openssl.h index af285232..2578a0d2 100644 --- a/src/lib/crypt/openssl.h +++ b/src/lib/crypt/openssl.h @@ -136,9 +136,24 @@ void openssl_auth_destroy_store(void * store); int openssl_auth_add_crt_to_store(void * store, void * crt); +void * openssl_auth_create_chain(void); + +void openssl_auth_destroy_chain(void * chain); + +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_pin(void * store, + void * untrusted, + void * crt, + void * pin); + +bool openssl_pk_requires_md(const EVP_PKEY * pk); + int openssl_sign(EVP_PKEY * pkp, int md_nid, buffer_t msg, diff --git a/src/lib/tests/auth_test.c b/src/lib/tests/auth_test.c index 0f3ef715..6a7666c1 100644 --- a/src/lib/tests/auth_test.c +++ b/src/lib/tests/auth_test.c @@ -400,6 +400,98 @@ static int test_verify_crt_missing_root_ca(void) return TEST_RC_FAIL; } +/* auth_verify_crt_pin: pin must lie in the verified chain (NULL: any) */ +static int test_verify_crt_pin(void) +{ + struct auth_ctx * auth; + void * _root_ca_crt; + void * _im_ca_crt; + void * _signed_server_crt; + void * _other_ca_crt; + + TEST_START(); + + auth = auth_create_ctx(); + if (auth == NULL) { + printf("Failed to create auth context.\n"); + goto fail_create_ctx; + } + + if (crypt_load_crt_str(root_ca_crt_ec, &_root_ca_crt) < 0) { + printf("Failed to load root crt from string.\n"); + goto fail_load_root_ca; + } + + if (crypt_load_crt_str(im_ca_crt_ec, &_im_ca_crt) < 0) { + printf("Failed to load intermediate crt from string.\n"); + goto fail_load_im_ca; + } + + if (crypt_load_crt_str(signed_server_crt_ec, &_signed_server_crt) < 0) { + printf("Failed to load signed crt from string.\n"); + goto fail_load_signed; + } + + if (crypt_load_crt_str(other_ca_crt_ec, &_other_ca_crt) < 0) { + printf("Failed to load out-of-chain crt from string.\n"); + goto fail_load_other; + } + + if (auth_add_crt_to_store(auth, _root_ca_crt) < 0) { + printf("Failed to add root ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_add_crt_to_store(auth, _im_ca_crt) < 0) { + printf("Failed to add intermediate ca crt to auth store.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _im_ca_crt) < 0) { + printf("Failed to accept pin on intermediate CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _root_ca_crt) < 0) { + printf("Failed to accept pin on root CA.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, _other_ca_crt) == 0) { + printf("Failed to reject out-of-chain pin.\n"); + goto fail_verify; + } + + if (auth_verify_crt_pin(auth, _signed_server_crt, NULL) < 0) { + printf("Failed to accept NULL (any) pin.\n"); + goto fail_verify; + } + + crypt_free_crt(_other_ca_crt); + crypt_free_crt(_signed_server_crt); + crypt_free_crt(_im_ca_crt); + crypt_free_crt(_root_ca_crt); + + auth_destroy_ctx(auth); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail_verify: + crypt_free_crt(_other_ca_crt); + fail_load_other: + crypt_free_crt(_signed_server_crt); + fail_load_signed: + crypt_free_crt(_im_ca_crt); + fail_load_im_ca: + crypt_free_crt(_root_ca_crt); + fail_load_root_ca: + auth_destroy_ctx(auth); + fail_create_ctx: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int test_auth_sign(void) { uint8_t buf[TEST_MSG_SIZE]; @@ -580,6 +672,7 @@ int auth_test(int argc, ret |= test_store_add(); ret |= test_verify_crt(); ret |= test_verify_crt_missing_root_ca(); + ret |= test_verify_crt_pin(); ret |= test_auth_sign(); ret |= test_auth_bad_signature(); ret |= test_crt_str(); @@ -593,6 +686,7 @@ int auth_test(int argc, (void) test_store_add; (void) test_verify_crt; (void) test_verify_crt_missing_root_ca; + (void) test_verify_crt_pin; (void) test_auth_sign; (void) test_auth_bad_signature; (void) test_crt_str; diff --git a/src/lib/tests/kex_test.c b/src/lib/tests/kex_test.c index 300a0607..7a4d36d8 100644 --- a/src/lib/tests/kex_test.c +++ b/src/lib/tests/kex_test.c @@ -80,6 +80,12 @@ "auth=required\n" \ "digest=sha512\n" +#define KEX_CONFIG_CACERT \ + "cacert=/etc/ouroboros/security/cacert/ca.crt\n" + +#define KEX_CONFIG_UNKNOWN_KEY \ + "autth=required\n" + /* Test key material for key loading tests */ #define X25519_PRIVKEY_PEM \ "-----BEGIN PRIVATE KEY-----\n" \ @@ -1055,6 +1061,81 @@ static int test_kex_parse_config_auth_no_enc(const char * config) return TEST_RC_FAIL; } +static int test_kex_parse_config_cacert(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_CACERT); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) < 0) { + printf("Failed to parse cacert config.\n"); + fclose(fp); + goto fail; + } + + if (strcmp(kex.cacert, + "/etc/ouroboros/security/cacert/ca.crt") != 0) { + printf("cacert not parsed correctly.\n"); + fclose(fp); + goto fail; + } + + if (kex.req_auth) { + printf("cacert must not imply req_auth.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +static int test_kex_parse_config_unknown_key(void) +{ + struct sec_config kex; + FILE * fp; + + TEST_START(); + + memset(&kex, 0, sizeof(kex)); + + fp = FMEMOPEN_STR(KEX_CONFIG_UNKNOWN_KEY); + if (fp == NULL) { + printf("Failed to open memory stream.\n"); + goto fail; + } + + if (parse_sec_config(&kex, fp) == 0) { + printf("Unknown key should be rejected.\n"); + fclose(fp); + goto fail; + } + + fclose(fp); + + TEST_SUCCESS(); + + return TEST_RC_SUCCESS; + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + int kex_test(int argc, char ** argv) { @@ -1073,6 +1154,8 @@ int kex_test(int argc, ret |= test_kex_parse_config_auth_optional(); ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_AUTH_THEN_NO_ENC); ret |= test_kex_parse_config_auth_no_enc(KEX_CONFIG_NO_ENC_THEN_AUTH); + ret |= test_kex_parse_config_cacert(); + ret |= test_kex_parse_config_unknown_key(); #ifdef HAVE_OPENSSL ret |= test_kex_parse_config_custom(); ret |= test_kex_parse_config_whitespace(); -- cgit v1.2.3