Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates.#632
Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates.#632studersi wants to merge 1 commit intoapache:trunkfrom
Conversation
… for client certificates.
|
Note that there is also #192, which goes in a similar direction but solves a different problem. Cases where The directive that is introduced in this pull request here can be used to keep all other checks in place with |
|
Here, I also added a version based on the commit 2a327c237b51fde187dde3910a4dde365759d7e0
Author: studersi <mail@studer.si>
Date: Fri Apr 10 14:04:27 2026 +0200
Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates.
diff --git a/docs/manual/mod/mod_ssl.xml b/docs/manual/mod/mod_ssl.xml
index 58844a654f..9edf188043 100644
--- a/docs/manual/mod/mod_ssl.xml
+++ b/docs/manual/mod/mod_ssl.xml
@@ -1382,6 +1382,48 @@ SSLVerifyClient require
</usage>
</directivesynopsis>
+<directivesynopsis>
+<name>SSLVerifyClientEKU</name>
+<description>Whether to enforce Extended Key Usage checks for Client Certificates</description>
+<syntax>SSLVerifyClientEKU on|off</syntax>
+<default>SSLVerifyClientEKU on</default>
+<contextlist><context>server config</context>
+<context>virtual host</context>
+<context>directory</context>
+<context>.htaccess</context></contextlist>
+<override>AuthConfig</override>
+
+<usage>
+<p>
+This directive controls whether mod_ssl enforces X.509 Extended Key Usage
+(EKU) <code>invalid purpose</code> checks during client certificate
+verification. The default value <code>on</code> preserves the standard
+behavior and rejects client certificates whose EKU does not allow client
+authentication.
+</p>
+<p>
+Setting this directive explicitly to <code>on</code> is identical to omitting
+the directive.
+</p>
+<p>
+When set to <code>off</code>, mod_ssl will ignore only the
+<code>invalid purpose</code> verification error for client certificates while
+leaving other verification checks (e.g. chain validation, signature, validity
+period, revocation checks) unchanged.
+</p>
+<p>
+This setting only affects client certificate verification performed by
+<directive module="mod_ssl">SSLVerifyClient</directive>.
+</p>
+<example><title>Example</title>
+<highlight language="config">
+SSLVerifyClient require
+SSLVerifyClientEKU off
+</highlight>
+</example>
+</usage>
+</directivesynopsis>
+
<directivesynopsis>
<name>SSLVerifyDepth</name>
<description>Maximum depth of CA Certificates in Client
diff --git a/modules/ssl/mod_ssl.c b/modules/ssl/mod_ssl.c
index c0fdafd582..aa7f9141d6 100644
--- a/modules/ssl/mod_ssl.c
+++ b/modules/ssl/mod_ssl.c
@@ -132,6 +132,9 @@ static const command_rec ssl_config_cmds[] = {
SSL_CMD_ALL(VerifyClient, TAKE1,
"SSL Client verify type "
"('none', 'optional', 'require', 'optional_no_ca')")
+ SSL_CMD_ALL(VerifyClientEKU, TAKE1,
+ "Whether to enforce client certificate Extended Key Usage "
+ "during SSL client verification ('on' or 'off')")
SSL_CMD_ALL(VerifyDepth, TAKE1,
"SSL Client verify depth "
"('N' - number of intermediate certificates)")
diff --git a/modules/ssl/ssl_engine_config.c b/modules/ssl/ssl_engine_config.c
index 84831dc5a4..5bebb6dbf5 100644
--- a/modules/ssl/ssl_engine_config.c
+++ b/modules/ssl/ssl_engine_config.c
@@ -147,6 +147,7 @@ static void modssl_ctx_init(modssl_ctx_t *mctx, apr_pool_t *p)
mctx->auth.cipher_suite = NULL;
mctx->auth.verify_depth = UNSET;
mctx->auth.verify_mode = SSL_CVERIFY_UNSET;
+ mctx->auth.verify_client_eku = SSL_VERIFY_EKU_UNSET;
mctx->auth.tls13_ciphers = NULL;
mctx->ocsp_mask = UNSET;
@@ -291,6 +292,7 @@ static void modssl_ctx_cfg_merge(apr_pool_t *p,
cfgMergeString(auth.cipher_suite);
cfgMergeInt(auth.verify_depth);
cfgMerge(auth.verify_mode, SSL_CVERIFY_UNSET);
+ cfgMerge(auth.verify_client_eku, SSL_VERIFY_EKU_UNSET);
cfgMergeString(auth.tls13_ciphers);
cfgMergeInt(ocsp_mask);
@@ -447,6 +449,7 @@ void *ssl_config_perdir_create(apr_pool_t *p, char *dir)
dc->szCipherSuite = NULL;
dc->nVerifyClient = SSL_CVERIFY_UNSET;
+ dc->nVerifyClientEKU = SSL_VERIFY_EKU_UNSET;
dc->nVerifyDepth = UNSET;
dc->szUserName = NULL;
@@ -503,6 +506,7 @@ void *ssl_config_perdir_merge(apr_pool_t *p, void *basev, void *addv)
cfgMergeString(szCipherSuite);
cfgMerge(nVerifyClient, SSL_CVERIFY_UNSET);
+ cfgMerge(nVerifyClientEKU, SSL_VERIFY_EKU_UNSET);
cfgMergeInt(nVerifyDepth);
cfgMergeString(szUserName);
@@ -1203,6 +1207,36 @@ const char *ssl_cmd_SSLVerifyClient(cmd_parms *cmd,
return NULL;
}
+const char *ssl_cmd_SSLVerifyClientEKU(cmd_parms *cmd,
+ void *dcfg,
+ const char *arg)
+{
+ SSLDirConfigRec *dc = (SSLDirConfigRec *)dcfg;
+ SSLSrvConfigRec *sc = mySrvConfig(cmd->server);
+ ssl_verify_eku_t mode;
+
+ if (strcEQ(arg, "on")) {
+ mode = SSL_VERIFY_EKU_UNSET;
+ }
+ else if (strcEQ(arg, "off")) {
+ mode = SSL_VERIFY_EKU_OFF;
+ }
+ else {
+ return apr_pstrcat(cmd->temp_pool, cmd->cmd->name,
+ ": Invalid argument '", arg,
+ "' (expected 'on' or 'off')", NULL);
+ }
+
+ if (cmd->path) {
+ dc->nVerifyClientEKU = mode;
+ }
+ else {
+ sc->server->auth.verify_client_eku = mode;
+ }
+
+ return NULL;
+}
+
static const char *ssl_cmd_verify_depth_parse(cmd_parms *parms,
const char *arg,
int *depth)
diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c
index 83ae90edeb..1e20110a1b 100644
--- a/modules/ssl/ssl_engine_kernel.c
+++ b/modules/ssl/ssl_engine_kernel.c
@@ -1583,6 +1583,7 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx)
int errdepth = X509_STORE_CTX_get_error_depth(ctx);
int depth = UNSET;
int verify = SSL_CVERIFY_UNSET;
+ ssl_verify_eku_t verify_eku = SSL_VERIFY_EKU_UNSET;
/*
* Log verification information
@@ -1610,6 +1611,13 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx)
verify = mctx->auth.verify_mode;
}
+ if (dc && !conn->outgoing) {
+ verify_eku = dc->nVerifyClientEKU;
+ }
+ if (verify_eku == SSL_VERIFY_EKU_UNSET) {
+ verify_eku = mctx->auth.verify_client_eku;
+ }
+
if (verify == SSL_CVERIFY_NONE) {
/*
* SSLProxyVerify is either not configured or set to "none".
@@ -1619,6 +1627,17 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx)
return TRUE;
}
+ if (!ok && !conn->outgoing
+ && errnum == X509_V_ERR_INVALID_PURPOSE
+ && verify_eku == SSL_VERIFY_EKU_OFF) {
+ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, conn,
+ "Certificate Verification: EKU check disabled by "
+ "SSLVerifyClientEKU, accepting invalid purpose");
+ X509_STORE_CTX_set_error(ctx, X509_V_OK);
+ errnum = X509_V_OK;
+ ok = TRUE;
+ }
+
if (ssl_verify_error_is_optional(errnum) &&
(verify == SSL_CVERIFY_OPTIONAL_NO_CA))
{
diff --git a/modules/ssl/ssl_private.h b/modules/ssl/ssl_private.h
index 1ec02f31ae..243b431558 100644
--- a/modules/ssl/ssl_private.h
+++ b/modules/ssl/ssl_private.h
@@ -468,6 +468,11 @@ typedef enum {
SSL_CVERIFY_OPTIONAL_NO_CA = 3
} ssl_verify_t;
+typedef enum {
+ SSL_VERIFY_EKU_UNSET = UNSET,
+ SSL_VERIFY_EKU_OFF = 0
+} ssl_verify_eku_t;
+
#define SSL_VERIFY_PEER_STRICT \
(SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
@@ -747,6 +752,7 @@ typedef struct {
/** for client or downstream server authentication */
int verify_depth;
ssl_verify_t verify_mode;
+ ssl_verify_eku_t verify_client_eku;
/** TLSv1.3 has its separate cipher list, separate from the
settings for older TLS protocol versions. Since which one takes
@@ -879,6 +885,7 @@ struct SSLDirConfigRec {
ssl_opt_t nOptionsDel;
const char *szCipherSuite;
ssl_verify_t nVerifyClient;
+ ssl_verify_eku_t nVerifyClientEKU;
int nVerifyDepth;
const char *szUserName;
apr_size_t nRenegBufferSize;
@@ -924,6 +931,7 @@ const char *ssl_cmd_SSLHonorCipherOrder(cmd_parms *cmd, void *dcfg, int flag);
const char *ssl_cmd_SSLCompression(cmd_parms *, void *, int flag);
const char *ssl_cmd_SSLSessionTickets(cmd_parms *, void *, int flag);
const char *ssl_cmd_SSLVerifyClient(cmd_parms *, void *, const char *);
+const char *ssl_cmd_SSLVerifyClientEKU(cmd_parms *, void *, const char *);
const char *ssl_cmd_SSLVerifyDepth(cmd_parms *, void *, const char *);
const char *ssl_cmd_SSLSessionCache(cmd_parms *, void *, const char *);
const char *ssl_cmd_SSLSessionCacheTimeout(cmd_parms *, void *, const char *); |
Summary
This change introduces
SSLVerifyClientEKU on|offinmod_sslto make EKU purpose enforcement configurable for inbound client-certificate verification (SSLVerifyClient).Default behavior remains unchanged (
on): certificates failing EKU purpose validation (X509_V_ERR_INVALID_PURPOSE) are rejected. If explicitly set tooff, only that specific error is ignored; all other verification checks still apply.Why this is needed now
In practice, many mTLS deployments have relied on publicly/centrally issued certificates that include
clientAuthEKU, and application/platform assumptions were built around that profile over time.The ecosystem is now moving toward tighter issuance policies where many CAs will no longer issue certificates suitable for client authentication in the same way. As that rollout progresses, existing deployments can encounter sudden
invalid purposefailures even when certificates are otherwise valid and trusted.For many operators, migrating to a dedicated client-certificate PKI (or fully re-profiling identity issuance flows) is non-trivial and may require significant lead time across multiple teams/systems. This change provides a controlled compatibility mechanism to support a grace period while migration completes.
What the directive enables
on).off) for environments in transition.X509_V_ERR_INVALID_PURPOSEon inbound client cert verification.Scope and safety boundaries
SSLVerifyClientflows (incoming client certs).SSLProxyVerify/ outgoing backend server verification.Operational intent
SSLVerifyClientEKU offis intended as a temporary transition aid, not a permanent policy state. Recommended usage is tightly scoped (specific vhost/location) with a clear decommission plan once proper client-cert PKI migration is complete.Notice
This pull request includes code developed with AI-assisted tools.