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:

+

+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:

+ +

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