From 75ae7964010169a5a563d8c250d67701a330da5a Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 16 Oct 2026 13:59:23 +0200 Subject: [PATCH 1/3] session server tls BUGFIX map CN by attribute OID The common-name cert-to-name mapping searched the subject string for "CN=", so an earlier attribute such as OU=CN=root could set the username. Get the commonName by its OID instead, and skip SAN values containing a NUL character, which would otherwise get truncated. --- src/session_mbedtls.c | 76 +++++++++++++++-- src/session_openssl.c | 69 +++++++++++++-- src/session_p.h | 5 ++ src/session_server_tls.c | 31 +------ src/session_wrapper.h | 15 +++- tests/data/ctn/cn_ou_inject.crt | 27 ++++++ tests/data/ctn/san_nul.crt | 26 ++++++ tests/test_tls.c | 144 ++++++++++++++++++++++++++++++++ 8 files changed, 349 insertions(+), 44 deletions(-) create mode 100644 tests/data/ctn/cn_ou_inject.crt create mode 100644 tests/data/ctn/san_nul.crt diff --git a/src/session_mbedtls.c b/src/session_mbedtls.c index a7fa5fe7..d0bb4f09 100644 --- a/src/session_mbedtls.c +++ b/src/session_mbedtls.c @@ -184,6 +184,12 @@ nc_server_tls_dn2str(const mbedtls_x509_name *dn) NC_CHECK_ERRMEM_RET(!str, NULL); while ((r = mbedtls_x509_dn_gets(str, len, dn)) == MBEDTLS_ERR_X509_BUFFER_TOO_SMALL) { + /* the error is also returned for any attribute value longer than MBEDTLS_X509_MAX_DN_NAME_SIZE, + * which no buffer size can fix, so limit the growth */ + if (len >= NC_TLS_DN_STR_MAX_LEN) { + break; + } + len <<= 1; str = nc_realloc(str, len); NC_CHECK_ERRMEM_RET(!str, NULL); @@ -888,6 +894,52 @@ nc_server_tls_get_issuer_wrap(void *cert) return nc_server_tls_dn2str(&(((mbedtls_x509_crt *)cert)->issuer)); } +int +nc_server_tls_get_cn_wrap(void *cert, char **cn) +{ + const mbedtls_x509_name *iter, *cn_attr = NULL; + + *cn = NULL; + + for (iter = &((mbedtls_x509_crt *)cert)->subject; iter; iter = iter->next) { + if (!iter->oid.p) { + continue; + } + + /* skip attributes other than commonName */ + if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &iter->oid)) { + continue; + } + + if (cn_attr) { + /* the name would be ambiguous */ + WRN(NULL, "Certificate includes multiple commonName fields."); + return 1; + } + cn_attr = iter; + } + if (!cn_attr) { + WRN(NULL, "Certificate does not include the commonName field."); + return 1; + } + + /* the name must be in UTF-8 (RFC 6353), so only accept string types that are UTF-8 compatible */ + if ((cn_attr->val.tag != MBEDTLS_ASN1_UTF8_STRING) && (cn_attr->val.tag != MBEDTLS_ASN1_PRINTABLE_STRING) && + (cn_attr->val.tag != MBEDTLS_ASN1_IA5_STRING)) { + WRN(NULL, "Certificate commonName uses an unsupported string type."); + return 1; + } + if (!cn_attr->val.len) { + WRN(NULL, "Certificate commonName is empty."); + return 1; + } + + *cn = strndup((const char *)cn_attr->val.p, cn_attr->val.len); + NC_CHECK_ERRMEM_RET(!*cn, -1); + + return 0; +} + void * nc_tls_get_sans_wrap(void *cert) { @@ -922,7 +974,7 @@ nc_tls_get_san_value_type_wrap(void *sans, int idx, char **san_value, NC_TLS_CTN int i, rc, ret = 0; mbedtls_x509_sequence *iter; mbedtls_x509_subject_alternative_name san = {0}; - const mbedtls_x509_buf *ip; + const mbedtls_x509_buf *ip, *str; *san_value = NULL; *san_type = NC_TLS_CTN_UNKNOWN; @@ -942,20 +994,28 @@ nc_tls_get_san_value_type_wrap(void *sans, int idx, char **san_value, NC_TLS_CTN /* get its type and value */ switch (san.type) { case MBEDTLS_X509_SAN_DNS_NAME: - *san_type = NC_TLS_CTN_SAN_DNS_NAME; - *san_value = strndup((const char *)san.san.unstructured_name.p, san.san.unstructured_name.len); - NC_CHECK_ERRMEM_GOTO(!*san_value, ret = -1, cleanup); - break; case MBEDTLS_X509_SAN_RFC822_NAME: - *san_type = NC_TLS_CTN_SAN_RFC822_NAME; - *san_value = strndup((const char *)san.san.unstructured_name.p, san.san.unstructured_name.len); + str = &san.san.unstructured_name; + if (!str->len) { + WRN(NULL, "SAN value is empty, skipping."); + ret = 1; + goto cleanup; + } else if (memchr(str->p, '\0', str->len)) { + /* the value would get truncated */ + WRN(NULL, "SAN value includes a NUL character, skipping."); + ret = 1; + goto cleanup; + } + + *san_type = (san.type == MBEDTLS_X509_SAN_DNS_NAME) ? NC_TLS_CTN_SAN_DNS_NAME : NC_TLS_CTN_SAN_RFC822_NAME; + *san_value = strndup((const char *)str->p, str->len); NC_CHECK_ERRMEM_GOTO(!*san_value, ret = -1, cleanup); break; case MBEDTLS_X509_SAN_IP_ADDRESS: *san_type = NC_TLS_CTN_SAN_IP_ADDRESS; ip = &san.san.unstructured_name; if (ip->len == 4) { - rc = asprintf(san_value, "%d.%d.%d.%d", ip->p[0], ip->p[1], ip->p[2], ip->p[3]) == -1; + rc = asprintf(san_value, "%d.%d.%d.%d", ip->p[0], ip->p[1], ip->p[2], ip->p[3]); NC_CHECK_ERRMEM_GOTO(rc == -1, ret = -1, cleanup); } else if (ip->len == 16) { rc = asprintf(san_value, "%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", diff --git a/src/session_openssl.c b/src/session_openssl.c index e54bb3c0..46a4b10b 100644 --- a/src/session_openssl.c +++ b/src/session_openssl.c @@ -595,6 +595,51 @@ nc_server_tls_get_issuer_wrap(void *cert) return X509_NAME_oneline(X509_get_issuer_name(cert), NULL, 0); } +int +nc_server_tls_get_cn_wrap(void *cert, char **cn) +{ + int rc = 0, idx, len; + X509_NAME *subject; + X509_NAME_ENTRY *entry; + unsigned char *utf8 = NULL; + + *cn = NULL; + + /* find the index of the first commonName attribute in the subject, -1 means searching from the start */ + subject = X509_get_subject_name(cert); + idx = X509_NAME_get_index_by_NID(subject, NID_commonName, -1); + if (idx < 0) { + WRN(NULL, "Certificate does not include the commonName field."); + return 1; + } + + /* search again after the found attribute, another commonName would make the name ambiguous */ + if (X509_NAME_get_index_by_NID(subject, NID_commonName, idx) > -1) { + WRN(NULL, "Certificate includes multiple commonName fields."); + return 1; + } + + /* the name must be in UTF-8 (RFC 6353), so convert the value from whatever string type it is */ + entry = X509_NAME_get_entry(subject, idx); + len = ASN1_STRING_to_UTF8(&utf8, X509_NAME_ENTRY_get_data(entry)); + if (len < 0) { + WRN(NULL, "Certificate commonName could not be converted to UTF-8 (%s).", ERR_reason_error_string(ERR_get_error())); + return 1; + } + if (!len) { + WRN(NULL, "Certificate commonName is empty."); + rc = 1; + goto cleanup; + } + + *cn = strdup((char *)utf8); + NC_CHECK_ERRMEM_GOTO(!*cn, rc = -1, cleanup); + +cleanup: + OPENSSL_free(utf8); + return rc; +} + void * nc_tls_get_sans_wrap(void *cert) { @@ -618,8 +663,9 @@ nc_tls_get_san_value_type_wrap(void *sans, int idx, char **san_value, NC_TLS_CTN { int ret = 0; GENERAL_NAME *san; - int ip_length; + int ip_length, str_len; const unsigned char *ip_data; + ASN1_IA5STRING *str; *san_value = NULL; *san_type = NC_TLS_CTN_UNKNOWN; @@ -633,13 +679,22 @@ nc_tls_get_san_value_type_wrap(void *sans, int idx, char **san_value, NC_TLS_CTN /* get its type and value */ switch (san->type) { case GEN_EMAIL: - *san_type = NC_TLS_CTN_SAN_RFC822_NAME; - *san_value = strdup((char *)ASN1_STRING_get0_data(san->d.rfc822Name)); - NC_CHECK_ERRMEM_RET(!*san_value, -1); - break; case GEN_DNS: - *san_type = NC_TLS_CTN_SAN_DNS_NAME; - *san_value = strdup((char *)ASN1_STRING_get0_data(san->d.dNSName)); + str = (san->type == GEN_EMAIL) ? san->d.rfc822Name : san->d.dNSName; + str_len = ASN1_STRING_length(str); + if (!str_len) { + WRN(NULL, "SAN value is empty, skipping."); + ret = 1; + break; + } else if (memchr(ASN1_STRING_get0_data(str), '\0', str_len)) { + /* the value would get truncated */ + WRN(NULL, "SAN value includes a NUL character, skipping."); + ret = 1; + break; + } + + *san_type = (san->type == GEN_EMAIL) ? NC_TLS_CTN_SAN_RFC822_NAME : NC_TLS_CTN_SAN_DNS_NAME; + *san_value = strndup((char *)ASN1_STRING_get0_data(str), str_len); NC_CHECK_ERRMEM_RET(!*san_value, -1); break; case GEN_IPADD: diff --git a/src/session_p.h b/src/session_p.h index 09af892f..e9ec9e9d 100644 --- a/src/session_p.h +++ b/src/session_p.h @@ -232,6 +232,11 @@ extern struct nc_server_opts server_opts; */ #define NC_TLS_KEYLOGFILE_ENV "SSLKEYLOGFILE" +/** + * Maximum size of a buffer for a certificate DN string (MbedTLS). + */ +#define NC_TLS_DN_STR_MAX_LEN 65536 + #define NC_VERSION_10_ENDTAG "]]>]]>" #define NC_VERSION_10_ENDTAG_LEN 6 diff --git a/src/session_server_tls.c b/src/session_server_tls.c index df3ef834..2fed4aea 100644 --- a/src/session_server_tls.c +++ b/src/session_server_tls.c @@ -278,41 +278,16 @@ nc_server_tls_sha512(void *cert) static int nc_server_tls_get_username(void *cert, struct nc_ctn *ctn, char **username) { - char *subject, *cn, *san_value = NULL, rdn_separator; + char *san_value = NULL; void *sans; int i, nsans = 0, rc; NC_TLS_CTN_MAPTYPE san_type = 0; -#ifdef HAVE_LIBMEDTLS - rdn_separator = ','; -#else - rdn_separator = '/'; -#endif - if (ctn->map_type == NC_TLS_CTN_SPECIFIED) { *username = strdup(ctn->name); NC_CHECK_ERRMEM_RET(!*username, -1); } else if (ctn->map_type == NC_TLS_CTN_COMMON_NAME) { - subject = nc_server_tls_get_subject_wrap(cert); - if (!subject) { - return -1; - } - - cn = strstr(subject, "CN="); - if (!cn) { - WRN(NULL, "Certificate does not include the commonName field."); - free(subject); - return 1; - } - - /* skip "CN=" */ - cn += 3; - if (strchr(cn, rdn_separator)) { - *strchr(cn, rdn_separator) = '\0'; - } - *username = strdup(cn); - free(subject); - NC_CHECK_ERRMEM_RET(!*username, -1); + return nc_server_tls_get_cn_wrap(cert, username); } else { sans = nc_tls_get_sans_wrap(cert); if (!sans) { @@ -329,7 +304,7 @@ nc_server_tls_get_username(void *cert, struct nc_ctn *ctn, char **username) return -1; } - /* got a type that we dont care about */ + /* got a type that we dont care about or an unusable value */ continue; } diff --git a/src/session_wrapper.h b/src/session_wrapper.h index d9ac1418..86aced69 100644 --- a/src/session_wrapper.h +++ b/src/session_wrapper.h @@ -269,6 +269,17 @@ char * nc_server_tls_get_subject_wrap(void *cert); */ char * nc_server_tls_get_issuer_wrap(void *cert); +/** + * @brief Get the commonName of a certificate. + * + * @param[in] cert Certificate to get the commonName from. + * @param[out] cn Parsed commonName. + * @return 0 on success. + * @return 1 if no usable commonName was found. + * @return -1 on error. + */ +int nc_server_tls_get_cn_wrap(void *cert, char **cn); + /** * @brief Get the Subject Alternative Names of the certificate. * @@ -299,7 +310,9 @@ int nc_tls_get_num_sans_wrap(void *sans); * @param[in] idx Index of the SAN. * @param[out] san_value SAN value. * @param[out] san_type SAN type. - * @return 0 on success, non-zero on fail. + * @return 0 on success. + * @return 1 if the SAN is of an unsupported type or its value cannot be used. + * @return -1 on error. */ int nc_tls_get_san_value_type_wrap(void *sans, int idx, char **san_value, NC_TLS_CTN_MAPTYPE *san_type); diff --git a/tests/data/ctn/cn_ou_inject.crt b/tests/data/ctn/cn_ou_inject.crt new file mode 100644 index 00000000..c15ee6cb --- /dev/null +++ b/tests/data/ctn/cn_ou_inject.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUARWttiJibIoVz3/GNzxlnjL0xnMwDQYJKoZIhvcNAQEL +BQAwYzELMAkGA1UEBhMCQ1oxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAcM +BEJybm8xDzANBgNVBAoMBkNFU05FVDEMMAoGA1UECwwDVE1DMREwDwYDVQQDDAhz +ZXJ2ZXJjYTAeFw0yNjEwMTYxMDQzNDNaFw0zNjEwMTMxMDQzNDNaMDIxCzAJBgNV +BAYTAkNaMRAwDgYDVQQLDAdDTj1yb290MREwDwYDVQQDDAhvcGVyYXRvcjCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL6So+oMv0ZtaapVCGniqVri9iji +QW6HYcbjxvtEun/iX+C6KTge+rZoNUUP2wFyzcV5BuOgsfVDh2hSvyMbpXAmx+i8 +TlIPdPx1UuzZ22lbQ7JdNxxvQj2U5UMzPp/ohQF7+bm+Txc1t1QgjQ0nzLV1DnVU +fd7vzgHO7s9J1oxoVpzh+TDyQkD0vGRaS/YUMULsgFxWNGC1v/XkTA4Mfzj07r5U +8SBfmB2jM2PlsvjmsoMG+/HQqMPt0UEYAgdtKOaOnscdp19gqq+1Ccxvzd9FwrrC +iHE0/e/LXfAFO1I6W86DXoZv7ZkHQ6vvCA7DX1m8Qhw2CzA/CEd9lA1xgw1UsK7T +YFLPB1jTQgSo0zQnkH9b/8okXe4UX9mo/sPbA7vDyPlQvaWm7EXtisZ5w1PUqvuC +Ad2TNwuV6SQ27vA1YoBmUnr4pu6fw+pltqYZcsbZSvVpkOTIrIP2fIzOxGd4CXE7 +4dtCEuiOKZRuwyb7CAvz3NyEtiQv8NKawmWvIDiqG/Mdpo40HDgNXlSFJssOlm1d +V+uf4wCbyEump/6BQJOGwU3yQOn2H1OD2/TVqL2MZv3jUeQW0ZhRYdLl2tLPdkud +IjqLX/Ap1uz2ez1ioMyqrZzfTm/mHu1ow3RvjQq4ZIgwTgrhw6x18KmsVgZlCAqN +pp1DCuybiTUmXuQzAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQD +AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBRcHzwAR0VfryCitovR +SC1Cq4layzAfBgNVHSMEGDAWgBTrScnirci1x9o44GMlKpp0A+IyDzANBgkqhkiG +9w0BAQsFAAOCAQEARgeByaWSrw4hMvtzz70Y35V8MDOgF3XWQwvEsVKxB3AWQKb6 +89M58cwf6R1CbNB3Gq2HQBmk/F6WdX2zqlZnGWuWWoIXhIRLHYfo4rKlCjDxgprG +CFWsBwt6tC09bI3Q6kjFdkN5IR/KOzLciBPLSdY8/NivQymA4T/2tLWlJnSXcm+L +cW/kBs3jZ63pqDbPblz8LKR9hA2gVzQywcR7u6cZO7Ys1B7RoqsKfZBLfGoTyxLn +DwX8zHArbvrwxD041eO3Q+kV/ZsXzThJ15JeVpsOpHNd5UWHNgifVs0xU64Lammz +Ma1XE91LfZNWQgKgxILKD49UXKQH2zDOkbK9HQ== +-----END CERTIFICATE----- diff --git a/tests/data/ctn/san_nul.crt b/tests/data/ctn/san_nul.crt new file mode 100644 index 00000000..f943a916 --- /dev/null +++ b/tests/data/ctn/san_nul.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEcTCCA1mgAwIBAgIUM8HxiglexMFAeSvCsAJa7MsgLJMwDQYJKoZIhvcNAQEL +BQAwYzELMAkGA1UEBhMCQ1oxEzARBgNVBAgMClNvbWUtU3RhdGUxDTALBgNVBAcM +BEJybm8xDzANBgNVBAoMBkNFU05FVDEMMAoGA1UECwwDVE1DMREwDwYDVQQDDAhz +ZXJ2ZXJjYTAeFw0yNjEwMTYxMTI4MTlaFw0zNjEwMTMxMTI4MTlaMCAxCzAJBgNV +BAYTAkNaMREwDwYDVQQDDAhvcGVyYXRvcjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAL6So+oMv0ZtaapVCGniqVri9ijiQW6HYcbjxvtEun/iX+C6KTge ++rZoNUUP2wFyzcV5BuOgsfVDh2hSvyMbpXAmx+i8TlIPdPx1UuzZ22lbQ7JdNxxv +Qj2U5UMzPp/ohQF7+bm+Txc1t1QgjQ0nzLV1DnVUfd7vzgHO7s9J1oxoVpzh+TDy +QkD0vGRaS/YUMULsgFxWNGC1v/XkTA4Mfzj07r5U8SBfmB2jM2PlsvjmsoMG+/HQ +qMPt0UEYAgdtKOaOnscdp19gqq+1Ccxvzd9FwrrCiHE0/e/LXfAFO1I6W86DXoZv +7ZkHQ6vvCA7DX1m8Qhw2CzA/CEd9lA1xgw1UsK7TYFLPB1jTQgSo0zQnkH9b/8ok +Xe4UX9mo/sPbA7vDyPlQvaWm7EXtisZ5w1PUqvuCAd2TNwuV6SQ27vA1YoBmUnr4 +pu6fw+pltqYZcsbZSvVpkOTIrIP2fIzOxGd4CXE74dtCEuiOKZRuwyb7CAvz3NyE +tiQv8NKawmWvIDiqG/Mdpo40HDgNXlSFJssOlm1dV+uf4wCbyEump/6BQJOGwU3y +QOn2H1OD2/TVqL2MZv3jUeQW0ZhRYdLl2tLPdkudIjqLX/Ap1uz2ez1ioMyqrZzf +Tm/mHu1ow3RvjQq4ZIgwTgrhw6x18KmsVgZlCAqNpp1DCuybiTUmXuQzAgMBAAGj +YDBeMBwGA1UdEQQVMBOBEXJvb3QAQGV4YW1wbGUuY29tMB0GA1UdDgQWBBRcHzwA +R0VfryCitovRSC1Cq4layzAfBgNVHSMEGDAWgBTrScnirci1x9o44GMlKpp0A+Iy +DzANBgkqhkiG9w0BAQsFAAOCAQEAZ0sQEwJTTpwx3LaRfAb0Uq5yb2S18j16fdMj +UL3TZV2C7olUYiYHSjnb56vcZXaajb3dgVNgtjh3Hj7yWY3layzVtoF0AGMMkJhK +QVdEiGbaRsfsw4f5qafQbAg0bnvuzkoS4zArb8b2XOgIsElk/+9EAbGWkQmbRGFf +JIq2g347TG6c2ZcdmVB5uHOXTet9tHo6Afqd9Y7WCtHMAot1qgxdcWFkUVz29JOD +c98Awm3wk7Tylm1O1aZEC0ol31Tz9McA+4B+1t16ltAbnX0ZoVz9cj9cQkXyavmz +Qt4lLs8CmBNOLapnyvfiwUQFImYOsTzyglbUcvs+4gtrW3Qjwg== +-----END CERTIFICATE----- diff --git a/tests/test_tls.c b/tests/test_tls.c index e21e15bc..2f9fea4a 100644 --- a/tests/test_tls.c +++ b/tests/test_tls.c @@ -36,6 +36,8 @@ struct test_tls_data { int root_ca; /**< Whether the root CA is used (client-only). */ int intermediate_ca; /**< Whether the intermediate CA is used (client-only). */ int expect_fail; /**< Whether the connection is expected to fail. */ + const char *client_cert; /**< Client certificate path (CTN tests). */ + const char *username; /**< Expected username, NULL if no username can be resolved (CTN tests). */ }; int TEST_PORT = 10050; @@ -1106,6 +1108,146 @@ test_nc_tls_ctn_san(void **state) } } +/* + * Test CTN common name and SAN values: the username must be the value of the actual commonName attribute and + * never a part of another subject attribute, a SAN value with a NUL character must not be truncated into a name. + */ + +static void * +server_thread_ctn(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; + struct test_tls_data *test_data = test_ctx->test_data; + + ps = nc_ps_new(); + assert_non_null(ps); + + /* wait for the client to be ready to connect */ + pthread_barrier_wait(&test_ctx->barrier); + + msgtype = nc_accept(NC_ACCEPT_TIMEOUT, test_ctx->ctx, &session); + if (!test_data->username) { + /* no username can be resolved, the client must not be authenticated */ + assert_int_equal(msgtype, NC_MSG_ERROR); + assert_null(session); + nc_ps_free(ps); + return NULL; + } + assert_int_equal(msgtype, NC_MSG_HELLO); + + assert_string_equal(nc_session_get_username(session), test_data->username); + + 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, &session); + assert_int_equal(ret & NC_PSPOLL_RPC, NC_PSPOLL_RPC); + } while (!(ret & NC_PSPOLL_SESSION_TERM)); + + /* the terminated session was removed from ps and we own it now */ + nc_session_free(session, NULL); + nc_ps_free(ps); + return NULL; +} + +static void * +client_thread_ctn(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); + + /* all the CTN client certs share the key */ + ret = nc_client_tls_set_cert_key_paths(test_data->client_cert, TESTS_DIR "/data/client.key"); + assert_int_equal(ret, 0); + + 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->username) { + /* 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_connect(void **state, const char *client_cert, const char *username) +{ + int ret, i; + pthread_t tids[2]; + struct ln2_test_ctx *test_ctx = *state; + struct test_tls_data *test_data = test_ctx->test_data; + + test_data->client_cert = client_cert; + test_data->username = username; + + ret = pthread_create(&tids[0], NULL, client_thread_ctn, *state); + assert_int_equal(ret, 0); + ret = pthread_create(&tids[1], NULL, server_thread_ctn, *state); + assert_int_equal(ret, 0); + + for (i = 0; i < 2; i++) { + pthread_join(tids[i], NULL); + } +} + +static void +test_nc_tls_ctn_cn(void **state) +{ + int ret; + struct ln2_test_ctx *test_ctx = *state; + struct test_tls_data *test_data = test_ctx->test_data; + + /* 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_COMMON_NAME, NULL, &test_data->tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_setup_data(test_data->tree); + assert_int_equal(ret, 0); + + /* subject C=CZ, OU=CN=root, CN=operator, a preceding attribute value must not be taken for the commonName */ + test_nc_tls_ctn_connect(state, TESTS_DIR "/data/ctn/cn_ou_inject.crt", "operator"); +} + +static void +test_nc_tls_ctn_san_nul(void **state) +{ + int ret; + struct ln2_test_ctx *test_ctx = *state; + struct test_tls_data *test_data = test_ctx->test_data; + + /* 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, &test_data->tree); + assert_int_equal(ret, 0); + + ret = nc_server_config_setup_data(test_data->tree); + assert_int_equal(ret, 0); + + /* the only SAN is rfc822Name "root\0@example.com", it must not be truncated to "root" */ + test_nc_tls_ctn_connect(state, TESTS_DIR "/data/ctn/san_nul.crt", NULL); +} + static void test_nc_tls_free_test_data(void *test_data) { @@ -1282,6 +1424,8 @@ main(void) 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), + cmocka_unit_test_setup_teardown(test_nc_tls_ctn_cn, setup_ctn_san, ln2_glob_test_teardown), + cmocka_unit_test_setup_teardown(test_nc_tls_ctn_san_nul, setup_ctn_san, ln2_glob_test_teardown), }; /* try to get ports from the environment, otherwise use the default */ From 8025dd620a6592987488932251e602a99aeae80e Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 16 Oct 2026 14:05:44 +0200 Subject: [PATCH 2/3] SOVERSION bump to version 5.5.4 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6403a7e8..c19568c3 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 5) -set(LIBNETCONF2_MICRO_SOVERSION 3) +set(LIBNETCONF2_MICRO_SOVERSION 4) set(LIBNETCONF2_SOVERSION_FULL ${LIBNETCONF2_MAJOR_SOVERSION}.${LIBNETCONF2_MINOR_SOVERSION}.${LIBNETCONF2_MICRO_SOVERSION}) set(LIBNETCONF2_SOVERSION ${LIBNETCONF2_MAJOR_SOVERSION}) From b7a2689810b3233fbb344ec51bf3356e15c70619 Mon Sep 17 00:00:00 2001 From: Roman Janota Date: Fri, 16 Oct 2026 14:05:44 +0200 Subject: [PATCH 3/3] VERSION bump to version 4.6.6 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c19568c3..14fe8d37 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 6) -set(LIBNETCONF2_MICRO_VERSION 5) +set(LIBNETCONF2_MICRO_VERSION 6) set(LIBNETCONF2_VERSION ${LIBNETCONF2_MAJOR_VERSION}.${LIBNETCONF2_MINOR_VERSION}.${LIBNETCONF2_MICRO_VERSION}) # Version of the library