Skip to content

Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates.#632

Open
studersi wants to merge 1 commit intoapache:trunkfrom
studersi:feature/SSLVerifyClientEKU
Open

Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates.#632
studersi wants to merge 1 commit intoapache:trunkfrom
studersi:feature/SSLVerifyClientEKU

Conversation

@studersi
Copy link
Copy Markdown

@studersi studersi commented Apr 12, 2026

Summary

This change introduces SSLVerifyClientEKU on|off in mod_ssl to 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 to off, 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 clientAuth EKU, 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 purpose failures 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

  • Maintains strict/default behavior for everyone by default (on).
  • Provides explicit, opt-in compatibility mode (off) for environments in transition.
  • Limits relaxation to one condition only: X509_V_ERR_INVALID_PURPOSE on inbound client cert verification.
  • Keeps all other certificate validation checks intact (chain, signature, validity period, revocation, etc.).

Scope and safety boundaries

  • Applies only to SSLVerifyClient flows (incoming client certs).
  • Does not apply to SSLProxyVerify / outgoing backend server verification.
  • Backward compatible by default.

Operational intent

SSLVerifyClientEKU off is 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.

@studersi
Copy link
Copy Markdown
Author

studersi commented Apr 12, 2026

Note that there is also #192, which goes in a similar direction but solves a different problem.

Cases where SSLProxyVerify optional_no_ca is used, will still break, once the client authentication EKU is removed from certificates. If optional_no_ca is used to ignore verification issues, it makes sense that error with respects to the certificate purpose are ignored as well.

The directive that is introduced in this pull request here can be used to keep all other checks in place with SSLProxyVerify require and only granularly disables the EKU checks until the migration to a more correct client certificate PKI has completed.

@studersi
Copy link
Copy Markdown
Author

Here, I also added a version based on the 2.4.66 tag that can be used with the latest tagged release instead of trunk: studersi@2a327c2.

httpd-2.4.66-mod_ssl-SSLVerifyClientEKU.patch
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 *);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant