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 *);
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