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/lib | |
| 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/lib')
| -rw-r--r-- | src/lib/crypt.c | 58 | ||||
| -rw-r--r-- | src/lib/crypt/openssl.c | 72 | ||||
| -rw-r--r-- | src/lib/crypt/openssl.h | 15 | ||||
| -rw-r--r-- | src/lib/tests/auth_test.c | 94 | ||||
| -rw-r--r-- | src/lib/tests/kex_test.c | 83 |
5 files changed, 316 insertions, 6 deletions
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(); |
