diff --git a/CMakeLists.txt b/CMakeLists.txt index 719d4d13..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 @@ -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}) 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 */