From 29be795e5a22022e4f6f8265506d127b5077455a Mon Sep 17 00:00:00 2001 From: studersi Date: Fri, 10 Apr 2026 14:04:27 +0200 Subject: [PATCH 1/3] Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates. --- docs/manual/mod/mod_ssl.xml | 42 +++++++++++++++++++++++++++++++++ modules/ssl/mod_ssl.c | 3 +++ modules/ssl/ssl_engine_config.c | 37 +++++++++++++++++++++++++++++ modules/ssl/ssl_engine_kernel.c | 19 +++++++++++++++ modules/ssl/ssl_private.h | 8 +++++++ 5 files changed, 109 insertions(+) diff --git a/docs/manual/mod/mod_ssl.xml b/docs/manual/mod/mod_ssl.xml index f08d83fbf48..9528d8b34df 100644 --- a/docs/manual/mod/mod_ssl.xml +++ b/docs/manual/mod/mod_ssl.xml @@ -1460,6 +1460,48 @@ SSLVerifyClient require + +SSLVerifyClientEKU +Whether to enforce Extended Key Usage checks for Client Certificates +SSLVerifyClientEKU on|off +SSLVerifyClientEKU on +server config +virtual host +directory +.htaccess +AuthConfig + + +

+This directive controls whether mod_ssl enforces X.509 Extended Key Usage +(EKU) invalid purpose checks during client certificate +verification. The default value on preserves the standard +behavior and rejects client certificates whose EKU does not allow client +authentication. +

+

+Setting this directive explicitly to on is identical to omitting +the directive. +

+

+When set to off, mod_ssl will ignore only the +invalid purpose verification error for client certificates while +leaving other verification checks (e.g. chain validation, signature, validity +period, revocation checks) unchanged. +

+

+This setting only affects client certificate verification performed by +SSLVerifyClient. +

+Example + +SSLVerifyClient require +SSLVerifyClientEKU off + + +
+
+ SSLVerifyDepth Maximum depth of CA Certificates in Client diff --git a/modules/ssl/mod_ssl.c b/modules/ssl/mod_ssl.c index 8f79d0a8bc0..6e2cffe0afb 100644 --- a/modules/ssl/mod_ssl.c +++ b/modules/ssl/mod_ssl.c @@ -152,6 +152,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 a38dd943e4c..fc92dd8e6e8 100644 --- a/modules/ssl/ssl_engine_config.c +++ b/modules/ssl/ssl_engine_config.c @@ -138,6 +138,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; @@ -284,6 +285,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); @@ -405,6 +407,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; @@ -461,6 +464,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); @@ -1321,6 +1325,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) @@ -2622,6 +2656,9 @@ static void modssl_auth_ctx_dump(modssl_auth_ctx_t *auth, apr_pool_t *p, int pro } #endif DMP_VERIFY(proxy? "SSLProxyVerify" : "SSLVerifyClient", auth->verify_mode); + if (!proxy) { + DMP_ON_OFF("SSLVerifyClientEKU", auth->verify_client_eku); + } DMP_LONG( proxy? "SSLProxyVerify" : "SSLVerifyDepth", auth->verify_depth); DMP_STRING(proxy? "SSLProxyCACertificateFile" : "SSLCACertificateFile", auth->ca_cert_file); DMP_STRING(proxy? "SSLProxyCACertificatePath" : "SSLCACertificatePath", auth->ca_cert_path); diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c index cb88f0112c6..6ce4f7253f0 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -1630,6 +1630,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 @@ -1657,6 +1658,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". @@ -1666,6 +1674,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 442b8b17ae4..ea07a1fb3f0 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -479,6 +479,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) @@ -791,6 +796,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 @@ -926,6 +932,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; @@ -977,6 +984,7 @@ const char *ssl_cmd_SSLClientHelloVars(cmd_parms *, void *, 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 *); From a9905991523d8e4fe16403edc73fc562ec4917d7 Mon Sep 17 00:00:00 2001 From: studersi Date: Fri, 1 May 2026 20:50:39 +0200 Subject: [PATCH 2/3] Revert "Add SSLVerifyClientEKU directive to control Extended Key Usage checks for client certificates." This reverts commit 7634be350583d0f51c39a70a0bd3a992067a01a3. --- docs/manual/mod/mod_ssl.xml | 42 --------------------------------- modules/ssl/mod_ssl.c | 3 --- modules/ssl/ssl_engine_config.c | 37 ----------------------------- modules/ssl/ssl_engine_kernel.c | 19 --------------- modules/ssl/ssl_private.h | 8 ------- 5 files changed, 109 deletions(-) diff --git a/docs/manual/mod/mod_ssl.xml b/docs/manual/mod/mod_ssl.xml index 9528d8b34df..f08d83fbf48 100644 --- a/docs/manual/mod/mod_ssl.xml +++ b/docs/manual/mod/mod_ssl.xml @@ -1460,48 +1460,6 @@ SSLVerifyClient require - -SSLVerifyClientEKU -Whether to enforce Extended Key Usage checks for Client Certificates -SSLVerifyClientEKU on|off -SSLVerifyClientEKU on -server config -virtual host -directory -.htaccess -AuthConfig - - -

-This directive controls whether mod_ssl enforces X.509 Extended Key Usage -(EKU) invalid purpose checks during client certificate -verification. The default value on preserves the standard -behavior and rejects client certificates whose EKU does not allow client -authentication. -

-

-Setting this directive explicitly to on is identical to omitting -the directive. -

-

-When set to off, mod_ssl will ignore only the -invalid purpose verification error for client certificates while -leaving other verification checks (e.g. chain validation, signature, validity -period, revocation checks) unchanged. -

-

-This setting only affects client certificate verification performed by -SSLVerifyClient. -

-Example - -SSLVerifyClient require -SSLVerifyClientEKU off - - -
-
- SSLVerifyDepth Maximum depth of CA Certificates in Client diff --git a/modules/ssl/mod_ssl.c b/modules/ssl/mod_ssl.c index 6e2cffe0afb..8f79d0a8bc0 100644 --- a/modules/ssl/mod_ssl.c +++ b/modules/ssl/mod_ssl.c @@ -152,9 +152,6 @@ 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 fc92dd8e6e8..a38dd943e4c 100644 --- a/modules/ssl/ssl_engine_config.c +++ b/modules/ssl/ssl_engine_config.c @@ -138,7 +138,6 @@ 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; @@ -285,7 +284,6 @@ 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); @@ -407,7 +405,6 @@ 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; @@ -464,7 +461,6 @@ 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); @@ -1325,36 +1321,6 @@ 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) @@ -2656,9 +2622,6 @@ static void modssl_auth_ctx_dump(modssl_auth_ctx_t *auth, apr_pool_t *p, int pro } #endif DMP_VERIFY(proxy? "SSLProxyVerify" : "SSLVerifyClient", auth->verify_mode); - if (!proxy) { - DMP_ON_OFF("SSLVerifyClientEKU", auth->verify_client_eku); - } DMP_LONG( proxy? "SSLProxyVerify" : "SSLVerifyDepth", auth->verify_depth); DMP_STRING(proxy? "SSLProxyCACertificateFile" : "SSLCACertificateFile", auth->ca_cert_file); DMP_STRING(proxy? "SSLProxyCACertificatePath" : "SSLCACertificatePath", auth->ca_cert_path); diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c index 6ce4f7253f0..cb88f0112c6 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -1630,7 +1630,6 @@ 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 @@ -1658,13 +1657,6 @@ 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". @@ -1674,17 +1666,6 @@ 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 ea07a1fb3f0..442b8b17ae4 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -479,11 +479,6 @@ 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) @@ -796,7 +791,6 @@ 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 @@ -932,7 +926,6 @@ 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; @@ -984,7 +977,6 @@ const char *ssl_cmd_SSLClientHelloVars(cmd_parms *, void *, 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 *); From 3a71706adefc2e444e20abb8e915879f6c5dbce5 Mon Sep 17 00:00:00 2001 From: studersi Date: Fri, 1 May 2026 22:37:19 +0200 Subject: [PATCH 3/3] Add a second argument to SSLVerifyClient to allow for specific TLS verification errors to be ignored. --- docs/manual/mod/mod_ssl.xml | 55 +++++++++++-- modules/ssl/mod_ssl.c | 5 +- modules/ssl/ssl_engine_config.c | 136 +++++++++++++++++++++++++++++++- modules/ssl/ssl_engine_io.c | 11 ++- modules/ssl/ssl_engine_kernel.c | 41 +++++++++- modules/ssl/ssl_private.h | 27 ++++++- 6 files changed, 256 insertions(+), 19 deletions(-) diff --git a/docs/manual/mod/mod_ssl.xml b/docs/manual/mod/mod_ssl.xml index f08d83fbf48..96a982c1d3d 100644 --- a/docs/manual/mod/mod_ssl.xml +++ b/docs/manual/mod/mod_ssl.xml @@ -1421,7 +1421,7 @@ SSLCARevocationCheck chain no_crl_for_cert_ok SSLVerifyClient Type of Client Certificate verification -SSLVerifyClient level +SSLVerifyClient level [accepted-errors] SSLVerifyClient none server config virtual host @@ -1442,19 +1442,60 @@ before the HTTP response is sent.

The following levels are available for level:

  • none: - no client Certificate is required at all
  • + no client certificate is required at all. When this level is used, + a second argument for accepted-errors is not permitted.
  • optional: - the client may present a valid Certificate
  • + the client may present a valid certificate
  • require: - the client has to present a valid Certificate
  • + the client has to present a valid certificate
  • optional_no_ca: - the client may present a valid Certificate
    + the client may present a valid certificate
    but it need not to be (successfully) verifiable. This option - cannot be relied upon for client authentication.
  • + cannot be relied upon for client authentication. This is now equivalent to + SSLVerifyClient optional X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT,X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN,X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY,X509_V_ERR_CERT_UNTRUSTED,X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE,X509_V_ERR_CERT_HAS_EXPIRED.
+

+The optional second argument accepted-errors can be used to specify a +comma-separated list of verification errors that should be accepted, even if the +verification level would otherwise reject the certificate. +Use of accepted-errors weakens client certificate verification and +should not be used in production deployments. +The following shorthand names are available for accepted-errors:

+
    +
  • self-signed: + Accepts X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT and + X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN.
  • +
  • untrusted-cert: + Accepts X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY and + X509_V_ERR_CERT_UNTRUSTED.
  • +
  • invalid-signature: + Accepts X509_V_ERR_CERT_SIGNATURE_FAILURE and + X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE.
  • +
  • expired-cert: + Accepts X509_V_ERR_CERT_HAS_EXPIRED and + X509_V_ERR_CERT_NOT_YET_VALID.
  • +
  • purpose-mismatch: + Accepts X509_V_ERR_INVALID_PURPOSE.
  • +
+

+Alternatively, any of the following OpenSSL X509 verification error names can be used directly, +such as X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT, +X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN, +X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY, +X509_V_ERR_CERT_UNTRUSTED, +X509_V_ERR_CERT_SIGNATURE_FAILURE, +X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE, +X509_V_ERR_CERT_HAS_EXPIRED, +X509_V_ERR_CERT_NOT_YET_VALID, or +X509_V_ERR_INVALID_PURPOSE.

Example -SSLVerifyClient require + SSLVerifyClient require purpose-mismatch + + +Example with accepted errors + + SSLVerifyClient optional self-signed,untrusted-cert,purpose-mismatch diff --git a/modules/ssl/mod_ssl.c b/modules/ssl/mod_ssl.c index 8f79d0a8bc0..b6f206f1992 100644 --- a/modules/ssl/mod_ssl.c +++ b/modules/ssl/mod_ssl.c @@ -149,9 +149,10 @@ static const command_rec ssl_config_cmds[] = { "('/path/to/file' - PEM encoded)") SSL_CMD_SRV(CARevocationCheck, RAW_ARGS, "SSL CA Certificate Revocation List (CRL) checking mode") - SSL_CMD_ALL(VerifyClient, TAKE1, + SSL_CMD_ALL(VerifyClient, TAKE12, "SSL Client verify type " - "('none', 'optional', 'require', 'optional_no_ca')") + "('none', 'optional', 'require', 'optional_no_ca' " + "[accepted-errors])") 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 a38dd943e4c..9344349549d 100644 --- a/modules/ssl/ssl_engine_config.c +++ b/modules/ssl/ssl_engine_config.c @@ -138,6 +138,8 @@ 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_error_mask = 0; + mctx->auth.verify_error_mask_set = FALSE; mctx->auth.tls13_ciphers = NULL; mctx->ocsp_mask = UNSET; @@ -284,6 +286,14 @@ 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); + if (add->auth.verify_error_mask_set) { + mrg->auth.verify_error_mask = add->auth.verify_error_mask; + mrg->auth.verify_error_mask_set = TRUE; + } + else { + mrg->auth.verify_error_mask = base->auth.verify_error_mask; + mrg->auth.verify_error_mask_set = base->auth.verify_error_mask_set; + } cfgMergeString(auth.tls13_ciphers); cfgMergeInt(ocsp_mask); @@ -405,6 +415,8 @@ void *ssl_config_perdir_create(apr_pool_t *p, char *dir) dc->szCipherSuite = NULL; dc->nVerifyClient = SSL_CVERIFY_UNSET; + dc->nVerifyClientErrorMask = 0; + dc->nVerifyClientErrorMaskSet = FALSE; dc->nVerifyDepth = UNSET; dc->szUserName = NULL; @@ -461,6 +473,14 @@ void *ssl_config_perdir_merge(apr_pool_t *p, void *basev, void *addv) cfgMergeString(szCipherSuite); cfgMerge(nVerifyClient, SSL_CVERIFY_UNSET); + if (add->nVerifyClientErrorMaskSet) { + mrg->nVerifyClientErrorMask = add->nVerifyClientErrorMask; + mrg->nVerifyClientErrorMaskSet = TRUE; + } + else { + mrg->nVerifyClientErrorMask = base->nVerifyClientErrorMask; + mrg->nVerifyClientErrorMaskSet = base->nVerifyClientErrorMaskSet; + } cfgMergeInt(nVerifyDepth); cfgMergeString(szUserName); @@ -1298,24 +1318,136 @@ static const char *ssl_cmd_verify_parse(cmd_parms *parms, return NULL; } +#define SSL_VERIFY_CLIENT_OPTIONAL_NO_CA_ERRORS \ + (ACCEPT_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT \ + | ACCEPT_X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN \ + | ACCEPT_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY \ + | ACCEPT_X509_V_ERR_CERT_UNTRUSTED \ + | ACCEPT_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE \ + | ACCEPT_X509_V_ERR_CERT_HAS_EXPIRED) + +static const char *ssl_cmd_verify_error_mask_add(cmd_parms *parms, + const char *token, + unsigned int *mask) +{ + if (strcEQ(token, "self-signed")) { + *mask |= ACCEPT_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT + | ACCEPT_X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN; + } + else if (strcEQ(token, "untrusted-cert")) { + *mask |= ACCEPT_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY + | ACCEPT_X509_V_ERR_CERT_UNTRUSTED; + } + else if (strcEQ(token, "invalid-signature")) { + *mask |= ACCEPT_X509_V_ERR_CERT_SIGNATURE_FAILURE + | ACCEPT_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE; + } + else if (strcEQ(token, "expired-cert")) { + *mask |= ACCEPT_X509_V_ERR_CERT_HAS_EXPIRED; + } + else if (strcEQ(token, "purpose-mismatch") || strcEQ(token, "X509_V_ERR_INVALID_PURPOSE")) { + *mask |= ACCEPT_X509_V_ERR_INVALID_PURPOSE; + } + else if (strcEQ(token, "X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT")) { + *mask |= ACCEPT_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT; + } + else if (strcEQ(token, "X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN")) { + *mask |= ACCEPT_X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN; + } + else if (strcEQ(token, "X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY")) { + *mask |= ACCEPT_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY; + } + else if (strcEQ(token, "X509_V_ERR_CERT_UNTRUSTED")) { + *mask |= ACCEPT_X509_V_ERR_CERT_UNTRUSTED; + } + else if (strcEQ(token, "X509_V_ERR_CERT_SIGNATURE_FAILURE")) { + *mask |= ACCEPT_X509_V_ERR_CERT_SIGNATURE_FAILURE; + } + else if (strcEQ(token, "X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE")) { + *mask |= ACCEPT_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE; + } + else if (strcEQ(token, "X509_V_ERR_CERT_HAS_EXPIRED")) { + *mask |= ACCEPT_X509_V_ERR_CERT_HAS_EXPIRED; + } + else if (strcEQ(token, "X509_V_ERR_CERT_NOT_YET_VALID")) { + *mask |= ACCEPT_X509_V_ERR_CERT_NOT_YET_VALID; + } + else { + return apr_pstrcat(parms->temp_pool, parms->cmd->name, + ": Invalid accepted-errors value '", token, "'", + NULL); + } + + return NULL; +} + +static const char *ssl_cmd_verify_error_mask_parse(cmd_parms *parms, + const char *arg, + unsigned int *mask) +{ + const char *token; + char *list; + const char *list_cursor; + const char *err; + + *mask = 0; + list = apr_pstrdup(parms->temp_pool, arg); + list_cursor = list; + + while (*list_cursor) { + token = ap_getword(parms->temp_pool, &list_cursor, ','); + if (!*token) { + return apr_pstrcat(parms->temp_pool, parms->cmd->name, + ": Invalid accepted-errors list", + NULL); + } + if ((err = ssl_cmd_verify_error_mask_add(parms, token, mask))) { + return err; + } + } + + return NULL; +} + const char *ssl_cmd_SSLVerifyClient(cmd_parms *cmd, void *dcfg, - const char *arg) + const char *arg1, + const char *arg2) { SSLDirConfigRec *dc = (SSLDirConfigRec *)dcfg; SSLSrvConfigRec *sc = mySrvConfig(cmd->server); ssl_verify_t mode = SSL_CVERIFY_NONE; + unsigned int error_mask = 0; const char *err; - if ((err = ssl_cmd_verify_parse(cmd, arg, &mode))) { + if ((err = ssl_cmd_verify_parse(cmd, arg1, &mode))) { return err; } + if (arg2 != NULL) { + if (mode == SSL_CVERIFY_NONE) { + return apr_pstrcat(cmd->temp_pool, cmd->cmd->name, + ": accepted-errors is not allowed when level is 'none'", + NULL); + } + + if ((err = ssl_cmd_verify_error_mask_parse(cmd, arg2, &error_mask))) { + return err; + } + } + else if (mode == SSL_CVERIFY_OPTIONAL_NO_CA) { + error_mask = SSL_VERIFY_CLIENT_OPTIONAL_NO_CA_ERRORS; + } + if (cmd->path) { dc->nVerifyClient = mode; + dc->nVerifyClientErrorMask = error_mask; + dc->nVerifyClientErrorMaskSet = TRUE; } else { sc->server->auth.verify_mode = mode; + sc->server->auth.verify_error_mask = error_mask; + sc->server->auth.verify_error_mask_set = TRUE; } return NULL; diff --git a/modules/ssl/ssl_engine_io.c b/modules/ssl/ssl_engine_io.c index 2156ab40a49..826df6e4eea 100644 --- a/modules/ssl/ssl_engine_io.c +++ b/modules/ssl/ssl_engine_io.c @@ -1482,8 +1482,13 @@ static apr_status_t ssl_io_filter_handshake(ssl_filter_ctx_t *filter_ctx) if ((verify_result != X509_V_OK) || sslconn->verify_error) { - if (ssl_verify_error_is_optional(verify_result) && - (sc->server->auth.verify_mode == SSL_CVERIFY_OPTIONAL_NO_CA)) + unsigned int verify_error_mask = sc->server->auth.verify_error_mask; + + if (sslconn->dc && sslconn->dc->nVerifyClient != SSL_CVERIFY_UNSET) { + verify_error_mask = sslconn->dc->nVerifyClientErrorMask; + } + + if (ssl_verify_error_is_accepted(verify_result, verify_error_mask)) { /* leaving this log message as an error for the moment, * according to the mod_ssl docs: @@ -1496,7 +1501,7 @@ static apr_status_t ssl_io_filter_handshake(ssl_filter_ctx_t *filter_ctx) ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, c, APLOGNO(02009) "SSL client authentication failed, " "accepting certificate based on " - "\"SSLVerifyClient optional_no_ca\" " + "\"SSLVerifyClient accepted-errors\" " "configuration"); ssl_log_ssl_error(SSLLOG_MARK, APLOG_INFO, server); diff --git a/modules/ssl/ssl_engine_kernel.c b/modules/ssl/ssl_engine_kernel.c index cb88f0112c6..44d6a74c280 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -1630,6 +1630,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; + unsigned int verify_error_mask = 0; /* * Log verification information @@ -1653,8 +1654,19 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx) verify = dc->nVerifyClient; } } - if (!dc || (verify == SSL_CVERIFY_UNSET)) { - verify = mctx->auth.verify_mode; + if (conn->outgoing) { + if (!dc || (verify == SSL_CVERIFY_UNSET)) { + verify = mctx->auth.verify_mode; + } + } + else { + if (!dc || (verify == SSL_CVERIFY_UNSET)) { + verify = mctx->auth.verify_mode; + verify_error_mask = mctx->auth.verify_error_mask; + } + else { + verify_error_mask = dc->nVerifyClientErrorMask; + } } if (verify == SSL_CVERIFY_NONE) { @@ -1666,7 +1678,7 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx) return TRUE; } - if (ssl_verify_error_is_optional(errnum) && + if (conn->outgoing && ssl_verify_error_is_optional(errnum) && (verify == SSL_CVERIFY_OPTIONAL_NO_CA)) { ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, conn, APLOGNO(02037) @@ -1677,6 +1689,16 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx) sslconn->verify_info = "GENEROUS"; ok = TRUE; } + else if (!conn->outgoing && ssl_verify_error_is_accepted(errnum, verify_error_mask)) + { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, conn, APLOGNO(02037) + "Certificate Verification: Verifiable Issuer is " + "configured as optional, therefore we're accepting " + "the certificate"); + + sslconn->verify_info = "GENEROUS"; + ok = TRUE; + } /* * Expired certificates vs. "expired" CRLs: by default, OpenSSL @@ -1714,7 +1736,8 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx) /* If there was an optional verification error, it's not * possible to perform OCSP validation since the issuer may be * missing/untrusted. Fail in that case. */ - if (ssl_verify_error_is_optional(errnum)) { + if (conn->outgoing + && ssl_verify_error_is_optional(errnum)) { X509_STORE_CTX_set_error(ctx, X509_V_ERR_APPLICATION_VERIFICATION); errnum = X509_V_ERR_APPLICATION_VERIFICATION; ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, conn, APLOGNO(02038) @@ -1722,6 +1745,16 @@ int ssl_callback_SSLVerify(int ok, X509_STORE_CTX *ctx) "if issuer has not been verified " "(optional_no_ca configured)"); ok = FALSE; + } + else if (!conn->outgoing + && ssl_verify_error_is_accepted(errnum, verify_error_mask)) { + X509_STORE_CTX_set_error(ctx, X509_V_ERR_APPLICATION_VERIFICATION); + errnum = X509_V_ERR_APPLICATION_VERIFICATION; + ap_log_cerror(APLOG_MARK, APLOG_ERR, 0, conn, APLOGNO(02038) + "cannot perform OCSP validation for cert " + "if issuer has not been verified " + "(accepted-errors configured)"); + ok = FALSE; } else { ok = modssl_verify_ocsp(ctx, sc, s, conn, conn->pool); if (!ok) { diff --git a/modules/ssl/ssl_private.h b/modules/ssl/ssl_private.h index 442b8b17ae4..63d7cfd6223 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -490,6 +490,27 @@ typedef enum { || (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) \ || (errnum == X509_V_ERR_CERT_HAS_EXPIRED)) +#define ACCEPT_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (1U<<0) +#define ACCEPT_X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN (1U<<1) +#define ACCEPT_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY (1U<<2) +#define ACCEPT_X509_V_ERR_CERT_UNTRUSTED (1U<<3) +#define ACCEPT_X509_V_ERR_CERT_SIGNATURE_FAILURE (1U<<4) +#define ACCEPT_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE (1U<<5) +#define ACCEPT_X509_V_ERR_INVALID_PURPOSE (1U<<6) +#define ACCEPT_X509_V_ERR_CERT_HAS_EXPIRED (1U<<7) +#define ACCEPT_X509_V_ERR_CERT_NOT_YET_VALID (1U<<8) + +#define ssl_verify_error_is_accepted(errnum, accepted_errors) \ + ((errnum == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT && ((accepted_errors) & ACCEPT_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)) \ + || (errnum == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN && ((accepted_errors) & ACCEPT_X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)) \ + || (errnum == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY && ((accepted_errors) & ACCEPT_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY)) \ + || (errnum == X509_V_ERR_CERT_UNTRUSTED && ((accepted_errors) & ACCEPT_X509_V_ERR_CERT_UNTRUSTED)) \ + || (errnum == X509_V_ERR_CERT_SIGNATURE_FAILURE && ((accepted_errors) & ACCEPT_X509_V_ERR_CERT_SIGNATURE_FAILURE)) \ + || (errnum == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE && ((accepted_errors) & ACCEPT_X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE)) \ + || (errnum == X509_V_ERR_INVALID_PURPOSE && ((accepted_errors) & ACCEPT_X509_V_ERR_INVALID_PURPOSE)) \ + || (errnum == X509_V_ERR_CERT_HAS_EXPIRED && ((accepted_errors) & ACCEPT_X509_V_ERR_CERT_HAS_EXPIRED)) \ + || (errnum == X509_V_ERR_CERT_NOT_YET_VALID && ((accepted_errors) & ACCEPT_X509_V_ERR_CERT_NOT_YET_VALID))) + /** * CRL checking mask (mode | flags) */ @@ -791,6 +812,8 @@ typedef struct { /** for client or downstream server authentication */ int verify_depth; ssl_verify_t verify_mode; + unsigned int verify_error_mask; + BOOL verify_error_mask_set; /** TLSv1.3 has its separate cipher list, separate from the settings for older TLS protocol versions. Since which one takes @@ -926,6 +949,8 @@ struct SSLDirConfigRec { ssl_opt_t nOptionsDel; const char *szCipherSuite; ssl_verify_t nVerifyClient; + unsigned int nVerifyClientErrorMask; + BOOL nVerifyClientErrorMaskSet; int nVerifyDepth; const char *szUserName; apr_size_t nRenegBufferSize; @@ -976,7 +1001,7 @@ const char *ssl_cmd_SSLHonorCipherOrder(cmd_parms *cmd, void *dcfg, int flag); const char *ssl_cmd_SSLClientHelloVars(cmd_parms *, void *, 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_SSLVerifyClient(cmd_parms *, void *, const char *, 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 *);