From fddc74b043757b7c156185f7d963bd2cb0a0c771 Mon Sep 17 00:00:00 2001 From: Joe Orton Date: Wed, 16 Sep 2026 19:52:27 +0100 Subject: [PATCH 1/2] * test/modules/ssl/test_002_verify_client.py: New test suite. Co-Authored-By: Claude Opus 5 (1M context) GitHub: PR #632 --- test/modules/ssl/test_002_verify_client.py | 217 +++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 test/modules/ssl/test_002_verify_client.py diff --git a/test/modules/ssl/test_002_verify_client.py b/test/modules/ssl/test_002_verify_client.py new file mode 100644 index 00000000000..8f89100e4f6 --- /dev/null +++ b/test/modules/ssl/test_002_verify_client.py @@ -0,0 +1,217 @@ +import os +from datetime import timedelta + +import pytest + +from pyhttpd.certs import CertificateSpec, HttpdTestCA +from pyhttpd.conf import HttpdConf + +# Client credentials, each chosen so that verification fails with exactly one +# of the X509_V_ERR_* codes that "SSLVerifyClient " +# can be told to accept. +CLIENT_SPECS = { + # issued by the server's own CA, clientAuth EKU: verifies cleanly + 'good': CertificateSpec(name="good-client", client=True), + # X509_V_ERR_CERT_HAS_EXPIRED + 'expired': CertificateSpec(name="expired-client", client=True, + valid_from=timedelta(days=-30), + valid_to=timedelta(days=-1)), + # X509_V_ERR_CERT_NOT_YET_VALID + 'future': CertificateSpec(name="future-client", client=True, + valid_from=timedelta(days=1), + valid_to=timedelta(days=30)), + # serverAuth EKU only -> X509_V_ERR_INVALID_PURPOSE + 'noeku': CertificateSpec(domains=["noteclient.example.org"]), +} + + +def client_creds(env, kind): + """(cert_file, pkey_file) for one of the CLIENT_SPECS, or for the two + kinds that need their own issuer.""" + store = os.path.join(env.gen_dir, "verify-clients") + os.makedirs(store, exist_ok=True) + if kind in ('untrusted', 'untrustedchain'): + ca = HttpdTestCA.create_root(name="untrusted-ca", + store_dir=os.path.join(store, "untrusted")) + creds = ca.issue_cert(CertificateSpec(name="untrusted-client", client=True)) + if kind == 'untrustedchain': + # Sending the issuer too turns "unable to verify the first + # certificate" (21) into "unable to get local issuer + # certificate" (20) -- different errors, different tokens. + cert_file = os.path.join(store, "untrustedchain.cert.pem") + pkey_file = os.path.join(store, "untrustedchain.pkey.pem") + with open(cert_file, "wb") as fd: + fd.write(creds.cert_pem) + fd.write(ca.cert_pem) + creds.save_pkey_pem(pkey_file) + return cert_file, pkey_file + elif kind == 'selfsigned': + # X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT + creds = HttpdTestCA.create_root(name="selfsigned-client", + store_dir=os.path.join(store, "selfsigned")) + else: + creds = env.ca.issue_cert(CLIENT_SPECS[kind]) + cert_file = os.path.join(store, f"{kind}.cert.pem") + pkey_file = os.path.join(store, f"{kind}.pkey.pem") + creds.save_cert_pem(cert_file) + creds.save_pkey_pem(pkey_file) + return cert_file, pkey_file + + +def ignore_verify_noise(env): + env.httpd_error_log.add_ignored_lognos(["AH10373", "AH02261", "AH02275"]) + env.httpd_error_log.add_ignored_matches([ + r'.*certificate verify failed.*', + r'.*Re-negotiation handshake failed.*', + r'.*No acceptable peer certificate available.*', + r'.*SSL Library Error.*', + r'.*Certificate Verification:.*', + ]) + + +def install(env, verify_line): + conf = HttpdConf(env, extras={ + f"test1.{env.http_tld}": f""" + SSLCACertificateFile "{env.ca.cert_file}" + {verify_line} + SSLVerifyDepth 5 + """, + }) + conf.add_vhost_test1() + conf.install() + + +def get(env, kind=None): + options = None + if kind is not None: + cert, key = client_creds(env, kind) + options = ['--cert', cert, '--key', key] + return env.curl_get(env.mkurl("https", "test1", "/index.html"), + options=options) + + +def accepted(r): + return r.exit_code == 0 and r.response and r.response["status"] == 200 + + +class TestVerifyClientBaseline: + """No accepted-errors: the existing levels behave as before.""" + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + ignore_verify_noise(env) + install(env, "SSLVerifyClient require") + assert env.apache_restart() == 0 + + def test_ssl_002_01(self, env): + assert accepted(get(env, 'good')) + + @pytest.mark.parametrize("kind", ['untrusted', 'untrustedchain', + 'selfsigned', 'expired', + 'future', 'noeku']) + def test_ssl_002_02(self, env, kind): + assert not accepted(get(env, kind)), \ + f"'{kind}' client cert accepted under plain 'require'" + + +class TestVerifyClientOptionalNoCA: + """optional_no_ca keeps its documented meaning.""" + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + ignore_verify_noise(env) + install(env, "SSLVerifyClient optional_no_ca") + assert env.apache_restart() == 0 + + # the errors optional_no_ca has always waived + @pytest.mark.parametrize("kind", ['untrusted', 'untrustedchain', + 'selfsigned', 'expired']) + def test_ssl_002_03(self, env, kind): + assert accepted(get(env, kind)), \ + f"'{kind}' rejected under optional_no_ca" + + # ...and no client certificate at all is still fine + def test_ssl_002_04(self, env): + assert accepted(get(env)) + + # optional_no_ca never waived these, and must not start now + @pytest.mark.parametrize("kind", ['future', 'noeku']) + def test_ssl_002_05(self, env, kind): + assert not accepted(get(env, kind)), \ + f"'{kind}' accepted under optional_no_ca" + + +# Each accepted-errors token, the client cert kind it is meant to waive, and +# the kinds it must NOT waive. +ACCEPT_CASES = [ + ('self-signed', 'selfsigned', ['expired', 'future', 'noeku']), + ('expired-cert', 'expired', ['selfsigned', 'future', 'noeku']), + ('purpose-mismatch', 'noeku', ['selfsigned', 'expired', 'future']), + ('X509_V_ERR_INVALID_PURPOSE', 'noeku', ['selfsigned', 'expired']), + ('X509_V_ERR_CERT_NOT_YET_VALID', 'future', ['selfsigned', 'expired']), + ('X509_V_ERR_CERT_HAS_EXPIRED', 'expired', ['selfsigned', 'future']), +] + + +@pytest.mark.parametrize("token,waived,rejected", ACCEPT_CASES, + ids=[c[0] for c in ACCEPT_CASES]) +class TestVerifyClientAcceptedErrors: + """SSLVerifyClient require waives exactly the named + error and nothing else.""" + + def test_ssl_002_06(self, env, token, waived, rejected): + ignore_verify_noise(env) + install(env, f"SSLVerifyClient require {token}") + assert env.apache_restart() == 0 + assert accepted(get(env, waived)), \ + f"'{waived}' rejected under 'require {token}'" + + def test_ssl_002_07(self, env, token, waived, rejected): + ignore_verify_noise(env) + install(env, f"SSLVerifyClient require {token}") + assert env.apache_restart() == 0 + for kind in rejected: + assert not accepted(get(env, kind)), \ + f"'{kind}' accepted under 'require {token}'" + # a cert with no problem at all still verifies + assert accepted(get(env, 'good')) + + +class TestVerifyClientAcceptedErrorsList: + """A comma-separated list waives each of its members. + + A certificate issued by a CA the server does not know raises more than one + error as the chain is walked -- the unknown issuer at depth 0 and the + self-signed root above it -- and every one of them has to be waived before + the handshake succeeds. So no single token reproduces optional_no_ca; it + takes the combination below. + """ + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + ignore_verify_noise(env) + install(env, "SSLVerifyClient require " + "self-signed,untrusted-cert,invalid-signature,expired-cert") + assert env.apache_restart() == 0 + + @pytest.mark.parametrize("kind", ['untrusted', 'untrustedchain', + 'selfsigned', 'expired', 'good']) + def test_ssl_002_08(self, env, kind): + assert accepted(get(env, kind)), f"'{kind}' rejected" + + # errors outside the list are still fatal + @pytest.mark.parametrize("kind", ['future', 'noeku']) + def test_ssl_002_09(self, env, kind): + assert not accepted(get(env, kind)), f"'{kind}' accepted" + + +class TestVerifyClientAcceptedErrorsConfig: + """Configuration errors are rejected at startup.""" + + def test_ssl_002_10(self, env): + install(env, "SSLVerifyClient require not-a-real-error") + assert env.apache_fail() == 0 + + def test_ssl_002_11(self, env): + install(env, "SSLVerifyClient none untrusted-cert") + assert env.apache_fail() == 0 From 23d65ebb27772caf365adc2fe7ffa8cd837cad48 Mon Sep 17 00:00:00 2001 From: studersi Date: Fri, 1 May 2026 22:37:19 +0200 Subject: [PATCH 2/2] Add a second argument to SSLVerifyClient to allow for specific TLS verification errors to be ignored. (cherry picked from commit 3a71706adefc2e444e20abb8e915879f6c5dbce5) --- 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 e4008ad9e37..80427e4f6a8 100644 --- a/docs/manual/mod/mod_ssl.xml +++ b/docs/manual/mod/mod_ssl.xml @@ -1632,7 +1632,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 @@ -1653,19 +1653,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 c456416e9f0..391efe54855 100644 --- a/modules/ssl/mod_ssl.c +++ b/modules/ssl/mod_ssl.c @@ -158,9 +158,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 3652b7f0cf7..2b4fe0ae684 100644 --- a/modules/ssl/ssl_engine_config.c +++ b/modules/ssl/ssl_engine_config.c @@ -140,6 +140,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; @@ -289,6 +291,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); @@ -413,6 +423,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; @@ -470,6 +482,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); @@ -1398,24 +1418,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 5967db58b7f..71e4656f5ca 100644 --- a/modules/ssl/ssl_engine_kernel.c +++ b/modules/ssl/ssl_engine_kernel.c @@ -1636,6 +1636,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 @@ -1659,8 +1660,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) { @@ -1672,7 +1684,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) @@ -1683,6 +1695,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 @@ -1720,7 +1742,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) @@ -1728,6 +1751,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 16014186e87..1070ac469fd 100644 --- a/modules/ssl/ssl_private.h +++ b/modules/ssl/ssl_private.h @@ -491,6 +491,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) */ @@ -797,6 +818,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 @@ -953,6 +976,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; @@ -1006,7 +1031,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 *);