/** * @file test_cert_exp_notif.c * @author Roman Janota * @brief libnetconf2 certificate expiration notification test * * @copyright * Copyright (c) 2024 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 #include #include #include #include #include #include #include #include "ln2_test.h" #ifdef HAVE_MBEDTLS #include #include #include #include #include #include #else #include #include #include #include #include #endif #define VALID_CERT_ADD_SEC 15 /* should be enough even with valgrind on slower machines */ #define EXPIRED_CERT_ADD_SEC -10 /* leave a 2 second leeway at most for both sending and receiving */ #define NC_RECV_NOTIF_TIMEOUT (VALID_CERT_ADD_SEC + 2) * 1000 #define NC_SEND_NOTIF_TIMEOUT (VALID_CERT_ADD_SEC + 2) * 1000 struct ly_ctx *server_ctx, *client_ctx; struct test_state { pthread_barrier_t barrier; pthread_barrier_t ntf_barrier; struct nc_server_notif *ntf; struct lyd_node *tree; }; int TEST_PORT = 10050; const char *TEST_PORT_STR = "10050"; #ifdef HAVE_MBEDTLS static const char * mbedtls_strerr(int err) { const char *err_str; err_str = mbedtls_high_level_strerr(err); if (err_str) { return err_str; } err_str = mbedtls_low_level_strerr(err); if (err_str) { return err_str; } return "unknown error"; } static int custom_exp_date_cert_create(long offset_sec, char cert_path[12]) { int ret = 0, fd; mbedtls_pk_context pkey; mbedtls_x509write_cert cert; mbedtls_entropy_context entropy; mbedtls_ctr_drbg_context ctr_drbg; time_t exp_time; struct tm *exp_tm; const char *not_before = "20000101000000"; char not_after[15]; unsigned char output_buf[4096] = {0}; mode_t umode; /* init */ mbedtls_ctr_drbg_init(&ctr_drbg); mbedtls_entropy_init(&entropy); mbedtls_pk_init(&pkey); mbedtls_x509write_crt_init(&cert); ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0); if (ret) { fprintf(stderr, "mbedtls_ctr_drbg_seed() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } /* parse the private key */ ret = mbedtls_pk_parse_keyfile(&pkey, TESTS_DIR "/data/client.key", NULL, mbedtls_ctr_drbg_random, &ctr_drbg); if (ret) { fprintf(stderr, "mbedtls_pk_parse_keyfile() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } /* cert will be self signed, so the subject's and issuer's key can be the same */ mbedtls_x509write_crt_set_subject_key(&cert, &pkey); mbedtls_x509write_crt_set_issuer_key(&cert, &pkey); /* likewise for their CNs */ ret = mbedtls_x509write_crt_set_subject_name(&cert, "CN=cert_exp_test"); if (ret) { fprintf(stderr, "mbedtls_x509write_crt_set_subject_name() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } ret = mbedtls_x509write_crt_set_issuer_name(&cert, "CN=cert_exp_test"); if (ret) { fprintf(stderr, "mbedtls_x509write_crt_set_issuer_name() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } /* set SN to 1 */ ret = mbedtls_x509write_crt_set_serial_raw(&cert, (unsigned char *)"1", 1); if (ret) { fprintf(stderr, "mbedtls_x509write_crt_set_serial_raw() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } /* generate the expiration date in GMT */ exp_time = time(NULL) + offset_sec; exp_tm = gmtime(&exp_time); ret = strftime(not_after, 15, "%Y%m%d%H%M%S", exp_tm); if (ret != 14) { fprintf(stderr, "strftime() failed (%s)\n", strerror(errno)); ret = 1; goto cleanup; } /* set the validity dates */ ret = mbedtls_x509write_crt_set_validity(&cert, not_before, not_after); if (ret) { fprintf(stderr, "mbedtls_x509write_crt_set_validity() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } mbedtls_x509write_crt_set_md_alg(&cert, MBEDTLS_MD_SHA256); /* write the cert to mem */ ret = mbedtls_x509write_crt_pem(&cert, output_buf, 4096, mbedtls_ctr_drbg_random, &ctr_drbg); if (ret < 0) { fprintf(stderr, "mbedtls_x509write_crt_pem() failed (%s)\n", mbedtls_strerr(ret)); goto cleanup; } /* then create a tmp file and write it from mem to this file */ umode = umask(0177); fd = mkstemp(cert_path); if (fd < 0) { fprintf(stderr, "mkstemp() failed (%s)\n", strerror(errno)); ret = 1; goto cleanup; } umask(umode); if (write(fd, output_buf, strlen((char *)output_buf)) < 0) { fprintf(stderr, "write() failed (%s)\n", strerror(errno)); ret = 1; goto cleanup; } cleanup: mbedtls_x509write_crt_free(&cert); mbedtls_pk_free(&pkey); mbedtls_ctr_drbg_free(&ctr_drbg); mbedtls_entropy_free(&entropy); return ret; } #else static const char * openssl_strerr(void) { return ERR_reason_error_string(ERR_get_error()); } static int custom_exp_date_cert_create(long offset_sec, char cert_path[12]) { int ret = 0; EVP_PKEY *pkey = NULL; X509 *cert = NULL; X509_NAME *name; FILE *f; mode_t umode; BIO *out_bio = NULL; int fd; /* get the private key */ f = fopen(TESTS_DIR "/data/client.key", "r"); if (!f) { fprintf(stderr, "fopen() failed (%s)\n", strerror(errno)); ret = 1; goto cleanup; } pkey = PEM_read_PrivateKey(f, NULL, NULL, NULL); if (!pkey) { fprintf(stderr, "PEM_read_PrivateKey() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* new cert */ cert = X509_new(); if (!cert) { fprintf(stderr, "X509_new() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* set the public key */ if (!X509_set_pubkey(cert, pkey)) { fprintf(stderr, "X509_set_pubkey() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* set the issuer's CN */ name = X509_get_subject_name(cert); if (!X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char *)"cert_exp_test", -1, -1, 0)) { fprintf(stderr, "X509_NAME_add_entry_by_txt() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } if (!X509_set_issuer_name(cert, name)) { fprintf(stderr, "X509_set_issuer_name() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* set SN to 1 */ if (!ASN1_INTEGER_set(X509_get_serialNumber(cert), 1)) { fprintf(stderr, "ASN1_INTEGER_set() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* set the validity dates */ if (!X509_gmtime_adj(X509_get_notBefore(cert), 0) || !X509_gmtime_adj(X509_get_notAfter(cert), offset_sec)) { fprintf(stderr, "X509_gmtime_adj() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* sign it using the private key */ if (!X509_sign(cert, pkey, EVP_sha256())) { fprintf(stderr, "X509_sign() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } /* write the cert to a file */ umode = umask(0177); fd = mkstemp(cert_path); if (fd < 0) { fprintf(stderr, "mkstemp() failed (%s)\n", strerror(errno)); ret = 1; goto cleanup; } umask(umode); out_bio = BIO_new_fd(fd, BIO_NOCLOSE); if (!out_bio) { fprintf(stderr, "BIO_new_fd() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } if (!PEM_write_bio_X509(out_bio, cert)) { fprintf(stderr, "PEM_write_bio_X509() failed (%s)\n", openssl_strerr()); ret = 1; goto cleanup; } cleanup: if (f) { fclose(f); } X509_free(cert); EVP_PKEY_free(pkey); BIO_free(out_bio); return ret; } #endif static void * server_thread(void *arg) { NC_MSG_TYPE msgtype; struct nc_session *session; struct test_state *state = arg; struct nc_pollsession *ps; int ret; ps = nc_ps_new(); assert_non_null(ps); /* wait until the client is ready to connect */ pthread_barrier_wait(&state->barrier); msgtype = nc_accept(NC_ACCEPT_TIMEOUT, server_ctx, &session); assert_int_equal(msgtype, NC_MSG_HELLO); /* add sess to ps */ ret = nc_ps_add_session(ps, session); assert_int_equal(ret, 0); /* serve all the RPCs */ do { ret = nc_ps_poll(ps, NC_PS_POLL_TIMEOUT, NULL); } while (ret & NC_PSPOLL_RPC); /* increase session notification subscription flag count */ nc_session_inc_notif_status(session); /* wait until the notif is ready to be sent */ printf("Server waiting for the certificate to expire...\n"); pthread_barrier_wait(&state->ntf_barrier); /* send the notif */ msgtype = nc_server_notif_send(session, state->ntf, NC_SEND_NOTIF_TIMEOUT); assert_int_equal(msgtype, NC_MSG_NOTIF); /* wait until the client has received the notif and closed the session */ pthread_barrier_wait(&state->barrier); nc_session_dec_notif_status(session); nc_server_notif_free(state->ntf); nc_ps_clear(ps, 1, NULL); nc_ps_free(ps); return NULL; } static void * client_thread(void *arg) { int ret; struct nc_session *session = NULL; struct test_state *state = arg; NC_MSG_TYPE msgtype; struct lyd_node *envp, *op, *node; /* set schema search path */ 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.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); /* wait until the server is ready to accept the connection */ pthread_barrier_wait(&state->barrier); session = nc_connect_tls("127.0.0.1", TEST_PORT, client_ctx); assert_non_null(session); /* receive the notif */ msgtype = nc_recv_notif(session, NC_RECV_NOTIF_TIMEOUT, &envp, &op); assert_int_equal(msgtype, NC_MSG_NOTIF); /* check the notif content and print the expiration date */ ret = lyd_find_path(op, "expiration-date", 0, &node); assert_int_equal(ret, 0); printf("Certificate expires on :%s\n", lyd_get_value(node)); /* close the session and signal the server */ lyd_free_all(envp); lyd_free_all(op); nc_session_free(session, NULL); pthread_barrier_wait(&state->barrier); return NULL; } static void nc_cert_exp_notif_cb(const char *exp_time, const char *xpath, void *user_data) { int ret; struct nc_server_notif *ntf = NULL; struct lyd_node *ntf_data = NULL; time_t ntf_time; char *ntf_time_str = NULL; struct test_state *state = user_data; /* create the notification data */ ret = lyd_new_path(NULL, server_ctx, xpath, exp_time, 0, &ntf_data); assert_int_equal(ret, 0); /* yang time str from time_t */ ntf_time = time(NULL); ret = ly_time_time2str(ntf_time, NULL, &ntf_time_str); assert_int_equal(ret, 0); /* create the notification */ ntf = nc_server_notif_new(ntf_data, ntf_time_str, NC_PARAMTYPE_FREE); assert_non_null(ntf); state->ntf = ntf; /* signal the server that the notif is ready to be sent */ pthread_barrier_wait(&state->ntf_barrier); } static void test_nc_cert_exp_notif_valid_cert(void **state) { int ret, i; pthread_t tids[2]; struct test_state *st = *state; char cert_path[12] = "/tmp/XXXXXX"; assert_non_null(state); /* create a soon expiring cert and get a path to it */ ret = custom_exp_date_cert_create(VALID_CERT_ADD_SEC, cert_path); assert_int_equal(ret, 0); /* create new end entity client cert data */ ret = nc_server_config_add_tls_client_cert(server_ctx, "endpt", "exp_cert_test", cert_path, &st->tree); assert_int_equal(ret, 0); /* configure the server based on the data */ ret = nc_server_config_setup_data(st->tree); assert_int_equal(ret, 0); /* start the cert exp notification thread */ ret = nc_server_notif_cert_expiration_thread_start(nc_cert_exp_notif_cb, st, NULL); assert_int_equal(ret, 0); /* start the client and server threads */ ret = pthread_create(&tids[0], NULL, client_thread, *state); assert_int_equal(ret, 0); ret = pthread_create(&tids[1], NULL, server_thread, *state); assert_int_equal(ret, 0); for (i = 0; i < 2; i++) { pthread_join(tids[i], NULL); } /* stop the cert exp notif thread */ nc_server_notif_cert_expiration_thread_stop(1); } static void test_nc_cert_exp_notif_expired_cert(void **state) { int ret, i; pthread_t tids[2]; struct test_state *st = *state; char cert_path[12] = "/tmp/XXXXXX"; assert_non_null(state); /* create an expired cert and get a path to it */ ret = custom_exp_date_cert_create(EXPIRED_CERT_ADD_SEC, cert_path); assert_int_equal(ret, 0); /* create new end entity client cert data from it */ ret = nc_server_config_add_tls_client_cert(server_ctx, "endpt", "exp_cert_test", cert_path, &st->tree); assert_int_equal(ret, 0); /* configure the server based on the data */ ret = nc_server_config_setup_data(st->tree); assert_int_equal(ret, 0); /* start the cert exp notification thread */ ret = nc_server_notif_cert_expiration_thread_start(nc_cert_exp_notif_cb, st, NULL); assert_int_equal(ret, 0); /* start the client and server threads */ ret = pthread_create(&tids[0], NULL, client_thread, *state); assert_int_equal(ret, 0); ret = pthread_create(&tids[1], NULL, server_thread, *state); assert_int_equal(ret, 0); for (i = 0; i < 2; i++) { pthread_join(tids[i], NULL); } /* stop the cert exp notif thread */ nc_server_notif_cert_expiration_thread_stop(1); } static void test_nc_cert_exp_notif_bad_interval_period(void **state) { int ret; struct lyd_node *tree = NULL; const char *invalid_data = "\n" " \n" " \n" " 5d\n" " 1w\n" " \n" " \n" ""; (void) state; /* validating this data should fail because of a must condition, the period * must not be bigger than the anchor (at least unit wise) */ ret = lyd_parse_data_mem(server_ctx, invalid_data, LYD_XML, 0, LYD_VALIDATE_PRESENT, &tree); assert_int_not_equal(ret, 0); } static void init_test_ctx(struct ly_ctx **ctx) { int ret; struct lys_module *mod; const char *ietf_ct_features[] = {"cleartext-passwords", "cleartext-private-keys", "certificate-expiration-notification", NULL}; ret = ly_ctx_new(MODULES_DIR, 0, ctx); assert_int_equal(ret, 0); ret = nc_server_init_ctx(ctx); assert_int_equal(ret, 0); ret = nc_server_config_load_modules(ctx); assert_int_equal(ret, 0); mod = ly_ctx_get_module_implemented(*ctx, "ietf-crypto-types"); assert_non_null(mod); /* enable the certificate-expiration-notification feature */ ret = lys_set_implemented(mod, ietf_ct_features); assert_int_equal(ret, 0); } static int setup_f(void **state) { int ret; struct test_state *st; nc_verbosity(NC_VERB_VERBOSE); /* init barriers */ st = malloc(sizeof *st); assert_non_null(st); ret = pthread_barrier_init(&st->barrier, NULL, 2); assert_int_equal(ret, 0); ret = pthread_barrier_init(&st->ntf_barrier, NULL, 2); assert_int_equal(ret, 0); st->tree = NULL; st->ntf = NULL; *state = st; /* init server */ ret = nc_server_init(); assert_int_equal(ret, 0); /* init client */ ret = nc_client_init(); assert_int_equal(ret, 0); /* init server ctx */ init_test_ctx(&server_ctx); /* init client ctx to avoid the need to implement get-schema */ init_test_ctx(&client_ctx); /* create new address and port data */ ret = nc_server_config_add_address_port(server_ctx, "endpt", NC_TI_TLS, "127.0.0.1", TEST_PORT, &st->tree); assert_int_equal(ret, 0); /* create new server certificate data */ ret = nc_server_config_add_tls_server_cert(server_ctx, "endpt", TESTS_DIR "/data/server.key", NULL, TESTS_DIR "/data/server.crt", &st->tree); assert_int_equal(ret, 0); /* create new end entity client cert data */ ret = nc_server_config_add_tls_client_cert(server_ctx, "endpt", "client_cert", TESTS_DIR "/data/client.crt", &st->tree); assert_int_equal(ret, 0); /* create new client ca data */ ret = nc_server_config_add_tls_ca_cert(server_ctx, "endpt", "client_ca", TESTS_DIR "/data/serverca.pem", &st->tree); assert_int_equal(ret, 0); /* create new cert-to-name */ ret = nc_server_config_add_tls_ctn(server_ctx, "endpt", 1, "04:85:6B:75:D1:1A:86:E0:D8:FE:5B:BD:72:F5:73:1D:07:EA:32:BF:09:11:21:6A:6E:23:78:8E:B6:D5:73:C3:2D", NC_TLS_CTN_SPECIFIED, "client", &st->tree); assert_int_equal(ret, 0); return 0; } static int teardown_f(void **state) { int ret = 0; struct test_state *test_state; assert_non_null(state); test_state = *state; ret = pthread_barrier_destroy(&test_state->barrier); assert_int_equal(ret, 0); ret = pthread_barrier_destroy(&test_state->ntf_barrier); assert_int_equal(ret, 0); lyd_free_all(test_state->tree); free(*state); nc_client_destroy(); nc_server_destroy(); ly_ctx_destroy(server_ctx); ly_ctx_destroy(client_ctx); return 0; } int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(test_nc_cert_exp_notif_valid_cert, setup_f, teardown_f), cmocka_unit_test_setup_teardown(test_nc_cert_exp_notif_expired_cert, setup_f, teardown_f), cmocka_unit_test_setup_teardown(test_nc_cert_exp_notif_bad_interval_period, setup_f, teardown_f), }; /* try to get ports from the environment, otherwise use the default */ if (ln2_glob_test_get_ports(1, &TEST_PORT, &TEST_PORT_STR)) { return 1; } setenv("CMOCKA_TEST_ABORT", "1", 1); return cmocka_run_group_tests(tests, NULL, NULL); }