diff --git a/CMakeLists.txt b/CMakeLists.txt index 9913b731..8726e742 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,11 +213,20 @@ endif() # put all binaries into one directory (even from subprojects) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) -# dependencies - OpenSSL (required by later libnetconf2 checks and not really the server itself) -find_package(OpenSSL 3.0.0) -if(OPENSSL_FOUND) - list(APPEND CMAKE_REQUIRED_INCLUDES ${OPENSSL_INCLUDE_DIR}) - list(APPEND CMAKE_REQUIRED_LIBRARIES ${OPENSSL_LIBRARIES}) +# dependencies - SSL library (required by later libnetconf2 checks and not really the server itself) +find_package(MbedTLS 3.5.0) +if (MBEDTLS_FOUND) + # dependencies - mbedtls + set(HAVE_MBEDTLS TRUE) + list(APPEND CMAKE_REQUIRED_INCLUDES ${MBEDTLS_INCLUDE_DIRS}) + list(APPEND CMAKE_REQUIRED_LIBRARIES ${MBEDTLS_LIBRARIES}) +else() + # dependencies - OpenSSL + find_package(OpenSSL 3.0.0) + if(OPENSSL_FOUND) + list(APPEND CMAKE_REQUIRED_INCLUDES ${OPENSSL_INCLUDE_DIR}) + list(APPEND CMAKE_REQUIRED_LIBRARIES ${OPENSSL_LIBRARIES}) + endif() endif() # dependencies - libssh (also required by libnetconf2 checks) diff --git a/CMakeModules/FindMbedTLS.cmake b/CMakeModules/FindMbedTLS.cmake new file mode 100644 index 00000000..f982d910 --- /dev/null +++ b/CMakeModules/FindMbedTLS.cmake @@ -0,0 +1,110 @@ +# - Try to find MbedTLS +# Once done this will define +# +# MBEDTLS_FOUND - MbedTLS was found +# MBEDTLS_INCLUDE_DIRS - MbedTLS include directories +# MBEDTLS_LIBRARIES - link these to use MbedTLS +# MBEDTLS_VERSION - version of MbedTLS +# +# Author Roman Janota +# Copyright (c) 2025 CESNET, z.s.p.o. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +include(FindPackageHandleStandardArgs) + +if(MBEDTLS_LIBRARIES AND MBEDTLS_INCLUDE_DIRS) + # in cache already + set(MBEDTLS_FOUND TRUE) +else() + find_path(MBEDTLS_INCLUDE_DIR + NAMES + mbedtls/ssl.h + PATHS + /opt/local/include + /sw/include + ${CMAKE_INCLUDE_PATH} + ${CMAKE_INSTALL_PREFIX}/include + ) + + find_library(MBEDTLS_LIBRARY + NAMES + libmbedtls.so + PATHS + /usr/lib + /usr/lib64 + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib + ) + + find_library(MBEDX509_LIBRARY + NAMES + libmbedx509.so + PATHS + /usr/lib + /usr/lib64 + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib + ) + + find_library(MBEDCRYPTO_LIBRARY + NAMES + libmbedcrypto.so + PATHS + /usr/lib + /usr/lib64 + /opt/local/lib + /sw/lib + ${CMAKE_LIBRARY_PATH} + ${CMAKE_INSTALL_PREFIX}/lib + ) + + if(MBEDTLS_INCLUDE_DIR AND MBEDTLS_LIBRARY AND MBEDX509_LIBRARY AND MBEDCRYPTO_LIBRARY) + # learn MbedTLS version + if(EXISTS "${MBEDTLS_INCLUDE_DIR}/mbedtls/build_info.h") + file(STRINGS "${MBEDTLS_INCLUDE_DIR}/mbedtls/build_info.h" MBEDTLS_VERSION + REGEX "#define[ \t]+MBEDTLS_VERSION_STRING[ \t]+\"([0-9]+\.[0-9]+\.[0-9]+)\"") + string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" MBEDTLS_VERSION ${MBEDTLS_VERSION}) + endif() + if(NOT MBEDTLS_VERSION) + message(STATUS "MBEDTLS_VERSION not found, assuming MbedTLS is too old and cannot be used!") + set(MBEDTLS_INCLUDE_DIR "MBEDTLS_INCLUDE_DIR-NOTFOUND") + set(MBEDTLS_LIBRARY "MBEDTLS_LIBRARY-NOTFOUND") + endif() + endif() + + set(MBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR}) + set(MBEDTLS_LIBRARIES ${MBEDTLS_LIBRARY} ${MBEDX509_LIBRARY} ${MBEDCRYPTO_LIBRARY}) + + find_package_handle_standard_args(MbedTLS FOUND_VAR MBEDTLS_FOUND + REQUIRED_VARS MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARIES + VERSION_VAR MBEDTLS_VERSION) + + # show the MBEDTLS_INCLUDE_DIR and MBEDTLS_LIBRARIES variables only in the advanced view + mark_as_advanced(MBEDTLS_INCLUDE_DIRS MBEDTLS_LIBRARIES) +endif() diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 07340d52..c4055964 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -41,12 +41,18 @@ if(LIBNETCONF2_ENABLED_SSH_TLS) target_link_libraries(netopeer2-cli ${LIBSSH_LIBRARIES}) include_directories(${LIBSSH_INCLUDE_DIRS}) - # - openssl - if(NOT OPENSSL_FOUND) - message(FATAL_ERROR "libnetconf2 supports TLS but OpenSSL was not found, CLI compilation failed!") + # - SSL library + if (MBEDTLS_FOUND) + # - MbedTLS (has priority over OpenSSL) + target_link_libraries(netopeer2-cli ${MBEDTLS_LIBRARIES}) + include_directories(${MBEDTLS_INCLUDE_DIRS}) + elseif(OPENSSL_FOUND) + # - OpenSSL + target_link_libraries(netopeer2-cli ${OPENSSL_LIBRARIES}) + include_directories(${OPENSSL_INCLUDE_DIR}) + else() + message(FATAL_ERROR "libnetconf2 supports TLS but neither MbedTLS nor OpenSSL were found, CLI compilation failed!") endif() - target_link_libraries(netopeer2-cli ${OPENSSL_LIBRARIES}) - include_directories(${OPENSSL_INCLUDE_DIR}) endif() # compat checks diff --git a/cli/cli_config.h.in b/cli/cli_config.h.in index 3fb2505a..d50b8b90 100644 --- a/cli/cli_config.h.in +++ b/cli/cli_config.h.in @@ -18,3 +18,8 @@ #define CLI_VERSION "@NP2CLI_VERSION@" #define NC_CLI_PROMPT "@CLI_PROMPT@ " + +/** + * @brief Whether mbedTLS is used for TLS support. + */ +#cmakedefine HAVE_MBEDTLS diff --git a/cli/commands.c b/cli/commands.c index 677fbe78..0954512e 100644 --- a/cli/commands.c +++ b/cli/commands.c @@ -34,11 +34,6 @@ #include #include -#ifdef NC_ENABLED_SSH_TLS -# include -# include -#endif - #ifndef HAVE_EACCESS #define eaccess access #endif @@ -48,6 +43,16 @@ #include "completion.h" #include "configuration.h" +#ifdef NC_ENABLED_SSH_TLS +#ifdef HAVE_MBEDTLS +# include +# include +#else +# include +# include +#endif +#endif + #define CLI_CH_TIMEOUT 60 /* 1 minute */ #define CLI_RPC_REPLY_TIMEOUT 5 /* 5 seconds */ @@ -1819,6 +1824,211 @@ cp(const char *to, const char *from) return -1; } +#ifdef HAVE_MBEDTLS + +/** + * @brief Converts a Distinguished Name to a string. + * + * @param[in] dn Internal mbedTLS DN representation. + * @return DN string on success, NULL of fail. + */ +static char * +cli_dn2str(const mbedtls_x509_name *dn) +{ + char *str; + size_t len = 64; + int r; + void *tmp; + + str = malloc(len); + if (!str) { + ERROR("dn2str", "Memory allocation failed for DN string conversion"); + return NULL; + } + + while ((r = mbedtls_x509_dn_gets(str, len, dn)) == MBEDTLS_ERR_X509_BUFFER_TOO_SMALL) { + len <<= 1; + tmp = realloc(str, len); + if (!tmp) { + free(str); + ERROR("dn2str", "Memory reallocation failed for DN string conversion"); + return NULL; + } + str = tmp; + } + if (r < 1) { + free(str); + ERROR("dn2str", "Failed to convert DN to string"); + return NULL; + } + + return str; +} + +/** + * @brief Converts a single Subject Alternative Name (SAN) to a string. + * + * @param[in] san_buf Internal mbedTLS SAN representation. + * @return SAN string on success, NULL on failure. + */ +static char * +cli_san2str(const mbedtls_x509_buf *san_buf) +{ + char *buf; + + buf = malloc(san_buf->len + 1); + if (!buf) { + ERROR("san2str", "Memory allocation failed for SAN string conversion"); + return NULL; + } + memcpy(buf, san_buf->p, san_buf->len); + buf[san_buf->len] = '\0'; + return buf; +} + +static void +parse_cert(const char *name, const char *path) +{ + int r, has_san, first_san; + char *subject = NULL, *issuer = NULL, *san_buf = NULL; + size_t i; + mbedtls_x509_crt cert; + mbedtls_x509_sequence *san_list; + mbedtls_x509_subject_alternative_name san = {0}; + mbedtls_x509_sequence *cur; + const mbedtls_x509_buf *ip; + + mbedtls_x509_crt_init(&cert); + + r = mbedtls_x509_crt_parse_file(&cert, path); + if (r) { + ERROR("parse_cert", "Unable to parse certificate: %s", path); + goto cleanup; + } + + /* print the serial number */ + printf("-----%s----- serial: ", name); + for (i = 0; i < cert.serial.len; i++) { + printf("%02x", cert.serial.p[i]); + } + printf("\n"); + + /* print the subject */ + printf("Subject: "); + subject = cli_dn2str(&cert.subject); + if (!subject) { + goto cleanup; + } + printf("%s\n", subject); + + /* issuer */ + printf("Issuer: "); + issuer = cli_dn2str(&cert.issuer); + if (!issuer) { + goto cleanup; + } + printf("%s\n", issuer); + + /* validity to */ + printf("Valid until: "); + printf("%04d-%02d-%02d %02d:%02d:%02d", + cert.valid_to.year, cert.valid_to.mon, cert.valid_to.day, + cert.valid_to.hour, cert.valid_to.min, cert.valid_to.sec); + printf("\n"); + + /* parse Subject Alternative Names */ + has_san = 0; + first_san = 1; + + san_list = &cert.subject_alt_names; + if (san_list->buf.p) { + cur = san_list; + + while (cur) { + r = mbedtls_x509_parse_subject_alt_name(&cur->buf, &san); + if (r && (r != MBEDTLS_ERR_X509_FEATURE_UNAVAILABLE)) { + ERROR("parse_cert", "Failed to parse Subject Alternative Name: %s", path); + goto cleanup; + } + + switch (san.type) { + case MBEDTLS_X509_SAN_DNS_NAME: + if (!has_san) { + printf("X509v3 Subject Alternative Name:\n\t"); + has_san = 1; + } + if (!first_san) { + printf(", "); + } + first_san = 0; + san_buf = cli_san2str(&san.san.unstructured_name); + if (!san_buf) { + goto cleanup; + } + printf("DNS:%s", san_buf); + free(san_buf); + break; + case MBEDTLS_X509_SAN_RFC822_NAME: + if (!has_san) { + printf("X509v3 Subject Alternative Name:\n\t"); + has_san = 1; + } + if (!first_san) { + printf(", "); + } + first_san = 0; + san_buf = cli_san2str(&san.san.unstructured_name); + if (!san_buf) { + goto cleanup; + } + printf("RFC822:%s", san_buf); + free(san_buf); + break; + case MBEDTLS_X509_SAN_IP_ADDRESS: + if (!has_san) { + printf("X509v3 Subject Alternative Name:\n\t"); + has_san = 1; + } + if (!first_san) { + printf(", "); + } + first_san = 0; + ip = &san.san.unstructured_name; + if (ip->len == 4) { + printf("IP:%d.%d.%d.%d", ip->p[0], ip->p[1], ip->p[2], ip->p[3]); + } else if (ip->len == 16) { + printf("IP:"); + for (i = 0; i < ip->len; ++i) { + if ((i > 0) && (i < 15) && (i % 2 == 1)) { + printf("%02x:", ip->p[i]); + } else { + printf("%02x", ip->p[i]); + } + } + } + break; + default: + /* unsupported SAN type, skip */ + break; + } + mbedtls_x509_free_subject_alt_name(&san); + cur = cur->next; + } + } + + if (has_san) { + printf("\n"); + } + printf("\n"); + +cleanup: + mbedtls_x509_crt_free(&cert); + free(subject); + free(issuer); +} + +#else + static void parse_cert(const char *name, const char *path) { @@ -1862,11 +2072,7 @@ parse_cert(const char *name, const char *path) BIO_printf(bio_out, "\n"); BIO_printf(bio_out, "Valid until: "); -#if OPENSSL_VERSION_NUMBER < 0x10100000L // < 1.1.0 - ASN1_TIME_print(bio_out, X509_get_notAfter(cert)); -#else ASN1_TIME_print(bio_out, X509_get0_notAfter(cert)); -#endif BIO_printf(bio_out, "\n"); has_san = 0; @@ -1887,18 +2093,10 @@ parse_cert(const char *name, const char *path) first_san = 0; } if (san_name->type == GEN_EMAIL) { -#if OPENSSL_VERSION_NUMBER < 0x10100000L // < 1.1.0 - BIO_printf(bio_out, "RFC822:%s", (char *) ASN1_STRING_data(san_name->d.rfc822Name)); -#else BIO_printf(bio_out, "RFC822:%s", (char *) ASN1_STRING_get0_data(san_name->d.rfc822Name)); -#endif } if (san_name->type == GEN_DNS) { -#if OPENSSL_VERSION_NUMBER < 0x10100000L // < 1.1.0 - BIO_printf(bio_out, "DNS:%s", (char *) ASN1_STRING_data(san_name->d.dNSName)); -#else BIO_printf(bio_out, "DNS:%s", (char *) ASN1_STRING_get0_data(san_name->d.dNSName)); -#endif } if (san_name->type == GEN_IPADD) { BIO_printf(bio_out, "IP:"); @@ -1929,6 +2127,8 @@ parse_cert(const char *name, const char *path) fclose(fp); } +#endif + static int cmd_cert(const char *arg, char **UNUSED(tmp_config_file)) { diff --git a/scripts/common.sh b/scripts/common.sh index 9e83d18b..88d39c12 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -45,11 +45,11 @@ function SYSREPOCFG_GET_PATH() { # from env SYSREPOCFG="$SYSREPOCFG_EXECUTABLE" elif [ $(id -u) -eq 0 ] && [ -n "$USER" ] && [ $(command -v su) ]; then - # running as root, avoid problems with sudo PATH ("|| true" used because "set -e" is applied) - SYSREPOCFG=$(su -c 'command -v sysrepocfg' -l "$USER") || true + # running as root, avoid problems with sudo PATH + SYSREPOCFG=$(su -c 'command -v sysrepocfg' -l "$USER") else # normal user - SYSREPOCFG=$(command -v sysrepocfg) || true + SYSREPOCFG=$(command -v sysrepocfg) fi if [ -z "$SYSREPOCFG" ]; then @@ -65,10 +65,10 @@ function SYSREPOCTL_GET_PATH() { SYSREPOCTL="$SYSREPOCTL_EXECUTABLE" elif [ $(id -u) -eq 0 ] && [ -n "$USER" ] && [ $(command -v su) ]; then # running as root, avoid problems with sudo PATH - SYSREPOCTL=$(su -c 'command -v sysrepoctl' -l "$USER") || true + SYSREPOCTL=$(su -c 'command -v sysrepoctl' -l "$USER") else # normal user - SYSREPOCTL=$(command -v sysrepoctl) || true + SYSREPOCTL=$(command -v sysrepoctl) fi if [ -z "$SYSREPOCTL" ]; then @@ -77,21 +77,40 @@ function SYSREPOCTL_GET_PATH() { fi } -# get path to the openssl executable +# get path to the mbedtls executable - store in $MBEDTLS +function MBEDTLS_GET_PATH() { + if [ -n "$MBEDTLS_EXECUTABLE" ]; then + # from env + MBEDTLS="$MBEDTLS_EXECUTABLE" + elif [ $(id -u) -eq 0 ] && [ -n "$USER" ] && [ $(command -v su) ]; then + # running as root, avoid problems with sudo PATH + MBEDTLS=$(su -c 'command -v gen_key' -l "$USER") + else + # normal user + MBEDTLS=$(command -v gen_key) + fi +} + +# get path to the openssl executable - store in $OPENSSL function OPENSSL_GET_PATH() { if [ -n "$OPENSSL_EXECUTABLE" ]; then # from env OPENSSL="$OPENSSL_EXECUTABLE" elif [ $(id -u) -eq 0 ] && [ -n "$USER" ] && [ $(command -v su) ]; then # running as root, avoid problems with sudo PATH - OPENSSL=$(su -c 'command -v openssl' -l "$USER") || true + OPENSSL=$(su -c 'command -v openssl' -l "$USER") else # normal user - OPENSSL=$(command -v openssl) || true + OPENSSL=$(command -v openssl) fi +} - if [ -z "$OPENSSL" ]; then - echo "$0: Unable to find openssl executable." >&2 +# get paths to the crypto key generation executables - store in $MBEDTLS and $OPENSSL +function CRYPTO_KEYGEN_GET_PATHS() { + MBEDTLS_GET_PATH + OPENSSL_GET_PATH + if [ -z "$MBEDTLS" ] && [ -z "$OPENSSL" ]; then + echo "$0: Unable to find mbedtls nor openssl executable." >&2 exit 1 fi } diff --git a/scripts/merge_config.sh b/scripts/merge_config.sh index eddada7d..3f3420ed 100755 --- a/scripts/merge_config.sh +++ b/scripts/merge_config.sh @@ -6,9 +6,15 @@ set -e script_directory=$(dirname "$0") source "${script_directory}/common.sh" +# temporarily disable "set -e", the script will still exit if sysrepocfg is not found +set +e + # get path to sysrepocfg executable, this will be stored in $SYSREPOCFG SYSREPOCFG_GET_PATH +# re-enable "set -e" +set -e + # check that there is no listen/Call Home configuration yet SERVER_CONFIG=$($SYSREPOCFG -X -x "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint | /ietf-netconf-server:netconf-server/call-home/netconf-client") if [ -n "$SERVER_CONFIG" ]; then diff --git a/scripts/merge_hostkey.sh b/scripts/merge_hostkey.sh index fedc3a78..d08980cf 100755 --- a/scripts/merge_hostkey.sh +++ b/scripts/merge_hostkey.sh @@ -6,9 +6,19 @@ set -e script_directory=$(dirname "$0") source "${script_directory}/common.sh" -# get path to sysrepocfg and openssl executables, these will be stored in $SYSREPOCFG and $OPENSSL, respectively + +# temporarily disable "set -e", the script will still exit if any of the executables is not found +set +e + +# get path to sysrepocfg executable - stored in $SYSREPOCFG SYSREPOCFG_GET_PATH -OPENSSL_GET_PATH + +# get paths to crypto key generation executables - stored in $MBEDTLS and $OPENSSL +CRYPTO_KEYGEN_GET_PATHS + +# re-enable "set -e" +set -e + # check that there is no SSH key with this name yet, if so just exit KEYSTORE_KEY=$($SYSREPOCFG -X -x "/ietf-keystore:keystore/asymmetric-keys/asymmetric-key[name='genkey']") @@ -16,8 +26,39 @@ if [ -n "$KEYSTORE_KEY" ]; then exit 0 fi -# generate a new key -PRIVPEM=$($OPENSSL genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM 2>/dev/null) +# save the current umask and set it to 077, so that the private key is not readable by others +OLD_UMASK=$(umask) +umask 077 + +# attempt to generate a private key using mbedtls first +if [ -n "$MBEDTLS" ]; then + PRIVATE_KEY_FILE="netopeer2_key.pem" + if "$MBEDTLS" type=rsa rsa_keysize=2048 filename="$PRIVATE_KEY_FILE" format=pem 2>/dev/null; then + if [ -s "$PRIVATE_KEY_FILE" ]; then + # key generated successfully, read it + PRIVPEM=$(cat "$PRIVATE_KEY_FILE") + # clean up the file + rm -f "$PRIVATE_KEY_FILE" + fi + else + # cleanup the file on failure + echo "Failed to generate RSA key with mbedtls." >&2 + rm -f "$PRIVATE_KEY_FILE" + fi +fi + +# restore the original umask +umask "$OLD_UMASK" + +# if mbedtls failed or is not available, use openssl +if [ -z "$PRIVPEM" ] && [ -n "$OPENSSL" ]; then + PRIVPEM=$($OPENSSL genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -outform PEM 2>/dev/null) + if [ -z "$PRIVPEM" ]; then + echo "Failed to generate RSA key with openssl." >&2 + exit 1 + fi +fi + # remove header/footer and newlines PRIVKEY=$(echo "$PRIVPEM" | grep -v -- "-----" | tr -d "\n")