From ea74c0acb8b85c5fc50158cd592a1b80c4edf6a6 Mon Sep 17 00:00:00 2001 From: Dmitry Verenitsin Date: Sat, 8 Aug 2026 20:14:08 +0500 Subject: [PATCH] Merge commit from fork * [Core, mod_commands] Interface allowlist (#3086) * [core] Add interface allowlist to gate module app/api registration Adds an optional, presence-activated allowlist in switch.conf.xml that controls which modules may register application / api / json_api / chat-application interfaces. With no configured nothing is enforced; when at least one entry is present, only listed interfaces register at load time and all others are refused (the module still loads and switch_loadable_module_process still returns SUCCESS -- the blocked interface is simply never exposed). Entries match at three levels of precision: mod_commands - whole module mod_commands.system - any interface named "system" mod_commands.system.api - a specific type (app|api|json_api|chat_app) Enforcement lives in switch_loadable_module_process() so every module, at boot and at runtime `load`, is subject to the same policy. This gives operators a way to disable the "system"/"spawn" shell-exec API commands (and equivalents) system-wide. Also adds the `interface_allowlist_dump [modules] [plain]` API, which walks the loaded modules and prints their interfaces in the allowlist key format so the current state can be captured and pruned offline into config. * [mod_commands] Add tests for the interface allowlist New test_interface_allowlist boots the core with an active (conf_interface_allowlist/) that permits only a couple of mod_commands interfaces, then loads mod_commands and verifies: - listed commands register and run (status, version) while unlisted and shell-exec commands are refused (system, spawn, uptime) -- refusal surfaces as switch_api_execute returning FALSE / command-not-found, with the command function never invoked; - a "module.name.type" entry gates by type: the API "status" loads while the JSON API of the same name stays blocked; - interface_allowlist_dump prints the config format in its xml, modules and plain variants, and reflects module capabilities (system appears in the dump even though it was blocked from registering). * [config] Fix interior -- in interface-allowlist comment breaking XML parse The explanatory comment used -- as em-dash pairs. The XML parser treats -- inside a comment as the comment close, causing an "unclosed + + + diff --git a/src/include/switch_core.h b/src/include/switch_core.h index f72cbc7880..40f126a818 100644 --- a/src/include/switch_core.h +++ b/src/include/switch_core.h @@ -163,6 +163,19 @@ typedef enum { DTLS_TYPE_RTCP = (1 << 3) } dtls_type_t; +typedef enum { + /* FreeSWITCH as DTLS server does not request the client certificate; its fingerprint is not checked. */ + DTLS_CLIENT_CERT_VERIFY_NONE, + /* FreeSWITCH as DTLS server requests the client certificate and binds it to the SDP a=fingerprint; the PKI chain is not enforced. */ + DTLS_CLIENT_CERT_VERIFY_FINGERPRINT, + /* Like DTLS_CLIENT_CERT_VERIFY_FINGERPRINT, and additionally OpenSSL enforces the PKI chain verdict. */ + DTLS_CLIENT_CERT_VERIFY_FULL +} dtls_client_cert_verify_t; + +/* Default policy for verifying the client certificate when FreeSWITCH is the DTLS server, + * applied when rtp_dtls_client_cert_verify_mode is unset. */ +#define DTLS_CLIENT_CERT_VERIFY_DEFAULT DTLS_CLIENT_CERT_VERIFY_NONE + typedef enum { DS_OFF, DS_HANDSHAKE, diff --git a/src/switch_core_cert.c b/src/switch_core_cert.c index 64f497ea1a..218016e63b 100644 --- a/src/switch_core_cert.c +++ b/src/switch_core_cert.c @@ -105,6 +105,10 @@ SWITCH_DECLARE(void) switch_ssl_destroy_ssl_locks(void) static const EVP_MD *get_evp_by_name(const char *name) { + if (zstr(name)) { + return NULL; + } + if (!strcasecmp(name, "md5")) return EVP_md5(); if (!strcasecmp(name, "sha1")) return EVP_sha1(); if (!strcasecmp(name, "sha-1")) return EVP_sha1(); @@ -182,6 +186,11 @@ SWITCH_DECLARE(int) switch_core_cert_extract_fingerprint(X509* x509, dtls_finger evp = get_evp_by_name(fp->type); + if (!evp) { + switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Missing or unsupported fingerprint hash type\n"); + return -1; + } + if (X509_digest(x509, evp, fp->data, &fp->len) != 1 || fp->len <= 0) { switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "FP DIGEST ERR!\n"); return -1; diff --git a/src/switch_rtp.c b/src/switch_rtp.c index 9ffcce84e0..3b14913abb 100644 --- a/src/switch_rtp.c +++ b/src/switch_rtp.c @@ -296,6 +296,9 @@ typedef struct switch_dtls_s { char *pem; struct switch_rtp *rtp_session; int mtu; + /* How FreeSWITCH, as the DTLS server, verifies the client certificate (from the rtp_dtls_client_cert_verify_mode + * channel variable). Server-only; the client role ignores it. */ + dtls_client_cert_verify_t client_cert_verify; } switch_dtls_t; typedef int (*dtls_state_handler_t)(switch_rtp_t *, switch_dtls_t *); @@ -3299,6 +3302,7 @@ static int dtls_state_setup(switch_rtp_t *rtp_session, switch_dtls_t *dtls) X509 *cert; switch_secure_settings_t ssec; /* Used just to wrap over params in a call to switch_rtp_add_crypto_key. */ int r = 0; + int peer_cert_present = 0; uint8_t raw_key_data[cr_kslen * 2]; unsigned char local_key_buf[cr_kslen]; @@ -3309,11 +3313,12 @@ static int dtls_state_setup(switch_rtp_t *rtp_session, switch_dtls_t *dtls) memset(&local_key_buf, 0, cr_kslen * sizeof(unsigned char)); memset(&remote_key_buf, 0, cr_kslen * sizeof(unsigned char)); - if ((dtls->type & DTLS_TYPE_SERVER)) { + if (dtls->client_cert_verify == DTLS_CLIENT_CERT_VERIFY_NONE && (dtls->type & DTLS_TYPE_SERVER)) { r = 1; } else if ((cert = SSL_get_peer_certificate(dtls->ssl))) { dtls_fingerprint_t fp = {0}; + peer_cert_present = 1; fp.type = dtls->remote_fp->type; switch_core_cert_extract_fingerprint(cert, &fp); @@ -3323,7 +3328,11 @@ static int dtls_state_setup(switch_rtp_t *rtp_session, switch_dtls_t *dtls) } if (!r) { - switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_ERROR, "%s Fingerprint Verification Failed!\n", rtp_type(rtp_session)); + if (peer_cert_present) { + switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_ERROR, "%s Fingerprint Verification Failed!\n", rtp_type(rtp_session)); + } else { + switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_ERROR, "%s No peer certificate presented; cannot verify SDP fingerprint\n", rtp_type(rtp_session)); + } dtls_set_state(dtls, DS_FAIL); return -1; } else { @@ -3514,37 +3523,33 @@ static int do_dtls(switch_rtp_t *rtp_session, switch_dtls_t *dtls) return r; } -#if VERIFY -static int cb_verify_peer(int preverify_ok, X509_STORE_CTX *ctx) +/* fingerprint mode: accept any client cert at the TLS layer (return 1, ignoring OpenSSL's chain + * verdict); the peer is authenticated by the SDP a=fingerprint match in dtls_state_setup(). */ +static int dtls_accept_any_cert(int preverify_ok, X509_STORE_CTX *ctx) { - SSL *ssl = NULL; - switch_dtls_t *dtls; - X509 *cert; - int r = 0; + return 1; +} - ssl = X509_STORE_CTX_get_app_data(ctx); - dtls = (switch_dtls_t *) SSL_get_app_data(ssl); +static dtls_client_cert_verify_t dtls_parse_client_cert_verify(switch_rtp_t *rtp_session, const char *str) +{ + if (!strcasecmp(str, "none")) { + return DTLS_CLIENT_CERT_VERIFY_NONE; + } - if (!(ssl && dtls)) { - return 0; + if (!strcasecmp(str, "fingerprint")) { + return DTLS_CLIENT_CERT_VERIFY_FINGERPRINT; } - if ((cert = SSL_get_peer_certificate(dtls->ssl))) { - dtls_fingerprint_t fp = {0}; - - fp.type = dtls->remote_fp->type; + if (!strcasecmp(str, "full")) { + return DTLS_CLIENT_CERT_VERIFY_FULL; + } - switch_core_cert_extract_fingerprint(cert, &fp); - r = (!zstr(fp.str) && !strncasecmp(fp.str, dtls->remote_fp->str, MAX_FPSTRLEN)); - - X509_free(cert); - } else { - switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(dtls->rtp_session->session), SWITCH_LOG_ERROR, "%s CERT ERR!\n", rtp_type(dtls->rtp_session)); - } + switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(rtp_session->session), SWITCH_LOG_WARNING, + "Unrecognized rtp_dtls_client_cert_verify_mode '%s'; falling back to 'fingerprint'. " + "Valid values: none, fingerprint, full\n", str); - return r; + return DTLS_CLIENT_CERT_VERIFY_FINGERPRINT; } -#endif //////////// @@ -4003,9 +4008,6 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_add_dtls(switch_rtp_t *rtp_session, d #endif SSL_CTX_set_mode(dtls->ssl_ctx, SSL_MODE_AUTO_RETRY); - //SSL_CTX_set_verify(dtls->ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); - SSL_CTX_set_verify(dtls->ssl_ctx, SSL_VERIFY_NONE, NULL); - //SSL_CTX_set_cipher_list(dtls->ssl_ctx, "ECDH:!RC4:!SSLv3:RSA_WITH_AES_128_CBC_SHA"); //SSL_CTX_set_cipher_list(dtls->ssl_ctx, "ECDHE-RSA-AES256-GCM-SHA384"); SSL_CTX_set_cipher_list(dtls->ssl_ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); @@ -4017,6 +4019,16 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_add_dtls(switch_rtp_t *rtp_session, d #endif dtls->type = type; + + dtls->client_cert_verify = DTLS_CLIENT_CERT_VERIFY_DEFAULT; + if (rtp_session->session) { + const char *verify_str = switch_channel_get_variable(switch_core_session_get_channel(rtp_session->session), "rtp_dtls_client_cert_verify_mode"); + + if (!zstr(verify_str)) { + dtls->client_cert_verify = dtls_parse_client_cert_verify(rtp_session, verify_str); + } + } + dtls->read_bio = BIO_new(BIO_s_mem()); switch_assert(dtls->read_bio); @@ -4068,8 +4080,6 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_add_dtls(switch_rtp_t *rtp_session, d SSL_set_read_ahead(dtls->ssl, 1); - //SSL_set_verify(dtls->ssl, (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT), cb_verify_peer); - #ifndef OPENSSL_NO_EC #if OPENSSL_VERSION_NUMBER < 0x10002000L ecdh = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); @@ -4085,7 +4095,32 @@ SWITCH_DECLARE(switch_status_t) switch_rtp_add_dtls(switch_rtp_t *rtp_session, d #endif #endif - SSL_set_verify(dtls->ssl, SSL_VERIFY_NONE, NULL); + /* Per-connection verify policy on the SSL object (overrides the CTX default SSL_new() copied in). + * Flag: a server sends a CertificateRequest under SSL_VERIFY_PEER, none under SSL_VERIFY_NONE. + * Callback: dtls_accept_any_cert() returns 1 (ignore OpenSSL's chain verdict), NULL runs the default + * verifier (enforce it). client_cert_verify is a server-only knob. The peer is authenticated in + * dtls_state_setup() by matching its certificate to the SDP a=fingerprint; the none case skips it. + * client role SSL_VERIFY_NONE + NULL (server cert always present, chain ignored) + * server, fingerprint SSL_VERIFY_PEER + dtls_accept_any_cert (request cert, ignore chain) + * server, full SSL_VERIFY_PEER + NULL (request cert, enforce chain) + * server, none (default) SSL_VERIFY_NONE + NULL (no client cert requested) + */ + if (!(type & DTLS_TYPE_SERVER)) { + SSL_set_verify(dtls->ssl, SSL_VERIFY_NONE, NULL); + } else { + /* No default case: the default is defined solely by DTLS_CLIENT_CERT_VERIFY_DEFAULT. */ + switch (dtls->client_cert_verify) { + case DTLS_CLIENT_CERT_VERIFY_FINGERPRINT: + SSL_set_verify(dtls->ssl, SSL_VERIFY_PEER, dtls_accept_any_cert); + break; + case DTLS_CLIENT_CERT_VERIFY_FULL: + SSL_set_verify(dtls->ssl, SSL_VERIFY_PEER, NULL); + break; + case DTLS_CLIENT_CERT_VERIFY_NONE: + SSL_set_verify(dtls->ssl, SSL_VERIFY_NONE, NULL); + break; + } + } SSL_set_app_data(dtls->ssl, dtls); dtls->local_fp = local_fp; diff --git a/tests/unit/switch_rtp.c b/tests/unit/switch_rtp.c index 7d22524358..66378765b1 100644 --- a/tests/unit/switch_rtp.c +++ b/tests/unit/switch_rtp.c @@ -2,6 +2,17 @@ #include #include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + #ifndef MSG_CONFIRM #define MSG_CONFIRM 0 #endif @@ -42,6 +53,244 @@ static void send_rtcp_event_handler(switch_event_t *event) show_event(event); } +/* SRTP profile FreeSWITCH offers; the client must offer it too so the DTLS-SRTP + * key material can be exported and the server can reach DS_READY. */ +#define TEST_SRTP_PROFILE "SRTP_AES128_CM_SHA1_80" + +/* Minimal OpenSSL DTLS client speaking to the FreeSWITCH RTP socket. */ +typedef struct { + SSL_CTX *ctx; + SSL *ssl; + int fd; +} dtls_test_client_t; + +/* + * Build a DTLS client bound to client_port and connected to the FreeSWITCH RTP socket at + * server_port. When present_cert is set the client loads the same PEM FreeSWITCH uses, so + * its certificate fingerprint equals the FreeSWITCH cert fingerprint; when clear the client + * presents no certificate. Returns 0 on success. + */ +static int dtls_test_client_create(dtls_test_client_t *client, switch_port_t client_port, switch_port_t server_port, int present_cert) +{ + struct sockaddr_in local_addr = { 0 }; + struct sockaddr_in server_addr = { 0 }; + BIO *bio = NULL; + int flags; + + memset(client, 0, sizeof(*client)); + client->fd = -1; + + client->fd = socket(AF_INET, SOCK_DGRAM, 0); + if (client->fd < 0) { + return -1; + } + + local_addr.sin_family = AF_INET; + local_addr.sin_addr.s_addr = inet_addr(rx_host); + local_addr.sin_port = htons(client_port); + if (bind(client->fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { + return -1; + } + + server_addr.sin_family = AF_INET; + server_addr.sin_addr.s_addr = inet_addr(rx_host); + server_addr.sin_port = htons(server_port); + if (connect(client->fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { + return -1; + } + + flags = fcntl(client->fd, F_GETFL, 0); + fcntl(client->fd, F_SETFL, flags | O_NONBLOCK); + + if (!(client->ctx = SSL_CTX_new(DTLS_client_method()))) { + return -1; + } + + /* The client does not validate the FreeSWITCH certificate; this test exercises the + * server-side check only. */ + SSL_CTX_set_verify(client->ctx, SSL_VERIFY_NONE, NULL); + SSL_CTX_set_cipher_list(client->ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); + SSL_CTX_set_tlsext_use_srtp(client->ctx, TEST_SRTP_PROFILE); + + if (present_cert) { + char pem[1024] = ""; + + switch_snprintf(pem, sizeof(pem), "%s%s%s.pem", SWITCH_GLOBAL_dirs.certs_dir, SWITCH_PATH_SEPARATOR, DTLS_SRTP_FNAME); + + if (SSL_CTX_use_certificate_file(client->ctx, pem, SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(client->ctx, pem, SSL_FILETYPE_PEM) != 1) { + return -1; + } + } + + if (!(client->ssl = SSL_new(client->ctx))) { + return -1; + } + + if (!(bio = BIO_new_dgram(client->fd, BIO_NOCLOSE))) { + return -1; + } + + BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &server_addr); + SSL_set_bio(client->ssl, bio, bio); + SSL_set_connect_state(client->ssl); + + return 0; +} + +static void dtls_test_client_destroy(dtls_test_client_t *client) +{ + if (client->ssl) { + SSL_free(client->ssl); + client->ssl = NULL; + } + + if (client->ctx) { + SSL_CTX_free(client->ctx); + client->ctx = NULL; + } + + if (client->fd >= 0) { + close(client->fd); + client->fd = -1; + } +} + +/* Advance the client handshake one step; returns 1 once the client handshake finishes. */ +static int dtls_test_client_step(dtls_test_client_t *client) +{ + if (SSL_do_handshake(client->ssl) == 1) { + return 1; + } + + /* WANT_READ/WANT_WRITE is expected on the non-blocking datagram BIO between flights. */ + return SSL_is_init_finished(client->ssl) ? 1 : 0; +} + +/* Which SDP a=fingerprint the harness hands FreeSWITCH for the peer. */ +typedef enum { + REMOTE_FP_ABSENT, /* peer advertised no fingerprint (remote_fp left empty) */ + REMOTE_FP_MATCH, /* fingerprint matches the client certificate */ + REMOTE_FP_DIFFERENT /* fingerprint present but does not match the client certificate */ +} remote_fp_case_t; + +/* + * Run one verification scenario end to end and return the terminal DTLS state FreeSWITCH + * reaches. verify_mode is the rtp_dtls_client_cert_verify_mode value (NULL leaves it unset, + * i.e. the default). present_cert controls whether the client sends a certificate. + * remote_fp_case selects the SDP a=fingerprint FreeSWITCH is given for the peer: absent, + * matching, or different. Returns DS_OFF if the harness could not be set up. + */ +static dtls_state_t run_client_cert_verify_case(const char *verify_mode, int present_cert, remote_fp_case_t remote_fp_case) +{ + static switch_port_t port_base = 50000; + switch_core_session_t *session = NULL; + switch_channel_t *channel = NULL; + switch_call_cause_t cause; + switch_rtp_t *dtls_rtp = NULL; + switch_rtp_flag_t dtls_flags[SWITCH_RTP_FLAG_INVALID] = { 0 }; + dtls_fingerprint_t local_fp = { 0 }; + dtls_fingerprint_t remote_fp = { 0 }; + dtls_test_client_t client; + dtls_state_t state = DS_OFF; + const char *dtls_err = NULL; + switch_port_t server_port, client_port; + char rbuf[SWITCH_RECOMMENDED_BUFFER_SIZE]; + int client_ready = 0, have_client = 0; + int i; + + memset(&client, 0, sizeof(client)); + client.fd = -1; + + /* Ensure the DTLS-SRTP certificate exists (idempotent; skips if already generated). */ + switch_core_gen_certs(DTLS_SRTP_FNAME); + + server_port = port_base++; + client_port = port_base++; + + if (switch_ivr_originate(NULL, &session, &cause, "null/+15553334444", 2, NULL, NULL, NULL, NULL, NULL, SOF_NONE, NULL, NULL) != SWITCH_STATUS_SUCCESS || !session) { + goto done; + } + + channel = switch_core_session_get_channel(session); + + if (!zstr(verify_mode)) { + switch_channel_set_variable(channel, "rtp_dtls_client_cert_verify_mode", verify_mode); + } + + /* FreeSWITCH binds server_port; its media destination is the client at client_port. The RTP + * session uses the call session's own pool, so rtp_session->session is populated (the pool + * carries the "__session" back-pointer switch_rtp_create() reads). */ + dtls_rtp = switch_rtp_new(rx_host, server_port, rx_host, client_port, TEST_PT, 8000, 20 * 1000, dtls_flags, "soft", &dtls_err, switch_core_session_get_pool(session), 0, 0); + if (!dtls_rtp || !switch_rtp_ready(dtls_rtp)) { + goto done; + } + + switch_core_media_set_rtp_session(session, SWITCH_MEDIA_TYPE_AUDIO, dtls_rtp); + switch_rtp_set_remote_address(dtls_rtp, rx_host, client_port, 0, SWITCH_FALSE, &dtls_err); + + if (dtls_test_client_create(&client, client_port, server_port, present_cert) != 0) { + goto done; + } + have_client = 1; + + /* FreeSWITCH cert fingerprint. The client (when it presents a cert) uses the same PEM, + * so a matching expected fingerprint is exactly the FreeSWITCH cert fingerprint. */ + local_fp.type = "sha-256"; + if (!switch_core_cert_gen_fingerprint(DTLS_SRTP_FNAME, &local_fp)) { + goto done; + } + + /* REMOTE_FP_ABSENT leaves remote_fp zeroed, emulating a peer that sent no SDP a=fingerprint. */ + if (remote_fp_case != REMOTE_FP_ABSENT) { + remote_fp = local_fp; + if (remote_fp_case == REMOTE_FP_DIFFERENT) { + /* Flip one hex nibble so the expected fingerprint cannot match the client cert. */ + remote_fp.str[0] = (remote_fp.str[0] == '0') ? '1' : '0'; + } + } + + if (switch_rtp_add_dtls(dtls_rtp, &local_fp, &remote_fp, DTLS_TYPE_SERVER | DTLS_TYPE_RTP, 0) != SWITCH_STATUS_SUCCESS) { + goto done; + } + + /* Pump both sides until the FreeSWITCH DTLS state machine settles. switch_rtp_read() + * drives do_dtls() on the FreeSWITCH side; dtls_test_client_step() advances the client. */ + for (i = 0; i < 200; i++) { + uint32_t rlen = sizeof(rbuf); + switch_payload_t pt = 0; + switch_frame_flag_t frame_flags = 0; + + if (!client_ready) { + client_ready = dtls_test_client_step(&client); + } + + switch_rtp_read(dtls_rtp, (void *)rbuf, &rlen, &pt, &frame_flags, 0); + + state = switch_rtp_dtls_state(dtls_rtp, DTLS_TYPE_RTP); + if (state == DS_READY || state == DS_FAIL) { + break; + } + } + + done: + + if (have_client) { + dtls_test_client_destroy(&client); + } + + if (dtls_rtp) { + switch_rtp_destroy(&dtls_rtp); + } + + if (session) { + switch_channel_hangup(switch_core_session_get_channel(session), SWITCH_CAUSE_NORMAL_CLEARING); + switch_core_session_rwunlock(session); + } + + return state; +} + FST_CORE_BEGIN("./conf") { FST_SUITE_BEGIN(switch_rtp) @@ -277,6 +526,38 @@ FST_TEARDOWN_END() } FST_TEST_END() + FST_TEST_BEGIN(test_client_cert_verify) + { + dtls_state_t state; + + /* fingerprint mode, matching client cert -> handshake completes, SRTP keys installed. */ + state = run_client_cert_verify_case("fingerprint", 1, REMOTE_FP_MATCH); + fst_xcheck(state == DS_READY, "fingerprint mode: matching client fingerprint reaches DS_READY"); + + /* fingerprint mode, client cert does not match the expected fingerprint -> rejected. */ + state = run_client_cert_verify_case("fingerprint", 1, REMOTE_FP_DIFFERENT); + fst_xcheck(state == DS_FAIL, "fingerprint mode: mismatched client fingerprint reaches DS_FAIL"); + + /* fingerprint mode, client presents no certificate -> cannot be verified -> rejected. */ + state = run_client_cert_verify_case("fingerprint", 0, REMOTE_FP_MATCH); + fst_xcheck(state == DS_FAIL, "fingerprint mode: absent client certificate reaches DS_FAIL"); + + /* fingerprint mode, client presents a cert but the peer advertised no SDP a=fingerprint -> + * nothing to bind the certificate to -> rejected (must not deref a NULL fingerprint type). */ + state = run_client_cert_verify_case("fingerprint", 1, REMOTE_FP_ABSENT); + fst_xcheck(state == DS_FAIL, "fingerprint mode: absent remote fingerprint reaches DS_FAIL"); + + /* none (default): client cert is neither requested nor checked, so even a mismatch is accepted. */ + state = run_client_cert_verify_case("none", 1, REMOTE_FP_DIFFERENT); + fst_xcheck(state == DS_READY, "none mode accepts an unverified client"); + + /* An unrecognized mode falls back to fingerprint (fail closed), so a mismatch is rejected + * rather than silently accepted the way none would. */ + state = run_client_cert_verify_case("bogus", 1, REMOTE_FP_DIFFERENT); + fst_xcheck(state == DS_FAIL, "unrecognized mode falls back to fingerprint and rejects a mismatch"); + } + FST_TEST_END() + } FST_SUITE_END() }