Index: mod_ssl.c =================================================================== RCS file: /home/cvs/httpd-2.0/modules/ssl/mod_ssl.c,v retrieving revision 1.74.2.6 diff -u -r1.74.2.6 mod_ssl.c --- mod_ssl.c 9 Feb 2004 20:53:20 -0000 1.74.2.6 +++ mod_ssl.c 3 Jun 2004 12:01:34 -0000 @@ -389,6 +389,7 @@ static void ssl_register_hooks(apr_pool_t *p) { ssl_io_filter_register(p); + ssl_reneg_filter_register(p); ap_hook_pre_connection(ssl_hook_pre_connection,NULL,NULL, APR_HOOK_MIDDLE); ap_hook_post_config (ssl_init_Module, NULL,NULL, APR_HOOK_MIDDLE); Index: mod_ssl.h =================================================================== RCS file: /home/cvs/httpd-2.0/modules/ssl/mod_ssl.h,v retrieving revision 1.122.2.9 diff -u -r1.122.2.9 mod_ssl.h --- mod_ssl.h 9 Feb 2004 20:53:20 -0000 1.122.2.9 +++ mod_ssl.h 3 Jun 2004 12:01:34 -0000 @@ -675,6 +675,7 @@ void ssl_io_filter_init(conn_rec *, SSL *); void ssl_io_filter_register(apr_pool_t *); long ssl_io_data_cb(BIO *, int, MODSSL_BIO_CB_ARG_TYPE *, int, long, long); +void ssl_reneg_filter_register(apr_pool_t *p); /* PRNG */ int ssl_rand_seed(server_rec *, apr_pool_t *, ssl_rsctx_t, char *); Index: ssl_engine_kernel.c =================================================================== RCS file: /home/cvs/httpd-2.0/modules/ssl/ssl_engine_kernel.c,v retrieving revision 1.82.2.12 diff -u -r1.82.2.12 ssl_engine_kernel.c --- ssl_engine_kernel.c 9 Feb 2004 20:53:20 -0000 1.82.2.12 +++ ssl_engine_kernel.c 3 Jun 2004 12:01:35 -0000 @@ -29,6 +29,8 @@ -- Unknown */ #include "mod_ssl.h" +static int reneg_and_check(request_rec *r, int quick); + /* * Post Read Request Handler */ @@ -159,6 +161,100 @@ return DECLINED; } +static ap_filter_rec_t *reneg_filter_rec; + +/* The renegotiation input filter is inserted into the input filter + * stack to perform an SSL renegotatiation after the request body has + * been read. It runs before the HTTP input filter and waits for it to return + * an EOS; at which point it is safe to perform the SSL handshake. */ +static apr_status_t reneg_in_filter(ap_filter_t *f, + apr_bucket_brigade *bb, + ap_input_mode_t mode, + apr_read_type_e block, + apr_off_t bytes) +{ + apr_bucket *bkt; + apr_status_t rv; + + /* This filter needs to buffer each brigade into memory to ensure + * that when an EOS is found, all data really has been read from + * the socket. So, ensure that not too much is buffered: */ + if (bytes > HUGE_STRING_LEN) { + bytes = HUGE_STRING_LEN; + } + + rv = ap_get_brigade(f->next, bb, mode, block, bytes); + if (rv != APR_SUCCESS) { + return rv; + } + + for (bkt = APR_BRIGADE_FIRST(bb); + bkt != APR_BRIGADE_SENTINEL(bb); + bkt = APR_BUCKET_NEXT(bkt)) + { + if (APR_BUCKET_IS_EOS(bkt)) { + /* No more work for this filter. */ + ap_remove_input_filter(f); + + /* Now really do the negotiation and access control checks. */ + if (reneg_and_check(f->r, 0)) { + + /* Access control checks failed: send a 403. */ + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, f->r, + "renegotiation failed; sending 403 error"); + bb = apr_brigade_create(f->r->pool, f->c->bucket_alloc); + bkt = ap_bucket_error_create(HTTP_FORBIDDEN, NULL, + f->r->pool, f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); + bkt = apr_bucket_eos_create(f->c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, bkt); + + rv = ap_pass_brigade(f->r->output_filters, bb); + if (rv) { + ap_log_rerror(APLOG_MARK, APLOG_INFO, rv, f->r, + "could not send 403 after renegotiation" + " failure"); + } + + /* Don't give anything back to the caller, just return + * an error. */ + apr_brigade_cleanup(bb); + return APR_EACCES; + } + + /* ignore the rest of the brigade */ + break; + } + + /* For any non-metadata buckets, read from the bucket to + * ensure that it is morphed into a heap bucket if it's a + * morphing bucket type. */ + if (!APR_BUCKET_IS_METADATA(bkt)) { + const char *buf; + apr_size_t len; + + rv = apr_bucket_read(bkt, &buf, &len, block); + if (rv) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, f->r, + "bucket read failed"); + apr_brigade_cleanup(bb); + return rv; + } + } + } + + return APR_SUCCESS; +} + +void ssl_reneg_filter_register(apr_pool_t *p) +{ + /* only requirement is that this input filter comes before the + * ap_http_filter. */ + reneg_filter_rec = + ap_register_input_filter("SSL_RENEG", reneg_in_filter, NULL, + AP_FTYPE_PROTOCOL - 1); +} + /* * Access Handler */ @@ -169,18 +265,12 @@ SSLConnRec *sslconn = myConnConfig(r->connection); SSL *ssl = sslconn ? sslconn->ssl : NULL; SSL_CTX *ctx = NULL; - apr_array_header_t *requires; - ssl_require_t *ssl_requires; char *cp; - int ok, i; BOOL renegotiate = FALSE, renegotiate_quick = FALSE; - X509 *cert; X509 *peercert; - X509_STORE *cert_store = NULL; - X509_STORE_CTX cert_store_ctx; STACK_OF(SSL_CIPHER) *cipher_list_old = NULL, *cipher_list = NULL; SSL_CIPHER *cipher = NULL; - int depth, verify_old, verify, n; + int verify_old, verify, n; if (ssl) { ctx = SSL_get_SSL_CTX(ssl); @@ -489,235 +579,214 @@ } #endif /* HAVE_SSL_SET_CERT_STORE */ - /* - * SSL renegotiations in conjunction with HTTP - * requests using the POST method are not supported. - * - * Background: - * - * 1. When the client sends a HTTP/HTTPS request, Apache's core code - * reads only the request line ("METHOD /path HTTP/x.y") and the - * attached MIME headers ("Foo: bar") up to the terminating line ("CR - * LF"). An attached request body (for instance the data of a POST - * method) is _NOT_ read. Instead it is read by mod_cgi's content - * handler and directly passed to the CGI script. - * - * 2. mod_ssl supports per-directory re-configuration of SSL parameters. - * This is implemented by performing an SSL renegotiation of the - * re-configured parameters after the request is read, but before the - * response is sent. In more detail: the renegotiation happens after the - * request line and MIME headers were read, but _before_ the attached - * request body is read. The reason simply is that in the HTTP protocol - * usually there is no acknowledgment step between the headers and the - * body (there is the 100-continue feature and the chunking facility - * only), so Apache has no API hook for this step. - * - * 3. the problem now occurs when the client sends a POST request for - * URL /foo via HTTPS the server and the server has SSL parameters - * re-configured on a per-URL basis for /foo. Then mod_ssl has to - * perform an SSL renegotiation after the request was read and before - * the response is sent. But the problem is the pending POST body data - * in the receive buffer of SSL (which Apache still has not read - it's - * pending until mod_cgi sucks it in). When mod_ssl now tries to perform - * the renegotiation the pending data leads to an I/O error. - * - * Solution Idea: - * - * There are only two solutions: Either to simply state that POST - * requests to URLs with SSL re-configurations are not allowed, or to - * renegotiate really after the _complete_ request (i.e. including - * the POST body) was read. Obviously the latter would be preferred, - * but it cannot be done easily inside Apache, because as already - * mentioned, there is no API step between the body reading and the body - * processing. And even when we mod_ssl would hook directly into the - * loop of mod_cgi, we wouldn't solve the problem for other handlers, of - * course. So the only general solution is to suck in the pending data - * of the request body from the OpenSSL BIO into the Apache BUFF. Then - * the renegotiation can be done and after this step Apache can proceed - * processing the request as before. - * - * Solution Implementation: - * - * We cannot simply suck in the data via an SSL_read-based loop because of - * HTTP chunking. Instead we _have_ to use the Apache API for this step which - * is aware of HTTP chunking. So the trick is to suck in the pending request - * data via the Apache API (which uses Apache's BUFF code and in the - * background mod_ssl's I/O glue code) and re-inject it later into the Apache - * BUFF code again. This way the data flows twice through the Apache BUFF, of - * course. But this way the solution doesn't depend on any Apache specifics - * and is fully transparent to Apache modules. - * - * !! BUT ALL THIS IS STILL NOT RE-IMPLEMENTED FOR APACHE 2.0 !! - */ - if (renegotiate && !renegotiate_quick && (r->method_number == M_POST)) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "SSL Re-negotiation in conjunction " - "with POST method not supported!\n" - "hint: try SSLOptions +OptRenegotiate"); - - return HTTP_METHOD_NOT_ALLOWED; + if (!renegotiate) { + /* Nothing more to do here. */ + return DECLINED; } - /* - * now do the renegotiation if anything was actually reconfigured - */ - if (renegotiate) { - /* - * Now we force the SSL renegotation by sending the Hello Request - * message to the client. Here we have to do a workaround: Actually - * OpenSSL returns immediately after sending the Hello Request (the - * intent AFAIK is because the SSL/TLS protocol says it's not a must - * that the client replies to a Hello Request). But because we insist - * on a reply (anything else is an error for us) we have to go to the - * ACCEPT state manually. Using SSL_set_accept_state() doesn't work - * here because it resets too much of the connection. So we set the - * state explicitly and continue the handshake manually. - */ + /* This function is called after reading the request-header. If + * the HTTP request includes a message body, then the client may + * already have sent all the SSL records containing that body. + * It's not possible to do a renegotiation until all those SSL + * records have been read and processed, so the renegotiation has + * to be delayed until that point (when an EOS is returned by the + * HTTP input filter). */ + + if (!renegotiate_quick && + (apr_table_get(r->headers_in, "Transfer-Encoding") || + ((cp = (char *)apr_table_get(r->headers_in, "Content-Length")) != NULL + && strcmp(cp, "0")))) { ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, - "Requesting connection re-negotiation"); - - if (renegotiate_quick) { - STACK_OF(X509) *cert_stack; - - /* perform just a manual re-verification of the peer */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "Performing quick renegotiation: " - "just re-verifying the peer"); - - cert_stack = (STACK_OF(X509) *)SSL_get_peer_cert_chain(ssl); - - cert = SSL_get_peer_certificate(ssl); - - if (!cert_stack && cert) { - /* client cert is in the session cache, but there is - * no chain, since ssl3_get_client_certificate() - * sk_X509_shift-ed the peer cert out of the chain. - * we put it back here for the purpose of quick_renegotiation. - */ - cert_stack = sk_new_null(); - sk_X509_push(cert_stack, MODSSL_PCHAR_CAST cert); - } - - if (!cert_stack || (sk_X509_num(cert_stack) == 0)) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Cannot find peer certificate chain"); - - return HTTP_FORBIDDEN; - } - - if (!(cert_store || - (cert_store = SSL_CTX_get_cert_store(ctx)))) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Cannot find certificate storage"); - - return HTTP_FORBIDDEN; - } - - if (!cert) { - cert = sk_X509_value(cert_stack, 0); - } - - X509_STORE_CTX_init(&cert_store_ctx, cert_store, cert, cert_stack); - depth = SSL_get_verify_depth(ssl); + "SSL re-negotiation needed for request with body: " + "delaying until after body has been read"); + ap_add_input_filter_handle(reneg_filter_rec, NULL, r, r->connection); - if (depth >= 0) { - X509_STORE_CTX_set_depth(&cert_store_ctx, depth); - } - - X509_STORE_CTX_set_ex_data(&cert_store_ctx, - SSL_get_ex_data_X509_STORE_CTX_idx(), - (char *)ssl); + return DECLINED; + } - if (!modssl_X509_verify_cert(&cert_store_ctx)) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Re-negotiation verification step failed"); - ssl_log_ssl_error(APLOG_MARK, APLOG_ERR, r->server); - } + /* Now actually perform the SSL renegotiation; return DECLINED on + * success, to allow mod_auth and other modules to deny access. */ + return (reneg_and_check(r, renegotiate_quick) + ? HTTP_FORBIDDEN : DECLINED); +} - SSL_set_verify_result(ssl, cert_store_ctx.error); - X509_STORE_CTX_cleanup(&cert_store_ctx); +/* Do an SSL renegotiation and perform access control checks; do a + * "quick" renegotation (which doesn't actually perform an SSL + * handshake), if 'quick' is non-zero. Returns non-zero if access + * control checks failed. */ +static int reneg_and_check(request_rec *r, int quick) +{ + SSLDirConfigRec *dc = myDirConfig(r); + SSLConnRec *sslconn = myConnConfig(r->connection); + SSL *ssl = sslconn ? sslconn->ssl : NULL; + apr_array_header_t *requires; + ssl_require_t *ssl_requires; + X509_STORE *cert_store = NULL; + X509_STORE_CTX cert_store_ctx; + X509 *cert; + SSL_CTX *ctx = SSL_get_SSL_CTX(ssl); + int depth, i, ok; + char *cp; - if (cert_stack != SSL_get_peer_cert_chain(ssl)) { - /* we created this ourselves, so free it */ - sk_X509_pop_free(cert_stack, X509_free); - } + /* + * Now we force the SSL renegotation by sending the Hello Request + * message to the client. Here we have to do a workaround: Actually + * OpenSSL returns immediately after sending the Hello Request (the + * intent AFAIK is because the SSL/TLS protocol says it's not a must + * that the client replies to a Hello Request). But because we insist + * on a reply (anything else is an error for us) we have to go to the + * ACCEPT state manually. Using SSL_set_accept_state() doesn't work + * here because it resets too much of the connection. So we set the + * state explicitly and continue the handshake manually. + */ + ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, + "Requesting connection re-negotiation"); + + if (quick) { + STACK_OF(X509) *cert_stack; + + /* perform just a manual re-verification of the peer */ + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "Performing quick renegotiation: " + "just re-verifying the peer"); + + cert_stack = (STACK_OF(X509) *)SSL_get_peer_cert_chain(ssl); + + cert = SSL_get_peer_certificate(ssl); + + if (!cert_stack && cert) { + /* client cert is in the session cache, but there is + * no chain, since ssl3_get_client_certificate() + * sk_X509_shift-ed the peer cert out of the chain. + * we put it back here for the purpose of quick_renegotiation. + */ + cert_stack = sk_new_null(); + sk_X509_push(cert_stack, MODSSL_PCHAR_CAST cert); } - else { - request_rec *id = r->main ? r->main : r; - - /* do a full renegotiation */ - ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, - "Performing full renegotiation: " - "complete handshake protocol"); - - SSL_set_session_id_context(ssl, - (unsigned char *)&id, - sizeof(id)); - - SSL_renegotiate(ssl); - SSL_do_handshake(ssl); - - if (SSL_get_state(ssl) != SSL_ST_OK) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Re-negotiation request failed"); - - r->connection->aborted = 1; - return HTTP_FORBIDDEN; - } - - ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, - "Awaiting re-negotiation handshake"); - - SSL_set_state(ssl, SSL_ST_ACCEPT); - SSL_do_handshake(ssl); - - if (SSL_get_state(ssl) != SSL_ST_OK) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Re-negotiation handshake failed: " - "Not accepted by client!?"); - - r->connection->aborted = 1; - return HTTP_FORBIDDEN; - } + + if (!cert_stack || (sk_X509_num(cert_stack) == 0)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Cannot find peer certificate chain"); + + return 1; } - - /* - * Remember the peer certificate's DN - */ - if ((cert = SSL_get_peer_certificate(ssl))) { - if (sslconn->client_cert) { - X509_free(sslconn->client_cert); - } - sslconn->client_cert = cert; - sslconn->client_dn = NULL; + + if (!(cert_store || + (cert_store = SSL_CTX_get_cert_store(ctx)))) + { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Cannot find certificate storage"); + + return 1; } - - /* - * Finally check for acceptable renegotiation results + + if (!cert) { + cert = sk_X509_value(cert_stack, 0); + } + + X509_STORE_CTX_init(&cert_store_ctx, cert_store, cert, cert_stack); + depth = SSL_get_verify_depth(ssl); + + if (depth >= 0) { + X509_STORE_CTX_set_depth(&cert_store_ctx, depth); + } + + X509_STORE_CTX_set_ex_data(&cert_store_ctx, + SSL_get_ex_data_X509_STORE_CTX_idx(), + (char *)ssl); + + if (!modssl_X509_verify_cert(&cert_store_ctx)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Re-negotiation verification step failed"); + ssl_log_ssl_error(APLOG_MARK, APLOG_ERR, r->server); + } + + SSL_set_verify_result(ssl, cert_store_ctx.error); + X509_STORE_CTX_cleanup(&cert_store_ctx); + + if (cert_stack != SSL_get_peer_cert_chain(ssl)) { + /* we created this ourselves, so free it */ + sk_X509_pop_free(cert_stack, X509_free); + } + } + else { + request_rec *id = r->main ? r->main : r; + + /* do a full renegotiation */ + ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, + "Performing full renegotiation: " + "complete handshake protocol"); + + SSL_set_session_id_context(ssl, + (unsigned char *)&id, + sizeof(id)); + + SSL_renegotiate(ssl); + SSL_do_handshake(ssl); + + if (SSL_get_state(ssl) != SSL_ST_OK) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Re-negotiation request failed"); + + r->connection->aborted = 1; + return 1; + } + + ap_log_error(APLOG_MARK, APLOG_INFO, 0, r->server, + "Awaiting re-negotiation handshake"); + + /* XXX: Should replace SSL_set_state with SSL_renegotiate(ssl); + * However, this causes failures in perl-framework currently, + * perhaps pre-test if we have already negotiated? */ - if (dc->nVerifyClient != SSL_CVERIFY_NONE) { - BOOL do_verify = (dc->nVerifyClient == SSL_CVERIFY_REQUIRE); + SSL_set_state(ssl, SSL_ST_ACCEPT); + SSL_do_handshake(ssl); + + if (SSL_get_state(ssl) != SSL_ST_OK) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Re-negotiation handshake failed: " + "Not accepted by client!?"); + + r->connection->aborted = 1; + return 1; + } + } + + /* + * Remember the peer certificate's DN + */ + if ((cert = SSL_get_peer_certificate(ssl))) { + if (sslconn->client_cert) { + X509_free(sslconn->client_cert); + } + sslconn->client_cert = cert; + sslconn->client_dn = NULL; + } + + /* + * Finally check for acceptable renegotiation results + */ + if (dc->nVerifyClient != SSL_CVERIFY_NONE) { + BOOL do_verify = (dc->nVerifyClient == SSL_CVERIFY_REQUIRE); + + if (do_verify && (SSL_get_verify_result(ssl) != X509_V_OK)) { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, + "Re-negotiation handshake failed: " + "Client verification failed"); + return 1; + } + + if (do_verify) { + X509 *peercert; - if (do_verify && (SSL_get_verify_result(ssl) != X509_V_OK)) { + if ((peercert = SSL_get_peer_certificate(ssl)) == NULL) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, "Re-negotiation handshake failed: " - "Client verification failed"); - - return HTTP_FORBIDDEN; - } - - if (do_verify) { - if ((peercert = SSL_get_peer_certificate(ssl)) == NULL) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, r->server, - "Re-negotiation handshake failed: " - "Client certificate missing"); - - return HTTP_FORBIDDEN; - } - - X509_free(peercert); + "Client certificate missing"); + return 1; } + + X509_free(peercert); } } @@ -744,7 +813,7 @@ /* remember forbidden access for strict require option */ apr_table_setn(r->notes, "ssl-access-forbidden", "1"); - return HTTP_FORBIDDEN; + return 1; } if (ok != 1) { @@ -765,18 +834,11 @@ /* remember forbidden access for strict require option */ apr_table_setn(r->notes, "ssl-access-forbidden", "1"); - return HTTP_FORBIDDEN; + return 1; } } - /* - * Else access is granted from our point of view (except vendor - * handlers override). But we have to return DECLINED here instead - * of OK, because mod_auth and other modules still might want to - * deny access. - */ - - return DECLINED; + return 0; } /*