From 45b63ccdbb39d6464fbf9858e501397bb23eecb9 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 3 Jun 2026 10:23:11 +0200 Subject: [PATCH 1/3] session tls UPDATE post-handshake CRL verification Move CRL verification from the TLS handshake to post-handshake so that CRLs for peer certificates received during the handshake can also be downloaded and checked. Add nc_session_tls_crl_verify_post_handshake() which downloads CRLs for the entire peer chain and then verifies it. Fixes CESNET/netopeer2#1809 --- src/session.c | 116 ++++++++++++++++++++++++++++++- src/session_client_tls.c | 8 +++ src/session_mbedtls.c | 73 +++++++++++++++++--- src/session_openssl.c | 146 +++++++++++++++++++++++++++++++++++---- src/session_p.h | 15 ++++ src/session_server_tls.c | 8 +++ src/session_wrapper.h | 56 ++++++++++++++- 7 files changed, 395 insertions(+), 27 deletions(-) diff --git a/src/session.c b/src/session.c index b9ae7f0e..ddb6dad9 100644 --- a/src/session.c +++ b/src/session.c @@ -2020,7 +2020,121 @@ nc_session_tls_crl_from_cert_ext_fetch(void *leaf_cert, void *cert_store, void * return ret; } -#endif +/** + * @brief Download CRLs for certificates in the peer's chain and merge them into crl_store. + * + * @param[in] peer_chain Peer's certificate chain. + * @param[in] cert_store Certificate store containing CAs (for resolving CRL distribution points). + * @param[in,out] crl_store CRL store to add downloaded CRLs to. + * @return 0 on success, non-zero on error. + */ +static int +nc_session_tls_crl_fetch_for_peer_chain(void *peer_chain, void *cert_store, void *crl_store) +{ + int i, ret = 0, uri_count = 0, j; + void *cert = NULL; + CURL *handle = NULL; + struct nc_curl_data downloaded = {0}; + char **uris = NULL; + + /* init curl */ + ret = nc_session_curl_init(&handle, &downloaded); + if (ret) { + goto cleanup; + } + + /* iterate over all certificates in the peer chain and collect CRL distribution point URIs */ + for (i = 0; i < nc_tls_get_num_certs_wrap(peer_chain); i++) { + cert = nc_tls_get_cert_wrap(peer_chain, i); + uris = NULL; + uri_count = 0; + + /* + * For the first (leaf) cert, pass cert_store to also extract CRL DPs + * from the CA certificates. For subsequent certs, pass NULL to only + * extract their own CRL DPs, avoiding duplicates from cert_store. + */ + ret = nc_server_tls_get_crl_distpoint_uris_wrap(cert, i == 0 ? cert_store : NULL, &uris, &uri_count); + if (ret) { + goto cleanup; + } + + for (j = 0; j < uri_count; j++) { + VRB(NULL, "Downloading CRL from \"%s\".", uris[j]); + ret = nc_session_curl_fetch(handle, uris[j]); + if (ret) { + WRN(NULL, "Failed to fetch CRL from \"%s\".", uris[j]); + continue; + } + + ret = nc_server_tls_add_crl_to_store_wrap(downloaded.data, downloaded.size, crl_store); + + free(downloaded.data); + downloaded.data = NULL; + downloaded.size = 0; + + if (ret) { + for (j = j + 1; j < uri_count; j++) { + free(uris[j]); + } + free(uris); + uris = NULL; + goto cleanup; + } + } + + for (j = 0; j < uri_count; j++) { + free(uris[j]); + } + free(uris); + uris = NULL; + } + +cleanup: + curl_easy_cleanup(handle); + free(downloaded.data); + if (uris) { + for (i = 0; i < uri_count; i++) { + free(uris[i]); + } + free(uris); + } + return ret; +} + +int +nc_session_tls_crl_verify_post_handshake(void *tls_session, void *cert_store, void *crl_store) +{ + int ret = 0; + void *peer_chain = NULL; + + peer_chain = nc_tls_get_peer_cert_chain_wrap(tls_session); + if (!peer_chain) { + /* no peer certificate, nothing to verify */ + return 0; + } + + if (!crl_store && !cert_store) { + /* no CRL store and no cert store, nothing to verify */ + return 0; + } + + /* download CRLs for peer certificates and add them to the CRL store */ + ret = nc_session_tls_crl_fetch_for_peer_chain(peer_chain, cert_store, crl_store); + if (ret) { + return ret; + } + + /* verify the peer chain against the CRLs */ + ret = nc_tls_verify_cert_chain_crl_wrap(peer_chain, cert_store, crl_store); + if (ret) { + ERR(NULL, "Post-handshake CRL verification failed."); + } + + return ret; +} + +#endif /* NC_ENABLED_SSH_TLS */ API const char * nc_yang_module_dir(void) diff --git a/src/session_client_tls.c b/src/session_client_tls.c index 4962441a..cdc3d9dd 100644 --- a/src/session_client_tls.c +++ b/src/session_client_tls.c @@ -306,6 +306,14 @@ nc_client_tls_session_new(int sock, const char *host, struct nc_client_tls_opts goto fail; } + /* post-handshake CRL verification: download CRLs for peer certs and verify the full chain */ + if (nc_session_tls_crl_verify_post_handshake(tls_session, + nc_tls_get_cert_store_wrap(tls_cfg, tls_ctx), + nc_tls_get_crl_store_wrap(tls_cfg, tls_ctx))) { + ERR(NULL, "TLS connect failed (post-handshake CRL verification)."); + goto fail; + } + *out_tls_cfg = tls_cfg; return tls_session; diff --git a/src/session_mbedtls.c b/src/session_mbedtls.c index 138460da..f4c97a3a 100644 --- a/src/session_mbedtls.c +++ b/src/session_mbedtls.c @@ -1181,6 +1181,48 @@ nc_client_tls_handshake_step_wrap(void *tls_session, int sock) } } +void * +nc_tls_get_peer_cert_chain_wrap(void *tls_session) +{ + return (mbedtls_x509_crt *)mbedtls_ssl_get_peer_cert(tls_session); +} + +int +nc_tls_verify_cert_chain_crl_wrap(void *cert_chain, void *cert_store, void *crl_store) +{ + const mbedtls_x509_crt *peer_chain = cert_chain; + const mbedtls_x509_crt *trust_ca = cert_store; + const mbedtls_x509_crl *ca_crl = crl_store; + uint32_t flags = 0; + int ret; + + if (!crl_store || !cert_store) { + return 0; + } + + ret = mbedtls_x509_crt_verify((mbedtls_x509_crt *)peer_chain, + (mbedtls_x509_crt *)trust_ca, (mbedtls_x509_crl *)ca_crl, + NULL, &flags, NULL, NULL); + if (ret != 0) { + ERR(NULL, "CRL verification failed (%s).", nc_tls_get_verify_err_str(flags)); + return 1; + } + + return 0; +} + +void * +nc_tls_get_cert_store_wrap(void *UNUSED(tls_cfg), struct nc_tls_ctx *tls_ctx) +{ + return tls_ctx->cert_store; +} + +void * +nc_tls_get_crl_store_wrap(void *UNUSED(tls_cfg), struct nc_tls_ctx *tls_ctx) +{ + return tls_ctx->crl_store; +} + void nc_tls_ctx_destroy_wrap(struct nc_tls_ctx *tls_ctx) { @@ -1315,8 +1357,12 @@ nc_tls_setup_config_from_ctx_wrap(struct nc_tls_ctx *tls_ctx, void *tls_cfg) mbedtls_ssl_conf_rng(tls_cfg, mbedtls_ctr_drbg_random, tls_ctx->ctr_drbg); /* set config's cert and key */ mbedtls_ssl_conf_own_cert(tls_cfg, tls_ctx->cert, tls_ctx->pkey); - /* set config's CA and CRL cert store */ - mbedtls_ssl_conf_ca_chain(tls_cfg, tls_ctx->cert_store, tls_ctx->crl_store); + /* + * Do NOT pass crl_store to mbedtls_ssl_conf_ca_chain. CRL verification is done + * post-handshake so that CRLs for peer certificates (received during the handshake) + * can also be downloaded and checked. crl_store is kept in tls_ctx for later use. + */ + mbedtls_ssl_conf_ca_chain(tls_cfg, tls_ctx->cert_store, NULL); return 0; } @@ -2000,20 +2046,27 @@ nc_server_tls_get_crl_distpoint_uris_wrap(void *leaf_cert, void *cert_store, cha size_t len; mbedtls_x509_buf ext_oid = {0}; - NC_CHECK_ARG_RET(NULL, cert_store, uris, uri_count, 1); + if (!uris || !uri_count) { + ERRINT; + return 1; + } *uris = NULL; *uri_count = 0; - /* get the number of certs in the store */ - cert = cert_store; - cert_count = 0; - while (cert) { - ++cert_count; - cert = cert->next; + if (cert_store) { + /* get the number of certs in the store */ + cert = cert_store; + cert_count = 0; + while (cert) { + ++cert_count; + cert = cert->next; + } + } else { + cert_count = 0; } - /* iterate over all the certs */ + /* iterate over leaf cert (i = -1) and all the certs in the store */ for (i = -1; i < cert_count; i++) { if (i == -1) { cert = leaf_cert; diff --git a/src/session_openssl.c b/src/session_openssl.c index a796ffa7..f269294d 100644 --- a/src/session_openssl.c +++ b/src/session_openssl.c @@ -304,6 +304,7 @@ nc_server_tls_add_crl_to_store_wrap(const unsigned char *crl_data, size_t size, int rc = 0; X509_CRL *crl = NULL; BIO *bio = NULL; + unsigned long err_code; if (!crl_data || !size) { ERR(NULL, "CRL data is empty."); @@ -331,9 +332,15 @@ nc_server_tls_add_crl_to_store_wrap(const unsigned char *crl_data, size_t size, /* we obtained the CRL, now add it to the CRL store */ if (X509_STORE_add_crl(crl_store, crl) != 1) { - ERR(NULL, "Error adding CRL to store (%s).", ERR_reason_error_string(ERR_get_error())); - rc = 1; - goto cleanup; + err_code = ERR_get_error(); + + if (ERR_GET_REASON(err_code) == X509_R_CRL_ALREADY_DELTA) { + VRB(NULL, "CRL already present in store, skipping."); + } else { + ERR(NULL, "Error adding CRL to store (%s).", ERR_reason_error_string(err_code)); + rc = 1; + goto cleanup; + } } cleanup: @@ -754,6 +761,107 @@ nc_client_tls_handshake_step_wrap(void *tls_session, int UNUSED(sock)) return -1; } +void * +nc_tls_get_peer_cert_chain_wrap(void *tls_session) +{ + /* SSL_get0_verified_chain() returns the full verified chain including the + * leaf certificate, unlike SSL_get_peer_cert_chain() which may exclude + * the leaf cert on the server side in TLS 1.3 */ + return SSL_get0_verified_chain(tls_session); +} + +int +nc_tls_verify_cert_chain_crl_wrap(void *cert_chain, void *cert_store, void *UNUSED(crl_store)) +{ + STACK_OF(X509) * chain = cert_chain; + X509_STORE *store = cert_store; + X509_STORE_CTX *verify_ctx = NULL; + + STACK_OF(X509_OBJECT) * objs; + STACK_OF(X509) * untrusted = NULL; + int i, ncerts, nobjs, have_crls = 0, ret = 1; + + ncerts = sk_X509_num(chain); + if (ncerts == 0) { + return 0; + } + + /* check if the store actually contains any CRLs */ + objs = X509_STORE_get0_objects(store); + if (objs) { + nobjs = sk_X509_OBJECT_num(objs); + for (i = 0; i < nobjs; i++) { + if (X509_OBJECT_get0_X509_CRL(sk_X509_OBJECT_value(objs, i))) { + have_crls = 1; + break; + } + } + } + + if (!have_crls) { + DBG(NULL, "No CRLs in store, skipping CRL verification."); + return 0; + } + + verify_ctx = X509_STORE_CTX_new(); + if (!verify_ctx) { + ERR(NULL, "Failed to create X509_STORE_CTX (%s).", ERR_reason_error_string(ERR_get_error())); + goto cleanup; + } + + if (!X509_STORE_CTX_init(verify_ctx, store, sk_X509_value(chain, 0), NULL)) { + ERR(NULL, "Failed to init X509_STORE_CTX (%s).", ERR_reason_error_string(ERR_get_error())); + goto cleanup; + } + + /* set intermediate certs as untrusted (all chain certs except the leaf at index 0) */ + if (ncerts > 1) { + untrusted = sk_X509_new_null(); + if (!untrusted) { + goto cleanup; + } + for (i = 1; i < ncerts; i++) { + if (!sk_X509_push(untrusted, sk_X509_value(chain, i))) { + goto cleanup; + } + } + X509_STORE_CTX_set0_untrusted(verify_ctx, untrusted); + + /* ownership transferred */ + untrusted = NULL; + } + + /* enable CRL checks for all certificates in the chain */ + X509_STORE_CTX_set_flags(verify_ctx, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); + + ret = X509_verify_cert(verify_ctx); + if (ret != 1) { + ERR(NULL, "CRL verification failed (%s).", + X509_verify_cert_error_string(X509_STORE_CTX_get_error(verify_ctx))); + ret = 1; + } else { + ret = 0; + } + +cleanup: + sk_X509_free(untrusted); + X509_STORE_CTX_free(verify_ctx); + return ret; +} + +void * +nc_tls_get_cert_store_wrap(void *tls_cfg, struct nc_tls_ctx *UNUSED(tls_ctx)) +{ + return SSL_CTX_get_cert_store(tls_cfg); +} + +void * +nc_tls_get_crl_store_wrap(void *tls_cfg, struct nc_tls_ctx *UNUSED(tls_ctx)) +{ + /* OpenSSL stores CRLs in the same X509_STORE as CA certs */ + return SSL_CTX_get_cert_store(tls_cfg); +} + void nc_tls_ctx_destroy_wrap(struct nc_tls_ctx *UNUSED(tls_ctx)) { @@ -904,8 +1012,11 @@ nc_tls_setup_config_from_ctx_wrap(struct nc_tls_ctx *tls_ctx, void *tls_cfg) return 1; } - /* enable CRL checks */ - X509_STORE_set_flags(tls_ctx->cert_store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); + /* + * Do NOT enable CRL checks here. CRL verification is done post-handshake + * so that CRLs for peer certificates (received during the handshake) can + * also be downloaded and checked. + */ } SSL_CTX_set_cert_store(tls_cfg, tls_ctx->cert_store); @@ -1553,21 +1664,26 @@ nc_server_tls_get_crl_distpoint_uris_wrap(void *leaf_cert, void *cert_store, cha ASN1_STRING *asn_string_uri; void *tmp; + if (!uris || !uri_count) { + ERRINT; + return 1; + } + *uris = NULL; *uri_count = 0; - NC_CHECK_ARG_RET(NULL, cert_store, uris, uri_count, 1); - - /* treat all entries in the cert_store as X509_OBJECTs */ - objs = X509_STORE_get0_objects(cert_store); - if (!objs) { - ERR(NULL, "Getting certificates from store failed (%s).", ERR_reason_error_string(ERR_get_error())); - ret = -1; - goto cleanup; + if (cert_store) { + /* treat all entries in the cert_store as X509_OBJECTs */ + objs = X509_STORE_get0_objects(cert_store); + if (!objs) { + ERR(NULL, "Getting certificates from store failed (%s).", ERR_reason_error_string(ERR_get_error())); + ret = -1; + goto cleanup; + } } - /* iterate over all the CAs */ - for (i = -1; i < sk_X509_OBJECT_num(objs); i++) { + /* iterate over leaf cert (i = -1) and all the CAs in the store */ + for (i = -1; cert_store ? (i < sk_X509_OBJECT_num(objs)) : (i < 0); i++) { if (i == -1) { cert = leaf_cert; } else { diff --git a/src/session_p.h b/src/session_p.h index 85b4a851..127391b7 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -1468,6 +1468,21 @@ void _nc_client_tls_destroy_opts(struct nc_client_tls_opts *opts); */ int nc_session_tls_crl_from_cert_ext_fetch(void *leaf_cert, void *cert_store, void **crl_store); +/** + * @brief Perform post-handshake CRL verification for the peer's certificate chain. + * + * Downloads CRLs from the peer certificates' CRL distribution point extensions + * and verifies the entire peer chain against all collected CRLs. + * + * @param[in] tls_session Established TLS session. + * @param[in] cert_store Certificate store containing CAs and local CRLs + * (OpenSSL: used for verification, MbedTLS: for CRL DP URI extraction and verification). + * @param[in] crl_store CRL store with pre-downloaded CRLs + * (OpenSSL: unused as CRLs are in cert_store, MbedTLS: used for verification). + * @return 0 on success, non-zero on failure. + */ +int nc_session_tls_crl_verify_post_handshake(void *tls_session, void *cert_store, void *crl_store); + #endif /* NC_ENABLED_SSH_TLS */ /** diff --git a/src/session_server_tls.c b/src/session_server_tls.c index 2783085e..3556a6f9 100644 --- a/src/session_server_tls.c +++ b/src/session_server_tls.c @@ -966,6 +966,14 @@ nc_accept_tls_session(struct nc_session *session, struct nc_server_tls_opts *opt goto fail; } + /* post-handshake CRL verification: download CRLs for peer certs and verify the full chain */ + if (nc_session_tls_crl_verify_post_handshake(session->ti.tls.session, + nc_tls_get_cert_store_wrap(session->ti.tls.config, &session->ti.tls.ctx), + nc_tls_get_crl_store_wrap(session->ti.tls.config, &session->ti.tls.ctx))) { + ERR(session, "TLS accept failed (post-handshake CRL verification)."); + goto fail; + } + return 1; fail: diff --git a/src/session_wrapper.h b/src/session_wrapper.h index e27dd3ca..d4cb4f4a 100644 --- a/src/session_wrapper.h +++ b/src/session_wrapper.h @@ -715,8 +715,11 @@ void * nc_tls_import_pubkey_file_wrap(const char *pubkey_path); /** * @brief Get all the URIs from the CRLDistributionPoints x509v3 extensions. * + * If cert_store is non-NULL, also extracts CRL DPs from CA certificates + * in the store. If NULL, only the leaf_cert's own CRL DPs are extracted. + * * @param[in] leaf_cert Server/client certificate. - * @param[in] cert_store Certificate store. + * @param[in] cert_store Certificate store (may be NULL to skip store iteration). * @param[out] uris URIs to download the CRLs from. * @param[out] uri_count Number of URIs found. * @return 0 on success, non-zero on fail. @@ -769,4 +772,55 @@ void nc_tls_keylog_session_wrap(void *session); */ int nc_tls_generate_random_bytes_wrap(void *buf, size_t num); +/** + * @brief Get the peer's certificate chain from an established TLS session. + * + * @param[in] tls_session TLS session. + * @return Peer's certificate chain, NULL on error or if no peer cert. + */ +void *nc_tls_get_peer_cert_chain_wrap(void *tls_session); + +/** + * @brief Verify a certificate chain against CRLs. + * + * For OpenSSL, uses X509_STORE_CTX with X509_V_FLAG_CRL_CHECK | + * X509_V_FLAG_CRL_CHECK_ALL to verify the chain including CRL signature checks. + * cert_store must contain both CAs and CRLs. crl_store is unused. + * + * For MbedTLS, uses mbedtls_x509_crt_verify with the CRL store, which verifies + * CRL signatures as part of chain verification. Both cert_store (CA certs) + * and crl_store (CRLs) are required. + * + * @param[in] cert_chain Peer's certificate chain. + * @param[in] cert_store Certificate store (OpenSSL: contains CAs + CRLs, MbedTLS: CA certs). + * @param[in] crl_store CRL store (OpenSSL: unused, MbedTLS: CRLs for verification). + * @return 0 on success, non-zero on failure. + */ +int nc_tls_verify_cert_chain_crl_wrap(void *cert_chain, void *cert_store, void *crl_store); + +/** + * @brief Get the certificate store from a TLS configuration. + * + * For OpenSSL, retrieves the X509_STORE from the SSL_CTX. + * For MbedTLS, returns the cert_store from the TLS context. + * + * @param[in] tls_cfg TLS configuration (SSL_CTX for OpenSSL, unused for MbedTLS). + * @param[in] tls_ctx TLS context (unused for OpenSSL, used for MbedTLS). + * @return Certificate store or NULL on error. + */ +void *nc_tls_get_cert_store_wrap(void *tls_cfg, struct nc_tls_ctx *tls_ctx); + +/** + * @brief Get the CRL store from a TLS context. + * + * For OpenSSL, returns the cert_store (X509_STORE), since CRLs are stored + * in the same X509_STORE as CA certs. + * For MbedTLS, returns the crl_store from the TLS context. + * + * @param[in] tls_cfg TLS configuration (SSL_CTX for OpenSSL, unused for MbedTLS). + * @param[in] tls_ctx TLS context (unused for OpenSSL, used for MbedTLS). + * @return CRL store or NULL. + */ +void *nc_tls_get_crl_store_wrap(void *tls_cfg, struct nc_tls_ctx *tls_ctx); + #endif From 45557efa6a974b2c6ba9af21fea50c50abb2b63a Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 3 Jun 2026 10:25:01 +0200 Subject: [PATCH 2/3] SOVERSION bump to version 5.4.8 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 57030534..cce25611 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,7 @@ set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION # with backward compatible change and micro version is connected with any internal change of the library. set(LIBNETCONF2_MAJOR_SOVERSION 5) set(LIBNETCONF2_MINOR_SOVERSION 4) -set(LIBNETCONF2_MICRO_SOVERSION 7) +set(LIBNETCONF2_MICRO_SOVERSION 8) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) From 23706b9572e9367b513e455226b1af869f6eb0c5 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Wed, 3 Jun 2026 10:25:19 +0200 Subject: [PATCH 3/3] VERSION bump to version 4.4.9 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cce25611..f8de21a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,7 @@ set(CMAKE_MACOSX_RPATH TRUE) # micro version is changed with a set of small changes or bugfixes anywhere in the project. set(LIBNETCONF2_MAJOR_VERSION 4) set(LIBNETCONF2_MINOR_VERSION 4) -set(LIBNETCONF2_MICRO_VERSION 8) +set(LIBNETCONF2_MICRO_VERSION 9) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library