Commit ea74c0acb8 for freeswitch.com
commit ea74c0acb8b85c5fc50158cd592a1b80c4edf6a6
Author: Dmitry Verenitsin <morbit85@gmail.com>
Date: Sat Aug 8 20:14:08 2026 +0500
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 <interface-allowlist> configured
nothing is enforced; when at least one <allow> 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
<interface-allowlist> (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 <!--"
error that prevents the whole freeswitch.xml from parsing (boot and
reloadxml both fail). Replace the -- pairs with ordinary punctuation.
* update .gitignore
* [core] Warn when interface-allowlist section is present but parses no entries
Co-authored-by: Chris Rienzo <chris@signalwire.com>
* Merge commit from fork
Add `switch_stun_packet_verify_integrity()`, an HMAC-SHA1
MESSAGE-INTEGRITY verifier that is const and non-mutating: it runs
over a private copy of the pristine network-order packet, so the
caller's buffer and byte order stay untouched, and walks attributes
with its own unsigned bounded helper `stun_wire_attr_bounds()`
instead of the host-order iterator macros. A trailing
MESSAGE-INTEGRITY-SHA256 or FINGERPRINT after MESSAGE-INTEGRITY is
tolerated; any other trailing attribute is rejected.
Gate it in `handle_ice()` behind `ice->verify_integrity`: verify
before any ICE state is touched, keyed by message type (local
`ice->pass` for a request, remote `ice->rpass` for a response or
error response), and drop on failure. Keepalive indications carry no
MESSAGE-INTEGRITY and are ignored.
`ice->verify_integrity` is read from the `ice_verify_message_integrity`
channel variable in `switch_rtp_activate_ice()` and defaults off, so
receive-path behavior is unchanged unless it is enabled. Adds unit
tests in `tests/unit/switch_stun.c`.
* Merge commit from fork
* [core] Verify DTLS client cert against SDP fingerprint (server role)
Add opt-in verification of the client certificate when FreeSWITCH is
the DTLS server, mirroring the binding the client role already performs
on the server certificate: match the peer certificate against the SDP
`a=fingerprint` in `dtls_state_setup()`.
Selected per call by the `rtp_dtls_client_cert_verify_mode` channel
variable (`dtls_client_cert_verify_t`). An unset variable keeps the
default `DTLS_CLIENT_CERT_VERIFY_NONE`, so existing behavior is
unchanged:
- `none`: the server does not request a client certificate.
- `fingerprint`: request it (`SSL_VERIFY_PEER` + `dtls_accept_any_cert`)
and require its fingerprint to match the SDP value; a self-signed
certificate is accepted at the TLS layer and the match provides
authenticity.
- `full`: additionally let OpenSSL enforce the certificate chain.
An unrecognized mode string falls back to `fingerprint` (fail closed)
with a warning.
The mode is set per `SSL` object in `switch_rtp_add_dtls()`. Verification
fails (`DS_FAIL`, no SRTP keys derived) when the client presents no
certificate, its fingerprint does not match, or the peer advertised no
usable `a=fingerprint`: `get_evp_by_name()` returns `NULL` for a missing
or empty hash type and `switch_core_cert_extract_fingerprint()` rejects a
`NULL` algorithm rather than passing it to `X509_digest()`.
`conf/vanilla/vars.xml` documents the knob as a disabled example. Tests
in `tests/unit/switch_rtp.c` cover matching, mismatched, absent-cert,
absent-fingerprint, `none`, and unrecognized-mode cases.
---------
Co-authored-by: Andrey Volk <andywolk@gmail.com>
Co-authored-by: Chris Rienzo <chris@signalwire.com>
diff --git a/conf/vanilla/vars.xml b/conf/vanilla/vars.xml
index c65d7c25d3..90ededbe61 100644
--- a/conf/vanilla/vars.xml
+++ b/conf/vanilla/vars.xml
@@ -430,6 +430,16 @@
dropped. Default off. Uncomment and change Z- prefix to X- to enable. -->
<!--<Z-PRE-PROCESS cmd="set" data="ice_verify_message_integrity=true"/>-->
+ <!-- DTLS-SRTP client certificate verification, server role only.
+ How FreeSWITCH, when it is the DTLS server (the peer is the DTLS client, i.e. a=setup:active),
+ verifies the client's certificate. No effect when FreeSWITCH is the DTLS client.
+ none - default; the client certificate is not requested or checked
+ fingerprint - request the client cert and require it to match the SDP a=fingerprint
+ full - also enforce the certificate's CA chain (needs a CA bundle; self-signed
+ WebRTC certificates will not pass)
+ Uncomment the line below and change Z- prefix to X- to enable. -->
+ <!--<Z-PRE-PROCESS cmd="set" data="rtp_dtls_client_cert_verify_mode=fingerprint"/>-->
+
<!-- Stock Video Avatars -->
<X-PRE-PROCESS cmd="set" data="video_mute_png=$${images_dir}/default-mute.png"/>
<X-PRE-PROCESS cmd="set" data="video_no_avatar_png=$${images_dir}/default-avatar.png"/>
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;
-
- ssl = X509_STORE_CTX_get_app_data(ctx);
- dtls = (switch_dtls_t *) SSL_get_app_data(ssl);
+ return 1;
+}
- if (!(ssl && dtls)) {
- return 0;
+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 ((cert = SSL_get_peer_certificate(dtls->ssl))) {
- dtls_fingerprint_t fp = {0};
-
- fp.type = dtls->remote_fp->type;
-
- switch_core_cert_extract_fingerprint(cert, &fp);
- r = (!zstr(fp.str) && !strncasecmp(fp.str, dtls->remote_fp->str, MAX_FPSTRLEN));
+ if (!strcasecmp(str, "fingerprint")) {
+ return DTLS_CLIENT_CERT_VERIFY_FINGERPRINT;
+ }
- 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));
+ if (!strcasecmp(str, "full")) {
+ return DTLS_CLIENT_CERT_VERIFY_FULL;
}
- return r;
+ 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 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 <switch.h>
#include <test/switch_test.h>
+#include <openssl/ssl.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
+#include <openssl/x509.h>
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <fcntl.h>
+#include <unistd.h>
+
#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()
}