diff --git a/changes-entries/pr69743.txt b/changes-entries/pr69743.txt new file mode 100644 index 00000000000..72dfc8db0cf --- /dev/null +++ b/changes-entries/pr69743.txt @@ -0,0 +1,3 @@ + *) mod_ssl: Add SSLVHostSNIPolicy directive to control the virtual + host compatibility policy. PR 69743. [Joe Orton] + diff --git a/docs/manual/mod/mod_ssl.xml b/docs/manual/mod/mod_ssl.xml index 137b1f74dd2..46530b22c8e 100644 --- a/docs/manual/mod/mod_ssl.xml +++ b/docs/manual/mod/mod_ssl.xml @@ -1816,6 +1816,97 @@ SSLStrictSNIVHostCheck on + +SSLVHostSNIPolicy +Set compatibility policy for SNI client access to virtual hosts. +SSLVHostSNIPolicy strict|secure|authonly|insecure +SSLVHostSNIPolicy secure +server config +Available in httpd 2.5 and later + +

This directive sets policy applied when checking whether the +VirtualHost +identified by the Host request header in an HTTP request +is compatible with the VirtualHost identified from the SNI +extension sent during the initial TLS connection handshake. If an HTTP +request is associated with a virtual host which has an incompatible +SSL/TLS configuration under the policy used, an HTTP error response +with status code 421 ("Misdirected Request") will be sent.

+ +

The policy also applies to TLS connections where an SNI extension +is not sent during the handshake, implicitly using the default or +first virtual host definition. If the Host header in an HTTP request +on such a connection identifies any other non-default virtual host, +the compatibility policy is tested.

+ +

The strict policy blocks all HTTP requests which are +identified with a different virtual host to that identifed by SNI. +The insecure policy allows all HTTP requests regardless +of virtual host identified; such a configuration may be vulnerable to +CVE-2025-23048. +

+ +

The (default) secure, and authonly +policies compare specific aspects of the SSL configuration for the two +virtual hosts, which are grouped into two categories:

+ + + +

This table illustrates whether an HTTP request will be blocked or +allowed when the virtual host configurations differ as described, +under each different policy setting:

+ + + + + + + + + + + + + + + + + + + + + + +
Policy modeAny VirtualHost mismatchServer certificate/key,
or protocol/cipher restrictions
Client verification/
authentication settings
strictblockedblockedblocked
secureallowedblockedblocked
authonlyallowedallowedblocked
insecureallowedallowedallowed
+ +Example + +SSLVHostSNIPolicy authonly + + + +
+
+ SSLProxyMachineCertificatePath Directory of PEM-encoded client certificates and keys to be used by the proxy diff --git a/modules/ssl/mod_ssl.c b/modules/ssl/mod_ssl.c index fb66d1825e6..c0fdafd5821 100644 --- a/modules/ssl/mod_ssl.c +++ b/modules/ssl/mod_ssl.c @@ -80,6 +80,8 @@ static const command_rec ssl_config_cmds[] = { SSL_CMD_SRV(RandomSeed, TAKE23, "SSL Pseudo Random Number Generator (PRNG) seeding source " "('startup|connect builtin|file:/path|exec:/path [bytes]')") + SSL_CMD_SRV(VHostSNIPolicy, TAKE1, + "SSL VirtualHost SNI compatibility policy setting") /* * Per-server context configuration directives diff --git a/modules/ssl/ssl_engine_config.c b/modules/ssl/ssl_engine_config.c index d1f4fad8e23..84831dc5a47 100644 --- a/modules/ssl/ssl_engine_config.c +++ b/modules/ssl/ssl_engine_config.c @@ -63,6 +63,9 @@ SSLModConfigRec *ssl_config_global_create(server_rec *s) mc->sesscache_mode = SSL_SESS_CACHE_OFF; mc->sesscache = NULL; mc->pMutex = NULL; +#ifdef HAVE_TLSEXT + mc->snivh_policy = MODSSL_SNIVH_SECURE; +#endif mc->aRandSeed = apr_array_make(pool, 4, sizeof(ssl_randseed_t)); mc->tVHostKeys = apr_hash_make(pool); @@ -1909,6 +1912,41 @@ const char *ssl_cmd_SSLStrictSNIVHostCheck(cmd_parms *cmd, void *dcfg, int flag #endif } +const char *ssl_cmd_SSLVHostSNIPolicy(cmd_parms *cmd, void *dcfg, const char *arg) +{ +#ifdef HAVE_TLSEXT + SSLModConfigRec *mc = myModConfig(cmd->server); + const char *err; + + if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) { + return err; + } + + if (strcEQ(arg, "secure")) { + mc->snivh_policy = MODSSL_SNIVH_SECURE; + } + else if (strcEQ(arg, "strict")) { + mc->snivh_policy = MODSSL_SNIVH_STRICT; + } + else if (strcEQ(arg, "insecure")) { + mc->snivh_policy = MODSSL_SNIVH_INSECURE; + } + else if (strcEQ(arg, "authonly")) { + mc->snivh_policy = MODSSL_SNIVH_AUTHONLY; + } + else { + return apr_psprintf(cmd->pool, "Invalid SSLVhostSNIPolicy " + "argument '%s'", arg); + } + + return NULL; +#else + return "SSLVHostSNIPolicy cannot be used, OpenSSL is not built with " + "support for TLS extensions and SNI indication. Refer to the " + "documentation, and build a compatible version of OpenSSL." +#endif +} + #ifdef HAVE_OCSP_STAPLING const char *ssl_cmd_SSLStaplingCache(cmd_parms *cmd, diff --git a/modules/ssl/ssl_engine_init.c b/modules/ssl/ssl_engine_init.c index 94cc2772e01..0fecdcfa6e8 100644 --- a/modules/ssl/ssl_engine_init.c +++ b/modules/ssl/ssl_engine_init.c @@ -28,6 +28,8 @@ -- Unknown */ #include "ssl_private.h" +#include + #include "mpm_common.h" #include "mod_md.h" @@ -186,6 +188,110 @@ static void ssl_add_version_components(apr_pool_t *ptemp, apr_pool_t *pconf, modver, AP_SERVER_BASEVERSION, incver); } +#ifdef HAVE_TLSEXT +/* Helper functions to create the SNI vhost policy hash. The policy + * hash captures the configuration elements relevant to the mode + * selected at runtime by SSLVHostSNIPolicy. */ + +#define md5_str_update(ctx_, pfx_, str_) do { apr_md5_update(ctx_, pfx_, strlen(pfx_)); apr_md5_update(ctx_, str_, strlen(str_)); } while (0) +#define md5_ifstr_update(ctx_, pfx_, str_) do { apr_md5_update(ctx_, pfx_, strlen(pfx_)); if (str_) apr_md5_update(ctx_, str_, strlen(str_)); } while (0) +#define md5_fmt_update(ctx_, fmt_, i_) do { char s_[128]; apr_snprintf(s_, sizeof s_, fmt_, i_); \ + apr_md5_update(ctx_, s_, strlen(s_)); } while (0) + +static int md5_strarray_cmp(const void *p1, const void *p2) +{ + return strcmp(*(char **)p1, *(char **)p2); +} + +/* Hashes an array of strings in sorted order. */ +static void md5_strarray_hash(apr_pool_t *ptemp, apr_md5_ctx_t *hash, + const char *pfx, apr_array_header_t *s) +{ + char **elts = apr_pmemdup(ptemp, s->elts, s->nelts * sizeof *elts); + int i; + + qsort(elts, s->nelts, sizeof(char *), md5_strarray_cmp); + + apr_md5_update(hash, pfx, strlen(pfx)); + for (i = 0; i < s->nelts; i++) { + md5_str_update(hash, "elm:", elts[i]); + } +} + +static void hash_sni_policy_pk(apr_pool_t *ptemp, apr_md5_ctx_t *hash, modssl_ctx_t *ctx) +{ + md5_fmt_update(hash, "protocol:%d", ctx->protocol); + + md5_ifstr_update(hash, "ciphers:", ctx->auth.cipher_suite); + md5_ifstr_update(hash, "tls13_ciphers:", ctx->auth.tls13_ciphers); + + md5_strarray_hash(ptemp, hash, "cert_files:", ctx->pks->cert_files); + md5_strarray_hash(ptemp, hash, "key_files:", ctx->pks->key_files); +} + +static void hash_sni_policy_auth(apr_md5_ctx_t *hash, modssl_ctx_t *ctx) +{ + modssl_pk_server_t *pks = ctx->pks; + modssl_auth_ctx_t *a = &ctx->auth; + + md5_fmt_update(hash, "verify_depth:%d", a->verify_depth); + md5_fmt_update(hash, "verify_mode:%d", a->verify_mode); + + md5_ifstr_update(hash, "ca_name_path:", pks->ca_name_path); + md5_ifstr_update(hash, "ca_name_file:", pks->ca_name_file); + md5_ifstr_update(hash, "ca_cert_path:", a->ca_cert_path); + md5_ifstr_update(hash, "ca_cert_file:", a->ca_cert_file); + md5_ifstr_update(hash, "crl_path:", ctx->crl_path); + md5_ifstr_update(hash, "crl_file:", ctx->crl_file); + md5_fmt_update(hash, "crl_check_mask:%d", ctx->crl_check_mask); + md5_fmt_update(hash, "ocsp_mask:%d", ctx->ocsp_mask); + md5_fmt_update(hash, "ocsp_force_default:%d", ctx->ocsp_force_default); + md5_ifstr_update(hash, "ocsp_responder:", ctx->ocsp_responder); + +#ifdef HAVE_SRP + md5_ifstr_update(hash, "srp_vfile:", ctx->srp_vfile); +#endif + +#ifdef HAVE_SSL_CONF_CMD + { + apr_array_header_t *parms = ctx->ssl_ctx_param; + int n; + + for (n = 0; n < parms->nelts; n++) { + ssl_ctx_param_t *p = &APR_ARRAY_IDX(parms, n, ssl_ctx_param_t); + + md5_str_update(hash, "param:", p->name); + md5_str_update(hash, "value:", p->value); + } + } +#endif +} +#endif + +static char *create_sni_policy_hash(apr_pool_t *p, apr_pool_t *ptemp, + modssl_snivhpolicy_t policy, + SSLSrvConfigRec *sc) +{ + char *rv = NULL; +#ifdef HAVE_TLSEXT + if (policy != MODSSL_SNIVH_STRICT && policy != MODSSL_SNIVH_INSECURE) { + apr_md5_ctx_t hash; + unsigned char digest[APR_MD5_DIGESTSIZE]; + + /* Create the vhost policy hash for comparison later. */ + apr_md5_init(&hash); + hash_sni_policy_auth(&hash, sc->server); + if (policy == MODSSL_SNIVH_SECURE) + hash_sni_policy_pk(ptemp, &hash, sc->server); + apr_md5_final(digest, &hash); + + rv = apr_palloc(p, 2 * APR_MD5_DIGESTSIZE + 1); + ap_bin2hex(digest, APR_MD5_DIGESTSIZE, rv); /* sets final '\0' */ + } +#endif + return rv; +} + /* _________________________________________________________________ ** ** Let other answer special connection attempts. @@ -439,6 +545,8 @@ apr_status_t ssl_init_Module(apr_pool_t *p, apr_pool_t *plog, return rv; } } + + sc->sni_policy_hash = create_sni_policy_hash(p, ptemp, mc->snivh_policy, sc); } /* diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c index 33aa1f71dc7..a6af6332f43 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -101,112 +101,28 @@ static int fill_reneg_buffer(request_rec *r, SSLDirConfigRec *dc) } #ifdef HAVE_TLSEXT -static int ap_array_same_str_set(apr_array_header_t *s1, apr_array_header_t *s2) +/* Check whether a transition from vhost sc1 to sc2 from SNI to Host: + * vhost selection is permitted according to the SSLVHostSNIPolicy + * setting. Returns 1 if the policy treats the vhosts as compatible, + * else 0. */ +static int ssl_check_vhost_sni_policy(SSLSrvConfigRec *sc1, + SSLSrvConfigRec *sc2) { - int i; - const char *c; - - if (s1 == s2) { + modssl_snivhpolicy_t policy = sc1->mc->snivh_policy; + + /* Policy: insecure => allow everything. */ + if (policy == MODSSL_SNIVH_INSECURE) return 1; - } - else if (!s1 || !s2 || (s1->nelts != s2->nelts)) { - return 0; - } - for (i = 0; i < s1->nelts; i++) { - c = APR_ARRAY_IDX(s1, i, const char *); - if (!c || !ap_array_str_contains(s2, c)) { - return 0; - } - } - return 1; -} - -static int ssl_pk_server_compatible(modssl_pk_server_t *pks1, - modssl_pk_server_t *pks2) -{ - if (!pks1 || !pks2) { - return 0; - } - /* both have the same certificates? */ - if ((pks1->ca_name_path != pks2->ca_name_path) - && (!pks1->ca_name_path || !pks2->ca_name_path - || strcmp(pks1->ca_name_path, pks2->ca_name_path))) { - return 0; - } - if ((pks1->ca_name_file != pks2->ca_name_file) - && (!pks1->ca_name_file || !pks2->ca_name_file - || strcmp(pks1->ca_name_file, pks2->ca_name_file))) { - return 0; - } - if (!ap_array_same_str_set(pks1->cert_files, pks2->cert_files) - || !ap_array_same_str_set(pks1->key_files, pks2->key_files)) { - return 0; - } - return 1; -} - -static int ssl_auth_compatible(modssl_auth_ctx_t *a1, - modssl_auth_ctx_t *a2) -{ - if (!a1 || !a2) { - return 0; - } - /* both have the same verification */ - if ((a1->verify_depth != a2->verify_depth) - || (a1->verify_mode != a2->verify_mode)) { - return 0; - } - /* both have the same ca path/file */ - if ((a1->ca_cert_path != a2->ca_cert_path) - && (!a1->ca_cert_path || !a2->ca_cert_path - || strcmp(a1->ca_cert_path, a2->ca_cert_path))) { - return 0; - } - if ((a1->ca_cert_file != a2->ca_cert_file) - && (!a1->ca_cert_file || !a2->ca_cert_file - || strcmp(a1->ca_cert_file, a2->ca_cert_file))) { - return 0; - } - /* both have the same ca cipher suite string */ - if ((a1->cipher_suite != a2->cipher_suite) - && (!a1->cipher_suite || !a2->cipher_suite - || strcmp(a1->cipher_suite, a2->cipher_suite))) { - return 0; - } - /* both have the same ca cipher suite string */ - if ((a1->tls13_ciphers != a2->tls13_ciphers) - && (!a1->tls13_ciphers || !a2->tls13_ciphers - || strcmp(a1->tls13_ciphers, a2->tls13_ciphers))) { - return 0; - } - return 1; -} - -static int ssl_ctx_compatible(modssl_ctx_t *ctx1, - modssl_ctx_t *ctx2) -{ - if (!ctx1 || !ctx2 - || (ctx1->protocol != ctx2->protocol) - || !ssl_auth_compatible(&ctx1->auth, &ctx2->auth) - || !ssl_pk_server_compatible(ctx1->pks, ctx2->pks)) { + /* Policy: strict => fail for any vhost transition. */ + if (policy == MODSSL_SNIVH_STRICT && sc1 != sc2) return 0; - } - return 1; -} -static int ssl_server_compatible(server_rec *s1, server_rec *s2) -{ - SSLSrvConfigRec *sc1 = s1? mySrvConfig(s1) : NULL; - SSLSrvConfigRec *sc2 = s2? mySrvConfig(s2) : NULL; + /* For authonly/secure policy, compare the hash. */ + AP_DEBUG_ASSERT(sc1->sni_policy_hash); + AP_DEBUG_ASSERT(sc2->sni_policy_hash); - /* both use the same TLS protocol? */ - if (!sc1 || !sc2 - || !ssl_ctx_compatible(sc1->server, sc2->server)) { - return 0; - } - - return 1; + return strcmp(sc1->sni_policy_hash, sc2->sni_policy_hash) == 0; } #endif @@ -275,6 +191,8 @@ int ssl_hook_ReadReq(request_rec *r) server_rec *handshakeserver = sslconn->server; SSLSrvConfigRec *hssc = mySrvConfig(handshakeserver); + AP_DEBUG_ASSERT(hssc); + if ((servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name))) { /* * The SNI extension supplied a hostname. So don't accept requests @@ -315,19 +233,14 @@ int ssl_hook_ReadReq(request_rec *r) "which is required to access this server.
\n"); return HTTP_FORBIDDEN; } - if (r->server != handshakeserver - && !ssl_server_compatible(sslconn->server, r->server)) { - /* - * The request does not select the virtual host that was - * selected for handshaking and its SSL parameters are different - */ - + /* Enforce SSL SNI vhost compatibility policy. */ + if (!ssl_check_vhost_sni_policy(sc, hssc)) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, APLOGNO(02032) "Hostname %s %s and hostname %s provided" - " via HTTP have no compatible SSL setup", + " via HTTP have no compatible SSL setup for policy '%s'", servername ? servername : handshakeserver->server_hostname, servername ? "provided via SNI" : "(default host as no SNI was provided)", - r->hostname); + r->hostname, MODSSL_SNIVH_NAME(sc->mc->snivh_policy)); return HTTP_MISDIRECTED_REQUEST; } } diff --git a/modules/ssl/ssl_private.h b/modules/ssl/ssl_private.h index 794e51aa937..1ec02f31aef 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -556,6 +556,19 @@ typedef struct { int nBytes; } ssl_randseed_t; +/* SNI vhost compatibility policy. */ +typedef enum { + MODSSL_SNIVH_STRICT = 0, + MODSSL_SNIVH_SECURE = 1, + MODSSL_SNIVH_AUTHONLY = 2, + MODSSL_SNIVH_INSECURE = 3 +} modssl_snivhpolicy_t; + +/* Maps modssl_snivhpolicy_t back into a config option string. */ +#define MODSSL_SNIVH_NAME(p_) ((p_) == MODSSL_SNIVH_STRICT ? "strict" : \ + ((p_) == MODSSL_SNIVH_SECURE ? "secure" : \ + ((p_) == MODSSL_SNIVH_AUTHONLY ? "authonly" : "insecure" ))) + /** * Define the structure of an ASN.1 anything */ @@ -689,6 +702,8 @@ typedef struct { #ifdef HAVE_FIPS BOOL fips; #endif + + modssl_snivhpolicy_t snivh_policy; } SSLModConfigRec; /** Structure representing configured filenames for certs and keys for @@ -843,6 +858,7 @@ struct SSLSrvConfigRec { modssl_ctx_t *server; #ifdef HAVE_TLSEXT ssl_enabled_t strict_sni_vhost_check; + const char *sni_policy_hash; #endif #ifndef OPENSSL_NO_COMP BOOL compression; @@ -918,6 +934,7 @@ const char *ssl_cmd_SSLRequire(cmd_parms *, void *, const char *); const char *ssl_cmd_SSLUserName(cmd_parms *, void *, const char *); const char *ssl_cmd_SSLRenegBufferSize(cmd_parms *cmd, void *dcfg, const char *arg); const char *ssl_cmd_SSLStrictSNIVHostCheck(cmd_parms *cmd, void *dcfg, int flag); +const char *ssl_cmd_SSLVHostSNIPolicy(cmd_parms *cmd, void *dcfg, const char *arg); const char *ssl_cmd_SSLInsecureRenegotiation(cmd_parms *cmd, void *dcfg, int flag); const char *ssl_cmd_SSLProxyEngine(cmd_parms *cmd, void *dcfg, int flag);