From c7e99963d07706d7634c55687c5d346a7ec45cc5 Mon Sep 17 00:00:00 2001 From: Roytak Date: Wed, 22 Apr 2026 13:52:21 +0200 Subject: [PATCH 1/3] session server tls UPDATE ctn logic Instead of obtaining username only from the cert with matching fingerprint, try to obtain it from the whole chain starting from the client cert. Also refactor the ctn code. Fixes CESNET/netopeer2#1797 --- src/server_config.h | 4 +- src/server_config_util_tls.c | 17 ++- src/session_mbedtls.c | 149 ++++++++++++++++--- src/session_openssl.c | 10 +- src/session_server_tls.c | 267 +++++++++++++++++++---------------- src/session_wrapper.h | 4 +- tests/data/client_SAN.crt | 31 ++++ tests/test_tls.c | 178 +++++++++++++++++++++++ 8 files changed, 503 insertions(+), 157 deletions(-) create mode 100644 tests/data/client_SAN.crt diff --git a/src/server_config.h b/src/server_config.h index 95569c61..b580920a 100644 --- a/src/server_config.h +++ b/src/server_config.h @@ -826,7 +826,7 @@ int nc_server_config_del_tls_endpoint_client_ref(const char *endpt_name, struct * @param[in] fingerprint Optional fingerprint of the entry. The fingerprint should always be set, however if it is * not set, it will match any certificate. Entry with no fingerprint should therefore be placed only as the last entry. * @param[in] map_type Mapping username to the certificate option. - * @param[in] name Username for this cert-to-name entry. + * @param[in] name Username for this cert-to-name entry. Mandatory if @p map_type is set to ::NC_TLS_CTN_SPECIFIED. * @param[in,out] config Configuration YANG data tree. If *config is NULL, it will be created. * Otherwise the new YANG data will be added to the previous data and may override it. * @return 0 on success, non-zero otherwise. @@ -1479,7 +1479,7 @@ int nc_server_config_del_ch_tls_ca_cert_truststore_ref(const char *client_name, * @param[in] fingerprint Optional fingerprint of the entry. The fingerprint should always be set, however if it is * not set, it will match any certificate. Entry with no fingerprint should therefore be placed only as the last entry. * @param[in] map_type Mapping username to the certificate option. - * @param[in] name Username for this cert-to-name entry. + * @param[in] name Username for this cert-to-name entry. Mandatory if @p map_type is set to ::NC_TLS_CTN_SPECIFIED. * @param[in,out] config Configuration YANG data tree. If *config is NULL, it will be created. * Otherwise the new YANG data will be added to the previous data and may override it. * @return 0 on success, non-zero otherwise. diff --git a/src/server_config_util_tls.c b/src/server_config_util_tls.c index 2f99d6cb..4e33bb8e 100644 --- a/src/server_config_util_tls.c +++ b/src/server_config_util_tls.c @@ -660,7 +660,10 @@ _nc_server_config_add_tls_ctn(const struct ly_ctx *ctx, const char *tree_path, c int ret = 0; const char *map; - NC_CHECK_ARG_RET(NULL, ctx, tree_path, name, config, 1); + if ((map_type == NC_TLS_CTN_SPECIFIED) && !name) { + ERR(NULL, "Client name must be provided for the \"specified\" CTN mapping type."); + return 1; + } if (fingerprint) { /* optional */ @@ -682,9 +685,11 @@ _nc_server_config_add_tls_ctn(const struct ly_ctx *ctx, const char *tree_path, c goto cleanup; } - ret = nc_server_config_append(ctx, tree_path, "name", name, config); - if (ret) { - goto cleanup; + if (name) { + ret = nc_server_config_append(ctx, tree_path, "name", name, config); + if (ret) { + goto cleanup; + } } cleanup: @@ -698,7 +703,7 @@ nc_server_config_add_tls_ctn(const struct ly_ctx *ctx, const char *endpt_name, u int ret = 0; char *path = NULL; - NC_CHECK_ARG_RET(NULL, ctx, endpt_name, id, name, config, 1); + NC_CHECK_ARG_RET(NULL, ctx, endpt_name, id, config, 1); ret = asprintf(&path, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/tls/netconf-server-parameters/" "client-identity-mappings/cert-to-name[id='%" PRIu32 "']", endpt_name, id); @@ -736,7 +741,7 @@ nc_server_config_add_ch_tls_ctn(const struct ly_ctx *ctx, const char *client_nam int ret = 0; char *path = NULL; - NC_CHECK_ARG_RET(NULL, ctx, client_name, endpt_name, id, name, config, 1); + NC_CHECK_ARG_RET(NULL, ctx, client_name, endpt_name, id, config, 1); ret = asprintf(&path, "/ietf-netconf-server:netconf-server/call-home/netconf-client[name='%s']/" "endpoints/endpoint[name='%s']/tls/netconf-server-parameters/client-identity-mappings/" diff --git a/src/session_mbedtls.c b/src/session_mbedtls.c index d398e103..f6a7414e 100644 --- a/src/session_mbedtls.c +++ b/src/session_mbedtls.c @@ -587,37 +587,125 @@ nc_tls_cert_dup(const mbedtls_x509_crt *cert) } /** - * @brief Duplicate a certificate and append it to a chain. + * @brief Duplicate a certificate and prepend it to a chain. * - * @param[in] cert Certificate to duplicate and append. - * @param[in,out] chain Chain to append the certificate to. + * @param[in] cert Certificate to duplicate and prepend. + * @param[in,out] chain Chain to prepend the certificate to. * @return 0 on success, -1 on error. */ static int -nc_server_tls_append_cert_to_chain(mbedtls_x509_crt *cert, mbedtls_x509_crt **chain) +nc_server_tls_prepend_cert_to_chain(mbedtls_x509_crt *cert, mbedtls_x509_crt **chain) { - mbedtls_x509_crt *iter, *copy; + mbedtls_x509_crt *copy; copy = nc_tls_cert_dup(cert); if (!copy) { return -1; } - if (!*chain) { - /* first in the list */ - *chain = copy; - } else { - /* find the last cert */ - iter = *chain; - while (iter->next) { - iter = iter->next; + copy->next = *chain; + *chain = copy; + + return 0; +} + +/** + * @brief Check whether a certificate is already present in a chain. + * + * @param[in] cert Certificate to search for. + * @param[in] chain Certificate chain. + * @return 1 if found, 0 otherwise. + */ +static int +nc_server_tls_cert_in_chain(const mbedtls_x509_crt *cert, const mbedtls_x509_crt *chain) +{ + const mbedtls_x509_crt *iter; + + for (iter = chain; iter; iter = iter->next) { + if (cert->raw.p && iter->raw.p && (cert->raw.len == iter->raw.len) && !memcmp(cert->raw.p, iter->raw.p, cert->raw.len)) { + return 1; } - iter->next = copy; } return 0; } +/** + * @brief Prepend issuer chain for a certificate. + * + * Repeatedly parse issuer_raw of @p cert into a certificate, then parse issuer_raw + * of that parsed issuer, and so on. Parsed issuers are prepended into @p chain in + * root-first order so that prepending the actual cert afterwards yields peer-first + * order. + * + * @param[in] cert Certificate whose issuers are to be prepended. + * @param[in,out] chain Chain to prepend issuer certificates to. + * @return 0 on success, -1 on error. + */ +static int +nc_server_tls_prepend_issuer_chain(mbedtls_x509_crt *cert, mbedtls_x509_crt **chain) +{ + size_t i = 0; + mbedtls_x509_crt *copy; + const mbedtls_x509_crt *issuer, *iter, *cur; + mbedtls_x509_crt *issuers[MBEDTLS_X509_MAX_VERIFY_CHAIN_SIZE - 1]; + + cur = cert; + while (cur && (i < (sizeof issuers / sizeof *issuers))) { + if (!cur->issuer_raw.p || !cur->issuer_raw.len) { + break; + } + + if ((cur->issuer_raw.len == cur->subject_raw.len) && !memcmp(cur->issuer_raw.p, cur->subject_raw.p, cur->issuer_raw.len)) { + /* self-issued root reached */ + break; + } + + /* find issuer cert by matching issuer DN to subject DN */ + issuer = NULL; + for (iter = cert; iter; iter = iter->next) { + if ((iter->subject_raw.len == cur->issuer_raw.len) && + !memcmp(iter->subject_raw.p, cur->issuer_raw.p, cur->issuer_raw.len)) { + issuer = iter; + break; + } + } + if (!issuer) { + /* no more issuers available in the certificate set */ + break; + } + + if (nc_server_tls_cert_in_chain(issuer, *chain)) { + /* already present in chain */ + break; + } + + copy = nc_tls_cert_dup(issuer); + if (!copy) { + goto error; + } + + issuers[i] = copy; + ++i; + cur = issuer; + } + + while (i) { + --i; + issuers[i]->next = *chain; + *chain = issuers[i]; + } + + return 0; + +error: + while (i) { + --i; + nc_tls_cert_destroy_wrap(issuers[i]); + } + return -1; +} + /** * @brief Verify a certificate. * @@ -634,11 +722,22 @@ nc_server_tls_verify_cb(void *cb_data, mbedtls_x509_crt *cert, int depth, uint32 struct nc_tls_verify_cb_data *data = cb_data; char *err; - /* append to the chain we're building */ - ret = nc_server_tls_append_cert_to_chain(cert, (mbedtls_x509_crt **)&data->chain); - if (ret) { - nc_tls_cert_destroy_wrap(data->chain); - return MBEDTLS_ERR_X509_ALLOC_FAILED; + if (!data->chain) { + /* first call, prepend the issuer chain so that we have the whole chain available for CTN */ + ret = nc_server_tls_prepend_issuer_chain(cert, (mbedtls_x509_crt **)&data->chain); + if (ret) { + nc_tls_cert_destroy_wrap(data->chain); + return MBEDTLS_ERR_X509_ALLOC_FAILED; + } + } + + /* prepend to the chain we're building to maintain peer-first order */ + if (!nc_server_tls_cert_in_chain(cert, (const mbedtls_x509_crt *)data->chain)) { + ret = nc_server_tls_prepend_cert_to_chain(cert, (mbedtls_x509_crt **)&data->chain); + if (ret) { + nc_tls_cert_destroy_wrap(data->chain); + return MBEDTLS_ERR_X509_ALLOC_FAILED; + } } if (!*flags) { @@ -836,18 +935,22 @@ nc_tls_get_num_certs_wrap(void *chain) return n; } -void -nc_tls_get_cert_wrap(void *chain, int idx, void **cert) +void * +nc_tls_get_cert_wrap(void *chain, int idx) { int i; mbedtls_x509_crt *iter; + if (!chain || (idx < 0)) { + return NULL; + } + iter = chain; - for (i = 0; i < idx; i++) { + for (i = 0; iter && (i < idx); i++) { iter = iter->next; } - *cert = iter; + return iter; } int diff --git a/src/session_openssl.c b/src/session_openssl.c index 6484a526..8f92dee1 100644 --- a/src/session_openssl.c +++ b/src/session_openssl.c @@ -616,10 +616,14 @@ nc_tls_get_num_certs_wrap(void *chain) return sk_X509_num(chain); } -void -nc_tls_get_cert_wrap(void *chain, int idx, void **cert) +void * +nc_tls_get_cert_wrap(void *chain, int idx) { - *cert = sk_X509_value(chain, idx); + if (!chain || (idx < 0)) { + return NULL; + } + + return sk_X509_value(chain, idx); } int diff --git a/src/session_server_tls.c b/src/session_server_tls.c index df2f1551..11a43a55 100644 --- a/src/session_server_tls.c +++ b/src/session_server_tls.c @@ -252,6 +252,14 @@ nc_server_tls_sha512(void *cert) return nc_server_tls_digest_to_hex(buf, buf_len); } +/** + * @brief Get a username from a certificate according to cert-to-name mapping. + * + * @param[in] cert Certificate to inspect. + * @param[in] ctn Cert-to-name entry defining the mapping type. + * @param[out] username Resolved username. + * @return 0 if username was resolved, 1 if no applicable field was found, -1 on error. + */ static int nc_server_tls_get_username(void *cert, struct nc_ctn *ctn, char **username) { @@ -344,152 +352,169 @@ nc_server_tls_get_username(void *cert, struct nc_ctn *ctn, char **username) return 0; } +/** + * @brief Get a certificate digest for the algorithm encoded in a TLS fingerprint. + * + * @param[in] cert Certificate to digest. + * @param[in] fingerprint Certificate fingerprint. + * @param[out] digest_hex Digest in hexadecimal form. + * @return 0 on success, 1 for unsupported algorithm, -1 on error. + */ +static int +nc_server_tls_fingerprint_get_digest(void *cert, const char *fingerprint, char **digest_hex) +{ + *digest_hex = NULL; + + if (!strncmp(fingerprint, "01", 2)) { + *digest_hex = nc_server_tls_md5(cert); + } else if (!strncmp(fingerprint, "02", 2)) { + *digest_hex = nc_server_tls_sha1(cert); + } else if (!strncmp(fingerprint, "03", 2)) { + *digest_hex = nc_server_tls_sha224(cert); + } else if (!strncmp(fingerprint, "04", 2)) { + *digest_hex = nc_server_tls_sha256(cert); + } else if (!strncmp(fingerprint, "05", 2)) { + *digest_hex = nc_server_tls_sha384(cert); + } else if (!strncmp(fingerprint, "06", 2)) { + *digest_hex = nc_server_tls_sha512(cert); + } else { + return 1; + } + + if (!*digest_hex) { + return -1; + } + return 0; +} + +/** + * @brief Get the digest part from a TLS fingerprint value. + * + * @param[in] fingerprint Certificate fingerprint. + * @return Fingerprint digest value, or NULL on error. + */ +static const char * +nc_server_tls_fingerprint_get_value(const char *fingerprint) +{ + if ((strlen(fingerprint) < 3) || (fingerprint[2] != ':')) { + return NULL; + } + return fingerprint + 3; +} + +/** + * @brief Check a cert-to-name entry against a certificate chain and resolve username. + * + * @param[in] ctn Cert-to-name entry to evaluate. + * @param[in] cert_chain Presented certificate chain, peer certificate first. + * @param[out] username Resolved username. + * @return 0 on match with resolved username, 1 on no match, -1 on error. + */ static int nc_server_tls_cert_to_name(struct nc_ctn *ctn, void *cert_chain, char **username) { - int ret = 1, i, cert_count, fingerprint_match; - char *digest_md5, *digest_sha1, *digest_sha224; - char *digest_sha256, *digest_sha384, *digest_sha512; + int rc = 1, i, cert_count, fingerprint_match = 0; + char *digest_hex = NULL; + const char *fingerprint_value; void *cert; /* first make sure the entry is valid */ if (!ctn->map_type || ((ctn->map_type == NC_TLS_CTN_SPECIFIED) && !ctn->name)) { - VRB(NULL, "Cert verify CTN: entry with id %u not valid, skipping.", ctn->id); + VRB(NULL, "Cert verify CTN: entry with id %" PRIu32 " not valid, skipping.", ctn->id); return 1; } - cert_count = nc_tls_get_num_certs_wrap(cert_chain); - for (i = 0; i < cert_count; i++) { - DBG(NULL, "Cert verify CTN: checking entry with id %" PRIu32 ".", ctn->id); + DBG(NULL, "Cert verify CTN: checking entry with id %" PRIu32 ".", ctn->id); - /* reset the flag */ - fingerprint_match = 0; + cert_count = nc_tls_get_num_certs_wrap(cert_chain); - /*get next cert */ - nc_tls_get_cert_wrap(cert_chain, i, &cert); - if (!cert) { - ERR(NULL, "Failed to get certificate from the chain."); - ret = -1; + if (!ctn->fingerprint) { + /* if ctn has no fingerprint, it will match any certificate */ + fingerprint_match = 1; + } else { + fingerprint_value = nc_server_tls_fingerprint_get_value(ctn->fingerprint); + if (!fingerprint_value) { + WRN(NULL, "Invalid fingerprint format used (%s), skipping.", ctn->fingerprint); + rc = 1; goto cleanup; } - if (!ctn->fingerprint) { - /* if ctn has no fingerprint, it will match any certificate */ - fingerprint_match = 1; - - /* MD5 */ - } else if (!strncmp(ctn->fingerprint, "01", 2)) { - digest_md5 = nc_server_tls_md5(cert); - if (!digest_md5) { - ret = -1; - goto cleanup; - } - - if (!strcasecmp(ctn->fingerprint + 3, digest_md5)) { - fingerprint_match = 1; - } - free(digest_md5); - - /* SHA-1 */ - } else if (!strncmp(ctn->fingerprint, "02", 2)) { - digest_sha1 = nc_server_tls_sha1(cert); - if (!digest_sha1) { - ret = -1; + /* try to match the fingerprint to any certificate in the chain */ + for (i = 0; i < cert_count; i++) { + if (!(cert = nc_tls_get_cert_wrap(cert_chain, i))) { + ERR(NULL, "Failed to get certificate from the chain."); + rc = -1; goto cleanup; } - if (!strcasecmp(ctn->fingerprint + 3, digest_sha1)) { - fingerprint_match = 1; - } - free(digest_sha1); - - /* SHA-224 */ - } else if (!strncmp(ctn->fingerprint, "03", 2)) { - digest_sha224 = nc_server_tls_sha224(cert); - if (!digest_sha224) { - ret = -1; - goto cleanup; - } - - if (!strcasecmp(ctn->fingerprint + 3, digest_sha224)) { - fingerprint_match = 1; - } - free(digest_sha224); - - /* SHA-256 */ - } else if (!strncmp(ctn->fingerprint, "04", 2)) { - digest_sha256 = nc_server_tls_sha256(cert); - if (!digest_sha256) { - ret = -1; - goto cleanup; - } - - if (!strcasecmp(ctn->fingerprint + 3, digest_sha256)) { - fingerprint_match = 1; - } - free(digest_sha256); - - /* SHA-384 */ - } else if (!strncmp(ctn->fingerprint, "05", 2)) { - digest_sha384 = nc_server_tls_sha384(cert); - if (!digest_sha384) { - ret = -1; + rc = nc_server_tls_fingerprint_get_digest(cert, ctn->fingerprint, &digest_hex); + if (rc) { + if (rc == 1) { + WRN(NULL, "Unsupported fingerprint algorithm used (%s), skipping.", ctn->fingerprint); + } goto cleanup; } - if (!strcasecmp(ctn->fingerprint + 3, digest_sha384)) { + if (!strcasecmp(fingerprint_value, digest_hex)) { + /* found a match */ fingerprint_match = 1; + break; } - free(digest_sha384); - /* SHA-512 */ - } else if (!strncmp(ctn->fingerprint, "06", 2)) { - digest_sha512 = nc_server_tls_sha512(cert); - if (!digest_sha512) { - ret = -1; - goto cleanup; - } + free(digest_hex); + digest_hex = NULL; + } + } + if (!fingerprint_match) { + /* no match found */ + rc = 1; + goto cleanup; + } - if (!strcasecmp(ctn->fingerprint + 3, digest_sha512)) { - fingerprint_match = 1; - } - free(digest_sha512); + VRB(NULL, "Cert verify CTN: entry with a matching fingerprint found."); - /* unknown */ - } else { - WRN(NULL, "Unknown fingerprint algorithm used (%s), skipping.", ctn->fingerprint); - continue; + /* found a matching fingerprint, + * try to obtain a username from the cert chain starting from the peer cert going up to the root */ + for (i = 0; i < cert_count; i++) { + if (!(cert = nc_tls_get_cert_wrap(cert_chain, i))) { + ERR(NULL, "Failed to get certificate from the chain."); + rc = -1; + goto cleanup; } - if (fingerprint_match) { - /* found a fingerprint match, try to obtain the username */ - VRB(NULL, "Cert verify CTN: entry with a matching fingerprint found."); - DBG(NULL, "Cert verify CTN: matched fingerprint: %s (id %" PRIu32 ").", ctn->fingerprint, ctn->id); - ret = nc_server_tls_get_username(cert, ctn, username); - if (ret == -1) { - /* fatal error */ - goto cleanup; - } else if (!ret) { - /* username found */ - goto cleanup; - } + rc = nc_server_tls_get_username(cert, ctn, username); + if (rc == -1) { + /* fatal error */ + goto cleanup; + } else if (!rc) { + /* username found */ + break; } } cleanup: - return ret; + free(digest_hex); + return rc; } +/** + * @brief Resolve username from cert-to-name entries of endpoint and referenced endpoint. + * + * @param[in] opts TLS options of the endpoint. + * @param[in] cert_chain Presented certificate chain, peer certificate first. + * @param[out] username Resolved username. + * @return 0 on success, 1 if no entry matched, -1 on error. + */ static int _nc_server_tls_cert_to_name(struct nc_server_tls_opts *opts, void *cert_chain, char **username) { - int ret = 1; + int rc = 1; struct nc_endpt *referenced_endpt; struct nc_ctn *ctn; for (ctn = opts->ctn; ctn; ctn = ctn->next) { - ret = nc_server_tls_cert_to_name(ctn, cert_chain, username); - if (ret != 1) { + rc = nc_server_tls_cert_to_name(ctn, cert_chain, username); + if (rc != 1) { /* fatal error or success */ goto cleanup; } @@ -499,13 +524,13 @@ _nc_server_tls_cert_to_name(struct nc_server_tls_opts *opts, void *cert_chain, c if (opts->referenced_endpt_name) { if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) { ERRINT; - ret = -1; + rc = -1; goto cleanup; } for (ctn = referenced_endpt->opts.tls->ctn; ctn; ctn = ctn->next) { - ret = nc_server_tls_cert_to_name(ctn, cert_chain, username); - if (ret != 1) { + rc = nc_server_tls_cert_to_name(ctn, cert_chain, username); + if (rc != 1) { /* fatal error or success */ goto cleanup; } @@ -513,13 +538,13 @@ _nc_server_tls_cert_to_name(struct nc_server_tls_opts *opts, void *cert_chain, c } cleanup: - return ret; + return rc; } static int _nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_client_auth *client_auth) { - int ret; + int rc; void *cert; struct nc_certificate *certs; uint32_t i, cert_count = 0; @@ -541,9 +566,9 @@ _nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_client_aut cert = nc_base64der_to_cert(certs[i].data); /* compare stored with received */ - ret = nc_server_tls_certs_match_wrap(peer_cert, cert); + rc = nc_server_tls_certs_match_wrap(peer_cert, cert); nc_tls_cert_destroy_wrap(cert); - if (ret) { + if (rc) { /* found a match */ VRB(NULL, "Cert verify: fail, but the end-entity certificate is trusted, continuing."); return 0; @@ -582,7 +607,7 @@ nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_opts *opts) int nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_verify_cb_data *cb_data) { - int ret = 0; + int rc = 0; char *subject = NULL, *issuer = NULL; struct nc_server_tls_opts *opts = cb_data->opts; struct nc_session *session = cb_data->session; @@ -597,7 +622,7 @@ nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_veri issuer = nc_server_tls_get_issuer_wrap(cert); if (!subject || !issuer) { ERR(session, "Failed to get certificate's subject or issuer."); - ret = -1; + rc = -1; goto cleanup; } @@ -609,8 +634,8 @@ nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_veri if (!trusted) { /* peer cert is not trusted, so it must match any configured end-entity cert * on the given endpoint in order for the client to be authenticated */ - ret = nc_server_tls_verify_peer_cert(cert, opts); - if (ret) { + rc = nc_server_tls_verify_peer_cert(cert, opts); + if (rc) { ERR(session, "Cert verify: fail (Client certificate not trusted and does not match any configured end-entity certificate)."); goto cleanup; } @@ -620,8 +645,8 @@ nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_veri * the whole chain is needed in order to comply with the following issue: * https://github.com/CESNET/netopeer2/issues/1596 */ - ret = _nc_server_tls_cert_to_name(opts, cert_chain, &session->username); - if (ret == -1) { + rc = _nc_server_tls_cert_to_name(opts, cert_chain, &session->username); + if (rc == -1) { /* fatal error */ goto cleanup; } @@ -630,13 +655,13 @@ nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_veri VRB(NULL, "Cert verify CTN: new client username recognized as \"%s\".", session->username); } else { VRB(NULL, "Cert verify CTN: unsuccessful, dropping the new client."); - ret = 1; + rc = 1; goto cleanup; } if (server_opts.user_verify_clb && !server_opts.user_verify_clb(session)) { VRB(session, "Cert verify: user verify callback revoked authorization."); - ret = 1; + rc = 1; goto cleanup; } } @@ -644,7 +669,7 @@ nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_veri cleanup: free(subject); free(issuer); - return ret; + return rc; } API const void * diff --git a/src/session_wrapper.h b/src/session_wrapper.h index 53248205..e27dd3ca 100644 --- a/src/session_wrapper.h +++ b/src/session_wrapper.h @@ -318,9 +318,9 @@ int nc_tls_get_num_certs_wrap(void *chain); * * @param[in] chain Certificate chain. * @param[in] idx Index of the certificate to get. - * @param[out] cert Certificate. + * @return Certificate or NULL if not found. */ -void nc_tls_get_cert_wrap(void *chain, int idx, void **cert); +void *nc_tls_get_cert_wrap(void *chain, int idx); /** * @brief Compare two certificates. diff --git a/tests/data/client_SAN.crt b/tests/data/client_SAN.crt new file mode 100644 index 00000000..e1d9111b --- /dev/null +++ b/tests/data/client_SAN.crt @@ -0,0 +1,31 @@ +# Client certificate with SAN +# Issued by: serverca.pem +# Contains SAN rfc822Name field: +# email:user@example.com +-----BEGIN CERTIFICATE----- +MIIEiDCCA3CgAwIBAgIUA3C1F5hKvDFHVFjJOibPZb0hy2YwDQYJKoZIhvcNAQEL +BQAwYzELMAkGA1UEBhMCQ1oxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAcM +BEJybm8xDzANBgNVBAoMBkNFU05FVDEMMAoGA1UECwwDVE1DMREwDwYDVQQDDAhz +ZXJ2ZXJjYTAeFw0yNjA0MjMxMTI5MDNaFw0zNjA0MjAxMTI5MDNaMC0xCzAJBgNV +BAYTAlVTMQ0wCwYDVQQKDARUZXN0MQ8wDQYDVQQDDAZjbGllbnQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC+kqPqDL9GbWmqVQhp4qla4vYo4kFuh2HG +48b7RLp/4l/guik4Hvq2aDVFD9sBcs3FeQbjoLH1Q4doUr8jG6VwJsfovE5SD3T8 +dVLs2dtpW0OyXTccb0I9lOVDMz6f6IUBe/m5vk8XNbdUII0NJ8y1dQ51VH3e784B +zu7PSdaMaFac4fkw8kJA9LxkWkv2FDFC7IBcVjRgtb/15EwODH849O6+VPEgX5gd +ozNj5bL45rKDBvvx0KjD7dFBGAIHbSjmjp7HHadfYKqvtQnMb83fRcK6wohxNP3v +y13wBTtSOlvOg16Gb+2ZB0Or7wgOw19ZvEIcNgswPwhHfZQNcYMNVLCu02BSzwdY +00IEqNM0J5B/W//KJF3uFF/ZqP7D2wO7w8j5UL2lpuxF7YrGecNT1Kr7ggHdkzcL +lekkNu7wNWKAZlJ6+Kbun8PqZbamGXLG2Ur1aZDkyKyD9nyMzsRneAlxO+HbQhLo +jimUbsMm+wgL89zchLYkL/DSmsJlryA4qhvzHaaONBw4DV5UhSbLDpZtXVfrn+MA +m8hLpqf+gUCThsFN8kDp9h9Tg9v01ai9jGb941HkFtGYUWHS5drSz3ZLnSI6i1/w +Kdbs9ns9YqDMqq2c305v5h7taMN0b40KuGSIME4K4cOsdfCprFYGZQgKjaadQwrs +m4k1Jl7kMwIDAQABo2owaDAJBgNVHRMEAjAAMBsGA1UdEQQUMBKBEHVzZXJAZXhh +bXBsZS5jb20wHQYDVR0OBBYEFFwfPABHRV+vIKK2i9FILUKriVrLMB8GA1UdIwQY +MBaAFOtJyeKtyLXH2jjgYyUqmnQD4jIPMA0GCSqGSIb3DQEBCwUAA4IBAQBF/9Ul +LYNNO40+2GWktnwlJx47UFGbP4NIAzN91qpmuLg7wVEfq+R5S0Ckr7RsHqbxunzW +UrX3YO4OvAL5GpqteySGp00XOqs4jhf5ZzlSiBY84qrir57xltJ0clOSG2XZpDqq +MH7A8stWqDJQx74SQK1FrFWpHA91BZjrfjOTk1JIcBl35MIyHGbmv6JMpERrsSU3 ++mmUcAfqeKXXppg/q75ZCCXtqJebUvBMCpLCnTYADmLCYs8M89dR8Mi2TVPlWxaX +McN6UtMRbvBC75aiv7QUuFFMNN85d34qF9Taj1PgkOaX+zD6ByKbR+wLEWgCNEHC +wzxZsDERZ3SA2VJz +-----END CERTIFICATE----- diff --git a/tests/test_tls.c b/tests/test_tls.c index 5900b006..ed70e9d5 100644 --- a/tests/test_tls.c +++ b/tests/test_tls.c @@ -972,6 +972,139 @@ test_tls_version(void **state) #endif } +/* + * Test CTN SAN: add end-entity cert with SAN rfc822Name field and check that the username is resolved from the SAN if: + * - the CTN entry matches the client cert's fingerprint + * - the CTN entry doesn't match the client cert's fingerprint but matches the client CA cert's fingerprint +*/ + +void * +server_thread_ctn_san(void *arg) +{ + int ret; + NC_MSG_TYPE msgtype; + struct nc_session *session = NULL; + struct nc_pollsession *ps = NULL; + struct ln2_test_ctx *test_ctx = arg; + const char *client_username; + + ps = nc_ps_new(); + assert_non_null(ps); + + /* wait for the client to be ready to connect */ + pthread_barrier_wait(&test_ctx->barrier); + + /* accept a session and add it to the poll session structure */ + msgtype = nc_accept(NC_ACCEPT_TIMEOUT, test_ctx->ctx, &session); + assert_int_equal(msgtype, NC_MSG_HELLO); + + /* the username should be resolved from the client cert's SAN rfc822Name field */ + client_username = nc_session_get_username(session); + assert_string_equal(client_username, "user@example.com"); + + ret = nc_ps_add_session(ps, session); + assert_int_equal(ret, 0); + + /* poll until the session is terminated by the client */ + do { + ret = nc_ps_poll(ps, NC_PS_POLL_TIMEOUT, NULL); + assert_int_equal(ret & NC_PSPOLL_RPC, NC_PSPOLL_RPC); + } while (!(ret & NC_PSPOLL_SESSION_TERM)); + + nc_ps_clear(ps, 1, NULL); + nc_ps_free(ps); + return NULL; +} + +static void * +client_thread_ctn_san(void *arg) +{ + int ret; + struct nc_session *session = NULL; + struct ln2_test_ctx *test_ctx = arg; + struct test_tls_data *test_data = test_ctx->test_data; + + ret = nc_client_set_schema_searchpath(MODULES_DIR); + assert_int_equal(ret, 0); + + /* set client cert */ + ret = nc_client_tls_set_cert_key_paths(TESTS_DIR "/data/client_SAN.crt", TESTS_DIR "/data/client.key"); + assert_int_equal(ret, 0); + + /* set client ca */ + ret = nc_client_tls_set_trusted_ca_paths(NULL, TESTS_DIR "/data"); + assert_int_equal(ret, 0); + + pthread_barrier_wait(&test_ctx->barrier); + session = nc_connect_tls("127.0.0.1", TEST_PORT, NULL); + + if (test_data->expect_fail) { + /* the connection is expected to fail */ + assert_null(session); + return NULL; + } + + assert_non_null(session); + + nc_session_free(session, NULL); + return NULL; +} + +static void +test_nc_tls_ctn_san(void **state) +{ + int ret, i; + pthread_t tids[2]; + struct ln2_test_ctx *test_ctx; + struct test_tls_data *test_data; + struct lyd_node *tree; + + assert_non_null(state); + test_ctx = *state; + test_data = test_ctx->test_data; + tree = test_data->tree; + + /* create CTN entry matching the client cert's fingerprint */ + ret = nc_server_config_add_tls_ctn(test_ctx->ctx, "endpt", 1, + "02:EC:08:61:BE:B4:5F:23:FF:47:7F:E4:C5:5D:81:0E:D3:C4:A5:97:0A", NC_TLS_CTN_SAN_RFC822_NAME, NULL, &tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_setup_data(tree); + assert_int_equal(ret, 0); + + /* username should be resolved from the SAN rfc822Name field of the client certificate */ + ret = pthread_create(&tids[0], NULL, client_thread_ctn_san, *state); + assert_int_equal(ret, 0); + ret = pthread_create(&tids[1], NULL, server_thread_ctn_san, *state); + assert_int_equal(ret, 0); + + for (i = 0; i < 2; i++) { + pthread_join(tids[i], NULL); + } + + /* delete the CTN entry */ + ret = nc_server_config_del_tls_ctn("endpt", 1, &tree); + assert_int_equal(ret, 0); + + /* create CTN entry matching the client CA cert's fingerprint */ + ret = nc_server_config_add_tls_ctn(test_ctx->ctx, "endpt", 1, + "02:E5:3F:E7:C7:28:8D:F8:3B:EC:AC:AB:85:AB:76:48:68:47:0D:B5:C3", NC_TLS_CTN_SAN_RFC822_NAME, NULL, &tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_setup_data(tree); + assert_int_equal(ret, 0); + + /* the username should still be resolved from the client cert's SAN */ + ret = pthread_create(&tids[0], NULL, client_thread_ctn_san, *state); + assert_int_equal(ret, 0); + ret = pthread_create(&tids[1], NULL, server_thread_ctn_san, *state); + assert_int_equal(ret, 0); + + for (i = 0; i < 2; i++) { + pthread_join(tids[i], NULL); + } +} + static void test_nc_tls_free_test_data(void *test_data) { @@ -1090,6 +1223,50 @@ keylog_setup_f(void **state) return setup_f(state); } +static int +setup_ctn_san(void **state) +{ + int ret; + struct lyd_node *tree = NULL; + struct ln2_test_ctx *test_ctx; + struct test_tls_data *test_data; + + ret = ln2_glob_test_setup(&test_ctx); + assert_int_equal(ret, 0); + + *state = test_ctx; + + /* create new address and port data */ + ret = nc_server_config_add_address_port(test_ctx->ctx, "endpt", NC_TI_TLS, "127.0.0.1", TEST_PORT, &tree); + assert_int_equal(ret, 0); + + /* create new server certificate data */ + ret = nc_server_config_add_tls_server_cert(test_ctx->ctx, "endpt", TESTS_DIR "/data/server.key", NULL, TESTS_DIR "/data/server.crt", &tree); + assert_int_equal(ret, 0); + + /* create new end entity client cert data */ + ret = nc_server_config_add_tls_client_cert(test_ctx->ctx, "endpt", "client_cert", TESTS_DIR "/data/client_SAN.crt", &tree); + assert_int_equal(ret, 0); + + /* create new client ca data */ + ret = nc_server_config_add_tls_ca_cert(test_ctx->ctx, "endpt", "client_ca", TESTS_DIR "/data/serverca.pem", &tree); + assert_int_equal(ret, 0); + + /* configure the server based on the data */ + ret = nc_server_config_setup_data(tree); + assert_int_equal(ret, 0); + + test_data = calloc(1, sizeof *test_data); + assert_non_null(test_data); + + test_data->tree = tree; + + test_ctx->test_data = test_data; + test_ctx->free_test_data = test_nc_tls_free_test_data; + + return 0; +} + int main(void) { @@ -1103,6 +1280,7 @@ main(void) cmocka_unit_test_setup_teardown(test_nc_tls_keylog, keylog_setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_cipher_suites, setup_f, ln2_glob_test_teardown), cmocka_unit_test_setup_teardown(test_tls_version, setup_f, ln2_glob_test_teardown), + cmocka_unit_test_setup_teardown(test_nc_tls_ctn_san, setup_ctn_san, ln2_glob_test_teardown), }; /* try to get ports from the environment, otherwise use the default */ From e2e43d2ef9ad05af90377dfb1a7244ff1442a461 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Thu, 23 Apr 2026 14:14:50 +0200 Subject: [PATCH 2/3] SOVERSION bump to version 5.3.11 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 719d4d13..8a729bc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,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 3) -set(LIBNETCONF2_MICRO_SOVERSION 10) +set(LIBNETCONF2_MICRO_SOVERSION 11) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) From d56829db4d8f2e0b34553a4b47b608e98b44ad19 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Thu, 23 Apr 2026 14:14:58 +0200 Subject: [PATCH 3/3] VERSION bump to version 4.3.5 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a729bc8..312b7f9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,7 +58,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 3) -set(LIBNETCONF2_MICRO_VERSION 4) +set(LIBNETCONF2_MICRO_VERSION 5) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library