summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--enc.conf.in13
-rw-r--r--include/ouroboros/crypt.h16
-rw-r--r--include/test/certs/ecdsa.h17
-rw-r--r--src/irmd/main.c12
-rw-r--r--src/irmd/oap/auth.c52
-rw-r--r--src/irmd/oap/auth.h9
-rw-r--r--src/irmd/oap/cli.c8
-rw-r--r--src/irmd/oap/internal.h7
-rw-r--r--src/irmd/oap/io.c11
-rw-r--r--src/irmd/oap/srv.c7
-rw-r--r--src/irmd/oap/tests/common.c12
-rw-r--r--src/irmd/oap/tests/common.h15
-rw-r--r--src/irmd/oap/tests/oap_test.c223
-rw-r--r--src/irmd/oap/tests/oap_test_ml_dsa.c21
-rw-r--r--src/lib/crypt.c58
-rw-r--r--src/lib/crypt/openssl.c72
-rw-r--r--src/lib/crypt/openssl.h15
-rw-r--r--src/lib/tests/auth_test.c94
-rw-r--r--src/lib/tests/kex_test.c83
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> 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 <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;
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();