diff --git a/configs/samples/pjsip.conf.sample b/configs/samples/pjsip.conf.sample
index a019708c39..9d73843030 100644
--- a/configs/samples/pjsip.conf.sample
+++ b/configs/samples/pjsip.conf.sample
@@ -1090,6 +1090,15 @@
; URI is not a hostname, the saved transport will be
; used and the 'x-ast-txp' parameter stripped from the
; outgoing packet.
+;allow_wildcard_certs=no ; In conjunction with verify_server, if 'yes' allow use
+ ; of wildcards, i.e. '*.' in certs for common, and
+ ; subject alt names of type DNS for TLS transport
+ ; types. Note, names must start with the wildcard.
+ ; Partial wildcards, e.g. 'f*.example.com' and
+ ; 'foo.*.com' are disallowed. As well, names only
+ ; match against a single level meaning '*.example.com'
+ ; matches 'foo.example.com', but not
+ ; 'foo.bar.example.com'. Defaults to 'no'.
;==========================AOR SECTION OPTIONS=========================
;[aor]
diff --git a/contrib/ast-db-manage/config/versions/58e440314c2a_allow_wildcard_certs.py b/contrib/ast-db-manage/config/versions/58e440314c2a_allow_wildcard_certs.py
new file mode 100644
index 0000000000..2e56f73160
--- /dev/null
+++ b/contrib/ast-db-manage/config/versions/58e440314c2a_allow_wildcard_certs.py
@@ -0,0 +1,29 @@
+"""allow_wildcard_certs
+
+Revision ID: 58e440314c2a
+Revises: 18e0805d367f
+Create Date: 2022-05-12 12:15:55.343743
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '58e440314c2a'
+down_revision = '18e0805d367f'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects.postgresql import ENUM
+
+YESNO_NAME = 'yesno_values'
+YESNO_VALUES = ['yes', 'no']
+
+def upgrade():
+ yesno_values = ENUM(*YESNO_VALUES, name=YESNO_NAME, create_type=False)
+
+ op.add_column('ps_transports', sa.Column('allow_wildcard_certs', type_=yesno_values))
+
+
+def downgrade():
+ if op.get_context().bind.dialect.name == 'mssql':
+ op.drop_constraint('ck_ps_transports_allow_wildcard_certs_yesno_values', 'ps_transports')
+ op.drop_column('ps_transports', 'allow_wildcard_certs')
diff --git a/doc/CHANGES-staging/allow_wildcard_certs.txt b/doc/CHANGES-staging/allow_wildcard_certs.txt
new file mode 100644
index 0000000000..29a53dd2dc
--- /dev/null
+++ b/doc/CHANGES-staging/allow_wildcard_certs.txt
@@ -0,0 +1,9 @@
+Subject: res_pjsip
+
+A new transport option 'allow_wildcard_certs' has been added that when it
+and 'verify_server' are both set to 'yes', enables verification against
+wildcards, i.e. '*.' in certs for common, and subject alt names of type DNS
+for TLS transport types. Names must start with the wildcard. Partial wildcards,
+e.g. 'f*.example.com' and 'foo.*.com' are not allowed. As well, names only
+match against a single level meaning '*.example.com' matches 'foo.example.com',
+but not 'foo.bar.example.com'.
diff --git a/include/asterisk/res_pjsip.h b/include/asterisk/res_pjsip.h
index ba95c276f2..b5b5a7256d 100644
--- a/include/asterisk/res_pjsip.h
+++ b/include/asterisk/res_pjsip.h
@@ -173,6 +173,14 @@ struct ast_sip_transport_state {
* \since 17.0.0
*/
struct ast_sip_service_route_vector *service_routes;
+ /*!
+ * Disregard RFC5922 7.2, and allow wildcard certs (TLS only)
+ */
+ int allow_wildcard_certs;
+ /*!
+ * If true, fail if server certificate cannot verify (TLS only)
+ */
+ int verify_server;
};
#define ast_sip_transport_is_nonlocal(transport_state, addr) \
diff --git a/res/res_pjsip/config_transport.c b/res/res_pjsip/config_transport.c
index 6319ecfafb..38bbecb9cf 100644
--- a/res/res_pjsip/config_transport.c
+++ b/res/res_pjsip/config_transport.c
@@ -839,6 +839,16 @@ static int transport_apply(const struct ast_sorcery *sorcery, void *obj)
&temp_state->state->host, NULL, transport->async_operations,
&temp_state->state->factory);
}
+
+ if (res == PJ_SUCCESS) {
+ temp_state->state->factory->info = pj_pool_alloc(
+ temp_state->state->factory->pool, (strlen(transport_id) + 1));
+ /*
+ * Store transport id on the factory instance so it can be used
+ * later to look up the transport state.
+ */
+ sprintf(temp_state->state->factory->info, "%s", transport_id);
+ }
#else
ast_log(LOG_ERROR, "Transport: %s: PJSIP has not been compiled with TLS transport support, ensure OpenSSL development packages are installed\n",
ast_sorcery_object_get_id(obj));
@@ -1063,11 +1073,13 @@ static int transport_tls_bool_handler(const struct aco_option *opt, struct ast_v
}
if (!strcasecmp(var->name, "verify_server")) {
- state->tls.verify_server = ast_true(var->value) ? PJ_TRUE : PJ_FALSE;
+ state->verify_server = ast_true(var->value);
} else if (!strcasecmp(var->name, "verify_client")) {
state->tls.verify_client = ast_true(var->value) ? PJ_TRUE : PJ_FALSE;
} else if (!strcasecmp(var->name, "require_client_cert")) {
state->tls.require_client_cert = ast_true(var->value) ? PJ_TRUE : PJ_FALSE;
+ } else if (!strcasecmp(var->name, "allow_wildcard_certs")) {
+ state->allow_wildcard_certs = ast_true(var->value);
} else {
return -1;
}
@@ -1084,7 +1096,7 @@ static int verify_server_to_str(const void *obj, const intptr_t *args, char **bu
return -1;
}
- *buf = ast_strdup(AST_YESNO(state->tls.verify_server));
+ *buf = ast_strdup(AST_YESNO(state->verify_server));
return 0;
}
@@ -1117,6 +1129,20 @@ static int require_client_cert_to_str(const void *obj, const intptr_t *args, cha
return 0;
}
+static int allow_wildcard_certs_to_str(const void *obj, const intptr_t *args, char **buf)
+{
+ struct ast_sip_transport_state *state = find_state_by_transport(obj);
+
+ if (!state) {
+ return -1;
+ }
+
+ *buf = ast_strdup(AST_YESNO(state->allow_wildcard_certs));
+ ao2_ref(state, -1);
+
+ return 0;
+}
+
/*! \brief Custom handler for TLS method setting */
static int transport_tls_method_handler(const struct aco_option *opt, struct ast_variable *var, void *obj)
{
@@ -1659,6 +1685,7 @@ int ast_sip_initialize_sorcery_transport(void)
ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_server", "", transport_tls_bool_handler, verify_server_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, "transport", "verify_client", "", transport_tls_bool_handler, verify_client_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, "transport", "require_client_cert", "", transport_tls_bool_handler, require_client_cert_to_str, NULL, 0, 0);
+ ast_sorcery_object_field_register_custom(sorcery, "transport", "allow_wildcard_certs", "", transport_tls_bool_handler, allow_wildcard_certs_to_str, NULL, 0, 0);
ast_sorcery_object_field_register_custom(sorcery, "transport", "method", "", transport_tls_method_handler, tls_method_to_str, NULL, 0, 0);
#if defined(PJ_HAS_SSL_SOCK) && PJ_HAS_SSL_SOCK != 0
ast_sorcery_object_field_register_custom(sorcery, "transport", "cipher", "", transport_tls_cipher_handler, transport_tls_cipher_to_str, NULL, 0, 0);
diff --git a/res/res_pjsip/pjsip_config.xml b/res/res_pjsip/pjsip_config.xml
index 84736d6ce5..e6fca2e86b 100644
--- a/res/res_pjsip/pjsip_config.xml
+++ b/res/res_pjsip/pjsip_config.xml
@@ -1763,6 +1763,18 @@
in-progress calls.
+
+ Allow use of wildcards in certificates (TLS ONLY)
+
+ In combination with verify_server, when enabled allow use of wildcards,
+ i.e. '*.' in certs for common,and subject alt names of type DNS for TLS
+ transport types. Names must start with the wildcard. Partial wildcards, e.g.
+ 'f*.example.com' and 'foo.*.com' are not allowed. As well, names only match
+ against a single level meaning '*.example.com' matches 'foo.example.com',
+ but not 'foo.bar.example.com'.
+
+
+
Use the same transport for outgoing requests as incoming ones.
diff --git a/res/res_pjsip/pjsip_transport_events.c b/res/res_pjsip/pjsip_transport_events.c
index 4df1d5e6e6..a816f4674c 100644
--- a/res/res_pjsip/pjsip_transport_events.c
+++ b/res/res_pjsip/pjsip_transport_events.c
@@ -142,6 +142,122 @@ static void transport_state_do_reg_callbacks(struct ao2_container *transports, p
}
}
+static void verify_log_result(int log_level, const pjsip_transport *transport,
+ pj_uint32_t verify_status)
+{
+ const char *status[32];
+ unsigned int count;
+ unsigned int i;
+
+ count = ARRAY_LEN(status);
+
+ if (pj_ssl_cert_get_verify_status_strings(verify_status, status, &count) != PJ_SUCCESS) {
+ ast_log(LOG_ERROR, "Error retrieving certificate verification result(s)\n");
+ return;
+ }
+
+ for (i = 0; i < count; ++i) {
+ ast_log(log_level, _A_, "Transport '%s' to remote '%.*s' - %s\n", transport->factory->info,
+ (int)pj_strlen(&transport->remote_name.host), pj_strbuf(&transport->remote_name.host),
+ status[i]);
+ }
+}
+
+static int verify_cert_name(const pj_str_t *local, const pj_str_t *remote)
+{
+ const char *p;
+ pj_ssize_t size;
+
+ ast_debug(3, "Verify certificate name: local = %.*s, remote = %.*s\n",
+ (unsigned int)pj_strlen(local), pj_strbuf(local),
+ (unsigned int)pj_strlen(remote), pj_strbuf(remote));
+
+ if (!pj_stricmp(remote, local)) {
+ return 1;
+ }
+
+ if (pj_strnicmp2(remote, "*.", 2)) {
+ return 0;
+ }
+
+ p = pj_strchr(local, '.');
+ if (!p) {
+ return 0;
+ }
+
+ size = pj_strbuf(local) + pj_strlen(local) - ++p;
+
+ return size == pj_strlen(remote) - 2 ?
+ !pj_memcmp(pj_strbuf(remote) + 2, p, size) : 0;
+}
+
+static int verify_cert_names(const pj_str_t *host, const pj_ssl_cert_info *remote)
+{
+ unsigned int i;
+
+ for (i = 0; i < remote->subj_alt_name.cnt; ++i) {
+ /*
+ * DNS is the only type we're matching wildcards against,
+ * so only recheck those.
+ */
+ if (remote->subj_alt_name.entry[i].type == PJ_SSL_CERT_NAME_DNS
+ && verify_cert_name(host, &remote->subj_alt_name.entry[i].name)) {
+ return 1;
+ }
+ }
+
+ return verify_cert_name(host, &remote->subject.cn);
+}
+
+static int transport_tls_verify(const pjsip_transport *transport,
+ const pjsip_tls_state_info *state_info)
+{
+ pj_uint32_t verify_status;
+ const struct ast_sip_transport_state *state;
+
+ if (transport->dir == PJSIP_TP_DIR_INCOMING) {
+ return 1;
+ }
+
+ /* transport_id should always be in factory info (see config_transport) */
+ ast_assert(!ast_strlen_zero(transport->factory->info));
+
+ state = ast_sip_get_transport_state(transport->factory->info);
+ if (!state) {
+ /*
+ * There should always be an associated state, but if for some
+ * reason there is not then fail verification
+ */
+ ast_log(LOG_ERROR, "Transport state not found for '%s'\n", transport->factory->info);
+ return 0;
+ }
+
+ verify_status = state_info->ssl_sock_info->verify_status;
+
+ /*
+ * By this point pjsip has already completed its verification process. If
+ * there was a name matching error it could be because they disallow wildcards.
+ * If this transport has been configured to allow wildcards then we'll need
+ * to re-check the name(s) for such.
+ */
+ if (state->allow_wildcard_certs &&
+ (verify_status & PJ_SSL_CERT_EIDENTITY_NOT_MATCH)) {
+ if (verify_cert_names(&transport->remote_name.host,
+ state_info->ssl_sock_info->remote_cert_info)) {
+ /* A name matched a wildcard, so clear the error */
+ verify_status &= ~PJ_SSL_CERT_EIDENTITY_NOT_MATCH;
+ }
+ }
+
+ if (state->verify_server && verify_status != PJ_SSL_CERT_ESUCCESS) {
+ verify_log_result(__LOG_ERROR, transport, verify_status);
+ return 0;
+ }
+
+ verify_log_result(__LOG_NOTICE, transport, verify_status);
+ return 1;
+}
+
/*! \brief Callback invoked when transport state changes occur */
static void transport_state_callback(pjsip_transport *transport,
pjsip_transport_state state, const pjsip_transport_state_info *info)
@@ -157,6 +273,12 @@ static void transport_state_callback(pjsip_transport *transport,
transport->obj_name, transport_state2str(state));
switch (state) {
case PJSIP_TP_STATE_CONNECTED:
+ if (PJSIP_TRANSPORT_IS_SECURE(transport) &&
+ !transport_tls_verify(transport, info->ext_info)) {
+ pjsip_transport_shutdown(transport);
+ return;
+ }
+
monitored = ao2_alloc_options(sizeof(*monitored),
transport_monitor_dtor, AO2_ALLOC_OPT_LOCK_NOLOCK);
if (!monitored) {