/**
 * @file session_server_tls.c
 * @author Michal Vasko <mvasko@cesnet.cz>
 * @author Roman Janota <janota@cesnet.cz>
 * @brief libnetconf2 TLS server session manipulation functions
 *
 * @copyright
 * Copyright (c) 2015 - 2023 CESNET, z.s.p.o.
 *
 * This source code is licensed under BSD 3-Clause License (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://opensource.org/licenses/BSD-3-Clause
 */

#define _GNU_SOURCE

#include <errno.h>
#include <poll.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <curl/curl.h>

#include "compat.h"
#include "config.h"
#include "log_p.h"
#include "session.h"
#include "session_p.h"
#include "session_wrapper.h"

/**
 * @brief Get certificate and private key data from keystore.
 *
 * @param[in] referenced_key_name Name of the asymmetric key in the keystore.
 * @param[in] referenced_cert_name Name of the certificate in the keystore.
 * @param[out] privkey_data Retrieved private key data.
 * @param[out] privkey_type Retrieved private key type.
 * @param[out] cert_data Retrieved certificate data.
 * @return 0 on success, -1 on error.
 */
static int
nc_server_tls_ks_ref_get_cert_key(const char *referenced_key_name, const char *referenced_cert_name,
        char **privkey_data, enum nc_privkey_format *privkey_type, char **cert_data)
{
    LY_ARRAY_COUNT_TYPE i, j;
    struct nc_keystore *ks = &server_opts.config.keystore;

    *privkey_data = NULL;
    *cert_data = NULL;

    /* lookup key */
    LY_ARRAY_FOR(ks->entries, i) {
        if (!strcmp(referenced_key_name, ks->entries[i].asym_key.name)) {
            break;
        }
    }
    if (i == LY_ARRAY_COUNT(ks->entries)) {
        ERR(NULL, "Keystore asymmetric key entry \"%s\" not found.", referenced_key_name);
        return -1;
    }

    LY_ARRAY_FOR(ks->entries[i].certs, j) {
        if (!strcmp(referenced_cert_name, ks->entries[i].certs[j].name)) {
            break;
        }
    }
    if (j == LY_ARRAY_COUNT(ks->entries[i].certs)) {
        ERR(NULL, "Keystore certificate entry \"%s\" associated with the key \"%s\" not found.",
                referenced_cert_name, referenced_key_name);
        return -1;
    }

    *privkey_data = ks->entries[i].asym_key.privkey.data;
    *privkey_type = ks->entries[i].asym_key.privkey.type;
    *cert_data = ks->entries[i].certs[j].data;
    return 0;
}

/**
 * @brief Get certificates from truststore.
 *
 * @param[in] referenced_name Name of the certificate bag in the truststore.
 * @param[out] certs Retrieved certificates.
 * @param[out] cert_count Number of retrieved certificates.
 * @return 0 on success, -1 on error.
 */
static int
nc_server_tls_truststore_ref_get_certs(const char *referenced_name, struct nc_certificate **certs, uint32_t *cert_count)
{
    LY_ARRAY_COUNT_TYPE i;
    struct nc_truststore *ts = &server_opts.config.truststore;

    *certs = NULL;
    *cert_count = 0;

    /* lookup name */
    LY_ARRAY_FOR(ts->cert_bags, i) {
        if (!strcmp(referenced_name, ts->cert_bags[i].name)) {
            break;
        }
    }
    if (i == LY_ARRAY_COUNT(ts->cert_bags)) {
        ERR(NULL, "Truststore certificate bag \"%s\" not found.", referenced_name);
        return -1;
    }

    *certs = ts->cert_bags[i].certs;
    *cert_count = LY_ARRAY_COUNT(ts->cert_bags[i].certs);
    return 0;
}

static void *
nc_base64der_to_privkey(const char *in, const char *key_str)
{
    char *buf = NULL;
    void *pkey;

    NC_CHECK_ARG_RET(NULL, in, NULL);

    if (asprintf(&buf, "%s%s%s%s%s%s%s", "-----BEGIN", key_str, "PRIVATE KEY-----\n", in, "\n-----END",
            key_str, "PRIVATE KEY-----") == -1) {
        ERRMEM;
        return NULL;
    }

    pkey = nc_tls_pem_to_privkey_wrap(buf);
    free(buf);
    return pkey;
}

static char *
nc_server_tls_digest_to_hex(const unsigned char *digest, unsigned int digest_len)
{
    unsigned int i;
    char *hex;

    hex = malloc(digest_len * 3);
    NC_CHECK_ERRMEM_RET(!hex, NULL);

    for (i = 0; i < digest_len - 1; ++i) {
        sprintf(hex + (i * 3), "%02x:", digest[i]);
    }
    sprintf(hex + (i * 3), "%02x", digest[i]);

    return hex;
}

static char *
nc_server_tls_md5(void *cert)
{
    int rc;
    unsigned int buf_len = 16;
    unsigned char buf[buf_len];

    /* compute MD-5 hash of cert and store it in buf */
    rc = nc_server_tls_md5_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    return nc_server_tls_digest_to_hex(buf, buf_len);
}

static char *
nc_server_tls_sha1(void *cert)
{
    int rc;
    unsigned int buf_len = 20;
    unsigned char buf[buf_len];

    /* compute SHA-1 hash of cert and store it in buf */
    rc = nc_server_tls_sha1_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    return nc_server_tls_digest_to_hex(buf, buf_len);
}

static char *
nc_server_tls_sha224(void *cert)
{
    int rc;
    unsigned int buf_len = 28;
    unsigned char buf[buf_len];

    /* compute SHA-224 hash of cert and store it in buf */
    rc = nc_server_tls_sha224_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    return nc_server_tls_digest_to_hex(buf, buf_len);
}

static char *
nc_server_tls_sha256(void *cert)
{
    int rc;
    unsigned int buf_len = 32;
    unsigned char buf[buf_len];

    /* compute SHA-256 hash of cert and store it in buf */
    rc = nc_server_tls_sha256_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    return nc_server_tls_digest_to_hex(buf, buf_len);
}

static char *
nc_server_tls_sha384(void *cert)
{
    int rc;
    unsigned int buf_len = 48;
    unsigned char buf[buf_len];

    /* compute SHA-384 hash of cert and store it in buf */
    rc = nc_server_tls_sha384_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    return nc_server_tls_digest_to_hex(buf, buf_len);
}

static char *
nc_server_tls_sha512(void *cert)
{
    int rc;
    unsigned int buf_len = 64;
    unsigned char buf[buf_len];

    /* compute SHA-512 hash of cert and store it in buf */
    rc = nc_server_tls_sha512_wrap(cert, buf);
    if (rc) {
        return NULL;
    }

    /* convert the hash to hex */
    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)
{
    char *subject, *cn, *san_value = NULL, rdn_separator;
    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);
    } else {
        sans = nc_tls_get_sans_wrap(cert);
        if (!sans) {
            WRN(NULL, "Certificate has no SANs or failed to retrieve them.");
            return 1;
        }
        nsans = nc_tls_get_num_sans_wrap(sans);

        for (i = 0; i < nsans; i++) {
            if ((rc = nc_tls_get_san_value_type_wrap(sans, i, &san_value, &san_type))) {
                if (rc == -1) {
                    /* fatal error */
                    nc_tls_sans_destroy_wrap(sans);
                    return -1;
                }

                /* got a type that we dont care about */
                continue;
            }

            if ((ctn->map_type == NC_TLS_CTN_SAN_ANY) || (ctn->map_type == san_type)) {
                /* found a match */
                *username = san_value;
                break;
            }
            free(san_value);
        }

        nc_tls_sans_destroy_wrap(sans);

        if (i == nsans) {
            switch (ctn->map_type) {
            case NC_TLS_CTN_SAN_RFC822_NAME:
                WRN(NULL, "Certificate does not include the SAN rfc822Name field.");
                break;
            case NC_TLS_CTN_SAN_DNS_NAME:
                WRN(NULL, "Certificate does not include the SAN dNSName field.");
                break;
            case NC_TLS_CTN_SAN_IP_ADDRESS:
                WRN(NULL, "Certificate does not include the SAN iPAddress field.");
                break;
            case NC_TLS_CTN_SAN_ANY:
                WRN(NULL, "Certificate does not include any relevant SAN fields.");
                break;
            default:
                break;
            }
            return 1;
        }
    }

    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 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 %" PRIu32 " not valid, skipping.", ctn->id);
        return 1;
    }

    DBG(NULL, "Cert verify CTN: checking entry with id %" PRIu32 ".", ctn->id);

    cert_count = nc_tls_get_num_certs_wrap(cert_chain);

    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;
        }

        /* 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;
            }

            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(fingerprint_value, digest_hex)) {
                /* found a match */
                fingerprint_match = 1;
                break;
            }

            free(digest_hex);
            digest_hex = NULL;
        }
    }
    if (!fingerprint_match) {
        /* no match found */
        rc = 1;
        goto cleanup;
    }

    VRB(NULL, "Cert verify CTN: entry with a matching fingerprint found.");

    /* 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;
        }

        rc = nc_server_tls_get_username(cert, ctn, username);
        if (rc == -1) {
            /* fatal error */
            goto cleanup;
        } else if (!rc) {
            /* username found */
            break;
        }
    }

cleanup:
    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 rc = 1;
    struct nc_endpt *referenced_endpt;
    struct nc_ctn *ctn;

    for (ctn = opts->ctn; ctn; ctn = ctn->next) {
        rc = nc_server_tls_cert_to_name(ctn, cert_chain, username);
        if (rc != 1) {
            /* fatal error or success */
            goto cleanup;
        }
    }

    /* do the same for referenced endpoint's ctn entries */
    if (opts->referenced_endpt_name) {
        if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) {
            ERRINT;
            rc = -1;
            goto cleanup;
        }

        for (ctn = referenced_endpt->opts.tls->ctn; ctn; ctn = ctn->next) {
            rc = nc_server_tls_cert_to_name(ctn, cert_chain, username);
            if (rc != 1) {
                /* fatal error or success */
                goto cleanup;
            }
        }
    }

cleanup:
    return rc;
}

static int
_nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_client_auth *client_auth)
{
    int rc;
    void *cert;
    struct nc_certificate *certs;
    uint32_t i, cert_count = 0;

    if (client_auth->ee_certs_store == NC_STORE_LOCAL) {
        /* local definition */
        certs = client_auth->ee_certs;
        cert_count = LY_ARRAY_COUNT(client_auth->ee_certs);
    } else if (client_auth->ee_certs_store == NC_STORE_TRUSTSTORE) {
        /* truststore reference */
        if (nc_server_tls_truststore_ref_get_certs(client_auth->ee_cert_bag_ts_ref, &certs, &cert_count)) {
            ERR(NULL, "Error getting end-entity certificates from the truststore reference \"%s\".", client_auth->ee_cert_bag_ts_ref);
            return -1;
        }
    }

    for (i = 0; i < cert_count; i++) {
        /* import stored cert */
        cert = nc_base64der_to_cert(certs[i].data);

        /* compare stored with received */
        rc = nc_server_tls_certs_match_wrap(peer_cert, cert);
        nc_tls_cert_destroy_wrap(cert);
        if (rc) {
            /* found a match */
            VRB(NULL, "Cert verify: fail, but the end-entity certificate is trusted, continuing.");
            return 0;
        }
    }

    return 1;
}

int
nc_server_tls_verify_peer_cert(void *peer_cert, struct nc_server_tls_opts *opts)
{
    int rc;
    struct nc_endpt *referenced_endpt;

    rc = _nc_server_tls_verify_peer_cert(peer_cert, &opts->client_auth);
    if (!rc) {
        return 0;
    }

    if (opts->referenced_endpt_name) {
        if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) {
            ERRINT;
            return -1;
        }

        rc = _nc_server_tls_verify_peer_cert(peer_cert, &referenced_endpt->opts.tls->client_auth);
        if (!rc) {
            return 0;
        }
    }

    return 1;
}

int
nc_server_tls_verify_cert(void *cert, int depth, int trusted, struct nc_tls_verify_cb_data *cb_data)
{
    int rc = 0;
    char *subject = NULL, *issuer = NULL;
    struct nc_server_tls_opts *opts = cb_data->opts;
    struct nc_session *session = cb_data->session;
    void *cert_chain = cb_data->chain;

    if (session->username) {
        /* already verified */
        return 0;
    }

    subject = nc_server_tls_get_subject_wrap(cert);
    issuer = nc_server_tls_get_issuer_wrap(cert);
    if (!subject || !issuer) {
        ERR(session, "Failed to get certificate's subject or issuer.");
        rc = -1;
        goto cleanup;
    }

    VRB(session, "Cert verify: depth %d.", depth);
    VRB(session, "Cert verify: subject: %s.", subject);
    VRB(session, "Cert verify: issuer: %s.", issuer);

    if (depth == 0) {
        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 */
            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;
            }
        }

        /* get username since we are at depth 0 and have the whole cert chain,
         * the whole chain is needed in order to comply with the following issue:
         * https://github.com/CESNET/netopeer2/issues/1596
         */
        rc = _nc_server_tls_cert_to_name(opts, cert_chain, &session->username);
        if (rc == -1) {
            /* fatal error */
            goto cleanup;
        }

        if (session->username) {
            VRB(NULL, "Cert verify CTN: new client username recognized as \"%s\".", session->username);
        } else {
            VRB(NULL, "Cert verify CTN: unsuccessful, dropping the new client.");
            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.");
            rc = 1;
            goto cleanup;
        }
    }

cleanup:
    free(subject);
    free(issuer);
    return rc;
}

API const void *
nc_session_get_client_cert(const struct nc_session *session)
{
    if (!session || (session->side != NC_SERVER)) {
        ERRARG(session, "session");
        return NULL;
    }

    return session->opts.server.client_cert;
}

API void
nc_server_tls_set_verify_clb(int (*verify_clb)(const struct nc_session *session))
{
    /* CONFIG LOCK */
    if (nc_rwlock_lock(&server_opts.config_lock, NC_RWLOCK_WRITE, NC_CONFIG_LOCK_TIMEOUT, __func__) != 1) {
        return;
    }

    server_opts.user_verify_clb = verify_clb;

    /* CONFIG UNLOCK */
    nc_rwlock_unlock(&server_opts.config_lock, __func__);
}

int
nc_server_tls_load_server_cert_key(struct nc_server_tls_opts *opts, void **srv_cert, void **srv_pkey)
{
    char *privkey_data = NULL, *cert_data = NULL;
    enum nc_privkey_format privkey_type;
    void *cert = NULL;
    void *pkey = NULL;

    *srv_cert = *srv_pkey = NULL;

    /* get data needed for setting the server cert */
    if (opts->cert_store == NC_STORE_LOCAL) {
        /* local definition */
        cert_data = opts->local.cert.data;
        privkey_data = opts->local.key.privkey.data;
        privkey_type = opts->local.key.privkey.type;
    } else if (opts->cert_store == NC_STORE_KEYSTORE) {
        /* keystore */
        if (nc_server_tls_ks_ref_get_cert_key(opts->keystore.asym_key_ref, opts->keystore.cert_ref, &privkey_data, &privkey_type, &cert_data)) {
            ERR(NULL, "Getting server certificate from the keystore reference \"%s\" failed.", opts->keystore.asym_key_ref);
            return 1;
        }
    }
    if (!cert_data || !privkey_data) {
        ERR(NULL, "Server certificate not configured.");
        return 1;
    }

    cert = nc_base64der_to_cert(cert_data);
    if (!cert) {
        return 1;
    }

    pkey = nc_base64der_to_privkey(privkey_data, nc_privkey_format_to_str(privkey_type));
    if (!pkey) {
        nc_tls_cert_destroy_wrap(cert);
        return 1;
    }

    *srv_cert = cert;
    *srv_pkey = pkey;
    return 0;
}

int
nc_server_tls_load_trusted_certs(struct nc_server_tls_client_auth *client_auth, void *cert_store)
{
    struct nc_certificate *certs;
    uint32_t i, cert_count = 0;
    void *cert;

    if (client_auth->ca_certs_store == NC_STORE_LOCAL) {
        /* local definition */
        certs = client_auth->ca_certs;
        cert_count = LY_ARRAY_COUNT(client_auth->ca_certs);
    } else if (client_auth->ca_certs_store == NC_STORE_TRUSTSTORE) {
        /* truststore */
        if (nc_server_tls_truststore_ref_get_certs(client_auth->ca_cert_bag_ts_ref, &certs, &cert_count)) {
            ERR(NULL, "Error getting certificate-authority certificates from the truststore reference \"%s\".", client_auth->ca_cert_bag_ts_ref);
            return 1;
        }
    }

    for (i = 0; i < cert_count; i++) {
        /* parse data into cert */
        cert = nc_base64der_to_cert(certs[i].data);
        if (!cert) {
            return 1;
        }

        /* store cert in cert store */
        if (nc_tls_add_cert_to_store_wrap(cert, cert_store)) {
            nc_tls_cert_destroy_wrap(cert);
            return 1;
        }
    }

    return 0;
}

static int
nc_server_tls_accept_check(int accept_ret, void *tls_session)
{
    uint32_t verify;
    char *err;

    /* check certificate verification result */
    verify = nc_tls_get_verify_result_wrap(tls_session);
    if (!verify && (accept_ret == 1)) {
        VRB(NULL, "Client certificate verified.");
    } else if (verify) {
        err = nc_tls_verify_error_string_wrap(verify);
        ERR(NULL, "Client certificate error (%s).", err);
        free(err);
    }

    if (accept_ret != 1) {
        nc_server_tls_print_accept_err_wrap(accept_ret, tls_session);
    }

    return accept_ret;
}

/**
 * @brief Get the number of certificates in a certificate grouping.
 *
 * @param[in] certs_grp Certificate grouping to get the number of certificates from.
 * @return Number of certificates in the grouping, or -1 on error.
 */
static uint32_t
nc_server_tls_get_num_certs(struct nc_server_tls_client_auth *client_auth)
{
    uint32_t ca_count = 0, ee_count = 0;
    struct nc_certificate *certs;

    if (client_auth->ca_certs_store == NC_STORE_LOCAL) {
        ca_count = LY_ARRAY_COUNT(client_auth->ca_certs);
    } else if (client_auth->ca_certs_store == NC_STORE_TRUSTSTORE) {
        if (nc_server_tls_truststore_ref_get_certs(client_auth->ca_cert_bag_ts_ref, &certs, &ca_count)) {
            ERR(NULL, "Getting CA certificates from the truststore reference \"%s\" failed.", client_auth->ca_cert_bag_ts_ref);
            return -1;
        }
    }

    if (client_auth->ee_certs_store == NC_STORE_LOCAL) {
        ee_count += LY_ARRAY_COUNT(client_auth->ee_certs);
    } else if (client_auth->ee_certs_store == NC_STORE_TRUSTSTORE) {
        if (nc_server_tls_truststore_ref_get_certs(client_auth->ee_cert_bag_ts_ref, &certs, &ee_count)) {
            ERR(NULL, "Getting end-entity certificates from the truststore reference \"%s\" failed.", client_auth->ee_cert_bag_ts_ref);
            return -1;
        }
    }

    return ca_count + ee_count;
}

int
nc_accept_tls_session(struct nc_session *session, struct nc_server_tls_opts *opts, int sock)
{
    int rc, timeouted = 0;
    struct timespec ts_timeout;
    struct nc_tls_verify_cb_data cb_data = {0};
    struct nc_endpt *referenced_endpt;
    void *tls_cfg, *srv_cert, *srv_pkey, *cert_store, *crl_store, *cipher_suites;
    uint32_t cert_count = 0;

    tls_cfg = srv_cert = srv_pkey = cert_store = crl_store = cipher_suites = NULL;

    /* set verify cb data */
    cb_data.session = session;
    cb_data.opts = opts;

    /* prepare TLS context from which a session will be created */
    tls_cfg = nc_tls_config_new_wrap(NC_SERVER);
    if (!tls_cfg) {
        goto fail;
    }

    /* opaque CA/CRL certificate store */
    cert_store = nc_tls_cert_store_new_wrap();
    if (!cert_store) {
        goto fail;
    }

    /* load server's key and certificate */
    if (nc_server_tls_load_server_cert_key(opts, &srv_cert, &srv_pkey)) {
        ERR(session, "Loading server certificate and/or private key failed.");
        goto fail;
    }

    /* load trusted CA certificates */
    if (nc_server_tls_load_trusted_certs(&opts->client_auth, cert_store)) {
        ERR(session, "Loading server CA certs failed.");
        goto fail;
    }

    /* load referenced endpoint's trusted CA certs if set */
    if (opts->referenced_endpt_name) {
        if (nc_server_endpt_get(opts->referenced_endpt_name, &referenced_endpt)) {
            ERR(session, "Referenced endpoint \"%s\" not found.", opts->referenced_endpt_name);
            goto fail;
        }

        if (nc_server_tls_load_trusted_certs(&referenced_endpt->opts.tls->client_auth, cert_store)) {
            ERR(session, "Loading server CA certs from referenced endpoint failed.");
            goto fail;
        }
    }

    /* Check if there are no CA/end entity certs configured, which is a valid config.
     * However, that would imply not using TLS for auth, which is not (yet) supported */
    if (!opts->referenced_endpt_name) {
        cert_count = nc_server_tls_get_num_certs(&opts->client_auth);
    } else {
        cert_count = nc_server_tls_get_num_certs(&opts->client_auth) + nc_server_tls_get_num_certs(&referenced_endpt->opts.tls->client_auth);
    }
    if (cert_count <= 0) {
        ERR(session, "Neither CA nor end-entity certificates configured.");
        goto fail;
    }

    if (nc_session_tls_crl_from_cert_ext_fetch(srv_cert, cert_store, &crl_store)) {
        ERR(session, "Loading server CRL failed.");
        goto fail;
    }

    /* set supported TLS versions */
    if (nc_server_tls_set_tls_versions_wrap(tls_cfg, opts->min_version, opts->max_version)) {
        ERR(session, "Setting supported server TLS versions failed.");
        goto fail;
    }

    /* set supported cipher suites if any, otherwise leave defaults */
    if (opts->cipher_suites) {
        if (nc_server_tls_set_cipher_suites_wrap(tls_cfg, opts->cipher_suites, &cipher_suites)) {
            goto fail;
        }
    }

    /* set verify flags, callback and its data */
    nc_server_tls_set_verify_wrap(tls_cfg, &cb_data);

    /* init TLS context and store data which may be needed later in it */
    if (nc_tls_init_ctx_wrap(srv_cert, srv_pkey, cert_store, crl_store, cipher_suites, &session->ti.tls.ctx)) {
        goto fail;
    }

    /* memory is managed by context now */
    srv_cert = srv_pkey = cert_store = crl_store = cipher_suites = NULL;

    /* setup config from ctx */
    if (nc_tls_setup_config_from_ctx_wrap(&session->ti.tls.ctx, tls_cfg)) {
        goto fail;
    }
    session->ti.tls.config = tls_cfg;
    tls_cfg = NULL;

    /* fill session data and create TLS session from config */
    session->ti_type = NC_TI_TLS;
    if (!(session->ti.tls.session = nc_tls_session_new_wrap(session->ti.tls.config))) {
        goto fail;
    }

    /* if keylog file is set, log the tls secrets there */
    if (server_opts.tls_keylog_file) {
        nc_tls_keylog_session_wrap(session->ti.tls.session);
    }

    /* set session fd */
    nc_tls_set_fd_wrap(session->ti.tls.session, sock, &session->ti.tls.ctx);

    sock = -1;

    /* do the handshake */
    nc_timeouttime_get(&ts_timeout, NC_TRANSPORT_HANDSHAKE_TIMEOUT);
    while ((rc = nc_server_tls_handshake_step_wrap(session->ti.tls.session)) == 0) {
        usleep(NC_TIMEOUT_STEP);
        if (nc_timeouttime_cur_diff(&ts_timeout) < 1) {
            ERR(session, "TLS accept timeout.");
            timeouted = 1;
            goto fail;
        }
    }

    /* check if handshake was ok */
    if (nc_server_tls_accept_check(rc, session->ti.tls.session) != 1) {
        goto fail;
    }

    /* post-handshake CRL verification: download CRLs for peer certs and verify the full chain */
    if (nc_session_tls_crl_verify_post_handshake(session->ti.tls.session,
            nc_tls_get_cert_store_wrap(session->ti.tls.config, &session->ti.tls.ctx),
            nc_tls_get_crl_store_wrap(session->ti.tls.config, &session->ti.tls.ctx))) {
        ERR(session, "TLS accept failed (post-handshake CRL verification).");
        goto fail;
    }

    return 1;

fail:
    if (sock > -1) {
        close(sock);
    }

    nc_tls_config_destroy_wrap(tls_cfg);
    nc_tls_cert_destroy_wrap(srv_cert);
    nc_tls_privkey_destroy_wrap(srv_pkey);
    nc_tls_cert_store_destroy_wrap(cert_store);
    nc_tls_crl_store_destroy_wrap(crl_store);
    free(cipher_suites);

    if (timeouted) {
        return 0;
    } else {
        return -1;
    }
}
